From 35d74f85f70d67f888ccdb394f69ed74ba767653 Mon Sep 17 00:00:00 2001 From: Yann Collet Date: Wed, 21 Oct 2026 14:06:14 -0700 Subject: [PATCH] lz4hc: LZ4_saveDictHC() before any compression no longer corrupts the next block On a new or reset stream (prefixStart == NULL), LZ4_saveDictHC() installed @safeBuffer as prefix, which skipped the initialization done by the next compression (LZ4HC_init_internal()). The stream then started at index 0, which the hash table uses for empty entries : positions matched themselves (offset 0), and the block decoded into wrong data, at all HC levels, also with an attached dictionary. Present in v1.10.0. It now saves nothing, leaving the stream to be initialized by the next compression. Found by lz4frame with uncompressed then compressed segments in linked blocks. tests : fuzzer.c saveDictHC test #926 compresses after LZ4_saveDictHC(), without and with an attached dictionary. --- NEWS | 1 + lib/lz4hc.c | 3 +++ tests/fuzzer.c | 28 ++++++++++++++++++++++++++++ 3 files changed, 32 insertions(+) diff --git a/NEWS b/NEWS index 2667ba43..e9f5b78d 100644 --- a/NEWS +++ b/NEWS @@ -4,6 +4,7 @@ fix : `LZ4_compress_fast_continue()` produced wrong data when a block overwrote fix : `LZ4_compress_destSize_extState()` corrupted data with `acceleration <= 0`, and could read out of bounds (since v1.10.0) fix : negative sizes crashed `LZ4_decompress_safe_partial()` (since v1.9.3) and `LZ4_decompress_fast()` (since v1.9.4) fix : `LZ4_saveDict()` with a negative `dictSize` copied up to 64 KB into `safeBuffer`, by @arthuz-th +fix : `LZ4_saveDictHC()` before any compression (new or reset stream) corrupted the next compressed block fix : `LZ4_decompress_safe_partial()` could leave previous `dst` content in decoded data with an invalid offset 0 (since v1.8.3) fix : heap overflow with an invalid `blockSizeID` in `LZ4F_compressBegin()` and `LZ4F_writeOpen()`, by @jaypatrickhoward fix : `LZ4F_compressBegin()` failing on allocation left an inconsistent frame in progress : it is now invalidated diff --git a/lib/lz4hc.c b/lib/lz4hc.c index 6fd6e7e4..a98bb881 100644 --- a/lib/lz4hc.c +++ b/lib/lz4hc.c @@ -1793,6 +1793,9 @@ int LZ4_saveDictHC (LZ4_streamHC_t* LZ4_streamHCPtr, char* safeBuffer, int dictS LZ4HC_CCtx_internal* const streamPtr = &LZ4_streamHCPtr->internal_donotuse; int const prefixSize = (int)(streamPtr->end - streamPtr->prefixStart); DEBUGLOG(5, "LZ4_saveDictHC(%p, %p, %d)", LZ4_streamHCPtr, safeBuffer, dictSize); + /* nothing compressed yet (new or reset stream) : nothing to save, + * the stream remains to be initialized by next compression (attached dictionary included) */ + if (streamPtr->prefixStart == NULL) return 0; assert(prefixSize >= 0); if (dictSize > 64 KB) dictSize = 64 KB; if (dictSize < 4) dictSize = 0; diff --git a/tests/fuzzer.c b/tests/fuzzer.c index 1daedd3e..fafc099d 100644 --- a/tests/fuzzer.c +++ b/tests/fuzzer.c @@ -1747,6 +1747,34 @@ static void FUZ_unitTests(int compressionLevel) { char tmp_buffer[240] = { 0 }; FUZ_CHECKTEST( LZ4_saveDictHC(ctx, tmp_buffer, sizeof(tmp_buffer)) != 0, "LZ4_saveDictHC() can't save anything since compression hasn't started"); + } + + /* next block is compressed as the first one of the stream, + * without (attach==0) or with (attach==1) an attached dictionary */ + { LZ4_streamHC_t* const dictHC = LZ4_createStreamHC(); + int const levels[] = { LZ4HC_CLEVEL_DEFAULT, 3, LZ4HC_CLEVEL_OPT_MIN, LZ4HC_CLEVEL_MAX }; /* first : level of LZ4_initStreamHC() */ + int attach, l; + FUZ_CHECKTEST(dictHC == NULL, "LZ4_createStreamHC() allocation failed"); + for (l = 0; l < (int)(sizeof(levels) / sizeof(levels[0])); l++) { + for (attach = 0; attach <= 1; attach++) { + char saveBuffer[240]; + int cSize, dSize; + if (l || attach) LZ4_resetStreamHC_fast(ctx, levels[l]); /* first test : right after init */ + if (attach) { + LZ4_resetStreamHC_fast(dictHC, levels[l]); + FUZ_CHECKTEST(LZ4_loadDictHC(dictHC, testInput, 64 KB) != 64 KB, "LZ4_loadDictHC() failed"); + LZ4_attach_HC_dictionary(ctx, dictHC); + } + FUZ_CHECKTEST( LZ4_saveDictHC(ctx, saveBuffer, sizeof(saveBuffer)) != 0, + "LZ4_saveDictHC() can't save anything since compression hasn't started"); + cSize = LZ4_compress_HC_continue(ctx, testInput + 64 KB, testCompressed, 64 KB, testCompressedSize); + FUZ_CHECKTEST(cSize <= 0, "LZ4_compress_HC_continue() failed"); + dSize = LZ4_decompress_safe_usingDict(testCompressed, testVerify, cSize, 64 KB, + testInput, attach ? 64 KB : 0); + FUZ_CHECKTEST(dSize != 64 KB || memcmp(testVerify, testInput + 64 KB, 64 KB), + "level %i, dictionary %i : block compressed after LZ4_saveDictHC() is corrupted", levels[l], attach); + } } + LZ4_freeStreamHC(dictHC); } } DISPLAYLEVEL(3, "OK \n");