Skip to content

Secure deserialization (.load()) by default#2264

Open
araffin wants to merge 16 commits into
masterfrom
fix/warn-pickle-load
Open

Secure deserialization (.load()) by default#2264
araffin wants to merge 16 commits into
masterfrom
fix/warn-pickle-load

Conversation

@araffin

@araffin araffin commented Jun 19, 2026

Copy link
Copy Markdown
Member

Description

See #1831

closes #1852

Related to DLR-RM/rl-baselines3-zoo#505

Note: using opencode with local Qwen 3.6 27B model

Motivation and Context

  • I have raised an issue to propose this change (required for new features and bug fixes)

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation (update in the documentation)

Checklist

  • I've read the CONTRIBUTION guide (required)
  • I have updated the changelog accordingly (docs/misc/changelog.md) (required).
  • My change requires a change to the documentation.
  • I have updated the tests accordingly (required for a bug fix or a new feature).
  • I have updated the documentation accordingly.
  • I have opened an associated PR on the SB3-Contrib repository (if necessary)
  • I have opened an associated PR on the RL-Zoo3 repository (if necessary)
  • I have reformatted the code using make format (required)
  • I have checked the codestyle using make check-codestyle and make lint (required)
  • I have ensured make pytest and make type both pass. (required)
  • I have checked that the documentation builds using make doc (required)

Note: You can run most of the checks using make commit-checks.

Note: we are using a maximum length of 127 characters per line

@araffin araffin changed the title Safe save/load by default Secure deserialization (.load()) by default Jun 19, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR hardens Stable-Baselines3 model/component loading against unsafe pickle/cloudpickle deserialization by introducing a default "safe" deserialization mode across load paths, backed by a restricted unpickler + allowlist, and updates tests/docs accordingly.

Changes:

  • Add deserialization_mode: Literal["safe","legacy"] across key load APIs and route checkpoint JSON/pickle loading through a restricted unpickler by default.
  • Introduce a centralized safe-globals allowlist (common/safe_globals.py) plus user extension APIs (add_safe_globals, safe_globals) and enable torch.load(..., weights_only=True) for policy loads.
  • Add regression/security tests and update docs/changelog for the breaking behavior.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
tests/test_vec_envs.py Adjusts parametrization input type for pytest collection stability.
tests/test_utils.py Adds tests for RMSpropTFLike; modifies schedule-related assertions.
tests/test_security_deserialization.py New end-to-end tests ensuring safe mode blocks RCE across load entry points.
tests/test_save_load.py Updates warning expectations and adds coverage for bad zip + open_path edge cases.
stable_baselines3/version.txt Bumps development version to 2.10.0a0.
stable_baselines3/common/vec_env/vec_normalize.py Adds deserialization_mode to VecNormalize.load() with safe/legacy behavior.
stable_baselines3/common/type_aliases.py Introduces DeserializationMode = Literal["safe","legacy"].
stable_baselines3/common/save_util.py Adds safe/legacy handling for JSON cloudpickle payloads and .pkl loads; threads mode into zip loading.
stable_baselines3/common/safe_globals.py New centralized allowlist + restricted unpickler + user extension APIs.
stable_baselines3/common/policies.py Switches policy .load() to torch.load(weights_only=True) and registers safe globals.
stable_baselines3/common/off_policy_algorithm.py Adds deserialization_mode to replay buffer load path.
stable_baselines3/common/base_class.py Adds deserialization_mode parameter to algorithm .load() and forwards it to zip loader.
docs/misc/changelog.md Documents breaking change and new APIs; adds release entry.
docs/guide/save_format.md Documents safe vs legacy deserialization and allowlist extension APIs.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +68 to +81
# numpy dtype descriptor classes (needed by torch weights_only)
types += [
np.dtypes.Float32DType,
np.dtypes.Float64DType,
np.dtypes.Int32DType,
np.dtypes.Int64DType,
np.dtypes.Int8DType,
np.dtypes.Int16DType,
np.dtypes.UInt8DType,
np.dtypes.UInt16DType,
np.dtypes.UInt32DType,
np.dtypes.UInt64DType,
np.dtypes.BoolDType,
]
Comment thread stable_baselines3/common/save_util.py
Comment thread stable_baselines3/common/save_util.py Outdated
Comment thread stable_baselines3/common/vec_env/vec_normalize.py Outdated
Comment thread tests/test_utils.py
Comment thread tests/test_save_load.py Outdated
Comment thread docs/guide/save_format.md
Comment thread docs/misc/changelog.md
Comment thread stable_baselines3/common/base_class.py Outdated
@araffin araffin marked this pull request as ready for review June 19, 2026 13:55
@araffin araffin requested a review from Copilot June 19, 2026 14:07

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated 6 comments.

