Conversation
|
Hi @hackchang not sure whether you've seen it but the checks failed (linting and unit tests). Could you fix the issues? After that I can review |
|
Thanks @dennisbader — fixed both: ran ruff format for the lint failure, and the test failures were Darts' NeuralForecast-wrapped models (neuralforecast.models.*) not being in the weights_only=True allow-list — now registered when neuralforecast is installed. Verified the previously-failing save/load + nf_model tests pass locally. Pushed; CI should be green now. |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #3183 +/- ##
==========================================
- Coverage 97.11% 97.05% -0.07%
==========================================
Files 167 167
Lines 18429 18480 +51
==========================================
+ Hits 17898 17936 +38
- Misses 531 544 +13 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
dennisbader
left a comment
There was a problem hiding this comment.
Thanks a lot @hackchang for this PR 🚀
The direction makes sense and looks like a good start. I do have some comments / suggestions:
- The .ckpt default to weights_only=True is good
- The import-time allow-list of half of torch/torchmetrics is not ideal: too much maintenance, runs on every import, and we'll miss edge cases (custom losses, new metric subpackages).
- Most loading paths still fully unpickle the
.ptfiles so the security fix is narrower than the description suggests.
I'd prefer a slightly reduced PR: with flipping weights_only defaults, adding docs/warnings, minimal Darts-only allow-list, and maybe checkpoint-driven registration on failure.
| # ``torchmetrics`` metrics store references to their own tensor-reduction / | ||
| # distributed helper *functions* in their pickled state (``_reductions``, | ||
| # ``dist_sync_fn``, ...). A ``weights_only=True`` load of a metric-bearing | ||
| # checkpoint needs these specific, known-safe library callables allow-listed. |
There was a problem hiding this comment.
We're allow-listing functions here, not just classes. PyTorch's serialization docs say registered functions can be called during unpickling. I don't think that's equivalent to "does not re-open the RCE surface" — worth being honest about in the PR description.
|
|
||
| class _DartsCheckpointIO(TorchCheckpointIO): | ||
| """Custom CheckpointIO that defaults ``weights_only`` to ``False``. | ||
| def _register_safe_globals() -> None: |
There was a problem hiding this comment.
This runs at import time for every torch model. That means we unconditionally import torchmetrics (and optionally neuralforecast) even if the user never touches checkpoint loading. Could we defer this to first load, or use the safe_globals context manager scoped around the actual torch.load call?
Also, scanning vars(module) for every loss/activation/optimizer/metric subclass feels brittle — torchmetrics adds subpackages over time and we'll keep chasing them. Have you tried get_unsafe_globals_in_checkpoint() on the specific file being loaded?
| if _PL_2_6_OR_ABOVE: | ||
| weights_only_kwargs["weights_only"] = ( | ||
| False if weights_only is None else weights_only | ||
| True if weights_only is None else weights_only |
There was a problem hiding this comment.
Flipping the default from False to True is a behavior change for all Lightning-internal checkpoint loads, not just our public APIs. fit(ckpt_path=...) still passes weights_only=False explicitly, but what about other trainer paths that go through the plugin without overriding? Worth a test for resume-from-checkpoint with optimizer state.
| if _PL_2_6_OR_ABOVE: | ||
| weights_only_kwargs["weights_only"] = ( | ||
| False if weights_only is None else weights_only | ||
| True if weights_only is None else weights_only |
There was a problem hiding this comment.
Comment on lines 772-774 still says we force weights_only=False for internal loading, but _DartsCheckpointIO now defaults to True. Stale, or did I miss something?
| base_model_path, weights_only=False, map_location=kwargs.get("map_location") | ||
| ) | ||
|
|
||
| # load PyTorch LightningModule from checkpoint |
There was a problem hiding this comment.
Just flagging for the PR description: even with this change, load_from_checkpoint still fully unpickles the base model file first. So the "safe-by-default against CWE-502" claim only really applies to the .ckpt half. The docstring warning helps but the top-level description oversells it.
| """ | ||
| Loads a model from a given file path. | ||
|
|
||
| .. warning:: |
There was a problem hiding this comment.
Fine for backwards compat, but means RNNModel.load("foo.pt") is still arbitrary code execution on a malicious file. If we're closing #3177 with this PR, we should be clear in the issue / CHANGELOG that .pt loads are out of scope.
| reloaded.load_weights(ckpt_path, map_location="cpu") | ||
| reloaded.predict(n=2, series=self.series[:20]) | ||
|
|
||
| # 2) craft a malicious checkpoint whose `__reduce__` writes a marker file -------- |
There was a problem hiding this comment.
Part 1 (legitimate model loads with default) is good. Part 2 only tests raw torch.load though — can you add a case where the evil payload is loaded via reloaded.load_weights(evil_path) or similar? That's the path users actually hit.
| ) | ||
| # cleanup the benign marker | ||
| os.remove(marker_path) | ||
|
|
There was a problem hiding this comment.
On test_load_weights_from_checkpoint (~line 1344–1353)
This expects ValueError when weights_only=True, but you removed that guard and made True the default -> the the load_weights_from_checkpoint above should then fail now. Did you mean to update this block (now it's probably handled via the safe globals)?
) Address @dennisbader's review: - Drop the import-time `add_safe_globals` scan of torch/torchmetrics/ neuralforecast. Allow-listing is now scoped to each load via `torch.serialization.safe_globals(...)` (`_load_ckpt_safely`) -- nothing is imported or registered unless a checkpoint is actually loaded. - Checkpoint-driven, per file: use `get_unsafe_globals_in_checkpoint()` and only allow-list the globals that file references, filtered to classes under torch/torchmetrics/darts/neuralforecast that subclass a known-safe base (nn.Module / Optimizer / LR scheduler / torchmetrics Metric|MetricCollection / Darts likelihood). Anything else stays blocked and the load fails loudly. - Stop scanning modules for functions. torchmetrics genuinely pickles references to its own reduction helpers, so allow-list only an explicit, audited set of 6 such functions by exact qualified name (added only when referenced), and document that registering a callable is not zero-risk. - Honesty: docstrings + CHANGELOG state this protects the `.ckpt` only; the Darts base model `.pt` is still fully unpickled (a malicious `.pt` is still RCE and is out of scope). Marked the change with the breaking-change flag and unit8co#3177 as "partially addresses". - Fix the now-stale internal-loading comment (internal loads are safe-by-default `weights_only=True`; only the training-resume path stays explicit `False`). - Tests: fix the now-stale `weights_only=True` block (it was actually catching an architecture mismatch); add a malicious-payload case through the real user path `load_weights_from_checkpoint(skip_checks=True)`; add a resume-from-checkpoint test that exercises optimizer/scheduler state under `weights_only=True`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015RFWdogyAg3fWKPv9pxXAd
|
Thanks for the detailed review — this was the right call. Pushed a reduced version in 2888876:
Happy to trim further to a Darts-classes-only allow-list (requiring weights_only=False for |
Fixes #3177.
What
Makes torch-model checkpoint loading safe-by-default against CWE-502 (a malicious
.ckptexecuting arbitrary code during unpickling), while keeping legitimate models loading unchanged.LikelihoodType, optimizer/scheduler/loss/activation classes, torchmetrics metric classes + their tensor-reduction helpers) viatorch.serialization.add_safe_globals, so aweights_only=Trueload of a legitimate checkpoint succeeds. Only class/type and argument-less library helpers are allow-listed — never callables that run on unpickle — so this does not re-open the RCE surface. No-op on torch < 2.6._DartsCheckpointIO,load_from_checkpoint,load_weights,load_weights_from_checkpoint) now default toweights_only=True, with a user-facingweights_onlyparameter to opt back into full unpickling for trusted files..ptpath inload()(an arbitrary object graph that cannot useweights_only=True) keepsweights_only=Falseby default but exposes the parameter and a security note.Behavior changes (please note)
load_from_checkpoint(),load_weights(),load_weights_from_checkpoint()now default toweights_only=True. Checkpoints using custom loss/optimizer/likelihood classes defined outside darts/torch may needweights_only=False.load_weights_from_checkpoint()previously raised onweights_only=True; it now works and is the default.Testing
test_load_checkpoint_weights_only_blocks_malicious_payload): a normal model still loads under the safe default; a crafted__reduce__payload is refused underweights_only=True(no code execution) and only loads under the explicitweights_only=Falseopt-out.test_torch_forecasting_model.pysuite passes locally (350 passed) on torch 2.13 / lightning 2.6.5.Transparency
This contribution was prepared with AI assistance; I reviewed, tested, and take responsibility for every change, and remain in the loop throughout, per the project's guidelines.