What pickle.load actually does
A pickle is a program, not a data dump. The unpickler reads opcodes and runs them. The
GLOBAL opcode names a Python callable and REDUCE calls it. Any file that
reaches those two opcodes can run code in the process that loads it.
The demo below is small and harmless: it asks for os.getcwd. A real attack names
os.system or subprocess.check_output instead. The file is 34 bytes.
0: \x80 PROTO 4
2: \x95 FRAME 23
11: \x8c SHORT_BINUNICODE 'posix'
18: \x94 MEMOIZE (as 0)
19: \x8c SHORT_BINUNICODE 'getcwd'
27: \x94 MEMOIZE (as 1)
28: \x93 STACK_GLOBAL
29: \x94 MEMOIZE (as 2)
30: ) EMPTY_TUPLE
31: R REDUCE
32: \x94 MEMOIZE (as 3)
33: . STOP
highest protocol among opcodes = 4Two opcodes in a 34-byte file are enough. The size of a model file tells you nothing about its payload, because the payload is usually tiny next to the weights.
Inspect the file before you load it
The Python standard library ships a disassembler. It does not run the file.
python3 -m pickletools model.pkl | grep -n -E "GLOBAL|REDUCE|INST|OBJ"
Any hit is a call into Python. Read the name that comes with it. If the name is not a tensor class from the framework you expect, do not load the file. This check is cheap enough to run in CI on every model artifact you download.
Use a format that cannot run code
safetensors stores a JSON header and raw tensor bytes. The format spec is 8 bytes of header length, then a JSON document, then the tensor data. There is no opcode stream, so loading cannot call a function. The project states the reason in its own documentation: the format stores tensors "safely (as opposed to pickle)" and lists "Pickle: Unsafe, runs arbitrary code" in its comparison table.
Conversion is a one-line change in most training scripts, and most model hubs publish safetensors alongside the pickle weights. Prefer the safetensors file when both exist.
If you must load a pickle
PyTorch 2.6 changed the default of the weights_only argument of torch.load
from False to True. The framework's own source comment says that setting it
back to False "can result in arbitrary code execution". On older versions, pass
weights_only=True yourself. Never pass False for a file you did not build.
weights_only=True is a smaller allow-list, not a proof. It still unpickles, so it still
trusts the set of names that survive the filter.
Serving checklist
- Pin the artifact by SHA-256 and verify the digest before load. A renamed file is not a verified file.
- Load model files in a worker with a read-only filesystem and no network egress. A reverse shell needs a network path.
- Run the loader as a non-root user with no cloud credentials in its environment.
- Scan every new artifact with
pickletoolsin CI and fail the build onGLOBALorREDUCE. - Keep the loader image minimal. Every package in it is a callable an attacker can name.
- Log the hash, the source URL and the load time for every model the service starts with.
Related LockML pages
See the secure ML deployment guide for the wider serving checklist, the model extraction prevention guide for query-side controls, and the ML model security audit tool to score a deployment.