Comment on lines +68 to +81
# numpy dtype descriptor classes (needed by torch weights_only)
types += [
np.dtypes.Float32DType,
np.dtypes.Float64DType,
np.dtypes.Int32DType,
np.dtypes.Int64DType,
np.dtypes.Int8DType,
np.dtypes.Int16DType,
np.dtypes.UInt8DType,
np.dtypes.UInt16DType,
np.dtypes.UInt32DType,
np.dtypes.UInt64DType,
np.dtypes.BoolDType,
]
Comment on lines +363 to +367
"cloudpickle.cloudpickle._class_setstate",
"cloudpickle.cloudpickle._fillvar",
"cloudpickle.cloudpickle.subimport",
"cloudpickle.cloudpickle._lookup_module_and_obj_in_qualname",
"cloudpickle.cloudpickle.whichmodule",
Comment thread stable_baselines3/common/policies.py Outdated
Comment on lines +991 to +1011
# ---------------------------------------------------------------------------
# Register base policy classes for torch.load with weights_only=True.
# These are defined in this module, so registration must happen after
# their class definitions (i.e. at the end of the file).
# ---------------------------------------------------------------------------

# Policy classes defined in this module
_safe_base_policies = [
BasePolicy,
ActorCriticPolicy,
ActorCriticCnnPolicy,
MultiInputActorCriticPolicy,
ContinuousCritic,
BernoulliDistribution,
CategoricalDistribution,
DiagGaussianDistribution,
MultiCategoricalDistribution,
StateDependentNoiseDistribution,
]

th.serialization.add_safe_globals(_safe_base_policies) # type: ignore[arg-type]
Comment on lines +192 to +208
if deserialization_mode == "safe":
pass
# warnings.warn(
# "Loading a model checkpoint that contains cloudpickle-serialized "
# "objects with a restricted (safe) deserializer. Only known-safe "
# "SB3/gymnasium/numpy types are allowed. "
# UserWarning,
# )
else:
warnings.warn(
"Loading a model checkpoint that contains cloudpickle-serialized "
"objects (deserialization_mode='legacy'). This allows arbitrary "
"Python code execution from the checkpoint file. Only load "
"checkpoints from trusted sources. To enable safe deserialization, "
"use deserialization_mode='safe'. ",
UserWarning,
)
Comment on lines +344 to +349
# warnings.warn(
# "Loading a VecNormalize pickle file with a restricted (safe) "
# "deserializer. Only known-safe SB3/gymnasium/numpy types are "
# "allowed.",
# UserWarning,
# )
Comment on lines 436 to 454
file = open_path(path, "r", verbose=verbose, suffix="pkl")
obj = pickle.load(file)

if deserialization_mode == "safe":
# Ensure SB3 types are registered for safe deserialization
from stable_baselines3.common.safe_globals import register_sb3_safe_globals

register_sb3_safe_globals()
obj = _RestrictedUnpickler(file).load()
else:
warnings.warn(
"Loading a .pkl file with pickle deserialization (deserialization_mode='legacy'). "
"This can execute arbitrary Python code from the file. Only load pickle files "
"from trusted sources. ",
UserWarning,
)
obj = pickle.load(file)
if isinstance(path, (str, pathlib.Path)):
file.close()
return obj
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

torch.load without weights_only parameter is unsafe

2 participants