YAML Metadata Warning:empty or missing yaml metadata in repo card
Check out the documentation for more information.
modelscan blind to PyTorch .pt2 (PT2-Archive): torch.export.load() unpickles an attacker payload (weights_only=False, gated by an in-archive JSON flag) while modelscan reports 0 issues
Severity: Critical (silent RCE on the load of an untrusted .pt2, past modelscan supply-chain gating)
Affected tool: modelscan 0.8.8 (coverage gap β does not scan .pt2). Loader: torch.export.load / torch.export.pt2_archive.load_pt2 (torch 2.12.0).
Category: ModelScan coverage-gap on a new $4k-class format β .pt2 is PyTorch's current, actively-promoted export format.
Summary
.pt2 (PT2-Archive) is PyTorch's recommended export format (torch.export.save/load), a ZIP written by PyTorchFileWriter. On load, torch.export.load() β load_pt2() β _load_state_dict()/_load_constants() calls torch.load(io.BytesIO(payload_bytes), weights_only=False) with weights_only hardcoded to False (it does not inherit torch.load's safe default). Whether an entry is unpickled vs. treated as a raw tensor is decided by the boolean use_pickle field in a per-model JSON config stored inside the archive (model_weights_config.json) β fully attacker-controlled. An attacker takes any benign .pt2, flips one entry to use_pickle=true / tensor_meta=null, and replaces that entry's extension-less payload file (data/weights/weight_0) with a torch.save(Evil()) blob whose __reduce__ returns os.system(...) β arbitrary code execution on load.
modelscan is blind to this: .pt2 is not in modelscan's supported_zip_extensions ([.zip,.npz]) or any scanner's extensions. modelscan does recurse the .pt2 (zip by magic), but the inner payloads are named weight_0/constant_0 with no extension, and FormatViaExtensionMiddleware assigns a scanner purely by suffix β empty suffix β no scanner runs β every entry SCAN_NOT_SUPPORTED. Net: the documented loader executes the payload while the flagship scanner reports 0 issues.
Root cause
- RCE sink (loader):
torch/export/pt2_archive/_package.pyβ_load_state_dict()lines 877-878 (torch.load(BytesIO(weight_bytes), weights_only=False));_load_constants()935-936 (same); custom-obj branch line 958 (torch._C._pickle_load_objβ raw native pickle, source-confirmed 3rd sink, not weaponized). Gate:use_pickleflag from attacker-controlled JSON via_load_payload_config(line 842) /_build_file_map(818). - Scanner gap (huntr target):
modelscan/settings.pysupported_zip_extensions=[".zip",".npz"](line 30); pickle/pytorchsupported_extensions(59-73) and FormatViaExtension formats (78-91) all omit.pt2.modelscan/middlewares/format_via_extension.py:8assigns format purely bysuffix.
Reproduce
python poc/poc_pt2_rce.py then poc/poc_modelscan_gap.py (env: torch 2.12.0+cpu, modelscan 0.8.8, Python 3.12; verified):
- RCE:
torch.export.load(malicious.pt2)wrotePWNED_pt2_load.txt(os.systemexecuted during the hardcodedweights_only=Falseunpickle; only a benign downstreamSpecViolationErrorfollows, after code execution). Innerarchive/data/weights/weight_0confirmedsuffix=''. - Scanner gap:
ModelScan().scan(malicious.pt2)βtotal_issues=0, all severities 0,total_scanned=0, total_skipped=9β every entrySCAN_NOT_SUPPORTED; the malicious payloadweight_0is not even enumerated.
Impact
ACE on the host when a victim calls torch.export.load() on an attacker-supplied .pt2. .pt2 is PyTorch's actively-promoted replacement for older export workflows, so this loader is increasingly used in MLOps/CI/model-hub pipelines β and because modelscan returns 0 issues, a malicious .pt2 passes automated supply-chain gating silently. Remediation: modelscan should add .pt2 to supported zip/scanner formats and scan inner pickle payloads by content/magic, not by extension.
Dup-check & honest framing
Clear of public prior art. The torch.load weights_only=True bypass family (CVE-2025-32434, CVE-2026-24747, GHSA-53q9-r3pm-6pq6) is the opposite mechanism (defeating the safe unpickler); here the .pt2 loader hardcodes weights_only=False (no safe mode to bypass) and is gated by an in-archive JSON flag. No public source describes torch.export.load/.pt2/load_pt2 as a pickle RCE sink or modelscan's .pt2 blind spot. Distinct from our R2 torch.package and R3 torchscript (different ZIP layout, loader path, and gate).
Filing guidance (honest): PyTorch may treat the loader side as by-design (
torch.export.loadcarries a generic "never load untrusted data" warning), so file this primarily as the modelscan.pt2coverage-gap (the scanner silently passes a code-executing model in PyTorch's current export format) β that is the bounty-relevant, undocumented vulnerability. The specificuse_picklein-archive payload vector strengthens it. Belongs in the "scanner β loader / coverage-gap" banker cluster.