Skip to content

fix: avoid stack overflow when decoding many DEFLATE blocks (#88) - #89

Merged
sile merged 3 commits into
masterfrom
fix-issue-88
Jul 25, 2026
Merged

fix: avoid stack overflow when decoding many DEFLATE blocks (#88)#89
sile merged 3 commits into
masterfrom
fix-issue-88

Conversation

@sile

@sile sile commented Jul 25, 2026

Copy link
Copy Markdown
Owner

Fixes #88.

impl Read for Decoder in both deflate and gzip::MultiDecoder was
written with self-recursive tail calls that advance one DEFLATE block
(or gzip member) per invocation. Rust does not guarantee tail-call
elimination, so a stream carrying many consecutive empty stored blocks
— or a gzip file with many members — can exhaust the thread stack and
abort the process. Both are rewritten as loops that reuse one stack
frame; every branch preserves the original semantics.

The other Read impls in the crate (zlib::Decoder, gzip::Decoder,
non_blocking::{deflate,gzip,zlib}::Decoder) do not contain the same
pattern, so no further changes are needed.

The reproduction payload and the loop-based fix were provided by
@michael-weigelt in the issue attachments; landed here (with minor
polish) after independent review.

sile added 3 commits July 25, 2026 19:23
`Read for Decoder` is defined with self-recursive calls in tail position.
Rust does not guarantee tail-call elimination, so a DEFLATE stream that
chains many empty stored blocks makes the recursion deep enough to
overflow the thread stack.

The new test builds such a stream (250_000 empty non-final stored
blocks followed by a small final block) and decodes it via the gzip
decoder; on master the process aborts with `has overflowed its stack`.
Both `deflate::Decoder::read` and `gzip::Decoder::read` were written as
self-recursive tail calls that advance the stream one DEFLATE block (or
one gzip member) per invocation. Rust does not guarantee tail-call
elimination, so a stream carrying enough consecutive empty stored
blocks — or a multi-member gzip file — can exhaust the thread stack and
abort the process. Rewrite both methods as `loop`s that reuse the same
stack frame, matching the pattern in the rest of the crate.

Also gate the `make_large_deflate_stream` test helper on the `std`
feature so `cargo test --no-default-features` still compiles.
- Rename `decode_large_deflate_stream` to `test_issue_88` to match the
  repo's naming convention for regression tests (`test_issue_64`,
  `test_issues_3`).
- Drop the redundant `pub` on the test-only helper.
- Hoist the WASM payload constant to module scope so the test and the
  helper share a single definition instead of two identical copies.
- Refresh the helper's docstring so it no longer implies the block
  decoder is still recursive.
- Guard `blocks - 1` with a `debug_assert!` so misuse (`blocks == 0`)
  fails loudly instead of wrap-then-OOM.
@sile
sile merged commit 816ad8e into master Jul 25, 2026
38 checks passed
@sile
sile deleted the fix-issue-88 branch July 25, 2026 12:41
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.

Stack overflow during decoding

1 participant