← All writing
Security▶ InteractiveRuns entirely in your browser

Pickle is a program: loading a checkpoint is running code

A classic PyTorch .pt file isn’t data with a format. It’s a zip with a data.pkl inside, and a pickle is a program for a small stack machine whose instruction set includes “import any function” and “call it”.

Unpickler, one opcode at a time Disassembles and simulates; never executes anything
Naive grep scanner
–
Opcode allowlist scan
–
What loading would do
–
Opcodes
    Stack after this opcode
      Imports seen so far
        Raw bytes (hex)

        How an honest checkpoint already “runs code”

        Step through the honest sample. The very first instruction is GLOBAL collections OrderedDict, then REDUCE calls it. Each tensor is GLOBAL torch._utils _rebuild_tensor_v2 applied to a storage reference that BINPERSID resolves out of the zip. Loading a state dict is a sequence of function calls chosen by the file. A malicious file just chooses different functions.

        The payload sample adds one entry. Its value is GLOBAL posix system called with a shell command. On Linux, os.system pickles as posix.system, so a scanner that greps for os.system reports the file clean. The obfuscated sample goes further: under protocol 4, STACK_GLOBAL takes the module and name from two separate strings on the stack, and the argument assembles 'o'+'s' at runtime. There’s no suspicious substring to find. Only following the opcodes, as the scanner here does, reveals builtins.eval being called.

        Why “safe mode” is not the fix

        An allowlist is the right idea if you must read pickle. torch.load now defaults to weights_only=True, which permits only tensor-rebuilding globals. But an allowlist in front of a stack machine is a sandbox, and sandboxes get bypassed: in PyTorch 2.5.1 and earlier, weights_only=True could still be driven to remote code execution. The safe position is a format with no code path at all.

        It isn’t only files

        The same deserializer shows up on the wire. vLLM’s Mooncake integration moved KV-cache metadata between prefill and decode nodes with pickle over ZeroMQ sockets bound to every interface, which made it an unauthenticated RCE on the inference fabric. And Wiz ran code inside Hugging Face’s inference infrastructure simply by uploading a pickle model. It’s the rule I’m holding kvwire’s control path to as it’s built: KV transfer is a network service, so its metadata gets a fixed schema and a code-free encoding.

        What I’d enforce

        Keep going

        Next · interactive
        The kernel can’t see your tensors: RDMA’s trust model
        Repo
        kvwire: KV-cache transport for disaggregated inference ↗