From 4f2a494933641939d8de0a5a385a94c99237cc44 Mon Sep 17 00:00:00 2001 From: Nitai Caro Date: Tue, 18 Aug 2026 20:14:27 +0000 Subject: [PATCH 1/2] fix(forkless): Only touch currentForklessSave on the main thread currentForklessSave was cleared in forklessSaveProcessor, which runs on the worker thread, while forklessSaveCancel reads it (and its iterator) on the main thread. Two threads writing/reading the same pointer with no lock is a data race that can crash the cancel path. Remove the worker-thread clear. The pointer is already cleared on the main thread on every end path: cleanupSaveInfoAndEmitEndMetrics() for a save that started, and the werr path in forklessSaveToDisk() if it failed to start. Now only the main thread accesses currentForklessSave, so cancel can no longer race the clear. Signed-off-by: Nitai Caro --- src/forkless.c | 1 - 1 file changed, 1 deletion(-) diff --git a/src/forkless.c b/src/forkless.c index 40c4e9154b1..68e8b5cc642 100644 --- a/src/forkless.c +++ b/src/forkless.c @@ -129,7 +129,6 @@ static void *forklessSaveProcessor(void *arg) { serverLog(LL_NOTICE, "forkless-save: background processor finished. %ld items processed. %s", items, message); - currentForklessSave = NULL; saveInfo->err_code = err; bgIteratorClose(saveInfo->iterator); return NULL; From c85c9bd5dec2c5f7b025e5fdf96f068d15de3020 Mon Sep 17 00:00:00 2001 From: Nitai Caro Date: Thu, 20 Aug 2026 21:29:03 +0000 Subject: [PATCH 2/2] fix(forkless): Clear currentForklessSave when the iterator is released currentForklessSave is the handle forklessSaveCancel() uses to terminate the running iterator. forklessSaveComplete() set the iterator to NULL but left currentForklessSave non-NULL until the later asynchronous file-close cleanup. During that window a BGSAVE CANCEL would call forklessSaveCancel() and pass the now-NULL iterator to bgIteratorTerminate(), which dereferences it. Clear currentForklessSave together with the iterator so the cancel handle never outlives what it cancels. It stays main-thread-only. Signed-off-by: Nitai Caro --- src/forkless.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/forkless.c b/src/forkless.c index 68e8b5cc642..e5d929d3412 100644 --- a/src/forkless.c +++ b/src/forkless.c @@ -220,6 +220,7 @@ void forklessSaveComplete(bool terminated, void *privdata) { saveInfo->terminated = terminated; /* The save iterator should be terminated and freed at this point in time. */ saveInfo->iterator = NULL; + currentForklessSave = NULL; /* For file based forkless save, we need to generate the RDB end marker. and complete the save */ if (!saveInfo->terminated && saveInfo->err_code == C_OK) { saveInfo->err_code = rdbWriteFooter(&saveInfo->save_rio, REPLICA_REQ_NONE) == C_ERR ? C_ERR : C_OK;