modelscan memo-poison scanner bypass β Proof of Concept
Security research artifact. This repository hosts a benign proof-of-concept for a
Protect AI modelscan detection bypass (root cause: STACK_GLOBAL memo reconstruction
desync). It is submitted as a coordinated huntr Model File Vulnerability (MFV) disclosure.
The payload is benign β it only prints/writes the literal marker CELVEX-MARKER. No
destructive, networked, or harmful behaviour.
Files
poison.pklβ a pickle that, onpickle.load(), resolves and runs the denylistedbuiltins.exec("print('CELVEX-MARKER')"), yet modelscan reportsNo issues found.
Root cause
modelscan's pickle scanner (modelscan/tools/picklescanner.py::_list_globals) memoizes the
textually previous opcode's parsed value (ops[n-1][1]) when it sees MEMOIZE/PUT/BINPUT,
instead of the actual pickle-VM stack top. If the opcode immediately before the memo-put is
stack-consuming (e.g. POP), its parsed value is None, so modelscan stores memo[k]=None.
A later BINGET k then injects None into the (module, name) pair reconstructed for
STACK_GLOBAL. _build_scan_result then evaluates <denied_name> in None, raising
TypeError: argument of type 'NoneType' is not iterable, which modelscan swallows as a
scanner error and reports zero issues β while the real pickle VM happily resolves the
denylisted callable and executes the payload.
This is a distinct root cause from CVE-2025-46417 (a range() ValueError crash), though it lands
in the same "scanner error β 0 issues" outcome class.
Reproduce
pip install modelscan
python -c "import pickle; pickle.load(open('poison.pkl','rb'))" # prints CELVEX-MARKER (payload runs)
modelscan -p poison.pkl # => "No issues found! π" (+ swallowed scanner error)
Verified against modelscan 0.8.8:
poison.pklβpickle.loadexecutesbuiltins.exec(marker fires) andmodelscan -pprintsNo issues found! π.- A control pickle using the same
builtins.execwithout the memo-poison is correctly flagged CRITICAL.
Remediation
In _list_globals, key the memo to the value actually pushed onto the pickle VM stack (track stack
effects per opcode) rather than the textually previous opcode's parsed value; and in
_build_scan_result, treat a reconstructed (None, β¦) / (β¦, None) global as suspicious instead
of letting the in None comparison raise and be swallowed.