Malicious AI Model Files: Pickle Exploits and Arbitrary Code Execution on Model Load
When you call torch.load("model.pt"), Python doesn’t just read numbers off disk. It executes code. That is not a bug in the conventional sense — it is a deliberate feature of the serialization format PyTorch has used since its inception. Older PyTorch versions defaulted to full pickle deserialization; PyTorch 2.6 shifted the default to weights_only=True. But the underlying format problem predates that change, legacy checkpoints are still in wide circulation, and — as we’ll see — weights_only=True itself has had exploitable bypass paths. For anyone downloading models from the internet, that design means every model file is a potential code execution vector.
This is distinct from the supply chain attacks covered in our earlier post or the weight-level trojans in backdoor attacks. Those attacks require compromising the training pipeline or poisoning the distribution channel. This attack works even when the distribution channel is exactly as intended — the model file reaches you unmodified, and it still runs attacker-controlled code the moment you load it. The file format itself is the weapon.
The Pickle Protocol: Why Serialization Implies Code Execution
Python’s pickle module is a general-purpose serialization format. It converts arbitrary Python objects — including class instances, functions, and closures — into a byte stream that can be written to disk and later reconstructed. The reconstruction process is the problem.
Pickle operates as a simple stack-based virtual machine. When deserializing, it executes a sequence of opcodes, and several of those opcodes are explicitly designed to call Python code. The most relevant is the REDUCE opcode, which calls a callable with a set of arguments.
The __reduce__ protocol is how objects declare how to serialize and deserialize themselves. An object can define __reduce__ to return any callable and any arguments. When pickle deserializes the object, it calls that callable with those arguments — unconditionally, before returning any result to the calling code.
Here’s a simplified, clearly educational-only illustration of the mechanism:
# WARNING: Educational illustration only — never deploy this pattern
import pickle, os
class MaliciousPayload:
def __reduce__(self):
# pickle will call os.system("id") on deserialization
return (os.system, ("id",))
# Serialize
data = pickle.dumps(MaliciousPayload())
# Deserialization executes os.system("id") before returning anything
result = pickle.loads(data)
The critical property: __reduce__ is evaluated at load time, not at use time. The caller doesn’t need to invoke any method on the returned object. The call to os.system("id") — or any other command — fires the moment pickle.loads() processes the byte stream.
This is not an obscure corner of the pickle API. It is the documented extension mechanism. And PyTorch’s .pt / .pth / .pkl file format is, at its core, a pickle file.
Why PyTorch Used Pickle
PyTorch adopted pickle for model serialization because it genuinely solved a hard problem: Python objects are complex. A model checkpoint includes tensors, optimizer state, custom layer objects, configuration dataclasses, and metadata — arbitrary Python objects that need to survive disk round-trips. Pickle handles this generically without requiring format designers to enumerate every possible object type in advance.
The flexibility that made pickle attractive for model serialization is identical to the flexibility that makes it dangerous for model loading from untrusted sources. These are not separable properties of the format.
Attack Mechanics: What a Weaponized Model File Does
A malicious model file that exploits pickle deserialization executes its payload at the moment torch.load() is called, before the calling code has any opportunity to inspect or validate the contents. The payload can be anything Python can express: a reverse shell, a credential harvester, a persistence mechanism, a staged downloader.
The attack is straightforwardly simple to construct. An attacker creates a legitimate-seeming model checkpoint — one that may genuinely contain real, functional weights — but embeds a malicious __reduce__ implementation in one of the objects serialized into the file. They upload it to a model hub under a plausible name with a polished model card. A practitioner downloads it, calls torch.load(), and the payload runs.
The payload executes with the privileges of the Python process loading the model. In typical ML workflows, that process has broad filesystem access, network access, and often access to credentials in environment variables (cloud provider tokens, API keys, HuggingFace tokens). The attacker doesn’t need to break the perimeter — they just need to be first in the search results.
What weights_only=True Was Supposed to Fix
PyTorch introduced a weights_only parameter to torch.load() as a mitigation. When set to True, the unpickler is restricted to a safe subset of Python globals — tensors, storage objects, and a small allow-listed set of types. Custom __reduce__ implementations that reference os.system or other dangerous callables would fail.
The intent was correct. The implementation proved incomplete.
CVE-2025-32434 documents a vulnerability in the weights_only=True unpickler in specific PyTorch versions: a crafted checkpoint file could bypass the allow-list restrictions and achieve arbitrary code execution even with the safety flag enabled. The restricted unpickler’s validation logic contained a bypass path that allowed attacker-controlled callables to execute through the nominally safe loading route. This vulnerability was subsequently patched; the post-patch weights_only=True is meaningfully safer than the default. The point stands structurally: a restricted-unpickler approach is a narrower attack surface, not a format-level guarantee.
This matters conceptually, not just technically. It demonstrates that a restricted unpickler is not a sound security boundary. Pickle deserialization of untrusted data is unsafe at the protocol level; incremental restrictions are incremental, not absolute.
Real-World Exploits: Malicious Models in the Wild
The attack isn’t theoretical. Malicious model files have been found and removed from HuggingFace’s model hub.
In early 2025, researchers documented malicious ML models on HuggingFace that exploited broken pickle files to evade detection. The evasion technique exploited a gap in Picklescan’s detection approach. Picklescan identifies dangerous pickle content by scanning files with specific extensions (.pkl, .pt). PyTorch’s internal loading mechanism allows referencing a secondary pickle file during model loading — one whose filename extension doesn’t match the scanner’s target patterns. The malicious payload lives in the unscanned secondary file, while the primary file appears clean.
This is a cat-and-mouse problem that illustrates the limits of scan-based defenses against a format where execution is the load mechanism. The scanner must correctly identify every pickle-containing file in every archive format; the attacker needs to find one blind spot.
CVE-2025-1889 documents exactly this class of vulnerability in Picklescan itself: the scanner’s reliance on file extension patterns as the detection signal creates bypass paths through unconventionally named pickle files embedded within model archives.
HuggingFace’s Response: Scanning, Safetensors, and Policy
HuggingFace has deployed a multi-layer response to the pickle risk:
Picklescan scanning — Every model uploaded to the hub is scanned by Picklescan, which identifies known-dangerous globals in pickle byte streams (calls to os.system, subprocess.Popen, exec, and similar). Models that fail the scan are flagged and may be suspended or reviewed; HuggingFace has removed confirmed malicious models, though the broken-pickle evasion campaign of early 2025 shows that flagging is not instantaneous for novel evasion techniques. Flagging status appears prominently on model cards.
Guardian integration — Following the broken-pickle bypass campaigns, HuggingFace layered Protect AI’s Guardian scanner on top of Picklescan, adding a second scan pipeline with different detection logic to reduce the single-scanner blindspot risk.
Repository-level security warnings — Model cards for repositories containing pickle files display an explicit security warning noting that the file format allows arbitrary code execution and encouraging users to switch to safetensors format.
Safetensors as the preferred format — HuggingFace developed and actively promotes the safetensors format as a safe alternative. Unlike pickle-based formats, safetensors is a statically typed, header-validated format that stores only raw tensor data and metadata. There is no execution step. Loading a safetensors file cannot run arbitrary code, by design.
The Safetensors Alternative
The safetensors format deserves more than a footnote. It was designed from scratch to be a safe tensor serialization format — and “safe” here means something precise: the format contains no execution pathway.
The structure is minimal: a header encoding metadata (tensor names, dtypes, shapes, byte offsets) followed by the raw tensor byte data. The header is JSON. There are no Python callables, no __reduce__, no opcodes, no code execution. A loader validates the header structure and memory-maps the tensor data. The attack surface is restricted to header parsing logic, not the full Python execution environment.
The practical adoption path for new models is simple: when choosing between a .pt and a .safetensors download of the same model, choose safetensors. For distributing your own models, save in safetensors format from the start using your own trusted weights.
If you need to load an existing pickle checkpoint (from a source you control and trust) in order to re-save it in safetensors for downstream distribution, do that in an isolated environment without access to credentials or sensitive data — the conversion step is the one moment that requires a trusted context. Tools like HuggingFace’s safetensors library provide conversion utilities with better handling of complex checkpoint structures than a manual snippet can capture.
Major model families — Llama, Mistral, Qwen, and most others on HuggingFace — now distribute safetensors-format checkpoints as the default alongside or instead of .pt files. If a safetensors version of the model you need exists, using it is the correct choice.
The Broader Model Registry Risk
The model registry trust problem is structurally analogous to the package registry trust problem — with some properties that make it worse.
When you run pip install <package>, you download code and run it. The security community has spent years building practices around this: pinned versions, dependency audits, known-vulnerability scanners, SBOM requirements. The risks are understood, the tooling exists, and the expectation of vetting has become normalized.
When you call torch.load("model.pt"), you download code and run it — immediately, at load time. But the cultural norms around model loading have historically been closer to “treat it like data” than “treat it like an executable.” Practitioners who would carefully vet a new Python dependency will download a model from a stranger’s HuggingFace repository and load it in a notebook without a second thought.
The “pip install package from stranger” analogy is the correct one. A model file is not data. A model file in pickle format is an executable that happens to also contain tensor weights. The security posture appropriate for executable code is the security posture appropriate for model files.
There is also a provenance gap that the software ecosystem has addressed (imperfectly, but substantively) that the model ecosystem has not. Container images have signed manifests. pip packages have increasingly adopted trusted publishing workflows, and npm has introduced provenance attestation for packages — though neither is a universal default across the entire ecosystem. Model file provenance verification — cryptographic attestation that a checkpoint was produced by a specific training run from a specific codebase on specific data — is further behind and represents an open problem with active research but limited production deployment. The ML model provenance post covers the current state of that tooling.
Defenses
The practical defense posture for model loading:
Prefer safetensors over pickle-based formats. When a safetensors version of a model exists, use it. The format eliminates the execution-at-load attack surface entirely.
Set weights_only=True as a default, with caveats. Even after CVE-2025-32434, weights_only=True raises the bar compared to loading with full pickle. But treat it as a hardening measure, not a security guarantee — it is not a sound boundary against a deliberately crafted malicious checkpoint.
Avoid loading arbitrary models in production environments. Model loading should happen in isolated, least-privilege contexts. A process that loads an untrusted model should have no access to credentials, no outbound network access to sensitive targets, and no filesystem access beyond what the loading task requires.
Scan before loading. Picklescan and Guardian provide a scan-before-load capability. Running a scan in CI or at deployment time adds a detection layer. Be aware of the format’s inherent limitations — a scanner that misses one malicious file is still preferable to no scanner — but don’t mistake a clean scan report for a security guarantee.
Verify model provenance. When using models from public hubs, prefer models from verified organizations, check download counts and community engagement as a weak signal, review the repository history for recent suspicious changes, and verify model card claims against independent sources when the stakes are high. Signed model provenance, when it becomes available at scale, should be part of this check.
Audit loading code. Any codebase that calls torch.load(), pickle.loads(), or equivalent deserialization functions with data from external sources is loading untrusted executables. Audit for these call sites, add suppression for default insecure options where possible, and document the trust assumptions at each call site.
Closing Frame
The pickle vulnerability in model files is not a recent discovery or an emergent edge case. It is a known, documented property of the format that the community has navigated through a combination of scanning, safer alternatives, and gradually shifting defaults. But the correct conclusion from that history is not that the problem is solved — it is that the mitigations are incomplete, active attackers have found scanner bypass techniques, and the cultural gap between “this is code” and “this is data” is still causing practitioners to load untrusted model files without appropriate isolation.
A malicious actor who wants to execute arbitrary code in your ML infrastructure doesn’t need to find a zero-day, compromise a build pipeline, or intercept your traffic. They need to get you to call torch.load() on their file. That is a remarkably low barrier. The appropriate response is to close it with format choices, isolation architecture, and load-time scanning — not to assume that the model hub’s scanner will catch everything, or that weights_only=True is a hard boundary.
The format is the attack surface. Treat it accordingly.
Related posts: AI Agent Supply Chain Attacks · ML Model Provenance: Signing, SBOMs, and Verification