You need to agree to share your contact information to access this model

This repository is publicly accessible, but you have to accept the conditions to access its files and content.

Log in or Sign Up to review the conditions and access this model content.

YAML Metadata Warning:empty or missing yaml metadata in repo card

Check out the documentation for more information.

modelscan bypass: torch.package (.pt) executes code from a bundled .py module body on load, scanner reports CLEAN

Target

  • huntr format dropdown: PyTorch (.pt / .pth) - the file is a real PyTorch torch.package archive produced by torch.package.PackageExporter and consumed by torch.package.PackageImporter.
  • Affected tool: modelscan (protectai/modelscan)
  • Affected version: 0.8.8 (tested against the v0.8.8 git tag; current latest release at time of writing)
  • Loader proving impact: PyTorch torch.package.PackageImporter, tested with torch 2.12.0.

Severity

High - silent scanner bypass leading to arbitrary code execution (RCE) on model load.

  • Class: scanner evasion -> arbitrary code execution when a victim loads an untrusted PyTorch package that modelscan certified as clean.
  • This is genuine OS-level code execution (the PoC writes a marker file via the module body, and on POSIX additionally reads /etc/passwd), not memory-corruption/DoS - so it sits at the top of the impact scale rather than the lower DoS tier.
  • Honest dollar framing: PyTorch .pt/.pth is not in huntr's documented $4,000 modelscan format set (that tier is .joblib, .keras, .gguf, .safetensors, TF SavedModel). PyTorch tensor files are scanned by modelscan as pickle-family content, which maps to the lower modelscan reward tier (historically up to ~$1,500 for pickle-family/.npy/.h5/.tflite/PMML). I am therefore positioning this as a pickle/PyTorch-tier scanner-bypass-to-RCE, realistically in the up-to-$1,500 band, not the $4k band. The RCE impact is real; the format places it in the lower payout tier. Final tier is the maintainers'/huntr's call.

Summary

modelscan unpacks a .pt zip container and routes each inner entry to a scanner by file extension. In a torch.package archive the executable attacker payload does not live in a pickle opcode - it lives in the body of a bundled .py source module. .py matches no scanner's supported_extensions, so modelscan never inspects it; the only entry it does scan (the inner model.pkl) contains nothing but a benign GLOBAL referencing the interned module's class. modelscan reports 0 issues / CLEAN.

When that same file is opened with its intended loader, torch.package.PackageImporter.load_pickle(...), unpickling resolves the GLOBAL by importing the bundled module, and PyTorch exec()s the module source. The top-level code in the bundled .py runs - arbitrary code execution on load, against a file the scanner declared safe.

A built-in control (the same os.system primitive placed in a normal pickle GLOBAL) is flagged CRITICAL by modelscan, proving the scanner's baseline works and that only the .py-carrier torch.package variant evades it.

Root cause

Two independent facts combine.

1. modelscan routes inner files by extension and has no scanner for .py. modelscan/settings.py - the scanner registry keys each scanner to a fixed extension list:

  • modelscan/settings.py:59-69 - PickleUnsafeOpScan.supported_extensions = [".pkl", ".pickle", ".joblib", ".dill", ".dat", ".data"]
  • modelscan/settings.py:70-73 - PyTorchUnsafeOpScan.supported_extensions = [".bin", ".pt", ".pth", ".ckpt"]
  • modelscan/settings.py:31-92 overall - no scanner lists .py.

For a zip-based .pt, modelscan enumerates inner members and dispatches each by its extension (via FormatViaExtensionMiddleware, settings.py:75-92). Result on the malicious package:

  • .../model/model.pkl -> routed to the pickle scanner, disassembled. It contains only a benign GLOBAL <interned_module> Payload; that module is not in unsafe_globals, so 0 issues.
  • .../evilmod.py -> matches no scanner -> skipped, never scanned. This is where the executable payload is.

2. torch.package executes the bundled module body on import. torch/package/package_importer.py (torch 2.12.0):

  • PackageImporter.load_pickle() unpickles with PackageUnpickler, whose find_class() calls self.import_module(module) for the pickle's GLOBAL.
  • import_module -> _load_module -> _make_module -> _compile_source:
    • package_importer.py:444-447 _compile_source reads the bundled source from the zip and compile(source, ..., "exec").
    • package_importer.py:412 exec(code, ns) - the compiled module body is executed.

So merely resolving the benign-looking class reference during unpickle triggers exec() of attacker-controlled top-level Python from the unscanned .py. The dangerous primitive never appears as a pickle opcode, which is the only thing modelscan's pickle scanner reasons about.

PoC

Files included:

  • reproduce.py - clean, self-contained, portable/Linux-friendly reviewer script (builds in a temp dir; payload uses tempfile.gettempdir() and a POSIX /etc/passwd read; benign marker only).
  • poc_final.py - the original verification harness (attack case A + control case B), pinned to a local modelscan v0.8.8 checkout.
  • evil_package.pt - a prebuilt malicious package (so a reviewer can scan it without building).
  • control.pkl - the prebuilt control pickle that modelscan flags.

Build (what the malicious package contains). The interned module body is the payload:

# evilmod.py  (interned into the .pt; runs at load time)
import os, tempfile
# ... writes a benign marker; on POSIX also reads /etc/passwd to show real OS access ...
class Payload:           # the pickle only references THIS benign class
    def __init__(self): self.x = 1
from torch.package import PackageExporter
with PackageExporter("evil_package.pt") as pe:
    pe.intern("evilmod")
    pe.save_pickle("model", "model.pkl", evilmod.Payload())

