Repository navigation
compress: wait for an abandoned frame's jobs before freeing what they use - #4871
Conversation
… use With nbWorkers >= 1, a job of an abandoned frame can still be running when the context frees memory it uses: - ZSTD_freeCCtx() frees the dictionary before ZSTDMT_freeCCtx() joins the workers; - ZSTD_CCtx_reset(session_and_parameters), or a session reset followed by loading another dictionary, frees it while the job runs; - with ZSTD_CCtx_refThreadPool(), ZSTDMT_freeCCtx() does not stop the shared pool and frees the jobs table and CCtx pool under the worker; - after a session reset, raising nbWorkers resizes the CCtx pool before the abandoned jobs are waited for. Move the "previous compression not correctly finished" wait of ZSTDMT_initCStream_internal() into ZSTDMT_waitForPendingJobs(), and call it on session reset, at the start of ZSTDMT_freeCCtx(), and before ZSTDMT_resize(). Free the MT context before the dictionaries. Fixes meta#4862 Signed-off-by: Eshan05 <nahareshan05@gmail.com>
|
Hi @Eshan05! Thank you for your pull request and welcome to our community. Action RequiredIn order to merge any pull request (code, docs, etc.), we require contributors to sign our Contributor License Agreement, and we don't seem to have one on file for you. ProcessIn order for us to review and merge your suggested changes, please sign at https://code.facebook.com/cla. If you are contributing on behalf of someone else (eg your employer), the individual CLA may not be sufficient and your employer may need to sign the corporate CLA. Once the CLA is signed, our tooling will perform checks and validations. Afterwards, the pull request will be tagged with If you have received this in error or have any questions, please contact us at cla@meta.com. Thanks! |
|
Thank you for signing our Contributor License Agreement. We can now accept your code for this (and any) Meta Open Source project. Thanks! |
ZSTDMT_waitForAllJobsCompleted() waited while consumed < src.size. An empty job, the only job of an empty frame, meets that condition before its worker starts, so the wait returned at once. The previous commit calls ZSTDMT_waitForPendingJobs() at the start of ZSTDMT_freeCCtx(), before POOL_free() joins the workers. When an empty frame was abandoned (ZSTD_e_end returned because the output buffer was full), freeing the context cleared the job while its worker started, and the worker dereferenced a NULL pool. Before that commit, POOL_free() joined the worker first. The worker now sets a completed flag when it no longer uses the job, and the wait uses this flag. The test gets a scenario for this case.
ZSTDMT_releaseAllJobResources() clears every job descriptor, including the job prepared while no worker was free (jobReady), but it left jobReady set. After a frame was abandoned in that state, for example with ZSTD_CCtx_reset(), the first ZSTDMT_createCompressionJob() of the next frame posted the cleared descriptor, and the worker dereferenced a NULL pool. The test gets a scenario for this case.
Remove the scenario that raises nbWorkers after a reset. Since ZSTD_CCtx_reset() waits for the jobs, this scenario can only fail when the one that loads a new dictionary after a reset fails too. The new frames are compressed, but no longer decompressed: the bugs show as memory errors in the worker. Each of the five scenarios still fails when its library change is removed.
|
Thank you for this fix, the clear description and the test. We keep the change as is: under AddressSanitizer, each of the four scenarios of the test fails without the library change, and passes with it. Your change waits for the jobs with
|
What and why
Fixes #4862.
With
nbWorkers >= 1, a caller can abandon a frame while one of its jobs is queued or running. The context then frees memory the job still uses, without waiting for it. This happens in four places:ZSTD_freeCCtx()frees the dictionary loaded withZSTD_CCtx_loadDictionary*()beforeZSTDMT_freeCCtx()joins the workers. This is the case reported in ZSTD_freeCCtx() frees a dictionary loaded with ZSTD_CCtx_loadDictionary() before stopping the worker threads #4862.ZSTD_CCtx_reset(session_and_parameters)frees the same dictionary while the job runs. So doesZSTD_CCtx_reset(session_only)followed byZSTD_CCtx_loadDictionary*(),refCDict()orrefPrefix(). The reorder suggested in the issue does not cover these.ZSTD_CCtx_refThreadPool(),ZSTDMT_freeCCtx()does not stop the shared pool. It frees the jobs table and the CCtx pool while a worker still holds a CCtx from that pool. No dictionary is needed for this one.ZSTD_CCtx_reset(session_only), raisingnbWorkersmakesZSTDMT_initCStream_internal()resize the CCtx pool before it waits for the abandoned jobs. The resize frees the CCtx a worker is using.AddressSanitizer reports a heap-use-after-free for all four on
dev.The change moves the existing "previous compression not correctly finished" block of
ZSTDMT_initCStream_internal()into a new function,ZSTDMT_waitForPendingJobs(). It calls that function in three places:ZSTD_CCtx_reset()when the session is reset, so no job of an abandoned frame is still running when the reset returns;ZSTDMT_freeCCtx(), before anything is freed, which also covers a shared pool;ZSTDMT_initCStream_internal()beforeZSTDMT_resize()instead of after it.ZSTD_freeCCtxContent()now frees the MT context before the dictionaries.After a completed frame,
allJobsCompletedis already 1 and these calls do nothing, so the normal path does not change. A session reset now blocks until the abandoned frame's running jobs finish. Before this change,ZSTDMT_initCStream_internal()already waited for them at the start of the next frame, andZSTD_freeCCtx()already joined an owned pool. I added a line about this to theZSTD_CCtx_reset()comment inzstd.h.Test
I added a test to
tests/zstreamtest.c, "abandon a multi-threaded frame with a job in progress". It starts a 1 MB job with a 1 MB dictionary, and then:nbWorkersto 4 and round-trips a frame.Without the fix, the test fails under AddressSanitizer with heap-use-after-free in 3 of 3 runs. With the fix, it passes.
On Windows x64 with MSVC 14.44 and
/fsanitize=address:zstreamtest -v -T90s,zstreamtest --newapi -t1 -T90sandfuzzer -v -T200spass with no AddressSanitizer reports;lib/andtests/warning flags and-Werror, with and withoutZSTD_MULTITHREAD.On Ubuntu 24.04 with clang 18:
make uasan-test-zstreampasses;make tsan-test-zstreamwith-T3mnandmake tsan-fuzztestpass with no ThreadSanitizer reports. My VM has 2 GB of memory, and the twohashLog = 29tests run out of memory under TSan, so I disabled them locally. These are zstreamtest test 70 and fuzzer test 214;Checklist
Tools used
Note
Claude Code was used to reproduce the issue, to find the shared-pool and resize cases while reading the code, and to write the change and the test.
I used AddressSanitizer to check each case before and after the change, and ThreadSanitizer to check the fixed code.