Skip to content

metaUtils: consume the trailer when the output buffer fills first - #147

Merged
dzenanz merged 1 commit into
Kitware:masterfrom
hjmjohnson:fix-uncompress-deferred-trailer
Sep 4, 2026
Merged

metaUtils: consume the trailer when the output buffer fills first#147
dzenanz merged 1 commit into
Kitware:masterfrom
hjmjohnson:fix-uncompress-deferred-trailer

Conversation

@hjmjohnson

Copy link
Copy Markdown
Contributor

Fixes a regression I introduced in #145: MET_PerformUncompression rejects valid compressed data when the gzip trailer lands in a different input chunk than the last data byte.

Reported by an automated review on the ITK side (InsightSoftwareConsortium/ITK#6835), where #145 arrived via the MetaIO sync.

Root cause

The decompression loop feeds input in MET_MaxChunkSize (1 GiB) pieces. When the output buffer is exactly full and the chunk boundary falls inside the 8-byte trailer, the next inflate call has no output capacity and returns Z_BUF_ERROR. The outer loop

} while (err != Z_STREAM_END && err >= 0);

treats that as terminal even though input remains, so the trailer is never consumed. The Z_STREAM_END requirement added in #145 then rejects a payload that was fully and correctly decompressed.

The inner loop already documents this return as benign:

