Skip to content

zip_bytes_match_after_hashes inflates committed .nupkg/.jar entries with no size cap #569

Description

[agent] Filed by the scheduled architecture audit routine (CLI and core). Register: discussion #560 register.

Kind: bug. Source: §1 #1; Part 5.5; register C01.

Problem

zip_bytes_match_after_hashes pre-allocates each entry from the archive's declared size and then calls read_to_end with no bound (vendor/common.rs#L310-L341):

let mut content = Vec::with_capacity(entry.size() as usize);
if entry.read_to_end(&mut content).is_err() {

The input is a committed, user-tamperable artifact (or a service archive). Only the compressed file size is capped, at 512 MiB, by read_zip_artifact (common.rs#L283-L304). The function's callers:

The same zip-entry read already exists twice in capped form, so the copies have drifted:

  • harvest_zip_blobs::capped (vendor/mod.rs#L528-L540) refuses a declared size over MAX_FILE_BYTES (64 MiB) and bounds the read with .take(MAX_FILE_BYTES + 1). Its comment explains why: the declared size is attacker-controlled, and the reader is bounded only by the compressed size.
  • An inline copy at vendor/mod.rs#L1158-L1171 repeats the same cap.

Reproduced twice on main @ 1169ae6 with an in-crate unit test (not committed). A 696 KiB zip holds one deflated entry of 700 MiB of zeros, under the matching key. Calling zip_bytes_match_after_hashes on it raises the process peak RSS (VmHWM) from 21 MiB to 726 MiB before it returns false. A committed .nupkg well under the 512 MiB file cap can therefore inflate ~1000× during a vendor or scan re-run.

Symptoms

None filed.

Impact

A committed artifact can exhaust memory (DoS) on a CI runner during vendored NuGet/Maven re-runs and service-archive verification. The fix is small.

Proposed change

  • Hoist one capped zip-entry reader into vendor/common.rs, for example read_zip_entry_capped(entry, cap) -> Option<Vec<u8>>. It refuses a declared size over the cap, reads through .take(cap + 1), and rejects any overflow.
  • Use it in zip_bytes_match_after_hashes and in both vendor/mod.rs sites. Delete the nested capped fn and the inline copy.
  • Use one per-entry cap constant (MAX_FILE_BYTES, 64 MiB). An over-cap entry returns false (out of sync), which is the fail-safe answer.

Size and scope

vendor/common.rs and vendor/mod.rs, ~40 production lines plus tests. Out of scope: unifying the 512/256/128 MiB archive caps (C01's second half, part of C15/C21).

Acceptance criteria

  • One capped zip-entry reader, used by all three sites. No Vec::with_capacity(entry.size()…) without a cap check remains in vendor/.
  • Regression test: a small zip whose entry inflates past the cap makes zip_bytes_match_after_hashes return false without allocating past the cap. Another test covers a zip whose entry declares a size over the cap.
  • The existing NuGet/Maven vendored, prestage and service-fetch tests stay green.

Dependencies

None.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    agent:triagedarch-auditFiled by a scheduled architecture audit routine (see the architecture review discussion)bugSomething isn't workingpriority:p3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions