From 6019e17f812fb899f3c55e5d2917f776d49e433d Mon Sep 17 00:00:00 2001 From: Nitai Caro Date: Wed, 19 Aug 2026 18:47:39 +0000 Subject: [PATCH] fix(forkless): Block ValkeyModule_Fork while a save is in progress A forkless save exists to avoid the copy-on-write memory spike of a fork-based save. But the save still writes to the pages of the data it walks (object refcounts, the iterator epoch in object metadata, and rehash state in collections), and the main thread keeps serving writes. If a module fork child is alive during the save, all of those writes trigger copy-on-write page copies. That brings back the exact memory spike forkless was made to avoid. Before forkless a module could not fork during a save anyway: a save was a child process, and serverFork() allows only one child at a time. A forkless save is a background thread, so that check no longer sees it and VM_Fork() slips through. Reject VM_Fork() while any save is in progress to restore the old behavior. Signed-off-by: Nitai Caro --- src/module.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/module.c b/src/module.c index 6258dfcf120..e5fd2f26110 100644 --- a/src/module.c +++ b/src/module.c @@ -12256,6 +12256,13 @@ int VM_ScanKey(ValkeyModuleKey *key, ValkeyModuleScanCursor *cursor, ValkeyModul int VM_Fork(ValkeyModuleForkDoneHandler cb, void *user_data) { pid_t childpid; + /* A module fork must not run concurrently with a background save. */ + if (isSaveInProgress()) { + serverLog(LL_WARNING, "Can't fork for module: a background save is in progress"); + errno = EALREADY; + return -1; + } + if ((childpid = serverFork(CHILD_TYPE_MODULE)) == 0) { /* Child */ if (strstr(server.exec_argv[0], "redis-server") != NULL) {