fix: reject truncated zstd input in buffered streaming decompression - #313
Open
detail-app[bot] wants to merge 1 commit into
Open
detail-app[bot] wants to merge 1 commit into
detail-app[bot] wants to merge 1 commit into
Conversation
ApprovabilityVerdict: Approved at 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. |
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.
Detail bug report: View on Detail
Fixes ENG-714
Bug
decompressZstdStreaminginsrc/pipeline/compress_buffered.zig(the cold-path §6.5 fallback and the test oracle for the streaming codecs inencoding.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_decompressStreamreturns0only when a frame is fully decoded, an error code on corruption (caught by the existingZSTD_isErrorguard), or a positive non-error hint meaning more input is needed. The streaming loop onlybreaked onresult == 0, so when input was exhausted while the last return was>0, thewhilecondition went false and control fell through toallocator.realloc(decompressed, out_buffer.pos)returning whatever completed blocks had been flushed. This branch is reached wheneverZSTD_getFrameContentSize == ZSTD_CONTENTSIZE_UNKNOWN, which is every frame the in-tree streamingZstdCompressorproduces (it never pledges a source size). The siblingdecompressGzipalready handled the analogous case via itsZ_BUF_ERRORbranch; the zstd truncation case was simply not guarded.Introduced in 465f1bd (PR #5).
Fix
Added a
frame_completeflag todecompressZstdStreaming. The loop sets it on theresult == 0(frame complete) path; after the loop, if the frame was never observed complete, the function returnserror.DecompressionFailedinstead of returning a partial buffer. This mirrors the existingdecompressGzipZ_BUF_ERRORtruncation 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
src/pipeline/encoding.zig"buffered zstd oracle rejects truncated streaming-encoded frame"— the realistic repro: stream-encodes 300 kB via the in-treeZstdCompressor(which yieldsZSTD_CONTENTSIZE_UNKNOWNframes → the streaming branch), then truncates the frame by 1, 16, 64, andlen/2bytes at both a fine (1 B) and coarse (4096 B) chunk size, assertingerror.DecompressionFailedfor 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 returnserror.InvalidCompressedDataand a complete frame round-trips; confirms the safe sibling subpath is unchanged.zig build test --summary allpasses (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 staysstd.log.err-free (Zig 0.16's default test runner fails on any unhandlederr-level log).if (!frame_complete)→if (false)) makes the streaming repro test fail withexpected error.DecompressionFailed, found { … }(a partial plaintext prefix returned as success) — confirming the test genuinely regresses the bug; restoring the guard returns it to green.zig build,zig fmt --check(changed files +build.zig), andziglint(no violations).ZSTD_isErrorerror), 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 siblingdecompressGzipZ_BUF_ERRORtruncation handling is unchanged (the fix does not touch the gzip path).Automatic Fixes PRs can be configured here.