From 271edcfb6edc4f52c549f9e05d9a46a637a9d48c Mon Sep 17 00:00:00 2001 From: "Hans J. Johnson" Date: Mon, 7 Sep 2026 07:28:27 -0500 Subject: [PATCH] metaUtils: clamp the chunk size and use one snapshot per operation MET_SetMaxChunkSize accepted values above zlib's uInt range. The codec loops cast the chunk size to uInt, so 4 GiB became 0: no input consumed, no output produced, and deflate looped without progress. Ignore values outside (0, uInt max]. Decompression re-read the global inside its inner loop while the outer loop used a snapshot, so one operation could run with two different limits. Use the snapshot in both places. Cover the corrupt trailer at a boundary chunk size as well as the default, so a regression in the drain path cannot pass, and assert that an oversized request is ignored. --- src/metaUtils.cxx | 9 ++--- src/metaUtils.h | 2 ++ .../testMeta15UncompressChunkBoundary.cxx | 36 +++++++++++++++++-- 3 files changed, 40 insertions(+), 7 deletions(-) diff --git a/src/metaUtils.cxx b/src/metaUtils.cxx index 670893a..a38100b 100644 --- a/src/metaUtils.cxx +++ b/src/metaUtils.cxx @@ -62,7 +62,9 @@ static std::streamoff MET_MaxChunkSize = 1024 * 1024 * 1024; void MET_SetMaxChunkSize(std::streamoff chunkSize) { - if (chunkSize > 0) + // zlib counts are uInt; a larger value truncates to 0 and the codec loops forever. + const std::streamoff maxRepresentable = static_cast(std::numeric_limits::max()); + if (chunkSize > 0 && chunkSize <= maxRepresentable) { MET_MaxChunkSize = chunkSize; } @@ -887,7 +889,7 @@ MET_PerformUncompression(const unsigned char * sourceCompressed, source_pos += d_stream.avail_in; do { - uInt cur_remain_chunk = static_cast(std::min(uncompressedDataSize - dest_pos, MET_MaxChunkSize)); + uInt cur_remain_chunk = static_cast(std::min(uncompressedDataSize - dest_pos, max_chunk_size)); d_stream.next_out = uncompressedData + dest_pos; d_stream.avail_out = cur_remain_chunk; err = inflate(&d_stream, Z_NO_FLUSH); @@ -905,8 +907,7 @@ MET_PerformUncompression(const unsigned char * sourceCompressed, } } while (d_stream.avail_out == 0); } while (err != Z_STREAM_END && err >= 0); - // The output buffer can fill before the trailer arrives in a later input - // chunk; keep feeding input so zlib can reach the CRC and report stream end. + // Keep feeding input after the output fills so zlib can reach the CRC and end the stream. unsigned char trailerScratch[1]; while (err == Z_BUF_ERROR && dest_pos == uncompressedDataSize) { diff --git a/src/metaUtils.h b/src/metaUtils.h index d8a0e11..a8f3b7d 100644 --- a/src/metaUtils.h +++ b/src/metaUtils.h @@ -340,6 +340,8 @@ MET_PerformCompression(const unsigned char * source, int compressionLevel); // Size of the input and output pieces the (de)compression loops work in. +// Process-wide and not synchronized: set it before any concurrent codec call. +// Values outside (0, uInt max] are ignored. METAIO_EXPORT void MET_SetMaxChunkSize(std::streamoff chunkSize); diff --git a/src/tests/testMeta15UncompressChunkBoundary.cxx b/src/tests/testMeta15UncompressChunkBoundary.cxx index 5b2d8d4..9d56585 100644 --- a/src/tests/testMeta15UncompressChunkBoundary.cxx +++ b/src/tests/testMeta15UncompressChunkBoundary.cxx @@ -1,5 +1,6 @@ #include #include +#include #include #include @@ -32,20 +33,47 @@ TestTrailerInLaterChunk(const std::vector & raw, const std::vecto return 0; } +// chunkSize <= 0 leaves the current setting alone, so the corrupt trailer is +// seen in the first input chunk; a boundary value routes it through the drain. static int -TestCorruptTrailerStillRejected(const std::vector & raw, std::vector compressed) +TestCorruptTrailerStillRejected(const std::vector & raw, + std::vector compressed, + std::streamoff chunkSize) { compressed[compressed.size() - 1] ^= 0xFF; + const std::streamoff savedChunkSize = MET_GetMaxChunkSize(); + MET_SetMaxChunkSize(chunkSize); + std::vector destination(raw.size(), 0); std::cerr << "--- expect an uncompression failure message below ---\n"; const bool accepted = MET_PerformUncompression(compressed.data(), static_cast(compressed.size()), destination.data(), static_cast(raw.size())); + MET_SetMaxChunkSize(savedChunkSize); + if (accepted) { - std::cerr << "FAILED: stream with a corrupt CRC trailer was accepted\n"; + std::cerr << "FAILED: stream with a corrupt CRC trailer was accepted (chunk size " << chunkSize << ")\n"; + return 1; + } + return 0; +} + +// An oversized request must be ignored rather than truncated to zero by the +// uInt casts in the codec loops, which would never make progress. +static int +TestOversizedChunkSizeIgnored() +{ + const std::streamoff savedChunkSize = MET_GetMaxChunkSize(); + MET_SetMaxChunkSize(static_cast(std::numeric_limits::max()) + 1); + const std::streamoff observed = MET_GetMaxChunkSize(); + MET_SetMaxChunkSize(savedChunkSize); + + if (observed != savedChunkSize) + { + std::cerr << "FAILED: an oversized chunk size was accepted (" << observed << ")\n"; return 1; } return 0; @@ -74,7 +102,9 @@ main(int, char *[]) int result = 0; result += TestTrailerInLaterChunk(raw, compressed); - result += TestCorruptTrailerStillRejected(raw, compressed); + result += TestCorruptTrailerStillRejected(raw, compressed, 0); + result += TestCorruptTrailerStillRejected(raw, compressed, static_cast(compressed.size()) - 4); + result += TestOversizedChunkSizeIgnored(); if (result == 0) {