From e908b4bfdb0dc39cfd13491c144d09bcb492087c Mon Sep 17 00:00:00 2001 From: huang <13165998033@163.com> Date: Tue, 20 Oct 2026 19:27:46 +0800 Subject: [PATCH] Fix RLE detection for the first block of every MT job ZSTDMT_compressionJob calls ZSTD_compressBegin_advanced_internal for every job, which resets isFirstBlock to 1. The non-first-job branch flushes the frame header with srcSize=0, and ZSTD_compressContinue_internal returns early without emitting a block, so isFirstBlock is still 1 when the job's first real block is compressed. The RLE guards therefore skip RLE for the first block of every job, not just the frame's true first block. Clear isFirstBlock in the non-first-job branch. The frame's first block stays protected for decoders <= v1.4.3. Adds a fuzzer test: 2 MiB of zeros compressed with nbWorkers=0 and with nbWorkers=2 + jobSize=512 KB must produce the same size and roundtrip. Before the fix the MT output is 104 bytes vs 83; after, both are 83. Fixes #4457. --- lib/compress/zstdmt_compress.c | 5 +++++ tests/fuzzer.c | 30 ++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/lib/compress/zstdmt_compress.c b/lib/compress/zstdmt_compress.c index cb710475c2a..05e688ca2a8 100644 --- a/lib/compress/zstdmt_compress.c +++ b/lib/compress/zstdmt_compress.c @@ -755,6 +755,11 @@ static void ZSTDMT_compressionJob(void* jobDescription) if (ZSTD_isError(hSize)) JOB_ERROR(hSize); DEBUGLOG(5, "ZSTDMT_compressionJob: flush and overwrite %u bytes of frame header (not first job)", (U32)hSize); ZSTD_invalidateRepCodes(cctx); + /* ZSTD_compressBegin() sets isFirstBlock=1 for every job, but only the + * frame's first block must skip RLE to stay compatible with decoders + * <= v1.4.3. Without this, the first block of each job is emitted as a + * regular compressed block instead of an RLE block. */ + cctx->isFirstBlock = 0; } /* compress the entire job by smaller chunks, for better granularity */ diff --git a/tests/fuzzer.c b/tests/fuzzer.c index 32360b9a58d..824c5539d39 100644 --- a/tests/fuzzer.c +++ b/tests/fuzzer.c @@ -4325,6 +4325,36 @@ static int basicUnitTests(U32 const seed, double compressibility) DISPLAYLEVEL(3, "OK \n"); } + /* The RLE guard must only skip the frame's first block, not the first + * block of every multithreaded job (issue #4457). */ + { size_t const srcSize = 2 MB; + size_t const jobSize = 512 KB; + size_t singleSize; + size_t mtSize; + ZSTD_CCtx* const cctx = ZSTD_createCCtx(); + DISPLAYLEVEL(3, "test%3i : RLE detection in multithreaded jobs : ", testNb++); + if (cctx == NULL) goto _output_error; + memset(CNBuffer, 0, srcSize); + CHECK_Z( ZSTD_CCtx_setParameter(cctx, ZSTD_c_compressionLevel, 1) ); + CHECK_Z( ZSTD_CCtx_setParameter(cctx, ZSTD_c_nbWorkers, 0) ); + singleSize = ZSTD_compress2(cctx, compressedBuffer, compressedBufferSize, CNBuffer, srcSize); + if (ZSTD_isError(singleSize)) goto _output_error; + CHECK_Z( ZSTD_CCtx_reset(cctx, ZSTD_reset_session_and_parameters) ); + CHECK_Z( ZSTD_CCtx_setParameter(cctx, ZSTD_c_compressionLevel, 1) ); + CHECK_Z( ZSTD_CCtx_setParameter(cctx, ZSTD_c_nbWorkers, 2) ); + CHECK_Z( ZSTD_CCtx_setParameter(cctx, ZSTD_c_jobSize, (int)jobSize) ); + mtSize = ZSTD_compress2(cctx, compressedBuffer, compressedBufferSize, CNBuffer, srcSize); + if (ZSTD_isError(mtSize)) goto _output_error; + if (mtSize != singleSize) { + DISPLAYLEVEL(4, "error: MT cSize %u != single-thread cSize %u ! \n", (unsigned)mtSize, (unsigned)singleSize); + goto _output_error; + } + { CHECK_NEWV(regenSize, ZSTD_decompress(decodedBuffer, srcSize, compressedBuffer, mtSize)); + if (regenSize != srcSize) goto _output_error; } + ZSTD_freeCCtx(cctx); + DISPLAYLEVEL(3, "OK \n"); + } + DISPLAYLEVEL(3, "test%3i : ZSTD_generateSequences decode from sequences test : ", testNb++); { size_t srcSize = 150 KB;