Bound DEFLATE decoder output buffering to prevent unbounded memory growth (issue #90) - #92
Merged
Merged
Conversation
DEFLATE compressed blocks have no maximum expanded size. Decoding a single highly-compressible block used to buffer the entire expansion in memory before the first read() returned, which could exhaust memory on untrusted input. The blocking and non-blocking DEFLATE decoders now yield output once the internal unread buffer reaches 64 KiB, keeping a live Huffman decoder so decoding can resume mid-block. Public method signatures are unchanged.
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.
Summary
Internally buffer the DEFLATE decoder's output in bounded chunks. Both the blocking and non-blocking decoders now stop decoding a compressed block once the unread decoded buffer reaches 64 KiB, then yield output to the caller. This prevents a single, highly-compressible block from growing the internal buffer without limit on untrusted input.
Closes #90
Problem
DEFLATE streams have no maximum expanded block size. A single highly-compressible block can expand to an arbitrarily large amount of output within one block. Previously, the decoders looped until
EndOfBlockand buffered the entire expansion before returning anything fromread, so the internal buffer could grow without bound and exhaust memory.Solution
src/deflate/decode.rs): the old block-reading path was split intoenter_compressed_block(loads the Huffman tables) andread_compressed_block(decodes symbols until EOB orMAX_INTERNAL_BUFFER). A newblock_decoder: Option<symbol::Decoder>field keeps the active Huffman decoder alive so decoding can resume exactly where it left off.readnow loops over: drain the buffered output → resume a suspended block → read the next block header → EOF.src/non_blocking/deflate/decode.rs):BlockDecoder::decodestops at the same 64 KiB threshold. TheDecoderState::DecodeBlockarm now drains any unread decoded data first, keeping the buffer bounded while preserving the existingWouldBlockstate-machine semantics.MAX_INTERNAL_BUFFERis 64 KiB. Since a single symbol emits at most 258 bytes (lz77::MAX_LENGTH), the buffer can overshoot by at most 257 bytes before yielding (total ≤MAX_INTERNAL_BUFFER + 258).Lz77Decoder::truncate_old_bufferstill only fires when the unread buffer is empty, so backward references remain valid across a suspend/resume boundary.Public API
No public API changes.
new,as_inner_ref,as_inner_mut,into_inner, andunread_decoded_dataare unchanged.Validation
cargo test --workspacecargo test --workspace --no-default-featurescargo clippy --lib --all-features -- -D warningscargo fmt --all -- --checkNew regression tests:
test_issue_90_bounded_buffering(blocking): an 800 KiB single-block input, asserting the unread buffer never exceeds the bound and the output is byte-exact.issue_90_byte_exact_resume_across_threshold(non-blocking): asserts both byte-exact output and the bound.Credits / Acknowledgment
Thanks to @optiklab for reporting the issue and for their proposed patch. While I did not adopt their diff verbatim — the implementation here differs in structure and keeps the constant local to each decode module — their report and work were instrumental in confirming the approach and the 64 KiB threshold.