An external audit of the Safetensors library, commissioned by Hugging Face in coalition with EleutherAI and Stability AI and reported on 2026-06-25, concluded the format is ready to serve as the new industry standard for AI weights. The reason is structural, not cosmetic: Pickle, the serialization PyTorch uses for .bin and .pth checkpoints, can execute arbitrary code the moment it is loaded. Safetensors cannot. That single property is the whole case for the migration, and the rest of this piece is the detail behind it.
What did the Trail of Bits audit actually conclude?
According to the secondary write-up of the audit, Trail of Bits found Safetensors ready to serve as the new standard for AI weights after a review commissioned by the Hugging Face coalition with EleutherAI and Stability AI, reported on 2026-06-25. The write-up frames the result for executives as “weight hygiene,” the idea that infrastructure security starts with the artifact format rather than with policy.
Two caveats matter for anyone acting on that sentence. First, the Trail of Bits report itself is not in the source set for this article; audit specifics are attributed to The Value Engineering’s coverage, which is a secondary and somewhat promotional outlet. Treat the “new industry standard” framing as that outlet’s gloss until an HF or Trail of Bits primary is checked. Second, “default” status predates this audit. Transformers’ from_pretrained() already loads weights in safetensors format when available and flags pickle as “known to be unsecure,” and Diffusers loads .safetensors from model subfolders by default. The audit did not flip a switch; it ratified a drift that was already underway. What it adds is a credible external attestation that the format is sound to standardize on, which carries weight in procurement and policy language that an internal preference does not.
Why is pickle a remote-code-execution risk at load time?
Pickle is not a data format in the way JSON is a data format. It is a bytecode interpreter that rebuilds Python objects by calling back into the runtime. The protocol calls each object’s __reduce__ method to decide how to reconstruct it, and __reduce__ can return any callable together with its arguments. Override __reduce__, and you control what runs when the file is unpickled. As huntr documents, a proof-of-concept payload is as terse as returning (os.system, ('touch /tmp/poc',)), which fires the moment torch.load() is called.
The practical consequence is that any .bin or .pth checkpoint downloaded from the Hub is untrusted code until proven otherwise. An attacker who can place a file on a model card can place code on a model card. Huntr pays bounties up to $4,000 for model-file vulnerabilities of this class, which is both a ceiling and a rough market signal for how routine these findings are. This is not a theoretical concern about a contrived protocol; it is the documented mechanism behind a recurring class of supply-chain incidents, and until recently it was the load-time default for the most widely used weights on the Hub.
CVE-2026-25874: pickle inside Hugging Face’s own LeRobot
The pickle problem is concrete enough to have a recent CVE inside Hugging Face’s own ecosystem. CVE-2026-25874 covers LeRobot through version 0.5.1, where pickle.loads() deserializes data received over unauthenticated gRPC channels without TLS. An unauthenticated, network-reachable attacker can achieve arbitrary code execution through the SendPolicyInstructions, SendObservations, or GetActions calls.
Two things make this CVE instructive for the safetensors discussion. It is not a weights-file issue; it is a gRPC deserialization issue. That distinction matters, because the rest of this article concerns a format that neutralizes pickle at the weights layer and does nothing for pickle anywhere else in a stack. And it is inside a first-party Hugging Face library rather than a random community upload, which sharpens an otherwise abstract risk into an engineering priority. The fix for a weights file is to change formats. The fix for an unauthenticated gRPC endpoint is to add TLS and stop deserializing untrusted input on it, which is a different conversation.
What does Safetensors block, and what can’t it express?
The Safetensors on-disk format is deliberately small. It begins with an 8-byte unsigned little-endian integer giving the header size N, then an N-byte JSON header that must begin with { and may be trailing-padded with whitespace, then a flat byte buffer for the tensor data. The header is capped at 100MB to prevent parsing attacks on large JSON, and the byte buffer must be entirely indexed with no holes, which the spec states prevents the creation of polyglot files. Duplicate header keys are disallowed. There is no bytecode, no __reduce__, and no object reconstruction, so the entire class of pickle attacks has no surface to attach to. For scale, the repository notes the reference implementation is on the order of 400 lines of code against roughly 210,000 for HDF5, which is a relevant data point when the attack surface under audit is the parser itself.
That safety comes from giving up expressiveness. The format comparison table in the Safetensors repository marks Flexibility as unsupported for safetensors and supported for pickle: you cannot save arbitrary Python objects or custom code and reload them later with zero extra code. The only non-tensor data allowed is a string-to-string __metadata__ map, and all values must be strings. Tensors cannot be strided; they must be packed into contiguous layout before serialization. For most weight checkpoints that is no loss, but it is a hard line, and crossing it means leaving the format.
| Property | Pickle (PyTorch) | Safetensors |
|---|---|---|
| Safe (no code execution on load) | No | Yes |
| Zero-copy reads | No | Yes |
| Lazy loading | No | Yes |
| Layout control | No | Yes |
| Flexibility (custom objects and code) | Yes | No |
| Native bfloat16 / fp8 | Yes | Yes |
Loading speed is a real, separate benefit, and the figures are worth stating precisely rather than rounded up. The Diffusers docs report loading 500MB of weights in 3.4873ms with safetensors versus 172.7537ms with PyTorch, roughly a 50x difference at that size, and loading the 176B-parameter BLOOM model on 8 GPUs in 45 seconds instead of 10 minutes with regular PyTorch weights, roughly 13x. Note the spread: the secondary write-up of the audit cites “up to 100x,” which overstates the vendor’s own published numbers. The 100x figure is the high end of a range, not a representative speedup, and it is the kind of round number that travels further than the underlying measurement.
What does “default” status change for model cards, revision pins, and download scripts?
The operational change for a practitioner is smaller than “new default” implies. New uploads are steered toward safetensors and the consuming libraries prefer it, but existing model cards are not rewritten. A card that ships pytorch_model.bin still ships pytorch_model.bin; it does not become unsafe retroactively, and a script pinned to it keeps working.
What does change is the set of guardrails around the Hub surface. Diffusers automatically loads .safetensors files from model subfolders by default, the Hub runs a security scanner to detect unsafe files and suspicious pickle imports, and a Convert Space downloads pickled weights, converts them, and opens a pull request rather than running the untrusted pickle on the user’s machine. The conversion happens in a sandboxed Space, not on your CI runner, which is the relevant threat-model point: the untrusted pickle executes on HF infrastructure, and you receive the converted output.
For download scripts, the durable advice is unchanged. Transformers’ docs recommend pinning a commit hash as an extra layer of security, on top of the format change. A revision pin protects against a maintainer pushing a malicious update to a model you already trust; the format change protects against a malicious artifact at the file layer. They are independent controls, and the migration to safetensors does not remove the need for the pin.
The migration path and residual risks safetensors does not cover
The vendor-neutral home matters for how durable this format is. On 2026-04-08 the PyTorch Foundation announced Safetensors joined as an officially hosted project under the Linux Foundation, alongside PyTorch, vLLM, DeepSpeed, and Ray, and Hugging Face said the existing format, APIs, and Hub integration remain identical. That move is what makes “new industry standard” a defensible claim rather than vendor marketing: the format is no longer a Hugging Face artifact, and the roadmap is set across the broader inference stack.
That roadmap includes device-aware loading directly to CUDA or ROCm without CPU staging, first-class tensor-parallel and pipeline-parallel loading APIs, quantization support across FP8, GPTQ, AWQ, and sub-byte integer types, and integrating safetensors as the serialization system inside PyTorch core. The last item is the one to watch: if safetensors becomes the default torch.save path, the pickle surface for weight checkpoints effectively disappears for new artifacts, and the migration stops being something practitioners opt into.
The residual risk this whole shift does not address is that weights are only one of several untrusted inputs a modern inference stack pulls from the Hub. Custom code under trust_remote_code, dataset loaders, and any dependency on a Hub-hosted Python package are all code-execution vectors that a weight format cannot neutralize. Safetensors closes the largest and most routine of these, the one with a fresh CVE and a documented bounty ceiling, and leaves the rest to be handled the way they always have been: pinning, scanning, and running untrusted code somewhere you can afford to lose.
Frequently Asked Questions
Does switching to safetensors guarantee my weights haven’t been tampered with, or only that loading them won’t execute code?
Safetensors closes the code-execution vector but offers no integrity check. A backdoored model whose weights produce wrong outputs only on a specific trigger input, or a checkpoint whose weights were silently swapped, loads as a perfectly valid safetensors file. The format guarantees the file cannot run code on you; it cannot guarantee the file contains the weights you expect. For that you still rely on the revision pin and on the maintainer’s reputation, which is the same trust posture pickle had at the file layer.
How is safetensors different from GGUF or ONNX, both of which also avoid pickle?
GGUF is a quantization-first container built for llama.cpp that bundles weights, tokenizer config, and metadata into one file and targets CPU and edge inference. ONNX is a computation-graph format that carries operator definitions alongside the weights, so loading it runs a graph interpreter. Safetensors stores weights and nothing else: no graph, no tokenizer, no quantized packing beyond what the tensor dtype already encodes. That narrow scope is the design choice, which is why the same file is consumed by PyTorch, Diffusers, and vLLM through their own loaders rather than forcing a runtime change.
Can I use safetensors for optimizer state and training checkpoints, or only for final inference weights?
Final weights map cleanly, but full training checkpoints are awkward. A PyTorch checkpoint usually nests optimizer state, scheduler state, and the RNG state in a single dict that may contain non-tensor objects, and safetensors’ only non-tensor slot is a string-valued metadata map. Frameworks that checkpoint to safetensors typically serialize the non-weight state separately (as JSON or a sidecar pickle) and keep safetensors for the tensor payloads, so the format replaces part of a checkpoint file rather than the whole thing.
What breaks when I convert a .bin checkpoint to .safetensors?
Three things catch people. Tied weights, where embedding and output layers share storage in the live model, are typically written as separate tensors in the safetensors file and re-tied by the loader from the model config, so a naive conversion can double those tensors on disk. Strided or non-contiguous tensors must be packed into contiguous layout first, because safetensors has no stride field. Any non-tensor entry in the checkpoint dict has to move to the metadata map as a string, which means custom classes lose their type on the round trip. The Convert Space handles all three automatically for supported architectures, but a one-off local conversion script needs to address them itself.
Why does memory-mapped loading matter beyond the headline speedup?
mmap lets the kernel page tensor bytes directly from disk into the process address space without a userspace copy, so a 70B model does not need its full weight footprint resident in free RAM during the load window. Multiple worker processes on the same machine can map the same file and share the same physical pages through the page cache, which cuts serving-memory cost on multi-replica endpoints. PyTorch’s pickle path deserializes into a fresh tensor allocation per process, which is part of where the 50x and 13x loading figures come from.