Skip to content

fix(picklescan): safely parse bounded PyTorch tensor batches - #1783

Open
mldangelo-oai wants to merge 32 commits into
mainfrom
fix/hf-pytorch-batched-setitems
Open

fix(picklescan): safely parse bounded PyTorch tensor batches#1783
mldangelo-oai wants to merge 32 commits into
mainfrom
fix/hf-pytorch-batched-setitems

Conversation

@mldangelo-oai

@mldangelo-oai mldangelo-oai commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Stream proven canonical tensor entries and inert checkpoint metadata without exceeding the existing 1,024-item simulated-stack bound.
  • Cover standard 600/1,000-entry benign batches, mixed metadata, oversized batches, and malicious near-matches.
  • Keep this change scoped to the independently released picklescan package; release-please will publish its next patch before any root dependency-floor change.

Verification

  • Immutable timm/mobilenetv3_small_100.lamb_in1k checkpoint 1824797e7887cbec1990e4adbd6675960a36c589 reproduced and scans clean without executing model contents.
  • Dependency-free standalone regressions pass, including the malicious mixed-metadata control.
  • Ruff, standalone mypy, both frozen lock checks, and the 187-test Rust suite pass.

Copilot AI review requested due to automatic review settings July 27, 2026 04:13

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 fixes a false-positive condition in the PyTorch ZIP “trusted storage reference” parsing path when state dictionaries are serialized using large SETITEMS batches (common in batched state dicts). It keeps the existing overall stack-depth bound while decoupling it from the smaller tuple-width limit, and adds regression coverage across both the standalone picklescan package and the root scanner integration.

Changes:

  • Adjust the PyTorch storage trust parser to apply SETITEMS batching limits using the stack-depth bound rather than the tuple-width bound.
  • Add deterministic unit/regression tests for large batched state dicts (benign and malicious near-match) and explicit stack/tuple bound behaviors.
  • Add changelog entries for both modelaudit and modelaudit-picklescan.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
tests/scanners/test_pytorch_zip_scanner.py Adds an integration regression ensuring large batched torch.save state dicts remain clean (no _rebuild_tensor_v2 false positives).
packages/modelaudit-picklescan/tests/test_api.py Adds deterministic ZIP fixtures + regressions for large batched SETITEMS, plus stack/tuple bound unit tests.
packages/modelaudit-picklescan/src/modelaudit_picklescan/api.py Updates marked-tuple parsing to accept a configurable width and uses stack-depth for SETITEMS batches.
packages/modelaudit-picklescan/CHANGELOG.md Notes the bug fix in the picklescan package changelog.
CHANGELOG.md Notes the bug fix in the root package changelog.

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

Comment thread packages/modelaudit-picklescan/tests/test_api.py Outdated
Comment thread packages/modelaudit-picklescan/src/modelaudit_picklescan/api.py Outdated
@github-actions

github-actions Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Workflow run and artifacts

Performance Benchmarks

Compared 13 shared benchmarks with a regression threshold of 15%.
Status: 0 regressions, 0 improved, 13 stable, 0 new, 0 missing.
Aggregate shared-benchmark median: 4.244s -> 4.211s (-0.8%).

