PoC: duplicate data.pkl central-directory record — an HONEST-ZIP container desync that bypasses picklescan 1.0.5, modelscan 0.8.8 AND fickling 0.1.12 while torch.load executes

Targets (all current PyPI releases, verified 2026-07-20): picklescan 1.0.5, modelscan 0.8.8, fickling 0.1.12 Real loader: torch.load(..., weights_only=False) — PyTorch 2.13.0+cpu (caffe2::serialize::PyTorchStreamReader / miniz) Class: CWE-436 Interpretation Conflict — ZIP container member-name resolution divergence → scanner evasion → RCE on model load Impact: A .pt/.bin PyTorch checkpoint that all three scanners certify CLEAN runs attacker os.system(...) the moment a victim runs the ordinary torch.load(weights_only=False).


Root cause — first-match (miniz) vs last-wins (CPython zipfile) on a duplicate member name

A new-format PyTorch checkpoint is a ZIP whose pickle lives in the record archive/data.pkl. The PoC ZIP contains two central-directory records both named archive/data.pkl:

index 0  archive/data.pkl              <- MALICIOUS pickle  (__reduce__ -> os.system)
index 1  archive/.format_version
...
index 6  archive/data.pkl              <- BENIGN pickle
EOCD: total-entries = 7   (matches the 7 physical central-dir records — see below)
  • PyTorch (miniz) resolves data.pkl via mz_zip_reader_locate_file, which returns the FIRST matching record → reads index 0 (MALICIOUS) → executes it.
  • CPython stdlib zipfile builds a NameToInfo dict by iterating every central-directory record with unconditional NameToInfo[name] = info, so a duplicate name is last-winszip.open("archive/data.pkl") returns index 6 (BENIGN).

All three scanners walk ZIP members with stdlib zipfile (picklescan/relaxed_zipfile.py, modelscan.py:_iterate_models, fickling.loader.scan_zip_archive), so every one of them scans the benign member and never sees the malicious data.pkl that miniz loads.

This ZIP is 100% standards-conformant — no forged fields

The EOCD "total entries" field equals the number of physical central-directory records (7 == 7), no offsets are patched, no CRCs are faked, and every record parses cleanly. It is produced by two plain ZipFile.writestr("archive/data.pkl", ...) calls. A scanner that validates the EOCD entry-count, CRCs, or offsets still accepts it and is still bypassed — the desync is purely in how a duplicate member name is resolved.

Verified differential (real released tools, one command each)

File torch.load(weights_only=False) picklescan 1.0.5 modelscan 0.8.8 fickling 0.1.12
dupcd_mal.pt (two data.pkl, mal-first) RCE — marker written Infected: 0 CLEAN (exit 0) No issues found 🎉 (exit 0) LIKELY_SAFE
control_mal.pt (single mal data.pkl) RCE Infected: 1 DETECTED CRITICAL: 1 (exit 1) LIKELY_OVERTLY_MALICIOUS

The control proves the payload is trivially detectable by every scanner in isolation — only the duplicate-record ordering hides it. A reversed build (benign-first / mal-last) flips every verdict (scanners flag it, torch.load runs the benign member), proving first-vs-last resolution is the exact lever.

Reproduce

# loader side (needs torch)
python build_poc.py           # writes dupcd_mal.pt / dupcd_mal.bin / control_mal.pt
python verify.py              # honest-count check + zipfile-vs-torch.load divergence + RCE marker

# scanner side (picklescan==1.0.5 fickling==0.1.12 modelscan==0.8.8, Python 3.10-3.12)
python -m picklescan -p dupcd_mal.pt        # Infected files: 0  (CLEAN)
modelscan -p dupcd_mal.pt                    # No issues found! 🎉
python -c "from fickling.loader import scan_zip_archive as s; print(s('dupcd_mal.pt'))"  # LIKELY_SAFE

Captured runs are in scan_results.txt.

Honest precondition

weights_only=False (PyTorch >= 2.6 defaults to True, an orthogonal mitigation). This is the same precondition the entire picklescan/modelscan/fickling threat model assumes, and it remains pervasive in training-resume, from_pretrained, and legacy checkpoint call sites.

Dedup / relationship to prior work

  • Distinct from the filed EOCD-trailer and PK\x05\x06-prefix container-routing bypasses: those defeat the front-blind zipfile.is_zipfile router; this one is a member-resolution divergence inside a fully valid ZIP that every router agrees is a torch zip.
  • Related to an internal backlog note on a modelscan-only .pt zipfile-vs-miniz differential (pt-zip-differential). This finding is materially stronger and corrects it:
    1. Honest EOCD entry-count — the prior modelscan variant forged the EOCD "total entries" to stop miniz early and asserted "honest count → both benign / no divergence". That is false: with a correctly-counted, standards-conformant ZIP, miniz still first-wins and zipfile still last-wins, so the divergence persists without any forgery. This defeats count-validating scanners the forged variant would not.
    2. Three scanners, not one — the prior scoped the differential to modelscan and claimed it did not generalize; picklescan 1.0.5 and fickling 0.1.12 use stdlib zipfile too and are equally bypassed, demonstrated here end-to-end.

Fix

Reject any archive containing duplicate member names (both scanners and torch.load should refuse a checkpoint with two data.pkl records), or resolve member names with the same first-match rule miniz uses so the scanned bytes equal the loaded bytes.

Downloads last month

-

Downloads are not tracked for this model. How to track
Inference Providers NEW
This model isn't deployed by any Inference Provider. 🙋 Ask for provider support
Free AI Image Generator No sign-up. Instant results. Open Now