Skip to content

Bound DEFLATE decoder output buffering to prevent unbounded memory growth (issue #90) - #92

Merged
sile merged 1 commit into
masterfrom
fix-issue-90-bounded-deflate-buffer
Sep 6, 2026
Merged

sile merged 1 commit into
masterfrom
fix-issue-90-bounded-deflate-buffer

Conversation

@sile

@sile sile commented Sep 6, 2026 •

Copy link
Copy Markdown
Owner

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 EndOfBlock and buffered the entire expansion before returning anything from read, so the internal buffer could grow without bound and exhaust memory.

Solution

  • Blocking decoder (src/deflate/decode.rs): the old block-reading path was split into enter_compressed_block (loads the Huffman tables) and read_compressed_block (decodes symbols until EOB or MAX_INTERNAL_BUFFER). A new block_decoder: Option<symbol::Decoder> field keeps the active Huffman decoder alive so decoding can resume exactly where it left off. read now loops over: drain the buffered output → resume a suspended block → read the next block header → EOF.
  • Non-blocking decoder (src/non_blocking/deflate/decode.rs): BlockDecoder::decode stops at the same 64 KiB threshold. The DecoderState::DecodeBlock arm now drains any unread decoded data first, keeping the buffer bounded while preserving the existing WouldBlock state-machine semantics.
  • Bound: MAX_INTERNAL_BUFFER is 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).
  • LZ77 correctness: sliding-window retention is unchanged. Lz77Decoder::truncate_old_buffer still 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, and unread_decoded_data are unchanged.

Validation

  • cargo test --workspace
  • cargo test --workspace --no-default-features
  • cargo clippy --lib --all-features -- -D warnings
  • cargo fmt --all -- --check

New 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.

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.
@sile
sile merged commit 171ed38 into master Sep 6, 2026
38 checks passed
@sile
sile deleted the fix-issue-90-bounded-deflate-buffer branch September 6, 2026 08:37
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.

DEFLATE decoder buffers unbounded output before read returns

1 participant