if (err != Z_STREAM_END && err != Z_BUF_ERROR) // Z_BUF_ERROR means there is still data to uncompress,
{                                              // but no space left in buffer; non-fatal

so the post-loop check contradicted a comment three lines above it. The underlying loop behavior predates #145; requiring Z_STREAM_END only made it observable.

The fix, and why not simply drop the check

zlib validates the CRC32 trailer only at stream end, so removing the requirement would restore a hole where a payload with a corrupt trailer is accepted whenever the byte count happens to match — the dest_pos != uncompressedDataSize test catches truncation but not corruption.

Instead, keep feeding the remaining input into a one-byte scratch output until zlib reports Z_STREAM_END or a genuine error. Valid deferred-trailer streams are accepted; corrupt and truncated ones are still rejected.

Verification

The trigger is narrow: crossing the 1 GiB boundary is not sufficient, since inflate returns Z_STREAM_END at an exactly-full output buffer as long as the trailer is in the same input chunk. A 1216502568-byte stream was accepted unchanged. The boundary must fall inside the trailer.

Binary-searching the raw size produced that alignment — raw=1018073420, compressed=1073741829, five trailer bytes in the second chunk:

Case before after
Trailer split across the chunk boundary rejected, zlib error -5 (content correct) accepted, content matches
Clean stream accepted accepted
Corrupt CRC trailer rejected rejected (-3)
Truncated stream rejected rejected (-5)

Behavior was exercised against a build of this function in ITK (which vendors zlib-ng); this branch also compiles clean in a standalone MetaIO build.

MET_PerformUncompression rejects valid compressed data when the gzip
trailer falls in a different input chunk than the last data byte. The
decompression loop feeds input in MET_MaxChunkSize (1 GiB) pieces; if
the output buffer is exactly full when a chunk boundary lands inside the
8-byte trailer, the next inflate call has no output capacity, returns
Z_BUF_ERROR, and the outer loop treats that as terminal even though
input remains. The stream-end requirement then rejects a payload that
was fully and correctly decompressed.

Keep feeding the remaining input into a one-byte scratch buffer until
zlib reports Z_STREAM_END or a real error, so the CRC is still verified
and corrupt or truncated streams are still rejected.

Verified against a 1073741829-byte compressed stream, aligned so five
trailer bytes fall in the second input chunk: rejected with zlib error
-5 before this change, accepted with matching content after. Corrupt-CRC
and truncated inputs remain rejected.

Assisted-by: Claude Code -- reproduction, alignment search, and verification

@dzenanz dzenanz left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good on a glance.

@hjmjohnson
hjmjohnson marked this pull request as ready for review September 4, 2026 20:27
@hjmjohnson
hjmjohnson marked this pull request as draft September 4, 2026 20:40
@hjmjohnson
hjmjohnson marked this pull request as ready for review September 4, 2026 22:55
@hjmjohnson

Copy link
Copy Markdown
Contributor Author

@dzenanz would you review and merge this one? I don't have merge rights here.

CI note: the red CircleCI check is pre-existing and unrelated to this change. GitHub Actions (Ubuntu 24.04), which builds and runs the suite, passes.

Evidence that CircleCI is failing project-wide

Every recent master build fails the same way:

build branch outcome
275 pull/148 success
274 pull/147 (this) failed
273 master failed
272 pull/146 failed
271 master failed
269 master failed
267 master failed
265 pull/141 failed

The step breakdown is identical on master and here:

'Spin up environment':             success
'Preparing environment variables': success
'Checkout code':                   success
'/usr/src/MetaIO/test/ci/test.sh': canceled

The job reports failed_reason: None and the step is canceled rather than failed, which points at the Docker-based test/ci/test.sh timing out or being unable to start — not at a test result. Worth a separate look at some point; it currently shows red on every PR the project receives.

Companion PR #148 adds a regression test for this defect. It depends on this one — please merge this first, then I will rebase #148 on top of it.

@dzenanz
dzenanz merged commit 8bdcd44 into Kitware:master Sep 4, 2026
1 of 2 checks passed
dzenanz pushed a commit to hjmjohnson/MetaIO that referenced this pull request Sep 4, 2026
…e trailer case

MET_MaxChunkSize was a file-local constexpr of 1 GiB, so the input-chunking
path in MET_PerformCompression and MET_PerformUncompression could only be
reached by a test allocating several gigabytes. Expose MET_SetMaxChunkSize
and MET_GetMaxChunkSize so the boundary can be placed where a test needs it,
following the existing META_DEBUG precedent for a settable diagnostic knob.
Values of zero or less are ignored.

testMeta15UncompressChunkBoundary uses this to put a chunk boundary inside
the 8-byte gzip trailer with 64 KiB of data, and asserts both that a valid
stream is accepted and that a corrupt CRC trailer is still rejected. The
test runs in 0.01 s.

The accepted case fails on master today; it passes with the fix in Kitware#147.
This change is independent of that one and can merge in either order.

Assisted-by: Claude Code -- test design and verification
hjmjohnson added a commit to hjmjohnson/MetaIO that referenced this pull request Sep 4, 2026
…e trailer case

MET_MaxChunkSize was a file-local constexpr of 1 GiB, so the input-chunking
path in MET_PerformCompression and MET_PerformUncompression could only be
reached by a test allocating several gigabytes. Expose MET_SetMaxChunkSize
and MET_GetMaxChunkSize so the boundary can be placed where a test needs it,
following the existing META_DEBUG precedent for a settable diagnostic knob.
Values of zero or less are ignored.

testMeta15UncompressChunkBoundary uses this to put a chunk boundary inside
the 8-byte gzip trailer with 64 KiB of data, and asserts both that a valid
stream is accepted and that a corrupt CRC trailer is still rejected. The
test runs in 0.01 s.

The accepted case fails on master today; it passes with the fix in Kitware#147.
This change is independent of that one and can merge in either order.

Assisted-by: Claude Code -- test design and verification
dzenanz pushed a commit that referenced this pull request Sep 5, 2026
…e trailer case

MET_MaxChunkSize was a file-local constexpr of 1 GiB, so the input-chunking
path in MET_PerformCompression and MET_PerformUncompression could only be
reached by a test allocating several gigabytes. Expose MET_SetMaxChunkSize
and MET_GetMaxChunkSize so the boundary can be placed where a test needs it,
following the existing META_DEBUG precedent for a settable diagnostic knob.
Values of zero or less are ignored.

testMeta15UncompressChunkBoundary uses this to put a chunk boundary inside
the 8-byte gzip trailer with 64 KiB of data, and asserts both that a valid
stream is accepted and that a corrupt CRC trailer is still rejected. The
test runs in 0.01 s.

The accepted case fails on master today; it passes with the fix in #147.
This change is independent of that one and can merge in either order.

Assisted-by: Claude Code -- test design and verification
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.

2 participants