From f9c9c683823ffc6fa6c773b7c015a855884baa99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ey=C3=BCp=20Can=20Akman?= Date: Fri, 6 Nov 2026 13:54:32 +0300 Subject: [PATCH 1/2] seekable_format: Reject offsets past the end in decompress ZSTD_seekable_decompress() limited len with offset + len > eos. An offset well past the end made eos - offset underflow into a huge byte count that ZSTD_isError() did not flag. An offset + len that wrapped around skipped the limit, and the loop never exited. Return parameter_outOfBound for an offset past the end, and compare len against eos - offset. Reading at the end still returns 0. --- .../seekable_format/tests/seekable_tests.c | 56 +++++++++++++++++++ contrib/seekable_format/zstdseek_decompress.c | 5 +- 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/contrib/seekable_format/tests/seekable_tests.c b/contrib/seekable_format/tests/seekable_tests.c index b535f5571ef..5fd9a51a80c 100644 --- a/contrib/seekable_format/tests/seekable_tests.c +++ b/contrib/seekable_format/tests/seekable_tests.c @@ -388,6 +388,62 @@ int main(int argc, const char** argv) } printf("Success!\n"); + printf("Test %u - decompress at and past the end of the data: ", testNb++); + { char const inBuffer[] = "Lorem ipsum dolor sit amet"; + size_t const inSize = sizeof(inBuffer); + + size_t const seekCapacity = 5000; + void* const seekBuffer = malloc(seekCapacity); + assert(seekBuffer != NULL); + size_t seekSize; + + char outBuffer[16]; + + ZSTD_seekable_CStream* const zscs = ZSTD_seekable_createCStream(); + assert(zscs != NULL); + { size_t const initStatus = ZSTD_seekable_initCStream(zscs, 9, 0 /* checksumFlag */, 0 /* maxFrameSize */); + assert(!ZSTD_isError(initStatus)); + } + + { ZSTD_outBuffer outb = { .dst=seekBuffer, .pos=0, .size=seekCapacity }; + ZSTD_inBuffer inb = { .src=inBuffer, .pos=0, .size=inSize }; + + size_t const cStatus = ZSTD_seekable_compressStream(zscs, &outb, &inb); + assert(!ZSTD_isError(cStatus)); + assert(inb.pos == inb.size); + + size_t const endStatus = ZSTD_seekable_endStream(zscs, &outb); + assert(!ZSTD_isError(endStatus)); + seekSize = outb.pos; + } + + ZSTD_seekable* const stream = ZSTD_seekable_create(); + assert(stream != NULL); + { size_t const initStatus = ZSTD_seekable_initBuff(stream, seekBuffer, seekSize); + assert(!ZSTD_isError(initStatus)); } + + { size_t const atEnd = ZSTD_seekable_decompress(stream, outBuffer, sizeof(outBuffer), inSize); + size_t const pastEnd = ZSTD_seekable_decompress(stream, outBuffer, sizeof(outBuffer), inSize + 5000); + int failed = atEnd != 0 || !ZSTD_isError(pastEnd); + if (!failed) { + /* offset + len wraps around to 0 */ + size_t const wrapped = ZSTD_seekable_decompress(stream, outBuffer, sizeof(outBuffer), 0ULL - sizeof(outBuffer)); + failed = !ZSTD_isError(wrapped); + } + if (failed) { + free(seekBuffer); + ZSTD_seekable_freeCStream(zscs); + ZSTD_seekable_free(stream); + goto _test_error; + } + } + + free(seekBuffer); + ZSTD_seekable_freeCStream(zscs); + ZSTD_seekable_free(stream); + } + printf("Success!\n"); + /* TODO: Add more tests */ printf("Finished tests\n"); return 0; diff --git a/contrib/seekable_format/zstdseek_decompress.c b/contrib/seekable_format/zstdseek_decompress.c index dc66c35f7f9..352fbb5e863 100644 --- a/contrib/seekable_format/zstdseek_decompress.c +++ b/contrib/seekable_format/zstdseek_decompress.c @@ -496,8 +496,9 @@ size_t ZSTD_seekable_initAdvanced(ZSTD_seekable* zs, ZSTD_seekable_customFile sr size_t ZSTD_seekable_decompress(ZSTD_seekable* zs, void* dst, size_t len, unsigned long long offset) { unsigned long long const eos = zs->seekTable.entries[zs->seekTable.tableLen].dOffset; - if (offset + len > eos) { - len = eos - offset; + if (offset > eos) return ERROR(parameter_outOfBound); + if (len > eos - offset) { + len = (size_t)(eos - offset); } U32 targetFrame = ZSTD_seekable_offsetToFrameIndex(zs, offset); From 6c93b99d29953e4557196f85e500b3c5aae70725 Mon Sep 17 00:00:00 2001 From: Yann Collet Date: Mon, 9 Nov 2026 11:40:54 -0800 Subject: [PATCH 2/2] seekable_format: document ZSTD_seekable_decompress() at the end of the data The return value is smaller than dstSize when the data ends first, 0 when offset is the end of the data, and an error past it. --- contrib/seekable_format/zstd_seekable.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/contrib/seekable_format/zstd_seekable.h b/contrib/seekable_format/zstd_seekable.h index b1f83d0e0a1..e24e4e33edd 100644 --- a/contrib/seekable_format/zstd_seekable.h +++ b/contrib/seekable_format/zstd_seekable.h @@ -152,6 +152,9 @@ ZSTDLIB_API size_t ZSTD_seekable_writeSeekTable(ZSTD_frameLog* fl, ZSTD_outBuffe * consecutive range of data, it will efficiently retain the decompressor object * and avoid redecompressing frame prefixes. The return value is the number of * bytes decompressed, or an error code checkable with ZSTD_isError(). +* It is smaller than `dstSize` only when the data ends before +* `offset + dstSize`, and it is 0 when `offset` is the end of the data. +* An `offset` past the end of the data is an error. * * The seek table access functions can be used to obtain the data contained * in the seek table. If frameIndex is larger than the value returned by