Workload Benchmark Target Size Files Baseline Current Change Status
warm-cache-rescan tests/benchmarks/test_scan_benchmarks.py::test_scan_warm_cached_repository_rescan release-candidate 547.3 KiB 32 143.92ms 135.07ms -6.2% stable
nested-payload-review tests/benchmarks/test_picklescan_benchmarks.py::test_picklescan_nested_payload_review[nested_base64] nested_base64 98 B 1 263.8us 253.6us -3.9% stable
suspicious-pickle-intake tests/benchmarks/test_scan_benchmarks.py::test_scan_suspicious_pickle_intake suspicious-intake 183.8 KiB 4 152.36ms 148.02ms -2.8% stable
nested-payload-review tests/benchmarks/test_picklescan_benchmarks.py::test_picklescan_nested_payload_review[nested_hex] nested_hex 130 B 1 270.7us 264.2us -2.4% stable
mixed-model-repository tests/benchmarks/test_scan_benchmarks.py::test_scan_release_candidate_repository release-candidate 547.3 KiB 32 636.61ms 627.97ms -1.4% stable
duplicate-heavy-registry tests/benchmarks/test_scan_benchmarks.py::test_scan_duplicate_registry_snapshot registry-snapshot 915.2 KiB 13 588.63ms 583.64ms -0.8% stable
nested-payload-review tests/benchmarks/test_picklescan_benchmarks.py::test_picklescan_nested_payload_review[nested_raw] nested_raw 78 B 1 245.0us 242.9us -0.8% stable
chunked-upload-stream tests/benchmarks/test_picklescan_benchmarks.py::test_picklescan_chunked_upload_stream chunked_stream 278.2 KiB 1 117.66ms 117.16ms -0.4% stable
padded-multi-stream-upload tests/benchmarks/test_picklescan_benchmarks.py::test_picklescan_padded_multi_stream_upload multi_stream_padded 4.1 KiB 1 296.8us 298.0us +0.4% stable
direct-malicious-upload tests/benchmarks/test_picklescan_benchmarks.py::test_picklescan_direct_malicious_upload malicious_reduce 52 B 1 191.6us 192.3us +0.4% stable
single-checkpoint-preflight tests/benchmarks/test_scan_benchmarks.py::test_scan_single_checkpoint_before_load single_checkpoint.pkl 183.0 KiB 1 107.65ms 107.30ms -0.3% stable
rejected-basic-auth-candidates tests/benchmarks/test_scan_benchmarks.py::test_rejected_basic_auth_candidates_scan_linearly - 371.1 KiB 1 2.382s 2.376s -0.2% stable
clean-training-checkpoint tests/benchmarks/test_picklescan_benchmarks.py::test_picklescan_clean_training_checkpoint safe_large 278.2 KiB 1 114.40ms 114.49ms +0.1% stable

Copilot AI review requested due to automatic review settings July 27, 2026 04:22

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 12 out of 15 changed files in this pull request and generated no new comments.

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6ab180a928

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/modelaudit-picklescan/src/modelaudit_picklescan/api.py
Copilot AI review requested due to automatic review settings July 27, 2026 04:37

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 7 out of 10 changed files in this pull request and generated 1 comment.

Comment thread packages/modelaudit-picklescan/src/modelaudit_picklescan/api.py

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 117dc52da6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/modelaudit-picklescan/pyproject.toml Outdated
Comment thread packages/modelaudit-picklescan/src/modelaudit_picklescan/api.py
Copilot AI review requested due to automatic review settings July 27, 2026 04:48
@mldangelo-oai mldangelo-oai changed the title fix: avoid false positives for batched PyTorch state dictionaries fix(picklescan): safely parse bounded PyTorch tensor batches Jul 27, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 4 out of 4 changed files in this pull request and generated no new comments.

Copilot AI review requested due to automatic review settings July 27, 2026 04:52

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 4 out of 4 changed files in this pull request and generated no new comments.

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 165da9330a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/modelaudit-picklescan/src/modelaudit_picklescan/api.py Outdated
Copilot AI review requested due to automatic review settings July 27, 2026 04:59

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 4 out of 4 changed files in this pull request and generated no new comments.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 4 out of 4 changed files in this pull request and generated no new comments.

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3e5b2486fa

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/modelaudit-picklescan/src/modelaudit_picklescan/api.py Outdated
Copilot AI review requested due to automatic review settings July 27, 2026 07:03

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 4 out of 4 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (1)

packages/modelaudit-picklescan/src/modelaudit_picklescan/api.py:2098

  • isinstance() cannot take a PEP 604 union (dict | _PytorchOrderedDictState) as its second argument; this will raise TypeError: isinstance() argument 2 cannot be a union at runtime on Python 3.10+ when the compaction path executes. Use a tuple of types instead.
            if not isinstance(target, dict | _PytorchOrderedDictState):

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 46a8fb7a2d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/modelaudit-picklescan/src/modelaudit_picklescan/api.py
Comment thread packages/modelaudit-picklescan/src/modelaudit_picklescan/api.py Outdated
Copilot AI review requested due to automatic review settings July 27, 2026 07:16

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 4 out of 4 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (1)

packages/modelaudit-picklescan/src/modelaudit_picklescan/api.py:2103

  • isinstance() cannot take a PEP 604 union (dict | _PytorchOrderedDictState) at runtime; this raises TypeError and will cause the trust parser to bail out early (caught by the broad except Exception, returning parse_complete=False). Use a tuple of types instead.
                continue
            target = stack[marker_index - 1]
            if not isinstance(target, dict | _PytorchOrderedDictState):
                continue
            if isinstance(target, _PytorchOrderedDictState) and target.used_as_hooks:

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d9f9103262

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

if (
len(setitem_pairs) * 2 > _PYTORCH_STORAGE_TRUST_MAX_TUPLE_WIDTH
and not batch_has_canonical_tensor
and not trusted_canonical_batch_seen

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Record canonical tensors inserted with SETITEM

When a valid state_dict contains one tensor and at least 33 module _metadata records, CPython serializes the tensor with SETITEM, not SETITEMS. Because only the SETITEMS branch sets trusted_canonical_batch_seen, this condition treats the later safe metadata batch as unproven and STOP returns parse_complete=False, restoring persistent-ID and _rebuild_tensor_v2 warnings. Fresh evidence beyond the earlier post-tensor report is that the current real-module regression has two tensor entries, while the one-entry OrderedDict regression strips the state dict's _metadata; have SETITEM recognize a canonical tensor and update the pending-batch state too.

AGENTS.md reference: AGENTS.md:L115-L117

Useful? React with 👍 / 👎.

or stack[-1] is not canonical_batch_target
or (len(setitem_items) - 1) % 2 != 0
):
clear_stack_after_malformed_provenance()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve popped SETITEMS provenance before clearing

When SETITEMS has odd arity or no target, pop_marked_tuple() has already removed its operands before this call clears the remaining stack, so a raw _PytorchStorageRef among those operands is never recorded as discarded. In a stream containing a newly accepted wide safe batch followed by this malformed operation and a later small canonical batch, the parser reports parse_complete=True with discarded_tracked_storage_references=False, and _trusted_pytorch_data_pkl_from_storage_member_sizes() consequently trusts the discarded storage key. A pickle hidden beyond the expanded 64 KiB member probe is then missed while its persistent-ID finding is suppressed; inspect setitem_items for tracked provenance before clearing it.

AGENTS.md reference: AGENTS.md:L115-L117

Useful? React with 👍 / 👎.

Copilot AI review requested due to automatic review settings July 27, 2026 07:31

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9226edde21

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1289 to +1292
except ValueError as error:
message = str(error).lower()
if "opcode" in message and "unknown" in message:
return False

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve generic detection for expanded storage probes

When a 33+-entry canonical state batch marks a storage member for an expanded probe, a member beginning \x80\x04NNNN\x00 (a binary header, harmless opcodes, then malformed padding) is rejected here before the 64 KiB sample is read. The same member is accepted by _zip_entry_looks_like_pickle() after its valid opcode prefix, allowing the Rust follow-on scanner to inspect a dangerous pickle placed later in the member; this early return instead prevents the member from ever being added to pickle_entries, so that payload is skipped. Keep this optimization at least as conservative as the generic probe.

AGENTS.md reference: AGENTS.md:L115-L115

Useful? React with 👍 / 👎.

message = str(error).lower()
if "opcode" in message and "unknown" in message:
return False
except Exception:

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 4 out of 4 changed files in this pull request and generated 1 comment.

