Most guides to open-weight models start with the licence and the benchmark table, and skip the step in between: you download a file and a program on your machine reads it. Whether that read can run someone else's code depends on the file format. This guide covers what pickle, safetensors and GGUF each do when loaded, what PyTorch changed about its default loader, and which risks remain even with a safe format. It complements Open-Weight Model Licences Compared and Open Weights vs Open Source AI, which cover what you may do with the weights; this one covers what loading them may do to you.
Why pickle is the problem
PyTorch model weights have traditionally been stored with Python's pickle. Hugging Face's Hub documentation puts it directly: "Pickle is a widely used serialization format in ML. Most notably, it is the default format for PyTorch model weights."[4] The issue is in what pickle is. Python's documentation opens its warning with "The pickle module is not secure. Only unpickle data you trust," and continues that it is "possible to construct malicious pickle data which will execute arbitrary code during unpickling."[5]
The mechanism is documented in both places. Hugging Face describes a pickle as "a stack of instructions or opcodes" read sequentially, where the GLOBAL, STACK_GLOBAL and REDUCE instructions import a function and call it.[4] Python's own example is a hand-crafted stream that imports os.system and runs echo hello world; the documentation notes that "by default, unpickling will import any class or function that it finds in the pickle data."[5] Nothing about this requires a bug. It is how the format works.
What PyTorch changed
PyTorch's torch.load uses "an unpickler under the hood" and still carries the warning "Never load data from an untrusted source."[7] What changed is the default. The serialization notes say: "Starting in version 2.6, torch.load will use weights_only=True if the pickle_module argument is not passed."[6]
With that setting the unpickler is restricted to "only executing functions/building classes" from a limited set and, unlike the standard unpickler, "is not allowed to dynamically import anything during unpickling."[6] PyTorch is explicit that this is a reduction, not a guarantee. It says the mode "narrows the surface of remote code execution attacks" and lists limits, including:[6]
- "weights_only=True does not guard against denial of service attacks."
- "We try to prevent memory corruptions during torch.load(weights_only=True) but they might still be possible."
- Loading "CAN create unexpected objects for the downstream code that can also lead to memory corruption."
Two practical consequences follow. Old code that passed weights_only=False to load a full nn.Module is opting back into the dangerous path; PyTorch's own error text says that route "can result in arbitrary code execution" and should be used only for files from a trusted source.[6] And a restricted unpickler still has to parse untrusted bytes, which is why the denial-of-service caveat remains.
What safetensors is, from its own spec
Safetensors was built to avoid the problem. Its documentation calls it "a new simple format for storing tensors safely (as opposed to pickle) and that is still fast (zero-copy)."[1] The layout, as written in the repository, has three parts:[2]
- 8 bytes: an unsigned little-endian 64-bit integer
N, the size of the header. Nbytes: a JSON UTF-8 header. It "MUST begin with a{character", duplicate keys are disallowed, and the optional__metadata__key may hold only a string-to-string map.- The rest of the file: a byte buffer of tensor data.
There is no instruction stream to interpret. The header lists each tensor's name, dtype, shape and byte offsets, and the loader reads the bytes at those offsets. A side effect is that you can inspect a model's tensors without downloading it: Hugging Face's metadata-parsing guide shows reading the first 8 bytes, then the header, using HTTP range requests.[12]
Here is a local version of the same idea, written from the layout above. It reads only the header and never touches tensor data. The size cap is our own precaution, not part of the specification:
import json
import struct
MAX_HEADER = 100 * 1024 * 1024 # our own sanity limit, not from the spec
def read_safetensors_header(path):
with open(path, "rb") as f:
(n,) = struct.unpack("<Q", f.read(8)) # little-endian u64
if n > MAX_HEADER:
raise ValueError(f"header size {n} exceeds sanity limit")
header = json.loads(f.read(n))
return header
if __name__ == "__main__":
import sys
header = read_safetensors_header(sys.argv[1])
meta = header.pop("__metadata__", {})
print("metadata:", meta)
for name, info in list(header.items())[:5]:
print(name, info["dtype"], info["shape"])Was it checked?
In 2023 Hugging Face, EleutherAI and Stability AI "ordered an external security audit" of the library from Trail of Bits. The result, as Hugging Face reported on 23 May 2023: "No critical security flaw leading to arbitrary code execution was found." The audit did find imprecisions in the specification, which were fixed, and missing validation that allowed polyglot files, also fixed. Hugging Face adds the caveat itself: "While it is impossible to prove the absence of flaws."[8] The audit covered the library, not the contents of any particular model.
The safetensors README also claims the format makes it "almost impossible to use malicious files" for denial-of-service attacks. Compare that with PyTorch's explicit statement that its restricted pickle mode does not guard against them.[2][6] Our reading: the claims are about different formats and do not contradict each other, but "almost impossible" is a design goal, and the independent audit is stronger evidence than the README sentence.
Speed is a secondary benefit
Hugging Face's speed page loads GPT-2 weights both ways. It reports 76.6 times faster on CPU (0.004 s against 0.307 s) and 2.1 times faster on GPU, measured on Ubuntu 18.04 with a Xeon CPU and a Tesla T4.[3] The page attributes the gains to memory-mapping and skipping copies. This is a vendor benchmark of a small model on old hardware; treat it as direction, not as a number to expect for a large checkpoint today.
GGUF: the format of llama.cpp-style runtimes
GGUF is the single-file format used by GGML-based executors. Its specification describes a header (magic number, version, tensor count, metadata count), typed key-value metadata (architecture, tokenizer, licence), tensor information with name, dimensions, type and offset, and the tensor data itself, which "may be quantized."[11] It is designed for "mmap compatibility," with tensor offsets aligned to a default of 32 bytes.[11]
The specification does not mention pickle or serialized code, and it has no security section. That supports a limited claim: GGUF is a plain data layout, not an instruction stream. It does not support a stronger one, such as "GGUF is audited" or "all GGUF readers handle malformed files safely"; we found no source for those and do not make them.
| Format | Can loading execute code by design? | What the documentation says |
|---|---|---|
Pickle (.bin, .pt checkpoints) | Yes, unless restricted | "Not secure"; can "execute arbitrary code during unpickling"[5] |
torch.load, weights_only=True (default from 2.6) | Restricted unpickler, no dynamic imports | Narrows attack surface; no protection against denial of service[6] |
| Safetensors | No instruction stream; JSON header plus bytes | Audited in 2023; no critical code-execution flaw found[2][8] |
| GGUF | Spec describes typed binary fields only | No security section in the specification[11] |
What a safe format does not cover
Custom model code. Many repositories ship Python for a new architecture. Hugging Face's Transformers guide says that for custom models "the modeling code is not from Transformers," that you load one by setting trust_remote_code=True, and that despite Hub malware scanning "you should still be careful to avoid inadvertently executing malicious code." Its extra precaution is to load "from a specific revision" using the commit hash, to avoid code that "may have changed."[10] Safetensors weights in a repository with trust_remote_code=True still run that repository's Python.
Scanners are advisory. Hugging Face says of its pickle import scan: "this is not 100% foolproof. It is your responsibility as a user to check if something is safe or not," and that its safe/unsafe import lists are maintained "in a best-effort manner."[4] In February 2024 JFrog reported "around 100 instances" of malicious models, a count that "excludes false positives," with one payload injected through pickle's __reduce__ method. JFrog also noted that Hugging Face marks pickle models as unsafe but "doesn't outright block or restrict them from being downloaded."[9] We have not independently verified those counts and the platform's policies may have changed since.
Provenance. Hugging Face notes that signed commits do not guarantee a file is safe, only its origin.[4] A trusted format from an unknown uploader is still an unknown upload.
Model behaviour. None of the sources here address what a model does once it is loaded. A file format that cannot run code on load says nothing about the model's outputs, which is a separate evaluation question (see How to Read an AI System Card).
A practical checklist
- Prefer repositories that publish
.safetensors(or.gguffor GGML runtimes). Treat a repository that offers only pickle checkpoints as a reason to ask why. - If you must load a pickle checkpoint, use a PyTorch version of 2.6 or later and keep
weights_only=True. Do not switch it off to silence an error without reading the file's source. - Do not set
trust_remote_code=Truecasually. If you do, read the Python files and pin the commit hash.[10] - Load unfamiliar files in an environment without credentials or sensitive data, which is our recommendation rather than something the cited documents prescribe.
- Inspect the safetensors header before loading when the model's size or dtypes look wrong, using the approach above.[12]
- Record the repository, revision and file hashes of what you deploy. Our interpretation: this makes later incident response possible; none of the sources require it.
What remains uncertain
- Hugging Face's pickle documentation page carries an older framing ("we are experimenting with a new, simple serialization format"), so it reads as dated next to the audit and the safetensors default in Transformers. Treat the scanner description as current only to the extent the page says so.
- We did not test any of this by loading hostile files, so every claim above is documented, not observed.
- Other loaders (ONNX, TensorFlow SavedModel, Keras formats) were outside this article and were not researched.




