[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
Dependencies
None.
[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_hashespre-allocates each entry from the archive's declared size and then callsread_to_endwith no bound (vendor/common.rs#L310-L341):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:nuget_feed.rs#L266maven_repo.rs#L736and#L1464service_fetch.rs#L312prestage.rs#L292and#L598common.rs#L974The 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 overMAX_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.vendor/mod.rs#L1158-L1171repeats the same cap.Reproduced twice on main @
1169ae6with an in-crate unit test (not committed). A 696 KiB zip holds one deflated entry of 700 MiB of zeros, under the matching key. Callingzip_bytes_match_after_hasheson it raises the process peak RSS (VmHWM) from 21 MiB to 726 MiB before it returnsfalse. A committed.nupkgwell under the 512 MiB file cap can therefore inflate ~1000× during avendororscanre-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
vendor/common.rs, for exampleread_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.zip_bytes_match_after_hashesand in bothvendor/mod.rssites. Delete the nestedcappedfn and the inline copy.MAX_FILE_BYTES, 64 MiB). An over-cap entry returnsfalse(out of sync), which is the fail-safe answer.Size and scope
vendor/common.rsandvendor/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
Vec::with_capacity(entry.size()…)without a cap check remains invendor/.zip_bytes_match_after_hashesreturnfalsewithout allocating past the cap. Another test covers a zip whose entry declares a size over the cap.Dependencies
None.