Repository navigation
fix: don't pre-allocate the declared size in the legacy decoders - #991
Merged
Pr0methean merged 2 commits intoSep 16, 2026
Merged
Conversation
Shrink, Reduce and Implode pass the archive-declared uncompressed size straight to Vec::reserve, so an entry declaring 0xFFFFFFFFFFFFFFFF panics with capacity overflow from the public read API. The decode loops append and grow on their own, so the reserve is only a hint; clamping it keeps large legitimate entries working while removing the panic.
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
Address the test assertion gap and document or improve the large-entry preallocation behavior.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Prevents legacy ZIP decoders from panicking on enormous declared uncompressed sizes by bounding preallocation.
Changes:
- Adds a 1 MiB preallocation cap.
- Applies it to Shrink, Reduce, and Implode decoders.
- Adds oversized-declaration regression tests.
File summaries
| File | Summary |
|---|---|
tests/legacy_oversized_declared_size.rs |
Adds regression cases; should assert malformed entries return errors. |
src/legacy/shrink.rs |
Applies bounded preallocation. |
src/legacy/reduce.rs |
Applies bounded preallocation. |
src/legacy/mod.rs |
Defines the shared allocation cap, which may regress large-entry performance. |
src/legacy/implode.rs |
Applies bounded preallocation. |
Review details
Suppressed comments (1)
src/legacy/mod.rs:8
- This hard 1 MiB cap means every valid legacy entry larger than 1 MiB now grows the output incrementally and copies it during geometric reallocations, rather than using the exact-size preallocation. That is a potentially substantial regression for large legacy archives, so the claim that clamping costs nothing is inaccurate; please use or document a bounded estimate that preserves more normal-case preallocation (for example, one derived from the compressed input and decoder limits).
/// Upper bound on the speculative pre-allocation made from an entry's declared
/// uncompressed size. That size comes from the archive, so it is not a fact:
/// reserving it outright panics with "capacity overflow" past `isize::MAX` and
/// aborts on allocation failure below it. The decoders append to the buffer as
/// they go, so the reservation is only a hint and clamping it costs nothing.
pub(crate) const MAX_PREALLOC: usize = 1 << 20;
- Files reviewed: 5/5 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Review follow-up: the regression tests only printed the read result, so they would still pass if a decoder accepted the garbage payload. Assert Err. And the MAX_PREALLOC comment claimed clamping costs nothing, which is not true for valid entries above 1 MiB - they grow the buffer geometrically instead of in one step. Say that instead.
Pr0methean
approved these changes
Sep 16, 2026
Pr0methean
left a comment
Member
There was a problem hiding this comment.
Thanks; this fix looks good.
Merged
4 tasks done
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The three legacy decoders hand an entry's declared uncompressed size straight to
Vec::reserve, so a 200-byte archive that declares0xFFFF_FFFF_FFFF_FFFFbytes panics out of the public read API:Reached by
ZipArchive::by_index(0)thenread_to_end. TheStoredsibling on an otherwise identical archive returnsErr("Invalid checksum"), which is what the template's "all the crate's methods remain panic-free" line asks for.Clamping rather than rejecting, because the template also says ZIP bombs are not filtered: the decode loops (
while dst.len() < uncomp_len) append and grow on their own, so thereserveis only a hint and a large legitimate entry still decodes.Five sites, all with the same shape:
shrink.rs:234,shrink.rs:347,reduce.rs:107,implode.rs:97,implode.rs:214(line numbers fromgit show HEAD:).Verification
Base
75eec5b.capacity overflowin Shrink, Reduce, Implode;Storedpasses)shrink.rs:234alonereduce.rs:107aloneMAX_PREALLOC = usize::MAXEach single-site revert is caught by its own decoder's case, so no site is redundant. The
Storedcase passes in every row, which is what makes the other three a real divergence rather than a broken harness.cargo test --no-default-features --features legacy-zip: 28 result blocks, 0 failures, and the repo's owntests/legacy_zip.rsround-trips ofshrink.zip,reduce.zipandimplode.zipstill pass.cargo clippy --no-default-features --features legacy-zip --lib -- -D warningsclean;cargo doc --no-depsclean;cargo fmt --check --allclean.cargo clippy --all-targets -- -D warningsfails, but identically with my changes and on an unmodified tree:tests/data_descriptor.rs:27tripsclippy::bool_assert_comparisonon rustc 1.97.0. Not from this change.Not run here: the Windows and macOS legs, the MSRV 1.88 leg, and
--all-features. The change isusize::minon ausize.Possibly relevant to why this lasted:
fuzz/readdrives exactly this path,by_indexplusio::copy, butfuzz/Cargo.tomlpinszipwithdefault-features = falseand only azip-defaultknob, andlegacy-zipis not indefault. The fuzzer has never compiled these decoders.Two things I did not do.
MAX_PREALLOC = 1 << 20is a guess at the right ceiling; deriving it from the compressed length would be just as easy if you prefer that. AndImplodeDecoder::readandShrinkDecoder::readeach reserve and then call a helper that reserves the same value again, which looks redundant but is a separate change.Disclosure: written with the help of Claude (an AI assistant). Every number above is from runs on my machine.