Pickle Deserialization Risk in Model Files

Loading a pickle runs code. pickle.load calls the functions a file names, so a .pt or .pkl from an untrusted source can execute commands on your server. Inspect the opcodes with pickletools first, prefer safetensors, and run any loader you cannot verify in a container with no network egress.

By Michael Lip · Updated 2026-10-28 · LockML / Guides

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 = 4

Two 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

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.

By the same builder: GitHub — theluckystrike BeLikeNative — Grammar AI EarlyThunder — Dev Blog Bug Bounty Reality Zovo — AI Dev Tools