YAML Metadata Warning:empty or missing yaml metadata in repo card
Check out the documentation for more information.
modelscan scans only the first pickle of a legacy PyTorch checkpoint β RCE passes clean
Severity: High (arbitrary code execution; scanner false-clean, no skip/error)
Affected tool: modelscan 0.8.8 (also current main) β modelscan/tools/picklescanner.py scan_pytorch / _list_globals.
Category: ModelScan scanner-bypass on .pt/.pth/.bin/.ckpt (in-scope, Model File Formats).
Summary
The legacy (non-zip) PyTorch on-disk format is a sequence of concatenated pickles (MAGIC_NUMBER, protocol, sys_info, then the real object). modelscan's scan_pytorch calls scan_pickle_bytes(..., multiple_pickles=False), so _list_globals scans only the first pickle (the benign magic integer) and stops. A malicious __reduce__ in a later pickle is never disassembled β modelscan returns 0 issues, no skip, no error (a true silent clean pass), while torch.load(weights_only=False) unpickles all of them and executes the payload.
Root cause
modelscan/tools/picklescanner.pyscan_pytorch()(~lines 242-269): afterget_magic_number()validates the torchMAGIC_NUMBER, it returnsscan_pickle_bytes(model, settings, scan_name, multiple_pickles=False)._list_globals()(lines 50-119):if not multiple_pickles: breakafter the first pickle buffer.- The magic number is the correct literal value, so the wrong-magic SKIP branch (lines 255-267) is not taken β the scanner runs to completion and reports clean.
Isolation proofs (in the PoC):
- modelscan's own generic scanner with
multiple_pickles=Trueβ detects('nt','system'). So this is loop-coverage, not a blocklist gap. - Standalone picklescan 1.0.4 detects the same file β its
scan_pytorchloopsfor _ in range(5): scan_pickle_bytes(... multiple_pickles=False). modelscan never ported that loop. - The same payload in the new zip format is caught by modelscan (single
data.pkl). The miss is specific to the legacy multi-pickle format.
Reproduce
python poc/POC_FINAL_legacy_multipickle.py (env: modelscan 0.8.8, picklescan 1.0.4, torch, Python 3.12). Builds a valid 574-byte legacy .pt via torch.save(..., _use_new_zipfile_serialization=False). Asserts: [1] modelscan scan_pytorch β CLEAN_0_ISSUES; [2] picklescan 1.0.4 β DETECTED [('nt','system')]; [3] torch.load(weights_only=False) β RCE marker written; [4] modelscan generic multiple_pickles=True β ISSUES (root-cause isolation). A 300k-case differential fuzz found no simpler opcode-parsing variant, supporting that this loop-coverage gap is the genuine residual.
Driver note: the env's unrelated TF/numpy mismatch prevents the TF-dependent CLI from launching, so the PoC loads the genuine installed
scan_pytorch/_list_globals/scan_pickle_bytes/DEFAULT_SETTINGSTF-free and exercises them directly (validated against known-detected and known-clean pickles)..pt/.ckpt/.bin/.pthdispatch toPyTorchUnsafeOpScanβscan_pytorch, the identical function.
Impact
A valid legacy-format PyTorch checkpoint carrying os.system via __reduce__ passes modelscan with total_issues=0 (CLEAN, no error), then executes on torch.load(weights_only=False). Defeats modelscan as a CI/supply-chain gate for legacy torch artifacts. Honest caveat: the executor is torch.load with weights_only=False (PyTorch β₯2.6 defaults to True); weights_only=False remains pervasive in training-resume/checkpoint code and universal pre-2.6, and gating exactly such files is modelscan's stated purpose.
Dup-check
Distinct from GHSA-9gvj STACK_GLOBAL off-by-one (a crash bypass), CVE-2025-1889 / GHSA-769v-p64c-89pr (picklescan file-extension/wrong-magic SKIP β here the magic is correct and no skip occurs), CVE-2025-10156 (ZIP CRC), CVE-2025-10157 (subclass blocklist), the 29-ways article, and arXiv:2508.19774 EOP-3 (the STACK_GLOBAL-offset + eval-MAGIC crash path). Confirmed protectai/modelscan main still has multiple_pickles=False (unpatched). None of our R1-R4 findings cover the first-pickle-only legacy-checkpoint miss.
Convergence note: two independent round-5 hunters arrived at this same root cause from different starting targets β a strong real-bug signal. (One skeptic pass flagged it as plausibly an extension of known modelscan pickle handling; included here with that caveat so triage isn't surprised.)