Comment on lines +1285 to 1296
if max_probe_bytes > _TRUSTED_STORAGE_PICKLE_PROBE_BYTES:
try:
for _opcode, _arg, _position in pickletools.genops(prefix):
pass
except ValueError as error:
message = str(error).lower()
if "opcode" in message and "unknown" in message:
return False
except Exception:
pass

sample = prefix
This restates ruff's current default select list, so it is a no-op today and a
liability later: it pins the package to today's defaults, silently opting out of
rules ruff adds to the default set. Lint passes without it, and it is unrelated
to the batched-SETITEMS fix this PR makes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 1, 2026 13:22

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 3 out of 3 changed files in this pull request and generated no new comments.

Suppressed comments (1)

packages/modelaudit-picklescan/src/modelaudit_picklescan/api.py:2112

  • isinstance() cannot take a PEP 604 union (dict | _PytorchOrderedDictState) as its second argument; this raises TypeError at runtime and will break the streaming compaction path for large batched state_dict pickles. Use a tuple of types instead.
                continue
            target = stack[marker_index - 1]
            if not isinstance(target, dict | _PytorchOrderedDictState):
                continue

@mldangelo
mldangelo enabled auto-merge (squash) August 1, 2026 13:31
Routing expanded-trust members through the 64 KiB probe instead of the 4 KiB
trusted probe changed sample_is_prefix, which is derived from how much of the
member was read. For members between the two probe sizes the flag flips True to
False, and the binary-pickle predicate refuses to scan a non-STOP-terminated
sample once it is no longer a prefix. A malicious pickle hidden in archive/data/N
was therefore scanned before this PR and silently skipped after it.

Reproduced end to end: a 5000-byte os.system payload in a 33-entry compacted
state dict reports MALICIOUS with the fix and CLEAN without it. The genops guard
added by this PR does not catch it either - the short probe is 16 bytes, so
genops fails with a stringnl error whose message contains neither 'opcode' nor
'unknown' and the guard falls through.

Re-checking the shorter view keeps a wider probe strictly additive. Applied to
the proto-0/1 predicate as well, which derives sample_is_prefix the same way.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 1, 2026 14:25

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 3 out of 3 changed files in this pull request and generated no new comments.

Suppressed comments (2)

packages/modelaudit-picklescan/tests/test_api.py:3431

  • This bytes literal combines two pickle opcodes (BINPUT + POP) into a single string ("q\x000"), which is easy to misread as a single opcode/argument sequence. Splitting it into explicit opcode bytes makes the intended pickle stream clearer and reduces the chance of accidentally breaking the test payload.
        + b"q\x000"

packages/modelaudit-picklescan/tests/test_api.py:3407

  • This bytes literal combines two pickle opcodes (BINPUT + POP) into a single string ("q\x000"), which is easy to misread as a single opcode/argument sequence. Splitting it into explicit opcode bytes makes the intended pickle stream clearer and reduces the chance of accidentally breaking the test payload.

This issue also appears on line 3431 of the same file.

        + b"q\x000"

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 21bc219377

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1970 to +1972
if any(value_contains_tracked_provenance(value) for value in stack if value is not marker) or any(
value_contains_tracked_provenance(value) for _key, value in canonical_batch_entries
):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve operands drained by malformed collection opcodes

Fresh evidence beyond the previously reported malformed SETITEMS case is a LIST, TUPLE, or DICT without a matching marker: pop_marked_tuple() drains its operands before this helper inspects stack, so a raw _PytorchStorageRef among those operands is never marked discarded. After an accepted 33-entry tensor batch, a raw storage PID followed by LIST, NONE, and STOP produces parse_complete=True, discarded_tracked_storage_references=False, and exact trust for every storage key; a malicious pickle placed beyond that member's 64 KiB expanded probe is therefore skipped. Pass the popped operands into this cleanup or inspect them before draining the stack.

AGENTS.md reference: AGENTS.md:L115-L117

Useful? React with 👍 / 👎.

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.

3 participants