Skip to content

metaUtils: report uncompression failures to the caller#142

Merged
dzenanz merged 2 commits into
Kitware:masterfrom
hjmjohnson:fix-compressed-read-errors
Jul 22, 2026
Merged

metaUtils: report uncompression failures to the caller#142
dzenanz merged 2 commits into
Kitware:masterfrom
hjmjohnson:fix-compressed-read-errors

Conversation

@hjmjohnson

Copy link
Copy Markdown
Contributor

A truncated or corrupt compressed stream is read as a successful image. MET_PerformUncompression always returns true, and neither MetaImage nor MetaArray inspects the result.

Found while auditing MetaIO as vendored by ITK (InsightSoftwareConsortium/ITK#6575, items B65 and B66). New test testMeta14ImageCompressed covers both shapes and fails before this change.

The byte accounting had to be fixed first

MET_PerformUncompression ends in an unconditional return true; — every error path merely breaks out of the loop. The natural success criterion is "did we produce the expected number of bytes?", but dest_pos could not answer that:

err = inflate(&d_stream, Z_NO_FLUSH);
if (err == Z_STREAM_END || err < 0)
{
  ...
  break;                                             // <-- leaves before counting
}
uInt count_uncompressed = cur_remain_chunk - d_stream.avail_out;
dest_pos += count_uncompressed;

The output of the final inflate call — the one returning Z_STREAM_END — was never added, so a stream that decompressed in a single pass left dest_pos at 0. Counting each call's output before acting on its status makes dest_pos a true total, after which requiring dest_pos == uncompressedDataSize is a sound check. Both call sites now honour the result.

Compressed size absent from the header

When CompressedDataSize / CompressedElementDataSize is missing, the readers took the whole file as the payload:

_fstream->seekg(0, std::ios::end);
m_CompressedDataSize = _fstream->tellg();
_fstream->seekg(0, std::ios::beg);      // <-- start of the FILE

For a single-file (LOCAL) image the file begins with the ASCII header, so the header text itself was handed to inflate. Measuring from the current position — where the element data actually begins — is correct for both the LOCAL and separate-data-file cases, since the latter is already positioned at 0.

MetaArray::M_ReadElements carried the same assumption, and additionally never freed its compressed buffer; both are fixed here.

Testing

testMeta14ImageCompressed writes a compressed LOCAL image, strips CompressedDataSize from the header, and requires the data to read back correctly; then truncates the stream and requires the read to fail.

Verified fail-before/pass-after — before the change the first case reports element 1 is 0, expected 1, i.e. a silently zero-filled buffer returned as success. Full suite 13/13 passing.

Also run through ITK's MetaImageIO with no regressions in its Meta test suite.

MET_PerformUncompression always returned true, and neither MetaImage nor
MetaArray looked at the result, so a truncated or corrupt stream produced a
partly written buffer and a successful read.

Its byte accounting also could not support a success check: dest_pos was
advanced after the test that breaks out of the loop, so the output of the
final inflate call -- the one reporting Z_STREAM_END -- was never counted,
leaving dest_pos at 0 for a stream that decompressed in one pass. Count each
call's output before acting on its status, then require that the expected
number of bytes was produced.

When the compressed size is absent from the header, the readers assumed the
data began at the start of the file. For a single-file image that is the
start of the ASCII header, which was then fed to inflate. Measure from the
current position instead, which is where the element data actually begins.

MetaArray::M_ReadElements also leaked its compressed buffer.
@hjmjohnson

Copy link
Copy Markdown
Contributor Author

@dzenanz Fixes before updating the ITK vendored copy two PR's pushed upstream:

MetaIO PRs are on Kitware/MetaIO:

@dzenanz

dzenanz commented Jul 22, 2026

Copy link
Copy Markdown
Member

Consider addressing #143, to have a reliable test here before integrating into ITK/VTK.

@dzenanz
dzenanz merged commit 8c41a1d into Kitware:master Jul 22, 2026
1 of 2 checks passed
dzenanz pushed a commit that referenced this pull request Jul 23, 2026
MET_PerformUncompression validated the number of bytes produced but not
that the zlib stream terminated with Z_STREAM_END. When the output fills
the destination buffer exactly, the next inflate() returns Z_BUF_ERROR
(deliberately excluded from the diagnostic), the loop exits, and the
byte-count check passes -- so a stream truncated at the size boundary is
accepted without validating its trailing Adler-32 checksum.

Reject the data unless inflate reported Z_STREAM_END, in addition to the
existing byte-count check. This closes the exact-size truncation gap left
by the byte-count check added in #142.

Fixes #144.
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