Skip to content

fix: reject truncated zstd input in buffered streaming decompression - #313

Open
detail-app[bot] wants to merge 1 commit into
masterfrom
detail/bug-fix/fix-reject-truncated-zstd-input-in-buffered-stream-6b7d98
Open

detail-app[bot] wants to merge 1 commit into
masterfrom
detail/bug-fix/fix-reject-truncated-zstd-input-in-buffered-stream-6b7d98

Conversation

@detail-app

@detail-app detail-app Bot commented Sep 16, 2026

Copy link
Copy Markdown

Detail bug report: View on Detail

Fixes ENG-714

Bug

decompressZstdStreaming in src/pipeline/compress_buffered.zig (the cold-path §6.5 fallback and the test oracle for the streaming codecs in encoding.zig) silently returned truncated plaintext as a successful result when its compressed input ran out mid-frame. The caller got a byte-exact prefix of the real plaintext (possibly empty) back as a []u8, indistinguishable from a complete, valid decompression — a latent silent-data-corruption defect.

Root cause: ZSTD_decompressStream returns 0 only when a frame is fully decoded, an error code on corruption (caught by the existing ZSTD_isError guard), or a positive non-error hint meaning more input is needed. The streaming loop only breaked on result == 0, so when input was exhausted while the last return was >0, the while condition went false and control fell through to allocator.realloc(decompressed, out_buffer.pos) returning whatever completed blocks had been flushed. This branch is reached whenever ZSTD_getFrameContentSize == ZSTD_CONTENTSIZE_UNKNOWN, which is every frame the in-tree streaming ZstdCompressor produces (it never pledges a source size). The sibling decompressGzip already handled the analogous case via its Z_BUF_ERROR branch; the zstd truncation case was simply not guarded.

Introduced in 465f1bd (PR #5).

Fix

Added a frame_complete flag to decompressZstdStreaming. The loop sets it on the result == 0 (frame complete) path; after the loop, if the frame was never observed complete, the function returns error.DecompressionFailed instead of returning a partial buffer. This mirrors the existing decompressGzip Z_BUF_ERROR truncation handling — zstd signals "input exhausted but frame incomplete" via a positive non-error hint rather than an error code, which is exactly the gap this guard closes. No other behavior is changed.

Testing

  • Added two inline regression tests:
    • src/pipeline/encoding.zig "buffered zstd oracle rejects truncated streaming-encoded frame" — the realistic repro: stream-encodes 300 kB via the in-tree ZstdCompressor (which yields ZSTD_CONTENTSIZE_UNKNOWN frames → the streaming branch), then truncates the frame by 1, 16, 64, and len/2 bytes at both a fine (1 B) and coarse (4096 B) chunk size, asserting error.DecompressionFailed for each and a byte-exact round-trip when complete.
    • src/pipeline/compress_buffered.zig "decompressZstd rejects truncated frame with known content size" — a contrast guard for the one-shot (known-size) branch, asserting header-truncated input returns error.InvalidCompressedData and a complete frame round-trips; confirms the safe sibling subpath is unchanged.
  • zig build test --summary all passes (test success, 521 pass / 1 skip / 0 fail, 0 error logs). The known-size contrast test targets the log-free header-validation subpath so the run stays std.log.err-free (Zig 0.16's default test runner fails on any unhandled err-level log).
  • Mutation check: temporarily neutralizing the new guard (if (!frame_complete)if (false)) makes the streaming repro test fail with expected error.DecompressionFailed, found { … } (a partial plaintext prefix returned as success) — confirming the test genuinely regresses the bug; restoring the guard returns it to green.
  • Routine checks pass: zig build, zig fmt --check (changed files + build.zig), and ziglint (no violations).
  • Verified out-of-tree that every streaming-branch truncation length I tested returns zstd's positive non-error hint (not a ZSTD_isError error), so the new guard — not the pre-existing error/log branch — is what rejects truncated input, keeping both behavior and logs correct. Also verified out-of-tree that the sibling decompressGzip Z_BUF_ERROR truncation handling is unchanged (the fix does not touch the gzip path).

Automatic Fixes PRs can be configured here.

@macroscopeapp

macroscopeapp Bot commented Sep 16, 2026

Copy link
Copy Markdown

Approvability

Verdict: Approved at 7ba5c46

Macroscope's review found this PR approvable — This is a small, self-contained fix that rejects incomplete unknown-size Zstandard frames instead of returning partial plaintext. Valid decompression behavior is preserved, and the added tests cover both streaming truncation and existing known-size handling.

You can add or adjust custom eligibility rules. Learn more.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant