PoC: dual central-directory container desync β€” a valid PyTorch checkpoint whose data.pkl is read from a DIFFERENT central directory by miniz (torch) than by stdlib zipfile (every scanner), bypassing picklescan 1.0.5, modelscan 0.8.8 and fickling 0.1.12 while torch.load executes it

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 central-directory location divergence β†’ scanner evasion β†’ RCE on model load Impact: A .pt/.bin checkpoint that all three scanners certify CLEAN runs attacker os.system(...) the moment a victim runs an ordinary torch.load(weights_only=False).


Root cause β€” miniz anchors the central directory at cd_ofs (forward); stdlib zipfile anchors it at EOCD βˆ’ size_cd (backward)

The PoC is a valid ZIP that contains two physically-separate central directories of equal byte-size S:

[ local file headers + member data ]
[ MAL cd ]  @ file offset cd_ofs           <- malicious data.pkl record (+ aux records)
[ BEN cd ]  @ file offset (EOCD_pos βˆ’ S)   <- benign   data.pkl record (+ aux records)
[ EOCD ]    cd_ofs = MAL cd , size_cd = S , entries = 6
  • PyTorch / miniz (mz_zip_reader_read_central_dir) parses exactly entries central records starting at the EOCD cd_ofs field, and only validates cd_ofs + size_cd <= filesize. It does not require the central directory to end at the EOCD. β†’ it parses MAL cd β†’ mz_zip_reader_locate_file("archive/data.pkl") β†’ malicious pickle β†’ os.system runs.
  • CPython stdlib zipfile (_RealGetContents) computes concat = EOCD_pos βˆ’ size_cd βˆ’ cd_ofs and seeks to start_dir = cd_ofs + concat = EOCD_pos βˆ’ size_cd, i.e. it anchors the directory to the size_cd bytes immediately before the EOCD (its self-extracting-archive / "prepended junk" support). β†’ it parses BEN cd β†’ benign pickle.

picklescan (RelaxedZipFile), modelscan and fickling all enumerate members with stdlib zipfile, so all three scan BEN cd and never see the malicious data.pkl that miniz loads.

Structural proof (from verify.py)

[struct] entries=6 size_cd=403 cd_ofs=814 eocd_pos=1620
[struct] miniz reads CD at cd_ofs=814; stdlib reads CD at eocd-size_cd=1217   <-- different directories
[stdlib] data.pkl -> BENIGN
[torch.load] RCE marker present: True

Verified differential (real released tools)

File torch.load(weights_only=False) picklescan 1.0.5 modelscan 0.8.8 fickling 0.1.12
dualcd_mal.pt (mal in miniz cd, benign in stdlib cd) RCE β€” marker written Dangerous globals: 0, exit 0 CLEAN No issues found πŸŽ‰, exit 0 LIKELY_SAFE
control_mal.pt (single malicious data.pkl) RCE Infected: 1 DETECTED CRITICAL: 1 (posix.system) LIKELY_OVERTLY_MALICIOUS
reverse.pt (benign in miniz cd, malicious in stdlib cd) benign β€” no marker Infected: 1 DETECTED DETECTED LIKELY_OVERTLY_MALICIOUS

The negative control proves the payload is trivially detectable in isolation. The reverse control swaps which directory holds the malicious record and flips every verdict β€” direct proof that miniz resolves data.pkl via cd_ofs while every scanner resolves it via EOCD βˆ’ size_cd.

Reproduce

# loader side needs torch; scanner side needs picklescan==1.0.5 modelscan==0.8.8 fickling==0.1.12 (Python 3.10-3.12)
python build_poc.py     # writes dualcd_mal.pt / .bin, control_mal.pt, reverse.pt (self-contained)
python verify.py        # structural proof + torch RCE + all three scanners clean
python -m picklescan -p dualcd_mal.pt     # Dangerous globals: 0  -> exit 0 (CLEAN)
modelscan -p dualcd_mal.pt                 # No issues found πŸŽ‰
python -c "from fickling.loader import scan_zip_archive as s; print(s('dualcd_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 / distinctness vs prior container-desync findings

This is a distinct fourth mechanism, not a variant of the three previously filed:

  • EOCD-trailer / PK\x05\x06-prefix bypasses defeat the front/back-blind zipfile.is_zipfile router on a file that is not really a torch zip. This PoC is a fully valid torch zip every router agrees on; the divergence is inside central-directory parsing.
  • duplicate-data.pkl (dupcd) uses one central directory containing two records of the same name and exploits first-match (miniz) vs last-wins (NameToInfo) resolution. This PoC uses two separate central directories and needs no duplicate name within a directory; the lever is the anchor point of the whole directory β€” cd_ofs (miniz, forward, count-based) vs EOCD βˆ’ size_cd (stdlib, backward, concat-based). A scanner hardened to reject duplicate member names (the dupcd fix) is still fully bypassed here.
  • pt-zip-differential (a modelscan-only note that forged the EOCD entry-count to stop miniz early) is superseded: here the entry-count and size_cd are honest for the miniz interpretation, the split comes purely from the two anchor points, and all three scanners are bypassed end-to-end.

Fix

Require the central directory to end at the EOCD (cd_ofs + size_cd == EOCD_position) and reject archives containing more than one central directory / any bytes between the directory and the EOCD, in both the scanners and PyTorchStreamReader. Equivalently, have scanners resolve members with the same cd_ofs-anchored walk 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