Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
afc9e01
Try to implement safe save/load
araffin Jun 19, 2026
49b44be
refactor: all safe globals in one place
araffin Jun 19, 2026
927039d
Fix for numpy 1.x
araffin Jun 19, 2026
2fba96b
Fix mypy and linter issues
araffin Jun 19, 2026
91bff4d
Use a special type for deserialization
araffin Jun 19, 2026
bb7ace3
Update documentation
araffin Jun 19, 2026
c170255
Remove duplicate entry in changelog
araffin Jun 19, 2026
41ba37d
Fixes for NumPy v1.x
araffin Jun 19, 2026
f19c031
Update tests
araffin Jun 19, 2026
4316580
Additional types for SB3 contrib, fix HER replay
araffin Jun 19, 2026
d8e6384
Update test_vec_envs.py
araffin Jun 19, 2026
2afad43
Improve test coverage
araffin Jun 19, 2026
c8def52
Improve test coverage of rms prop
araffin Jun 19, 2026
25a1466
Fixes from code review
araffin Jun 19, 2026
75b9dbc
Remove duplicated code
araffin Jun 19, 2026
eafcf58
Remove subimport from whitelist and rename context manager
araffin Jun 19, 2026
fe1ec1d
Fix mypy errors
araffin Jul 18, 2026
b5b6eae
Replace literal with enum for deserialization mode
araffin Jul 18, 2026
f2895b0
Update tests, move imports
araffin Jul 18, 2026
6b3a567
Remove warnings and add doc
araffin Jul 18, 2026
ef1a041
Also register for pytorch custom objects
araffin Jul 18, 2026
c1d0d20
Revert "Also register for pytorch custom objects"
araffin Jul 18, 2026
c8d765c
Remove redundant code
araffin Jul 18, 2026
f629fb1
Add note in the doc
araffin Jul 18, 2026
68074c0
Update changelog.md
araffin Jul 18, 2026
0b889d1
Mark subimport as unsafe
araffin Jul 18, 2026
2448267
Add new exploit examples
araffin Jul 18, 2026
533775a
fix: remove cloudpickle unsafe methods
araffin Jul 18, 2026
552548c
Update tests to use legacy mode when needed
araffin Jul 18, 2026
afe4670
Fix moviepy test
araffin Jul 18, 2026
cef493f
Update security tests
araffin Jul 18, 2026
424a835
Ignore tensorboard warning
araffin Jul 18, 2026
7419afa
Catch all the warnings
araffin Jul 18, 2026
2416e2c
chore: rename single character variables
araffin Jul 18, 2026
4627bd3
Reformat
araffin Jul 18, 2026
dd9510f
Merge branch 'master' into fix/warn-pickle-load
araffin Jul 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions docs/guide/save_format.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,91 @@ Cons:
- More complex implementation.
- Still relies partly on cloudpickle for complex objects (e.g. custom functions)
with can lead to [incompatibilities](https://github.com/DLR-RM/stable-baselines3/issues/172) between Python versions.

## Secure Deserialization

:::{warning}
**Loading untrusted checkpoints can execute arbitrary Python code.**
Starting with SB3 2.10, all `load()` methods use `deserialization_mode="safe"` by default, which blocks
arbitrary code execution during deserialization at the cost of skipping non-whitelisted serialized entries.
:::

The `deserialization_mode` parameter is available on all load methods:

- `stable_baselines3.common.base_class.BaseAlgorithm.load`
- `stable_baselines3.common.save_util.json_to_data`
- `stable_baselines3.common.save_util.load_from_pkl`
- `stable_baselines3.common.save_util.load_from_zip_file`
- `stable_baselines3.common.off_policy_algorithm.OffPolicyAlgorithm.load_replay_buffer`
- `stable_baselines3.common.vec_env.vec_normalize.VecNormalize.load`

Each accepts one of two modes:

### Safe mode (default)

In `deserialization_mode="safe"`, SB3 uses a restricted unpickler that only allows a fixed allowlist of
known-safe types (SB3 classes, gymnasium spaces, numpy types, PyTorch types, cloudpickle internals).
If a serialized entry references a type outside the allowlist, it throws an error (that is caught), and the
user must supply a safe replacement via the `custom_objects` argument or extends the whitelist (see below).

```python
from stable_baselines3 import PPO

# If the checkpoint contains a custom learning-rate schedule that is not
# in the allowlist, you must provide it via custom_objects:
loaded = PPO.load(
"model.zip",
custom_objects={
"learning_rate": 0.0003,
"lr_schedule": lambda progress: progress * 0.0003,
},
)
```

### Legacy mode

In `deserialization_mode="legacy"`, SB3 falls back to the standard `cloudpickle` / `pickle` loader.
This preserves full backward compatibility with models saved before SB3 2.10 but **may execute arbitrary
Python code** embedded in the checkpoint. A `UserWarning` is emitted.

```python
# Restores the pre-2.10 loading behavior for checkpoints that contain
# lambda functions, local classes, or custom gym environments:
loaded = PPO.load("model.zip", deserialization_mode="legacy")

# If you only load models from trusted sources, you can silence the warning with:
# import warnings
# warnings.filterwarnings("ignore", message="Loading a model checkpoint that contains cloudpickle-serialized objects", category=UserWarning)
```

### Extending the Safe Allowlist

If you have custom types (e.g. a custom environment or a custom space) that are safe to deserialize,
you can register them with the allowlist using `stable_baselines3.common.safe_globals.add_safe_globals`:

```python
from stable_baselines3.common.safe_globals import add_safe_globals
from my_module import MyCustomSpace

add_safe_globals(MyCustomSpace)
loaded = PPO.load("model.zip", deserialization_mode="safe")
```

For a temporary, scope-limited registration, use the `stable_baselines3.common.safe_globals.safe_globals`
context manager:
Comment on lines +131 to +132

```python
from stable_baselines3.common.safe_globals import SafeGlobals
from my_module import MyCustomSpace

with SafeGlobals(MyCustomSpace):
loaded = PPO.load("model.zip", deserialization_mode="safe")
# MyCustomSpace is automatically removed from the allowlist on exit
```

:::{note}
With the default `deserialization_mode="safe"`, you may encounter a `pickle.UnpicklingError` or `Could not deserialize object` warning when loading checkpoints containing custom types that are not in the allowlist. The error message
will indicate the missing type (e.g., `Global 'my_module.MyCustomType' is not in the safe deserialization allowlist`).
You can either use `add_safe_globals()` or `SafeGlobals` to register your custom types, pass `custom_objects=...` at load time, or switch to `deserialization_mode="legacy"`
if you trust the checkpoint source.
:::
36 changes: 36 additions & 0 deletions docs/misc/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,41 @@

# Changelog

## Release 2.10.0a0 (TBD)

**Secure deserialization by default**

:::{warning}
Models saved with cloudpickle that contain arbitrary Python code (e.g. lambda functions, local classes, or custom spaces) will now produce warnings and skip the affected entries when loaded with the new default `deserialization_mode="safe"`. Use `deserialization_mode="legacy"` to restore the old behavior. You can find more information in the [Saving and Loading](https://stable-baselines3.readthedocs.io/en/master/guide/save_reload.html) documentation.
:::

### Breaking Changes:

- `deserialization_mode` now defaults to `"safe"` for all load methods (`BaseAlgorithm.load`, `json_to_data`, `load_from_pkl`, `load_from_zip_file`, `load_replay_buffer`, `VecNormalize.load`). This blocks arbitrary code execution during deserialization at the cost of skipping non-whitelisted serialized entries.
- `BaseModel.load()` now uses `torch.load(..., weights_only=True)` with a safe-globals allowlist for policy state-dicts.

### New Features:

- Added `deserialization_mode` parameter to all load methods (`"safe"` or `"legacy"`) to mitigate deserialization of Untrusted Data. `"safe"` mode uses a restricted unpickler that only allows a fixed allowlist of known-safe SB3/gymnasium/numpy types.
- Added `add_safe_globals()` function and context manager to register custom classes as safe for restricted deserialization (à la `torch.serialization.add_safe_globals`).

### Bug Fixes:

### [SB3-Contrib]

### [RL Zoo]

### [SBX] (SB3 + Jax)

### Deprecations:

### Others:

### Documentation:

- Updated save/reload guide with a dedicated section on secure deserialization, explaining safe vs. legacy mode and how to handle `custom_objects` in safe mode.


## Release 2.9.2a0 (2026-07-18)

### Breaking Changes:
Expand All @@ -28,6 +63,7 @@

### Documentation:


## Release 2.9.0 (2026-06-15)

**Updated dependencies (pandas is now optional, gymnasium 1.3.0 support, torch>=2.8)**
Expand Down
20 changes: 19 additions & 1 deletion stable_baselines3/common/base_class.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,13 @@
from stable_baselines3.common.policies import BasePolicy
from stable_baselines3.common.preprocessing import check_for_nested_spaces, is_image_space, is_image_space_channels_first
from stable_baselines3.common.save_util import load_from_zip_file, recursive_getattr, recursive_setattr, save_to_zip_file
from stable_baselines3.common.type_aliases import GymEnv, MaybeCallback, Schedule, TensorDict
from stable_baselines3.common.type_aliases import (
DeserializationMode,
GymEnv,
MaybeCallback,
Schedule,
TensorDict,
)
from stable_baselines3.common.utils import (
FloatSchedule,
check_for_correct_spaces,
Expand Down Expand Up @@ -648,6 +654,7 @@ def load( # noqa: C901
custom_objects: dict[str, Any] | None = None,
print_system_info: bool = False,
force_reset: bool = True,
deserialization_mode: DeserializationMode = DeserializationMode.SAFE,
**kwargs,
) -> SelfBaseAlgorithm:
"""
Expand All @@ -671,6 +678,16 @@ def load( # noqa: C901
:param force_reset: Force call to ``reset()`` before training
to avoid unexpected behavior.
See https://github.com/DLR-RM/stable-baselines3/issues/597
:param deserialization_mode: How to handle cloudpickle-serialized objects
in the checkpoint's ``data`` JSON.

- ``"legacy"``: Deserialize with cloudpickle for full
backward compatibility. A security warning is emitted because
cloudpickle deserialization can execute arbitrary Python code.
- ``"safe"`` (default): Attempt restricted deserialization using an
allowlist of known-safe globals. Entries that cannot be safely
deserialized (and are not provided via ``custom_objects``) are skipped
with a warning.
:param kwargs: extra arguments to change the model when loading
:return: new model instance with loaded parameters
"""
Expand All @@ -683,6 +700,7 @@ def load( # noqa: C901
device=device,
custom_objects=custom_objects,
print_system_info=print_system_info,
deserialization_mode=deserialization_mode,
)

assert data is not None, "No data found in the saved file"
Expand Down
23 changes: 21 additions & 2 deletions stable_baselines3/common/off_policy_algorithm.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,15 @@
from stable_baselines3.common.noise import ActionNoise, VectorizedActionNoise
from stable_baselines3.common.policies import BasePolicy
from stable_baselines3.common.save_util import load_from_pkl, save_to_pkl
from stable_baselines3.common.type_aliases import GymEnv, MaybeCallback, RolloutReturn, Schedule, TrainFreq, TrainFrequencyUnit
from stable_baselines3.common.type_aliases import (
DeserializationMode,
GymEnv,
MaybeCallback,
RolloutReturn,
Schedule,
TrainFreq,
TrainFrequencyUnit,
)
from stable_baselines3.common.utils import safe_mean, should_collect_more_steps
from stable_baselines3.common.vec_env import VecEnv
from stable_baselines3.her.her_replay_buffer import HerReplayBuffer
Expand Down Expand Up @@ -228,6 +236,7 @@ def load_replay_buffer(
self,
path: str | pathlib.Path | io.BufferedIOBase,
truncate_last_traj: bool = True,
deserialization_mode: DeserializationMode = DeserializationMode.SAFE,
) -> None:
"""
Load a replay buffer from a pickle file.
Expand All @@ -237,8 +246,18 @@ def load_replay_buffer(
If set to ``True``, we assume that the last trajectory in the replay buffer was finished
(and truncate it).
If set to ``False``, we assume that we continue the same trajectory (same episode).
:param deserialization_mode: How to handle pickle deserialization.

- ``"safe"`` (default): Deserialize using a restricted unpickler that
only allows a fixed allowlist of known-safe SB3/gymnasium/numpy types.
Any pickle payload referencing a type outside this allowlist is
rejected with a clear error.
- ``"legacy"``: Deserialize with ``pickle.load()``. This preserves
backward compatibility but can **execute arbitrary Python code**
embedded in the pickle file.

"""
self.replay_buffer = load_from_pkl(path, self.verbose)
self.replay_buffer = load_from_pkl(path, self.verbose, deserialization_mode=deserialization_mode)
assert isinstance(self.replay_buffer, ReplayBuffer), "The replay buffer must inherit from ReplayBuffer class"

# Backward compatibility with SB3 < 2.1.0 replay buffer
Expand Down
7 changes: 4 additions & 3 deletions stable_baselines3/common/policies.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
make_proba_distribution,
)
from stable_baselines3.common.preprocessing import get_action_dim, is_image_space, maybe_transpose, preprocess_obs
from stable_baselines3.common.safe_globals import register_sb3_safe_globals
from stable_baselines3.common.torch_layers import (
BaseFeaturesExtractor,
CombinedExtractor,
Expand Down Expand Up @@ -173,9 +174,9 @@ def load(cls: type[SelfBaseModel], path: str, device: th.device | str = "auto")
:return:
"""
device = get_device(device)
# Note(antonin): we cannot use `weights_only=True` here because we need to allow
# gymnasium imports for the policy to be loaded successfully
saved_variables = th.load(path, map_location=device, weights_only=False)
# Ensure SB3 policy types are registered for torch.load with weights_only=True
register_sb3_safe_globals()
saved_variables = th.load(path, map_location=device, weights_only=True)

# Create policy object
model = cls(**saved_variables["data"])
Expand Down
Loading