Skip to content

Make py_zstd_compress_mt2 thread-safe#321

Open
KowalskiThomas wants to merge 1 commit into
sergey-dryabzhinsky:masterfrom
KowalskiThomas:kowalski/fix-thread-safety
Open

Make py_zstd_compress_mt2 thread-safe#321
KowalskiThomas wants to merge 1 commit into
sergey-dryabzhinsky:masterfrom
KowalskiThomas:kowalski/fix-thread-safety

Conversation

@KowalskiThomas

Copy link
Copy Markdown

What is this PR?

This PR is a fix for a crash we witnessed in a service of ours. py_zstd_compress_mt2 used a single global compression context (at the module level), m_cctx, which is thus shared across all calls:

static ZSTD_CCtx* m_cctx;

return NULL;
}
ZSTD_CCtx_setParameter(cctx, ZSTD_c_compressionLevel, level);

Since ZSTD_CCtx is not thread-safe, and because compression releases the GIL, two Python threads calling these functions at the same time use the same context concurrently. This can corrupt the state and lead to out-of-bounds access inside zstd.

The crash was detected in this form:

ZSTD_compressBlock_doubleFast_noDict_generic   zstd/lib/compress/zstd_double_fast.c:185
ZSTD_compressBlock_doubleFast_noDict_4         zstd/lib/compress/zstd_double_fast.c:557
ZSTD_compressBlock_doubleFast                  zstd/lib/compress/zstd_double_fast.c:577
ZSTD_buildSeqStore                             zstd/lib/compress/zstd_compress.c:3420
ZSTD_compressBlock_internal                    zstd/lib/compress/zstd_compress.c:4399
ZSTD_compress_frameChunk                       zstd/lib/compress/zstd_compress.c:4646
ZSTD_compressContinue_internal                 zstd/lib/compress/zstd_compress.c:4834
ZSTD_compressEnd_public                        zstd/lib/compress/zstd_compress.c:5412
ZSTD_compressStream_generic                    zstd/lib/compress/zstd_compress.c:6151
ZSTD_compressStream2                           zstd/lib/compress/zstd_compress.c:6540
ZSTD_compressStream2_simpleArgs                zstd/lib/compress/zstd_compress.c:6561
ZSTD_compress2                                 zstd/lib/compress/zstd_compress.c:6581
py_zstd_compress_mt2                           src/python-zstd.c:276
_PyObject_MakeTpCall
_PyEval_EvalFrameDefault
_PyObject_MakeTpCall
_PyEval_EvalFrameDefault

Looking further into it, we found another race as well. When called with different level values, reset_cContext frees and recreates m_cctx. If one thread does this while another is still inside the GIL-released ZSTD_compress2, the latter compresses into a freed context.

Proposed fix

The fix proposed in this PR is to make py_zstd_compress_mt2 create and free its own ZSTD_CCtx on each call, instead of sharing the module-global one. This is the same pattern already used by py_zstd_compress_mt:

cctx = ZSTD_createCCtx();
ZSTD_CCtx_setParameter(cctx, ZSTD_c_compressionLevel, level);
ZSTD_CCtx_setParameter(cctx, ZSTD_c_nbWorkers, threads);
Py_BEGIN_ALLOW_THREADS
cSize = ZSTD_compress2(cctx, dest, (size_t)dest_size, source, (size_t)source_size);
Py_END_ALLOW_THREADS
ZSTD_freeCCtx(cctx);

@KowalskiThomas KowalskiThomas marked this pull request as ready for review June 19, 2026 12:56
@KowalskiThomas KowalskiThomas force-pushed the kowalski/fix-thread-safety branch from 645526c to 693cd72 Compare June 19, 2026 12:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant