Lesson 3 of 10 · 6 min read
Unsafe Model Files: Pickle versus Safetensors
In this lesson
TL;DR: Python pickle files carry instructions the loader executes, so a model checkpoint in pickle form (traditional PyTorch
.pt,.bin, and many older formats) can run arbitrary code when loaded. Safetensors is a data-only format (a JSON header describing tensors, then raw bytes) that cannot execute code on load and is fast to memory-map. Switching formats fixes code execution at load time; it does not address backdoored weights, custom code that some models require (trust_remote_code), malicious tokenizers or configs that pull code, or bugs in format parsers. Treat every downloaded model as untrusted input.
Before you read
Pick an answer before you read further. The lesson below explains it, and guessing first, right or wrong, helps the explanation stay with you.
1A data scientist downloads a fine-tuned model from a public hub and loads it in a notebook on a shared GPU server that has cloud credentials in its environment. The file is a pickle-based checkpoint. What is the risk?
Why pickle checkpoints are dangerous
Python's pickle format serialises object graphs by recording a small program that the unpickler runs to rebuild them, and objects can specify functions to call during reconstruction. Python's own documentation warns that unpickling untrusted data can execute arbitrary code. That is the general insecure deserialization problem.
PyTorch's traditional torch.save and torch.load used pickle for checkpoints, and much of the ecosystem inherited it. Downloading a model and loading it was, in effect, downloading and running a program from its author. Security researchers have repeatedly found malicious models on public model hubs that execute code on load.
PyTorch 2.6 (January 2025) changed torch.load to default to weights_only=True, which restricts unpickling to tensors and a small allowlist of types. That closes the obvious path for code written against current PyTorch, but older code, other libraries and explicit weights_only=False calls remain.
What safetensors changes
Safetensors, developed by Hugging Face, stores:
- An 8-byte length.
- A JSON header listing each tensor's name, data type, shape and byte offsets.
- The raw tensor bytes.
There is no code, no object construction and nothing for a loader to execute. The format also supports memory-mapping and lazy loading, which makes it faster for large models, one reason adoption has been broad. An external security audit by Trail of Bits was published in 2023.
Protecting: Training and serving hosts
- 1
1 · Safetensors only
format
- Stops: Code in a pickle checkpoint
- 2
2 · No remote code
loader policy
- Stops: Malicious custom model code
- 3
3 · Hash + signature check
provenance
- Stops: Tampered file on a mirror
- 4
4 · Behavioural evaluation
probabilistic
- Stops: Backdoored weights
- 5
5 · Isolated loading env
containment
- Stops: Code in a pickle checkpoint
- Stops: Malicious custom model code
- Stops: Exploit of a loader bug
The safetensors format, precisely
A safetensors file is an 8-byte little-endian length, a JSON header describing each tensor's name, type, shape and byte offsets, and then the raw bytes. The format's rules exist for security: the header is capped at 100 MB so a parser never processes an enormous JSON document, duplicate keys are not allowed, and the byte buffer cannot contain holes, which prevents polyglot files that are valid safetensors and something else at once. Trail of Bits audited the library in March 2023 for Hugging Face, EleutherAI and Stability AI and found no critical flaw leading to arbitrary code execution; the specification imprecisions it found were fixed.
Why scanning pickles is not enough
Model hubs scan uploads, and the scanners help, but their limits are documented by the hubs and by attackers:
- What scanners look for. Hugging Face lists the imports a pickle would perform by walking its opcodes without executing them: the dangerous pattern is an opcode that imports a callable (
GLOBALorSTACK_GLOBAL) followed by one that calls it (REDUCE). Its documentation calls the scan "not 100% foolproof". - Evasion through errors. In February 2025 ReversingLabs found two malicious models ("nullifAI") that used 7z compression instead of ZIP and a deliberately broken pickle stream with the payload at the start. Unpickling runs opcodes in order, so the payload executed before the stream broke; the scanner errored out and reported nothing. The lesson for any intake pipeline: a scan error is a failure, not an inconclusive pass.
- Scanner bugs. The widely used picklescan tool had several 2025 CVEs, including bypasses through file-extension handling, a ZIP entry with a bad CRC that halted the scan, and submodule imports that evaded its exact-match list of unsafe globals; versions 0.0.31 and later fix them.
- Scale. Protect AI reported in April 2025 that its scanner had examined 4.47 million model versions in 1.41 million Hugging Face repositories and flagged issues in about 51,700 models, roughly 3.7% of the repositories scanned. A public hub is not a vetted source.
- The flag itself. PyTorch 2.6 (January 2025) made
torch.loaddefault toweights_only=True, but CVE-2025-32434 showed that mode could still be made to execute code in versions up to 2.5.1. Upgrade and restrict, and still load untrusted files in isolation.
Scan error treated as inconclusive
7z archive, broken stream
Scanner errors, reports nothing
payload sits before the break
Loaded: payload runs first
Outcome, unsafe: Code execution on load
The evasion was the error itself.
Format allowlist, errors fail closed
Not safetensors or GGUF
Converted in a throwaway microVM
no network, no credentials
Any scan error rejects
Outcome, safe: Rejected or safely converted
Only data-only formats reach serving.
Lenient intake
Unpickling runs opcodes in order, so a payload before a deliberate error executes while the scanner gives up.
What safetensors does not fix
| Risk | Why safetensors does not help | What does |
|---|---|---|
| Backdoored or poisoned weights | The numbers themselves encode the behaviour | Provenance, evaluation, red teaming; detection is limited |
| Custom model code | Some architectures require running Python shipped in the repository (trust_remote_code=True in Hugging Face Transformers) | Review the code, pin a commit, or avoid such models |
| Malicious companion files | Tokenizers, configs, preprocessing scripts and notebooks travel with weights | Inspect everything you load, not just the weights file |
| Loader and parser bugs | Any parser of untrusted bytes can have memory-safety bugs; other formats (for example GGUF) have had parser vulnerabilities | Patch loaders; isolate loading |
| Tampering in transit or on mirrors | A modified safetensors file is still a valid safetensors file | Hashes and signatures checked against trusted references |
A safe model intake process
- Prefer safetensors, and convert legacy checkpoints in an isolated environment once, then use the converted file.
- Restrict loading:
weights_only=Truefor any remaining PyTorch checkpoints; never enable remote code without reviewing and pinning it. - Verify provenance: pin model versions by commit or hash, verify publisher signatures where available (model signing), and keep an AI bill of materials.
- Scan: model scanners flag pickle opcodes that import or call functions, and unexpected files.
- Isolate: load and evaluate new models in an environment without credentials, network egress or access to production data.
- Evaluate behaviour before deployment, knowing that backdoors can be designed to evade evaluation.
Misconceptions interviewers listen for
- "weights_only=True makes torch.load safe." Not on PyTorch 2.5.1 and earlier; upgrade and still isolate.
- "The hub scanned it, so it is clean." Scanners have been evaded and have had bypass bugs; hubs say so.
- "A scanner error means inconclusive." It was the evasion technique; fail closed.
- "Safetensors solves the model supply chain." It removes load-time code execution for that file; other formats, remote code, configuration files and backdoored weights remain.
- "Keras safe_mode is a sandbox." CVE-2025-1550 executed code through a crafted configuration despite it.
What if · interview follow-ups
Hold your answer as the constraints change
You run the model intake pipeline for a platform where customers upload fine-tuned models and engineers pull open models from public hubs. The interviewer probes one file at a time.
Start here
What if an engineer argues a .bin checkpoint is fine because they load it with weights_only=True?
Which PyTorch version, and was that mode ever bypassed?
On PyTorch 2.5.1 and earlier, CVE-2025-32434 allowed code execution even in that mode, and restricted unpicklers are code with bugs. Require 2.6.0 or later, convert untrusted checkpoints to safetensors in a throwaway sandbox with no network or credentials, and serve only the converted file.
Carry it forwardRestrictions inside a code-executing format reduce risk; data-only formats remove it.
Weak answerTrusting the flag regardless of version.
Constraint changes
What if the scanner reports an error on a file instead of a verdict?
What did nullifAI do?
Treat it as malicious until proven otherwise: nullifAI placed its payload before a deliberately broken stream, so loading executed it while the scanner errored out. Reject files that fail to scan cleanly, and investigate them in isolation if needed.
Carry it forwardIn intake pipelines, errors fail closed.
Weak answerLetting files through because the scanner could not decide.
Constraint changes
What if the model is distributed as safetensors plus a GGUF quantisation and a .keras variant?
How many parsers does that put in the path?
Each format has its own parser and code paths: GGUF parsers have had heap overflows, and Keras archives have executed code through configuration even in safe mode. Allow one format per use case, keep its parser patched, and load everything in isolation.
Carry it forwardEvery additional format is another parser with its own vulnerabilities.
Weak answerAssuming the whole bundle is safe because one file is safetensors.
Constraint changes
What if a popular model requires trust_remote_code=True to load?
What does that flag actually allow?
It downloads and runs Python from the model repository at load time, which is executing a stranger's code. Pin the exact commit, review the code, vendor it into your own repository, or reject the model; and run loading in a sandbox regardless, since at least one library path ran remote code even with the flag off.
Carry it forwardRemote code in a model repository is a dependency; pin, review and vendor it or refuse it.
Weak answerEnabling the flag globally to make models work.
Constraint changes
What if a clean safetensors file is backdoored to misbehave on a trigger phrase?
Can a format or a scanner see behaviour?
No: the file is valid data and loads safely; the problem is in what the weights do. Add provenance requirements (signed models, known publishers, pinned digests) and behavioural evaluation, including trigger and red-team testing, before promotion, and monitor production behaviour.
Carry it forwardSafe loading and trustworthy behaviour are separate properties needing separate controls.
Weak answerTreating a passed format check as a clean bill of health.
Now defend it
As the defender, what would your model intake metrics show?
Files rejected by format and the reason, scanner errors (treated as failures) and detections by scanner, conversions performed in isolation, models loaded with remote code and their pinned commits, digests of everything promoted to the internal registry, and behavioural evaluation results before promotion.
How this shows up in interviews
"Why are pickle-based model files dangerous, and what does safetensors fix and not fix?" is one of the most common AI supply chain questions. The strong answer explains pickle's execution model, describes safetensors' structure, and then spends equal time on the limits: backdoors, remote code, companion files and parsers. Mentioning weights_only and isolation shows practical experience.
Say it in 30 seconds: "Pickle checkpoints contain instructions the loader executes, so loading an untrusted model runs the author's code with your permissions. Safetensors is just a JSON header and raw tensor bytes, so loading can't run code, and it's faster. That fixes load-time code execution only: backdoored weights, models that need remote custom code, malicious tokenizers and parser bugs are untouched. So intake is safetensors plus weights_only, no unreviewed remote code, pinned hashes and signatures, scanning, isolated loading and behavioural evaluation."
Check yourself
Say or write your answer first, then reveal. Explaining it from memory, in your own words, is the skill an interviewer is checking.
1A model card says to load the model with trust_remote_code=True, and the weights are in safetensors format. Is it safe to load on a production host?
2Why convert a legacy pickle checkpoint to safetensors in an isolated environment rather than on a developer laptop?
3Which pickle opcodes are the dangerous pattern scanners look for?
4Why should an intake pipeline fail closed when a model scanner errors?