From ddafdb1b9bc69177c70fee614639b9e91ced776f Mon Sep 17 00:00:00 2001 From: Chieler Date: Thu, 15 Oct 2026 13:54:35 -0500 Subject: [PATCH] Drop single-use prefix in ZSTD_DCtx_reset if frame started When decompressing a malformed frame header using a single-use prefix (ZSTD_DCtx_refPrefix), decompression aborts before ZSTD_getDDict() can consume the dictionary. A subsequent ZSTD_DCtx_reset(session_only) reset streamStage back to zdss_init without clearing dictUses == ZSTD_use_once, allowing the discarded prefix buffer to leak into subsequent frames and causing heap use-after-free if the caller freed the prefix buffer. Clear the prefix during session reset if a frame decompression was already initiated (dctx->streamStage != zdss_init). Add regression tests in zstreamtest to verify that the prefix is dropped after a bad header and preserved if reset occurs prior to ingesting input. Fixes #4843 --- lib/decompress/zstd_decompress.c | 4 ++++ tests/zstreamtest.c | 33 ++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/lib/decompress/zstd_decompress.c b/lib/decompress/zstd_decompress.c index 325f0b54dfc..0fb5eeffbba 100644 --- a/lib/decompress/zstd_decompress.c +++ b/lib/decompress/zstd_decompress.c @@ -1947,6 +1947,10 @@ size_t ZSTD_DCtx_reset(ZSTD_DCtx* dctx, ZSTD_ResetDirective reset) { if ( (reset == ZSTD_reset_session_only) || (reset == ZSTD_reset_session_and_parameters) ) { + /* A prefix is only used once : if a frame was started, it is abandoned now, + * so the prefix must not leak into the next frame */ + if (dctx->streamStage != zdss_init && dctx->dictUses == ZSTD_use_once) + ZSTD_clearDict(dctx); dctx->streamStage = zdss_init; dctx->noForwardProgress = 0; dctx->isFrameDecompression = 1; diff --git a/tests/zstreamtest.c b/tests/zstreamtest.c index 88a20a3e270..39a21a434b8 100644 --- a/tests/zstreamtest.c +++ b/tests/zstreamtest.c @@ -1406,6 +1406,39 @@ static int basicUnitTests(U32 seed, double compressibility, int bigTests) DISPLAYLEVEL(3, "OK (%s)\n", ZSTD_getErrorName(r)); } + DISPLAYLEVEL(3, "test%3i : prefix is dropped by session reset after a rejected frame header : ", testNb++); + { BYTE const badHeader[6] = { 0x28, 0xB5, 0x2F, 0xFD, 0x08, 0x00 }; /* reserved bit set */ + CHECK_Z( ZSTD_DCtx_refPrefix(zd, dictionary.start, dictionary.filled) ); + outBuff.dst = decodedBuffer; + outBuff.size = CNBufferSize; + outBuff.pos = 0; + inBuff.src = badHeader; + inBuff.size = sizeof(badHeader); + inBuff.pos = 0; + if (!ZSTD_isError(ZSTD_decompressStream(zd, &outBuff, &inBuff))) goto _output_error; + CHECK_Z( ZSTD_DCtx_reset(zd, ZSTD_reset_session_only) ); + outBuff.pos = 0; + inBuff.src = compressedBuffer; + inBuff.size = cSize; + inBuff.pos = 0; + if (!ZSTD_isError(ZSTD_decompressStream(zd, &outBuff, &inBuff))) goto _output_error; /* prefix must be gone */ + CHECK_Z( ZSTD_DCtx_reset(zd, ZSTD_reset_session_only) ); + } + DISPLAYLEVEL(3, "OK \n"); + + DISPLAYLEVEL(3, "test%3i : prefix survives session reset before any input : ", testNb++); + CHECK_Z( ZSTD_DCtx_refPrefix(zd, dictionary.start, dictionary.filled) ); + CHECK_Z( ZSTD_DCtx_reset(zd, ZSTD_reset_session_only) ); + outBuff.dst = decodedBuffer; + outBuff.size = CNBufferSize; + outBuff.pos = 0; + inBuff.src = compressedBuffer; + inBuff.size = cSize; + inBuff.pos = 0; + CHECK_Z( ZSTD_decompressStream(zd, &outBuff, &inBuff) ); + if (outBuff.pos != CNBufferSize) goto _output_error; /* must regenerate whole input */ + DISPLAYLEVEL(3, "OK \n"); + DISPLAYLEVEL(3, "test%3i : compress again with ZSTD_compressStream2 : ", testNb++); outBuff.dst = compressedBuffer; outBuff.size = compressedBufferSize;