The decisive assertions (from reproduce.py):

  • assert total == 0 - modelscan reports the package CLEAN.
  • assert pkl_scanned and py_unscanned - the .pkl was scanned, the .py was not.
  • assert os.path.exists(MARK) after PackageImporter(...).load_pickle(...) - the bundled module body executed.
  • control: assert ctotal >= 1 - the same os.system primitive in a plain pickle is flagged.

Captured output (re-verified, torch 2.12.0 / modelscan 0.8.8):

[*] malicious torch.package built: evil_package.pt
    inner files: ['evil_package/model/model.pkl', 'evil_package/evilmod.py',
                  'evil_package/.data/extern_modules', 'evil_package/.data/python_version',
                  'evil_package/.data/version', 'evil_package/byteorder',
                  'evil_package/.data/serialization_id']

(A) MALICIOUS torch.package:
    modelscan total_issues  : 0
    inner .pkl WAS scanned  : True
    inner .py left unscanned: True
    issues                  : []
    => modelscan verdict: CLEAN

(A continued) load via torch.package.PackageImporter:
    [load child]: LOADED <torch_package_0>.evilmod Payload
    marker file written by module body: True
    proof: 'CODE EXECUTED FROM torch.package MODULE BODY AT LOAD TIME'
    => PackageImporter load: ARBITRARY CODE EXECUTED

(B) CONTROL - same os.system primitive as a plain pickle GLOBAL:
    modelscan total_issues  : 1
    issues                  : [{"description": "Use of unsafe operator 'system' from module 'nt'",
                                "operator": "system", "module": "nt", "source": "control.pkl",
                                "scanner": "modelscan.scanners.PickleUnsafeOpScan",
                                "severity": "CRITICAL"}]
    => modelscan verdict: FLAGGED (scanner baseline works)

(The <torch_package_0> module prefix in the load line confirms the class came from inside the package, not from disk - the load child first asserts evilmod is not importable from the filesystem.)

Impact

Realistic threat model - modelscan's stated purpose is to gate untrusted model files:

  1. A victim downloads a PyTorch model packaged as a torch.package .pt from a hub/registry/colleague/CI artifact.
  2. They run modelscan (manually, or as a pre-load / CI / registry-ingest gate). modelscan returns clean - 0 issues, the explicit green light to proceed.
  3. They load it with the standard PyTorch API torch.package.PackageImporter(...).load_pickle(...). The bundled .py module body executes immediately - arbitrary code in the victim's process and OS context (here: dropping a marker file and reading /etc/passwd; trivially generalizes to reverse shell, credential theft, supply-chain implant).

The danger is specifically that modelscan certifies the file as safe, so a security-conscious user who scans before loading is given false assurance. torch.package is a documented, first-party PyTorch packaging format, so a .pt produced this way is a legitimate artifact, not a malformed file.

Honest duplicate / prior-art and SCOPE note

Prior art (general class). It is well known that pickle-based model formats enable code execution on load and that scanners reason about pickle opcodes (modelscan's whole pickle scanner, plus public writeups on pickle/__reduce__ RCE). modelscan also already detects the ordinary pickle path - case B here proves that.

Why this is distinct. Those known cases put the dangerous callable in a pickle GLOBAL/REDUCE opcode, which modelscan's pickle scanner is designed to catch. This finding is different in mechanism:

  • The executable payload is not a pickle opcode at all - it is plain Python in the body of a bundled .py module inside the torch.package zip.
  • modelscan never scans that .py (no scanner claims the extension), and the inner pickle it does scan is genuinely benign (only a class reference).
  • Execution is triggered by torch.package's import_module -> exec(module_body) machinery (package_importer.py:412), not by a pickle reduce.

I could not find prior art describing this specific torch.package bundled-.py-body vector as a modelscan bypass. If the maintainers consider any .pt that internally executes Python on load to be already-covered-by-design, that adjudication is theirs to make; the concrete, reproducible fact is that modelscan 0.8.8 returns CLEAN for a file that runs code on load, while flagging the equivalent plain-pickle payload.

Scope note (read before triaging). This report stays squarely inside huntr's modelscan scope:

  • Tool = modelscan (not picklescan, not modelaudit) - so no out-of-scope-tool concern.
  • Format = PyTorch .pt / torch.package, a first-party PyTorch format and one of modelscan's named PyTorch extensions (settings.py:70-73) - not orbax or any unscoped format.
  • The only scope nuance is the reward tier: PyTorch tensor files are pickle-tier for modelscan, not the $4k named-format set - see Severity. Flagging this explicitly so the payout tier is set correctly rather than overclaimed.

Remediation

Root issue: modelscan trusts file extensions for zip members and has no notion that a .pt/torch.package archive can carry executable .py source that the loader will exec().

Suggested fixes (defense in depth):

  1. Scan bundled source modules in zip-based PyTorch packages. When a .pt/.zip is a torch.package (detectable via .data/extern_modules, .data/python_version, and interned .py members), treat every inner .py as executable-on-load and scan it (e.g. AST scan for os.system/subprocess/eval/exec/__import__/network calls at module scope), or at minimum flag the presence of interned source modules as a finding (a torch.package that ships arbitrary Python is inherently load-time-code-exec).
  2. Stop silently skipping unknown extensions inside model archives. An inner member that no scanner can analyze (especially .py) should raise a "could not be scanned / unsupported content" warning rather than contributing to a clean verdict - otherwise "0 issues" is misleading.
  3. Recognize the torch.package GLOBAL->import->exec path. Even though the inner pickle's GLOBAL looks benign, in a torch.package it forces an import that executes the referenced module's body; modelscan could treat a GLOBAL pointing at an interned package module as code-execution-bearing.

End users, independently: prefer weights_only/safetensors for untrusted weights and never load untrusted torch.package archives even if a scanner reports them clean.

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