Security Bug Report

PyTorch 2.6 torch.load fails: Weights only load failed, Unsupported global

After upgrading to PyTorch 2.6, every checkpoint load in our evaluation pipeline failed with Weights only load failed. The default for weights_only in torch.load flipped to True, and our checkpoints contained a NumPy scalar. We fixed the checkpoints instead of turning the safety off.

Ryan Pham Ryan Pham @ryan_bugs · March 05, 2025 · 6 min read · 12 Views
Banner: PyTorch 2.6 torch.load rejecting checkpoints with unsupported pickled objects

The nightly evaluation job for our recommendation models failed for every model after a dependency update pulled PyTorch 2.6.0:

_pickle.UnpicklingError: Weights only load failed. This file can still be loaded, to do so you have
two options, do those steps only if you trust the source of the checkpoint.
	(1) In PyTorch 2.6, we changed the default value of the `weights_only` argument in `torch.load`
	from `False` to `True`. Re-running `torch.load` with `weights_only` set to `False` will likely
	succeed, but it can result in arbitrary code execution. Do it only if you got the file from a
	trusted source.
	(2) Alternatively, to load with `weights_only=True` please check the recommended steps in the
	following error message.
	WeightsUnpickler error: Unsupported global: GLOBAL numpy.core.multiarray.scalar was not an
	allowed global by default. Please use `torch.serialization.add_safe_globals([scalar])` or the
	`torch.serialization.safe_globals([scalar])` context manager to allowlist this global if you
	trust this class/function.

Reproducing it

Our training loop saved checkpoints like this:

torch.save({
    "model": model.state_dict(),
    "optimizer": optimizer.state_dict(),
    "epoch": epoch,
    "best_val_auc": best_auc,          # a numpy.float64 from sklearn.metrics.roc_auc_score
}, path)

roc_auc_score returns a NumPy float, which is pickled as a NumPy scalar object. Loading with PyTorch 2.6 defaults fails on that one value.

What we ruled out

  • Corrupt files. Loading with PyTorch 2.5 worked.
  • A malicious checkpoint. The files came from our own training runs, stored in our bucket.

Root cause

PyTorch 2.6 changed the default of torch.load(weights_only=...) from False to True. With weights_only=True, PyTorch uses a restricted unpickler that only allows tensors, primitive types, and a small allowlist of known-safe classes. Our checkpoints contained a NumPy scalar, which is not on the default allowlist, so loading was refused.

What is happening under the hood

torch.save uses Python's pickle format. Unpickling with the standard pickler can execute arbitrary code: a crafted file can import and call any function during load. That makes loading checkpoints from untrusted sources (model hubs, shared drives, user uploads) a remote code execution risk. The weights_only=True mode restricts unpickling to safe types, closing that hole for normal checkpoints.

Anything else in the file, such as NumPy scalars, argparse.Namespace objects with training arguments, custom config classes or entire pickled model objects, fails with "Unsupported global" until you either allowlist it or stop saving it.

safetensors is a separate format designed for tensors only, with no code execution at load at all, and is now the default for many model hubs.

Environment

Component Version / value
Framework PyTorch 2.5.1 to 2.6.0
Python 3.11
NumPy 1.26
Checkpoints ~2,000 historical files in object storage
Consumers Evaluation job, serving loader, notebooks

Minimal reproduction

import numpy as np, torch
torch.save({"w": torch.ones(2), "best": np.float64(0.91)}, "ckpt.pt")
torch.load("ckpt.pt")      # 2.6+: UnpicklingError, Unsupported global numpy scalar

The fix

We did not set weights_only=False globally. That would make every load path, including notebooks loading files from colleagues or the internet, vulnerable again.

1. Save only safe types going forward.

torch.save({
    "model": model.state_dict(),
    "optimizer": optimizer.state_dict(),
    "epoch": int(epoch),
    "best_val_auc": float(best_auc),        # plain Python float
    "config": dataclasses.asdict(cfg),      # dict of primitives instead of a Namespace
}, path)

For model weights we ship to serving, we export safetensors:

from safetensors.torch import save_file, load_file
save_file(model.state_dict(), "model.safetensors")
model.load_state_dict(load_file("model.safetensors"))

2. Load historical checkpoints with a narrow allowlist. For our own old files, we allowlist exactly the NumPy types they contain, in a context manager, inside the one function that reads legacy checkpoints:

import numpy as np
import torch
from numpy.core.multiarray import scalar

def load_legacy_checkpoint(path: str) -> dict:
    with torch.serialization.safe_globals([scalar, np.dtype, np.dtypes.Float64DType]):
        return torch.load(path, map_location="cpu", weights_only=True)

3. Migrate history. A one-off job loaded each legacy checkpoint with the allowlist, converted the extra fields to plain Python types, and re-saved it, so the special loader can eventually be deleted.

Alternatives and trade-offs

  • weights_only=False for trusted files only. Acceptable in a tightly controlled pipeline that only reads its own outputs, if you are sure no untrusted file can ever reach it. We preferred not to depend on that.
  • Pin PyTorch below 2.6. Postpones the problem and misses security and performance updates.

How to test the fix

A test saves a checkpoint with the training code and loads it with default torch.load settings, and a second test scans the checkpoint with pickletools to assert no globals other than allowed ones appear:

import pickletools, zipfile

def globals_in_checkpoint(path):
    with zipfile.ZipFile(path) as z:
        data = z.read(next(n for n in z.namelist() if n.endswith("data.pkl")))
    return {arg for op, arg, _ in pickletools.genops(data) if op.name in ("GLOBAL", "STACK_GLOBAL") and arg}

Confirming in production

  • The evaluation job ran successfully on PyTorch 2.6 the following night.
  • All 2,000 legacy checkpoints were migrated in about an hour; the legacy loader logs a warning if it is ever used again.
  • Serving loads only safetensors files.

Regression guard

  • CI test that new checkpoints load with default settings.
  • A lint rule rejects weights_only=False outside an allowlisted module.
  • Model files downloaded from external hubs must be safetensors, enforced in our model download helper.

Security implications

This change in PyTorch is a security fix. Pickle-based checkpoints from untrusted sources have been used to deliver malicious code, and public model hubs have found such files. Keeping weights_only=True, using narrow allowlists only for our own legacy files, and preferring safetensors for anything shared closes that path. Treat model files like executables when they use pickle.

Prevention checklist

  • Do not disable weights_only globally.
  • Save only tensors and primitive types in checkpoints.
  • Use safetensors for weights that leave the training environment.
  • Allowlist specific globals narrowly for legacy files you trust.
  • Never load pickle-based models from unknown sources.

References

Add a label

#pytorch #checkpoints #safetensors #pickle #model-security
Written by
Ryan Pham @ryan_bugs

I build and run the platforms other engineers ship on: CI/CD pipelines, Kubernetes clusters, GPU inference services, LLM features in production, and the cloud bills that come with all of it. I have also spent years writing and operating automated trading systems, where a rounding bug or a missed websocket message is not a log line but a position you did not intend to hold. Each post here starts from a real failure: the alert, the exact error, what we ruled out, the root cause, and how we proved the fix. Same path every time: Investigate, Explain, Fix, Verify. No hype, no price predictions, just engineering.

Advertisement

Comments

0

No comments yet. Be the first to comment!

Sign In to post a comment.