From a2026f948c533ff42138d4cf0fdc0b4089c0d677 Mon Sep 17 00:00:00 2001 From: Yann Collet Date: Tue, 10 Nov 2026 12:28:04 -0800 Subject: [PATCH 1/2] compress: stop using the previous pool after ZSTD_CCtx_refThreadPool() ZSTD_CCtx_refThreadPool() only changed cctx->pool. Once created, the MT context kept the pool it was created with, so the next frames still ran on the previous pool, including after ZSTD_CCtx_refThreadPool(cctx, NULL). If the caller then freed the previous pool, the next frame used freed memory. ZSTD_CCtx_refThreadPool() now frees the MT context when the pool changes, and the next frame creates one with the new pool. The pool can only change between frames, and ZSTDMT_freeCCtx() waits for the jobs of an abandoned frame. --- lib/compress/zstd_compress.c | 9 +++++++++ lib/zstd.h | 2 +- tests/fuzzer.c | 19 +++++++++++++++++++ 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/lib/compress/zstd_compress.c b/lib/compress/zstd_compress.c index 88e1df51232..c51fd1853ff 100644 --- a/lib/compress/zstd_compress.c +++ b/lib/compress/zstd_compress.c @@ -1360,6 +1360,15 @@ size_t ZSTD_CCtx_refThreadPool(ZSTD_CCtx* cctx, ZSTD_threadPool* pool) { RETURN_ERROR_IF(cctx->streamStage != zcss_init, stage_wrong, "Can't ref a pool when ctx not in init stage."); +#ifdef ZSTD_MULTITHREAD + if (pool != cctx->pool) { + /* The MT context, created by the first multi-threaded frame and then reused, + * runs its jobs on the pool it was created with : without this, the next frames + * would still use the previous pool, which the caller may free. No job is running : + * the stage is zcss_init, and ZSTDMT_freeCCtx() waits for pending jobs. */ + ZSTDMT_freeCCtx(cctx->mtctx); cctx->mtctx = NULL; + } +#endif cctx->pool = pool; return 0; } diff --git a/lib/zstd.h b/lib/zstd.h index d06c05edd80..8d50c039383 100644 --- a/lib/zstd.h +++ b/lib/zstd.h @@ -1901,7 +1901,7 @@ ZSTDLIB_STATIC_API ZSTD_CDict* ZSTD_createCDict_advanced(const void* dict, size_ * ZSTD_createThreadPool creates a new thread pool with a given number of threads. * Note that the lifetime of such pool must exist while being used. * ZSTD_CCtx_refThreadPool assigns a thread pool to a context (use NULL argument value - * to use an internal thread pool). + * to use an internal thread pool). The context then no longer uses its previous pool. * ZSTD_freeThreadPool frees a thread pool, accepts NULL pointer. */ typedef struct POOL_ctx_s ZSTD_threadPool; diff --git a/tests/fuzzer.c b/tests/fuzzer.c index 3d3cd66abf8..9e955b77f8b 100644 --- a/tests/fuzzer.c +++ b/tests/fuzzer.c @@ -483,6 +483,25 @@ static int threadPoolTests(void) { } DISPLAYLEVEL(3, "OK \n"); + DISPLAYLEVEL(3, "thread pool test : free the previous pool after a rebind: "); + { ZSTD_CCtx* const cctx = ZSTD_createCCtx(); + ZSTD_threadPool* const pool1 = ZSTD_createThreadPool(1); + ZSTD_threadPool* const pool2 = ZSTD_createThreadPool(1); + CHECK(cctx != NULL && pool1 != NULL && pool2 != NULL); + CHECK(!ZSTD_isError( ZSTD_CCtx_setParameter(cctx, ZSTD_c_nbWorkers, 2) )); + CHECK(!ZSTD_isError( ZSTD_CCtx_refThreadPool(cctx, pool1) )); + CHECK(!ZSTD_isError( ZSTD_compress2(cctx, compressedBuffer, compressedBufferSize, CNBuffer, CNBuffSize) )); + /* the next frames must use pool2, then an internal pool */ + CHECK(!ZSTD_isError( ZSTD_CCtx_refThreadPool(cctx, pool2) )); + ZSTD_freeThreadPool(pool1); + CHECK(!ZSTD_isError( ZSTD_compress2(cctx, compressedBuffer, compressedBufferSize, CNBuffer, CNBuffSize) )); + CHECK(!ZSTD_isError( ZSTD_CCtx_refThreadPool(cctx, NULL) )); + ZSTD_freeThreadPool(pool2); + CHECK(!ZSTD_isError( ZSTD_compress2(cctx, compressedBuffer, compressedBufferSize, CNBuffer, CNBuffSize) )); + ZSTD_freeCCtx(cctx); + } + DISPLAYLEVEL(3, "OK \n"); + _end: free(CNBuffer); free(compressedBuffer); From 31db1af2942e393bab01c3967f59ca9f8efa9b02 Mon Sep 17 00:00:00 2001 From: Yann Collet Date: Tue, 10 Nov 2026 12:30:33 -0800 Subject: [PATCH 2/2] compress: don't resize a thread pool provided by the application When ZSTD_c_nbWorkers changed between two frames, ZSTDMT_resize() called POOL_resize() on the context's pool, even on a pool given with ZSTD_CCtx_refThreadPool(). One context could then change the number of threads of a pool shared with other contexts, although such a pool is meant to limit them. ZSTDMT_createCCtx_advanced() already leaves a provided pool unchanged. ZSTDMT_resize() now does the same. --- lib/compress/zstdmt_compress.c | 4 +++- lib/zstd.h | 1 + tests/fuzzer.c | 9 ++++++++- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/lib/compress/zstdmt_compress.c b/lib/compress/zstdmt_compress.c index 7118bbe7f4a..38ad31d0e0d 100644 --- a/lib/compress/zstdmt_compress.c +++ b/lib/compress/zstdmt_compress.c @@ -1091,7 +1091,9 @@ size_t ZSTDMT_sizeof_CCtx(ZSTDMT_CCtx* mtctx) * @return : error code if fails, 0 on success */ static size_t ZSTDMT_resize(ZSTDMT_CCtx* mtctx, unsigned nbWorkers) { - if (POOL_resize(mtctx->factory, nbWorkers)) return ERROR(memory_allocation); + if (!mtctx->providedFactory) { /* a pool provided by the application keeps its size */ + if (POOL_resize(mtctx->factory, nbWorkers)) return ERROR(memory_allocation); + } FORWARD_IF_ERROR( ZSTDMT_expandJobsTable(mtctx, nbWorkers) , ""); mtctx->bufPool = ZSTDMT_expandBufferPool(mtctx->bufPool, BUF_POOL_MAX_NB_BUFFERS(nbWorkers)); if (mtctx->bufPool == NULL) return ERROR(memory_allocation); diff --git a/lib/zstd.h b/lib/zstd.h index 8d50c039383..e0862eb3fa8 100644 --- a/lib/zstd.h +++ b/lib/zstd.h @@ -1899,6 +1899,7 @@ ZSTDLIB_STATIC_API ZSTD_CDict* ZSTD_createCDict_advanced(const void* dict, size_ * This can limit resources for applications with multiple threads where each one uses * a threaded compression mode (via ZSTD_c_nbWorkers parameter). * ZSTD_createThreadPool creates a new thread pool with a given number of threads. + * ZSTD_c_nbWorkers does not change the number of threads of such a pool. * Note that the lifetime of such pool must exist while being used. * ZSTD_CCtx_refThreadPool assigns a thread pool to a context (use NULL argument value * to use an internal thread pool). The context then no longer uses its previous pool. diff --git a/tests/fuzzer.c b/tests/fuzzer.c index 9e955b77f8b..cbc324a98ca 100644 --- a/tests/fuzzer.c +++ b/tests/fuzzer.c @@ -44,6 +44,7 @@ /* must be included after util.h, due to ERROR macro redefinition issue on Visual Studio */ #include "zstd_internal.h" /* ZSTD_WORKSPACETOOLARGE_MAXDURATION, ZSTD_WORKSPACETOOLARGE_FACTOR, KB, MB */ #include "threading.h" /* ZSTD_pthread_create, ZSTD_pthread_join */ +#include "pool.h" /* POOL_sizeof */ #include "compress/hist.h" /* HIST_count_wksp */ #include "compress/zstd_compress_internal.h" /* ZSTD_get1BlockSummary */ @@ -483,7 +484,7 @@ static int threadPoolTests(void) { } DISPLAYLEVEL(3, "OK \n"); - DISPLAYLEVEL(3, "thread pool test : free the previous pool after a rebind: "); + DISPLAYLEVEL(3, "thread pool test : change the pool and nbWorkers of a context: "); { ZSTD_CCtx* const cctx = ZSTD_createCCtx(); ZSTD_threadPool* const pool1 = ZSTD_createThreadPool(1); ZSTD_threadPool* const pool2 = ZSTD_createThreadPool(1); @@ -495,6 +496,12 @@ static int threadPoolTests(void) { CHECK(!ZSTD_isError( ZSTD_CCtx_refThreadPool(cctx, pool2) )); ZSTD_freeThreadPool(pool1); CHECK(!ZSTD_isError( ZSTD_compress2(cctx, compressedBuffer, compressedBufferSize, CNBuffer, CNBuffSize) )); + { size_t const pool2Size = POOL_sizeof(pool2); + /* the context must not resize a pool it shares */ + CHECK(!ZSTD_isError( ZSTD_CCtx_setParameter(cctx, ZSTD_c_nbWorkers, 4) )); + CHECK(!ZSTD_isError( ZSTD_compress2(cctx, compressedBuffer, compressedBufferSize, CNBuffer, CNBuffSize) )); + CHECK(POOL_sizeof(pool2) == pool2Size); + } CHECK(!ZSTD_isError( ZSTD_CCtx_refThreadPool(cctx, NULL) )); ZSTD_freeThreadPool(pool2); CHECK(!ZSTD_isError( ZSTD_compress2(cctx, compressedBuffer, compressedBufferSize, CNBuffer, CNBuffSize) ));