Version in base suite: 8.1.1+dfsg1-3+deb13u2 Base version: valkey_8.1.1+dfsg1-3+deb13u2 Target version: valkey_8.1.1+dfsg1-3+deb13u3 Base file: /srv/ftp-master.debian.org/ftp/pool/main/v/valkey/valkey_8.1.1+dfsg1-3+deb13u2.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/v/valkey/valkey_8.1.1+dfsg1-3+deb13u3.dsc changelog | 41 ++++++ patches/CVE-2026-23479.patch | 27 ++++ patches/CVE-2026-23631.patch | 24 +++ patches/CVE-2026-25243.patch | 106 +++++++++++++++++ patches/CVE-2026-56684.patch | 48 +++++++ patches/CVE-2026-63639.patch | 26 ++++ patches/CVE-2026-82631.patch | 268 +++++++++++++++++++++++++++++++++++++++++++ patches/CVE-2026-82677.patch | 158 +++++++++++++++++++++++++ patches/series | 7 + 9 files changed, 705 insertions(+) dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmpyqx1y7zv/valkey_8.1.1+dfsg1-3+deb13u2.dsc: no acceptable signature found dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmpyqx1y7zv/valkey_8.1.1+dfsg1-3+deb13u3.dsc: no acceptable signature found diff -Nru valkey-8.1.1+dfsg1/debian/changelog valkey-8.1.1+dfsg1/debian/changelog --- valkey-8.1.1+dfsg1/debian/changelog 2026-03-29 08:34:29.000000000 +0000 +++ valkey-8.1.1+dfsg1/debian/changelog 2026-10-24 15:19:26.000000000 +0000 @@ -1,3 +1,44 @@ +valkey (8.1.1+dfsg1-3+deb13u3) trixie-security; urgency=medium + + * Fix CVE-2026-56684 (Closes: #1144956) + Valkey's tlsProcessPendingData function iterates pending_list while an + authenticated client can trigger CLIENT KILL, causing connTLSClose to + delete the iterator's cached next node and producing a use-after-free that + can crash the server or potentially allow remote code execution when TLS is + enabled. + - d/p/CVE-2026-56684.patch + * Fix CVE-2026-63639 (Closes: #1144957) + Valkey's RESTORE command accepts a malformed RDB stream payload that + assigns one Pending Entry List NACK to multiple consumers during stream + consumer-group deserialization, causing a use-after-free when one consumer + is deleted while another still references the shared NACK and potentially + allowing remote code execution. + - d/p/CVE-2026-63639.patch + * Fix CVE-2026-82631 and CVE-2026-82677 (Closes: #1146641) + A server-side use-after-free can occur in handleClientsBlockedOnKey. + - d/p/CVE-2026-82631.patch + A server-side double free can occur in moduleTimerHandler. + - d/p/CVE-2026-82677.patch + * Fix CVE-2026-23479 + Valkey's unblockClientOnKey function re-executes a blocked client's + pending command and then accesses the client without checking whether it + was freed during processing, causing a use-after-free. + - d/p/CVE-2026-23479.patch + * Fix CVE-2026-23631 + Valkey frees the functions/scripting engine before loading the RDB during + a replica full sync, which can happen while a Lua script is yielding via + the long-command mechanism, causing a use-after-free when the script + resumes. + - d/p/CVE-2026-23631.patch + * Fix CVE-2026-25243 + Valkey's zipmapValidateIntegrity function accepts overlong length + encodings that zipmapNext later interprets differently, allowing a crafted + RESTORE payload to cause a heap buffer over-read during zipmap to listpack + conversion. + - d/p/CVE-2026-25243.patch + + -- Lena Voytek Sat, 24 Oct 2026 10:19:26 -0500 + valkey (8.1.1+dfsg1-3+deb13u2) trixie-security; urgency=medium * Non-maintainer upload. diff -Nru valkey-8.1.1+dfsg1/debian/patches/CVE-2026-23479.patch valkey-8.1.1+dfsg1/debian/patches/CVE-2026-23479.patch --- valkey-8.1.1+dfsg1/debian/patches/CVE-2026-23479.patch 1970-01-01 00:00:00.000000000 +0000 +++ valkey-8.1.1+dfsg1/debian/patches/CVE-2026-23479.patch 2026-10-24 15:19:26.000000000 +0000 @@ -0,0 +1,27 @@ +Author: Ran Shidlansik +Date: Wed, 6 May 2026 03:03:02 +0300 +Description: Fix UAF in unblockClientOnKey when reprocessed command frees the client. + +Origin: upstream, https://github.com/valkey-io/valkey/commit/ba11c9e4580bb1b84ca9d0fae83dcfe26ba47344 +Bug: https://github.com/valkey-io/valkey/pull/3613 +Bug-Security: https://security-tracker.debian.org/tracker/CVE-2026-23479 +--- +diff --git a/src/blocked.c b/src/blocked.c +index 577baf66f0b..5de3a7b44f6 100644 +--- a/src/blocked.c ++++ b/src/blocked.c +@@ -705,7 +705,13 @@ static void unblockClientOnKey(client *c, robj *key) { + client *old_client = server.current_client; + server.current_client = c; + enterExecutionUnit(1, 0); +- processCommandAndResetClient(c); ++ if (processCommandAndResetClient(c) == C_ERR) { ++ /* Client was freed during command processing, exit immediately */ ++ exitExecutionUnit(); ++ server.current_client = old_client; ++ return; ++ } ++ + if (!c->flag.blocked) { + if (c->flag.module) { + moduleCallCommandUnblockedHandler(c); diff -Nru valkey-8.1.1+dfsg1/debian/patches/CVE-2026-23631.patch valkey-8.1.1+dfsg1/debian/patches/CVE-2026-23631.patch --- valkey-8.1.1+dfsg1/debian/patches/CVE-2026-23631.patch 1970-01-01 00:00:00.000000000 +0000 +++ valkey-8.1.1+dfsg1/debian/patches/CVE-2026-23631.patch 2026-10-24 15:19:26.000000000 +0000 @@ -0,0 +1,24 @@ +Author: Ran Shidlansik +Date: Wed, 6 May 2026 00:34:47 +0300 +Description: Delay full sync during yielding Lua scripts to prevent use-after-free. + +Origin: upstream, https://github.com/valkey-io/valkey/commit/8c26ce460b1adf39f51c1821b845acd2d4eb6433 +Bug: https://github.com/valkey-io/valkey/pull/3625 +Bug-Security: https://security-tracker.debian.org/tracker/CVE-2026-23631 +--- +diff --git a/src/replication.c b/src/replication.c +index 7820976814e..59bdb975636 100644 +--- a/src/replication.c ++++ b/src/replication.c +@@ -2103,6 +2103,11 @@ void replicationAttachToNewPrimary(void) { + /* Asynchronously read the SYNC payload we receive from a primary */ + #define REPL_MAX_WRITTEN_BEFORE_FSYNC (1024 * 1024 * 8) /* 8 MB */ + void readSyncBulkPayload(connection *conn) { ++ /* During full sync, the functions engine is freed right before loading ++ * the RDB. To avoid this happening while a function is still running, ++ * delay full sync processing until it finishes. */ ++ if (isInsideYieldingLongCommand()) return; ++ + char buf[PROTO_IOBUF_LEN]; + ssize_t nread, readlen, nwritten; + int use_diskless_load = useDisklessLoad(); diff -Nru valkey-8.1.1+dfsg1/debian/patches/CVE-2026-25243.patch valkey-8.1.1+dfsg1/debian/patches/CVE-2026-25243.patch --- valkey-8.1.1+dfsg1/debian/patches/CVE-2026-25243.patch 1970-01-01 00:00:00.000000000 +0000 +++ valkey-8.1.1+dfsg1/debian/patches/CVE-2026-25243.patch 2026-10-24 15:19:26.000000000 +0000 @@ -0,0 +1,106 @@ +Author: Ran Shidlansik +Date: Wed, 6 May 2026 02:48:33 +0300 +Description: Fix invalid memory access in RESTORE with malformed zipmap. + +Origin: upstream, https://github.com/valkey-io/valkey/commit/7dba5b6977e515830c5fdaaf69cbd783a20e7827 +Bug: https://github.com/valkey-io/valkey/pull/3619 +Bug-Security: https://security-tracker.debian.org/tracker/CVE-2026-25243 +--- +diff --git a/src/rdb.c b/src/rdb.c +index 07c6af67ab0..e351b71603c 100644 +--- a/src/rdb.c ++++ b/src/rdb.c +@@ -2306,11 +2306,12 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) { + + /* search for duplicate records */ + sds field = sdstrynewlen(fstr, flen); +- if (!field || dictAdd(dupSearchDict, field, NULL) != DICT_OK || +- !lpSafeToAdd(lp, (size_t)flen + vlen)) { ++ if (!field || !lpSafeToAdd(lp, (size_t)flen + vlen) || ++ dictAdd(dupSearchDict, field, NULL) != DICT_OK) { + rdbReportCorruptRDB("Hash zipmap with dup elements, or big length (%u)", flen); + dictRelease(dupSearchDict); + sdsfree(field); ++ lpFree(lp); + zfree(encoded); + o->ptr = NULL; + decrRefCount(o); +@@ -2758,7 +2759,6 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) { + " loading a stream consumer " + "group"); + decrRefCount(o); +- streamFreeNACK(nack); + return NULL; + } + } +diff --git a/src/zipmap.c b/src/zipmap.c +index 571b58b6e7e..5214eaa5782 100644 +--- a/src/zipmap.c ++++ b/src/zipmap.c +@@ -196,6 +196,10 @@ int zipmapValidateIntegrity(unsigned char *zm, size_t size, int deep) { + + /* read the field name length */ + l = zipmapDecodeLength(p); ++ /* Sanity check: length < 254 must be encoded in 1 byte, not 5 bytes */ ++ if (l < ZIPMAP_BIGLEN && s != 1) ++ return 0; ++ + p += s; /* skip the encoded field size */ + p += l; /* skip the field */ + +@@ -209,6 +213,9 @@ int zipmapValidateIntegrity(unsigned char *zm, size_t size, int deep) { + + /* read the value length */ + l = zipmapDecodeLength(p); ++ /* Sanity check: length < 254 must be encoded in 1 byte, not 5 bytes */ ++ if (l < ZIPMAP_BIGLEN && s != 1) ++ return 0; + p += s; /* skip the encoded value size*/ + e = *p++; /* skip the encoded free space (always encoded in one byte) */ + p += l + e; /* skip the value and free space */ +diff --git a/tests/unit/dump.tcl b/tests/unit/dump.tcl +index 37eaffc0c58..0fcda245189 100644 +--- a/tests/unit/dump.tcl ++++ b/tests/unit/dump.tcl +@@ -424,4 +424,41 @@ start_server {tags {"dump"}} { + assert_match {*WRONGPASS*} $err + } + } {} {external:skip} ++ ++ test {RESTORE rejects zipmap with overlong field length encoding (CVE-2026-25243)} { ++ # Craft a RESTORE payload containing a hash-zipmap (RDB type 9) where ++ # the field-name length is encoded using the 5-byte format (0xfe prefix) ++ # even though the actual length (3) fits in a single byte. ++ # ++ # The bug: zipmapValidateIntegrity() walks the zipmap using the actual ++ # encoded size (5 bytes for 0xfe prefix), but zipmapNext() recalculates ++ # the encoding size via zipmapEncodeLength(NULL, len) which returns 1 ++ # for lengths < 254. This 4-byte mismatch causes zipmapNext() to read ++ # at wrong offsets during the hash conversion loop after validation, ++ # leading to invalid memory access (heap buffer over-read). ++ # ++ # Zipmap layout (2 entries, 24 bytes): ++ # 02 - zmlen (2 entries) ++ # fe 03000000 - field length = 3, overlong 5-byte encoding ++ # 616263 - "abc" ++ # 03 - value length = 3 ++ # 00 - free = 0 ++ # 646566 - "def" ++ # 03 - field length = 3 (normal, padding entry) ++ # 676869 - "ghi" ++ # 03 - value length = 3 ++ # 00 - free = 0 ++ # 6a6b6c - "jkl" ++ # ff - ZIPMAP_END ++ # ++ # Post-patch: zipmapValidateIntegrity() rejects (l < 254 && s != 1). ++ # ++ # RESTORE payload: ++ ++ r debug set-skip-checksum-validation 1 ++ set payload "\x09\x18\x02\xfe\x03\x00\x00\x00\x61\x62\x63\x03\x00\x64\x65\x66\x03\x67\x68\x69\x03\x00\x6a\x6b\x6c\xff\x0b\x00\x00\x00\x00\x00\x00\x00\x00\x00" ++ catch {r restore zipmap_test 0 $payload} err ++ r debug set-skip-checksum-validation 0 ++ assert_match {*Bad data format*} $err ++ } {} {needs:debug} + } diff -Nru valkey-8.1.1+dfsg1/debian/patches/CVE-2026-56684.patch valkey-8.1.1+dfsg1/debian/patches/CVE-2026-56684.patch --- valkey-8.1.1+dfsg1/debian/patches/CVE-2026-56684.patch 1970-01-01 00:00:00.000000000 +0000 +++ valkey-8.1.1+dfsg1/debian/patches/CVE-2026-56684.patch 2026-10-24 15:19:26.000000000 +0000 @@ -0,0 +1,48 @@ +Author: Madelyn Olson +Date: Tue, 21 Jul 2026 09:03:05 -0700 +Description: Fix use-after-free in tlsProcessPendingData() on CLIENT KILL. + +Origin: upstream, https://github.com/valkey-io/valkey/commit/33b14adf2026cfd8728607b026edb24843805844 +Bug: https://github.com/valkey-io/valkey/pull/4234 +Bug-Debian: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1144956 +Bug-Security: https://security-tracker.debian.org/tracker/CVE-2026-56684 +--- + src/tls.c | 21 +++++++++++++++++---- + 1 file changed, 17 insertions(+), 4 deletions(-) + +diff --git a/src/tls.c b/src/tls.c +index 411053183a3..35685e8598d 100644 +--- a/src/tls.c ++++ b/src/tls.c +@@ -1132,14 +1132,27 @@ static int tlsHasPendingData(void) { + } + + static int tlsProcessPendingData(void) { +- listIter li; + listNode *ln; + + int processed = 0; +- listRewind(pending_list, &li); +- while ((ln = listNext(&li))) { ++ /* Pop each connection off the list before handling it. A handler may ++ * synchronously free another pending connection (e.g. CLIENT KILL -> ++ * freeClient -> connTLSClose -> listDelNode), so we must not hold an ++ * iterator into a node that could be freed out from under us. ++ * ++ * Connections with buffered data re-add themselves to the tail, so the ++ * length captured on entry bounds the loop and guarantees termination. */ ++ unsigned long remaining = listLength(pending_list); ++ while (remaining-- > 0 && (ln = listFirst(pending_list)) != NULL) { + tls_connection *conn = listNodeValue(ln); +- if (conn->flags & TLS_CONN_FLAG_POSTPONE_UPDATE_STATE) continue; ++ listDelNode(pending_list, ln); ++ conn->pending_list_node = NULL; ++ if (conn->flags & TLS_CONN_FLAG_POSTPONE_UPDATE_STATE) { ++ /* Not handled now, but keep it pending for a later call. */ ++ listAddNodeTail(pending_list, conn); ++ conn->pending_list_node = listLast(pending_list); ++ continue; ++ } + tlsHandleEvent(conn, AE_READABLE); + processed++; + } diff -Nru valkey-8.1.1+dfsg1/debian/patches/CVE-2026-63639.patch valkey-8.1.1+dfsg1/debian/patches/CVE-2026-63639.patch --- valkey-8.1.1+dfsg1/debian/patches/CVE-2026-63639.patch 1970-01-01 00:00:00.000000000 +0000 +++ valkey-8.1.1+dfsg1/debian/patches/CVE-2026-63639.patch 2026-10-24 15:19:26.000000000 +0000 @@ -0,0 +1,26 @@ +Author: Saurabh K +Date: Tue, 30 Jun 2026 16:30:18 -0700 +Description: Reject corrupt stream RDB with shared NACK across consumers. + +Origin: backport, https://github.com/valkey-io/valkey/commit/98465eaffe3f95524a5046318bfbc4bdb9798291 +Bug: https://github.com/valkey-io/valkey/pull/4073 +Bug-Debian: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1144957 +Bug-Security: https://security-tracker.debian.org/tracker/CVE-2026-63639 +--- +diff --git a/src/rdb.c b/src/rdb.c +index 72853eb2d89..99e76ffeb85 100644 +--- a/src/rdb.c ++++ b/src/rdb.c +@@ -2780,6 +2780,12 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) { + /* Set the NACK consumer, that was left to NULL when + * loading the global PEL. Then set the same shared + * NACK structure also in the consumer-specific PEL. */ ++ if (nack->consumer && nack->consumer != consumer) { ++ rdbReportCorruptRDB("NACK already assigned to a different consumer, " ++ "shared NACKs across consumers are not valid"); ++ decrRefCount(o); ++ return NULL; ++ } + nack->consumer = consumer; + if (!raxTryInsert(consumer->pel, rawid, sizeof(rawid), nack, NULL)) { + rdbReportCorruptRDB("Duplicated consumer PEL entry " diff -Nru valkey-8.1.1+dfsg1/debian/patches/CVE-2026-82631.patch valkey-8.1.1+dfsg1/debian/patches/CVE-2026-82631.patch --- valkey-8.1.1+dfsg1/debian/patches/CVE-2026-82631.patch 1970-01-01 00:00:00.000000000 +0000 +++ valkey-8.1.1+dfsg1/debian/patches/CVE-2026-82631.patch 2026-10-24 15:19:26.000000000 +0000 @@ -0,0 +1,268 @@ +Author: Quanye Yang +Date: Sat, 8 Aug 2026 00:11:29 +0800 +Description: Fix case where client could get unblocked while processing another client. + +Origin: upstream, https://github.com/valkey-io/valkey/commit/f17a71df6ccd9a2476fd7534deb135593a8242bb +Bug: https://github.com/valkey-io/valkey/pull/4212 +Bug-Debian: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1146641 +Bug-Security: https://security-tracker.debian.org/tracker/CVE-2026-82631 + +--- +src/blocked.c | 37 ++++++++++++-- + tests/modules/blockonkeys.c | 72 ++++++++++++++++++++++++++++ + tests/unit/client-eviction.tcl | 56 ++++++++++++++++++++++ + tests/unit/moduleapi/blockonkeys.tcl | 20 ++++++++ + 4 files changed, 182 insertions(+), 3 deletions(-) + +diff --git a/src/blocked.c b/src/blocked.c +index 5de3a7b44f6..3abd0661322 100644 +--- a/src/blocked.c ++++ b/src/blocked.c +@@ -599,6 +599,21 @@ void signalDeletedKeyAsReady(serverDb *db, robj *key, int type) { + signalKeyAsReadyLogic(db, key, type, 1); + } + ++/* Find a waiter for rl->key by client id (real or RM_Call fake clients). */ ++static client *getClientFromBlockingKeysList(readyList *rl, uint64_t id) { ++ list *client_list = dictFetchValue(rl->db->blocking_keys, rl->key); ++ listNode *ln; ++ listIter li; ++ ++ if (client_list == NULL) return NULL; ++ listRewind(client_list, &li); ++ while ((ln = listNext(&li)) != NULL) { ++ client *c = listNodeValue(ln); ++ if (c->id == id) return c; ++ } ++ return NULL; ++} ++ + /* Helper function for handleClientsBlockedOnKeys(). This function is called + * whenever a key is ready. we iterate over all the clients blocked on this key + * and try to re-execute the command (in case the key is still available). */ +@@ -611,13 +626,28 @@ static void handleClientsBlockedOnKey(readyList *rl) { + list *clients = dictGetVal(de); + listNode *ln; + listIter li; ++ long count = listLength(clients); ++ long snapshot_len = 0; ++ uint64_t *ids; ++ long i; ++ ++ /* Snapshot ids: serve may freeClient() another waiter and invalidate ++ * a listIter successor. Re-resolve each id from the live list. */ ++ ids = zmalloc(sizeof(*ids) * count); + listRewind(clients, &li); ++ while ((ln = listNext(&li)) != NULL && snapshot_len < count) { ++ client *c = listNodeValue(ln); ++ ids[snapshot_len++] = c->id; ++ } + + /* Avoid processing more than the initial count so that we're not stuck + * in an endless loop in case the reprocessing of the command blocks again. */ +- long count = listLength(clients); +- while ((ln = listNext(&li)) && count--) { +- client *receiver = listNodeValue(ln); ++ for (i = 0; i < snapshot_len; i++) { ++ client *receiver = getClientFromBlockingKeysList(rl, ids[i]); ++ ++ /* Freed / unlinked mid-loop: skip. Still on the waiter list: serve. */ ++ if (receiver == NULL) continue; ++ + robj *o = lookupKeyReadWithFlags(rl->db, rl->key, LOOKUP_NOEFFECTS); + /* 1. In case new key was added/touched we need to verify it satisfy the + * blocked type, since we might process the wrong key type. +@@ -634,6 +664,7 @@ static void handleClientsBlockedOnKey(readyList *rl) { + moduleUnblockClientOnKey(receiver, rl->key); + } + } ++ zfree(ids); + } + } + +diff --git a/tests/modules/blockonkeys.c b/tests/modules/blockonkeys.c +index 78e18344809..6789ea80b15 100644 +--- a/tests/modules/blockonkeys.c ++++ b/tests/modules/blockonkeys.c +@@ -583,6 +583,69 @@ int blockonkeys_blpopn(ValkeyModuleCtx *ctx, ValkeyModuleString **argv, int argc + return VALKEYMODULE_OK; + } + ++/* --- Regression for ready-key iteration UAF when reply kills a successor --- */ ++ ++static unsigned long long blockonkeys_kill_successor_id = 0; ++ ++static int blockonkeys_kill_successor_timeout(ValkeyModuleCtx *ctx, ValkeyModuleString **argv, int argc) { ++ VALKEYMODULE_NOT_USED(argv); ++ VALKEYMODULE_NOT_USED(argc); ++ return ValkeyModule_ReplyWithSimpleString(ctx, "TIMEOUT"); ++} ++ ++static void blockonkeys_kill_successor_free(ValkeyModuleCtx *ctx, void *privdata) { ++ VALKEYMODULE_NOT_USED(ctx); ++ ValkeyModule_Free(privdata); ++} ++ ++static int blockonkeys_kill_successor_reply(ValkeyModuleCtx *ctx, ValkeyModuleString **argv, int argc) { ++ VALKEYMODULE_NOT_USED(argv); ++ VALKEYMODULE_NOT_USED(argc); ++ int *role = ValkeyModule_GetBlockedClientPrivateData(ctx); ++ if (role && *role == 1 && blockonkeys_kill_successor_id) { ++ ValkeyModuleCallReply *r = ValkeyModule_Call(ctx, "CLIENT", "ccl", "KILL", "ID", ++ (long long)blockonkeys_kill_successor_id); ++ if (r) ValkeyModule_FreeCallReply(r); ++ } ++ return ValkeyModule_ReplyWithSimpleString(ctx, role && *role == 1 ? "FIRST" : "SECOND"); ++} ++ ++/* BLOCKONKEYS.BLOCK_KILL_SUCCESSOR key first|second ++ * ++ * Blocks on key. The first blocked client's reply callback kills the second ++ * blocked client via CLIENT KILL. Used to verify ready-key iteration survives ++ * reentrant teardown of a successor list node. */ ++static int blockonkeys_block_kill_successor(ValkeyModuleCtx *ctx, ValkeyModuleString **argv, int argc) { ++ if (argc != 3) return ValkeyModule_WrongArity(ctx); ++ ++ size_t len = 0; ++ const char *role_str = ValkeyModule_StringPtrLen(argv[2], &len); ++ int *role = ValkeyModule_Alloc(sizeof(*role)); ++ if (len == 5 && !memcmp(role_str, "first", 5)) { ++ *role = 1; ++ } else if (len == 6 && !memcmp(role_str, "second", 6)) { ++ *role = 2; ++ blockonkeys_kill_successor_id = ValkeyModule_GetClientId(ctx); ++ } else { ++ ValkeyModule_Free(role); ++ return ValkeyModule_ReplyWithError(ctx, "ERR role must be first|second"); ++ } ++ ++ ValkeyModule_BlockClientOnKeys(ctx, blockonkeys_kill_successor_reply, ++ blockonkeys_kill_successor_timeout, ++ blockonkeys_kill_successor_free, 0, &argv[1], 1, role); ++ return VALKEYMODULE_OK; ++} ++ ++/* BLOCKONKEYS.SIGNAL_READY key - SET key and signal it ready for blocked modules. */ ++static int blockonkeys_signal_ready(ValkeyModuleCtx *ctx, ValkeyModuleString **argv, int argc) { ++ if (argc != 2) return ValkeyModule_WrongArity(ctx); ++ ValkeyModuleCallReply *r = ValkeyModule_Call(ctx, "SET", "sc", argv[1], "x"); ++ if (r) ValkeyModule_FreeCallReply(r); ++ ValkeyModule_SignalKeyAsReady(ctx, argv[1]); ++ return ValkeyModule_ReplyWithSimpleString(ctx, "OK"); ++} ++ + int ValkeyModule_OnLoad(ValkeyModuleCtx *ctx, ValkeyModuleString **argv, int argc) { + VALKEYMODULE_NOT_USED(argv); + VALKEYMODULE_NOT_USED(argc); +@@ -641,5 +704,14 @@ int ValkeyModule_OnLoad(ValkeyModuleCtx *ctx, ValkeyModuleString **argv, int arg + if (ValkeyModule_CreateCommand(ctx, "blockonkeys.blpopn_or_unblock", blockonkeys_blpopn, + "write", 1, 1, 1) == VALKEYMODULE_ERR) + return VALKEYMODULE_ERR; ++ ++ if (ValkeyModule_CreateCommand(ctx, "blockonkeys.block_kill_successor", ++ blockonkeys_block_kill_successor, "", 1, 1, 1) == VALKEYMODULE_ERR) ++ return VALKEYMODULE_ERR; ++ ++ if (ValkeyModule_CreateCommand(ctx, "blockonkeys.signal_ready", ++ blockonkeys_signal_ready, "write", 1, 1, 1) == VALKEYMODULE_ERR) ++ return VALKEYMODULE_ERR; ++ + return VALKEYMODULE_OK; + } +diff --git a/tests/unit/client-eviction.tcl b/tests/unit/client-eviction.tcl +index 38ed8ed4251..47873ac0c82 100644 +--- a/tests/unit/client-eviction.tcl ++++ b/tests/unit/client-eviction.tcl +@@ -601,5 +601,61 @@ start_server {} { + } + } + ++# Regression: BLPOP reexec → evictClients() freeing a same-key successor ++# must not UAF the ready-key waiter list (same class as #4198, no module). ++start_server {} { ++ test {BLPOP + maxmemory-clients: successor eviction during ready-key reexec} { ++ r flushdb ++ r client no-evict on ++ r config set maxmemory-clients 100mb ++ r config set client-output-buffer-limit "normal 0 0 0" ++ ++ r setrange fat 300000 x ++ ++ # First waiter (served first). Must not be the eviction victim. ++ set rdA [valkey_deferring_client] ++ $rdA client no-evict on ++ $rdA flush ++ assert_equal OK [$rdA read] ++ $rdA client setname blpop_a ++ $rdA flush ++ assert_equal OK [$rdA read] ++ $rdA blpop mylist 0 ++ $rdA flush ++ ++ # Second waiter: inflate server-side output buffer, then block as successor. ++ set rdB [valkey_deferring_client] ++ $rdB client setname blpop_b ++ $rdB flush ++ assert_equal OK [$rdB read] ++ while {[client_field blpop_b tot-mem] < 2000000} { ++ $rdB get fat ++ $rdB flush ++ } ++ # Leave GET replies unread so tot-mem stays large while blocked. ++ $rdB blpop mylist 0 ++ $rdB flush ++ ++ wait_for_blocked_clients_count 2 ++ assert {[client_field blpop_b tot-mem] >= 2000000} ++ ++ # 128KiB is the server's minimum client-eviction limit. ++ set low_limit [expr {128 * 1024}] ++ r multi ++ r config set maxmemory-clients $low_limit ++ r lpush mylist v1 ++ assert_equal {OK 1} [r exec] ++ ++ # Surviving here (especially under ASan) means the ready-key iterator ++ # did not UAF when the successor was freed mid-iteration. ++ assert_equal PONG [r ping] ++ assert_equal {mylist v1} [$rdA read] ++ assert {![client_exists blpop_b]} ++ ++ catch {$rdA close} ++ catch {$rdB close} ++ } ++} ++ + } ;# tags + +diff --git a/tests/unit/moduleapi/blockonkeys.tcl b/tests/unit/moduleapi/blockonkeys.tcl +index 9014e930cf9..cd8ede3f782 100644 +--- a/tests/unit/moduleapi/blockonkeys.tcl ++++ b/tests/unit/moduleapi/blockonkeys.tcl +@@ -217,6 +217,26 @@ start_server {tags {"modules"}} { + r client kill id $cid ;# try to smoke-out client-related memory leak + } + ++ test {Module blocked-on-keys reply killing successor must not UAF} { ++ # Regression for #4198: serving a BLOCKED_MODULE client may run a reply ++ # callback that CLIENT KILLs another client still on the same ready-key ++ # list. Iteration must not retain a stale listNode* across that teardown. ++ r del race ++ set rd1 [valkey_deferring_client] ++ set rd2 [valkey_deferring_client] ++ ++ $rd1 blockonkeys.block_kill_successor race first ++ wait_for_blocked_clients_count 1 ++ $rd2 blockonkeys.block_kill_successor race second ++ wait_for_blocked_clients_count 2 ++ ++ assert_equal {OK} [r blockonkeys.signal_ready race] ++ assert_equal {FIRST} [$rd1 read] ++ assert_equal {PONG} [r ping] ++ $rd1 close ++ catch {$rd2 close} ++ } ++ + test {Module client blocked on keys (with metadata): Blocked, CLIENT UNBLOCK TIMEOUT} { + r del k + r fsl.push k 32 diff -Nru valkey-8.1.1+dfsg1/debian/patches/CVE-2026-82677.patch valkey-8.1.1+dfsg1/debian/patches/CVE-2026-82677.patch --- valkey-8.1.1+dfsg1/debian/patches/CVE-2026-82677.patch 1970-01-01 00:00:00.000000000 +0000 +++ valkey-8.1.1+dfsg1/debian/patches/CVE-2026-82677.patch 2026-10-24 15:19:26.000000000 +0000 @@ -0,0 +1,158 @@ +Author: Quanye Yang +Date: Mon, 3 Aug 2026 10:38:09 +0800 +Description: Prevent double-free of the module timer when the callback stops it. + +Origin: upstream, https://github.com/valkey-io/valkey/commit/345a057196edbd94f2f0be87d26a1299fcd95e60 +Bug: https://github.com/valkey-io/valkey/issues/4200 +Bug-Debian: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1146641 +Bug-Security: https://security-tracker.debian.org/tracker/CVE-2026-82677 + +--- + src/module.c | 16 +++++++++-- + tests/modules/timer.c | 49 ++++++++++++++++++++++++++++++++++ + tests/unit/moduleapi/timer.tcl | 18 +++++++++++++ + 3 files changed, 81 insertions(+), 2 deletions(-) + +diff --git a/src/module.c b/src/module.c +index 07fe73d8c92..ae13665b28d 100644 +--- a/src/module.c ++++ b/src/module.c +@@ -9351,14 +9351,26 @@ long long moduleTimerHandler(struct aeEventLoop *eventLoop, long long id, void * + memcpy(&expiretime, ri.key, sizeof(expiretime)); + expiretime = ntohu64(expiretime); + if (now >= expiretime) { ++ /* Preserve the timer ID before invoking the callback. The callback ++ * may call ValkeyModule_StopTimer() on the currently firing timer, ++ * which removes the radix-tree entry and frees the timer object. ++ * Also, other tree mutations in the callback can invalidate ri.key. */ ++ ValkeyModuleTimerID current_id; ++ memcpy(¤t_id, ri.key, sizeof(current_id)); ++ + ValkeyModuleTimer *timer = ri.data; + ValkeyModuleCtx ctx; + moduleCreateContext(&ctx, timer->module, VALKEYMODULE_CTX_TEMP_CLIENT); + selectDb(ctx.client, timer->dbid); + timer->callback(&ctx, timer->data); + moduleFreeContext(&ctx); +- raxRemove(Timers, (unsigned char *)ri.key, ri.key_len, NULL); +- zfree(timer); ++ ++ /* Skip cleanup if the callback already stopped this timer. */ ++ void *live = NULL; ++ if (raxFind(Timers, (unsigned char *)¤t_id, sizeof(current_id), &live) && live == timer) { ++ raxRemove(Timers, (unsigned char *)¤t_id, sizeof(current_id), NULL); ++ zfree(timer); ++ } + } else { + /* We call ustime() again instead of using the cached 'now' so that + * 'next_period' isn't affected by the time it took to execute +diff --git a/tests/modules/timer.c b/tests/modules/timer.c +index f5eea192acf..fe106c52db7 100644 +--- a/tests/modules/timer.c ++++ b/tests/modules/timer.c +@@ -1,6 +1,11 @@ + + #include "valkeymodule.h" + ++typedef struct { ++ ValkeyModuleTimerID id; ++ ValkeyModuleString *keyname; ++} selfstopTimerData; ++ + static void timer_callback(ValkeyModuleCtx *ctx, void *data) + { + ValkeyModuleString *keyname = data; +@@ -12,6 +17,24 @@ static void timer_callback(ValkeyModuleCtx *ctx, void *data) + ValkeyModule_FreeString(ctx, keyname); + } + ++/* Stops the currently firing timer from inside its own callback. ++ * This used to double-free the ValkeyModuleTimer in moduleTimerHandler(). */ ++static void selfstop_timer_callback(ValkeyModuleCtx *ctx, void *data) ++{ ++ selfstopTimerData *d = data; ++ ValkeyModuleCallReply *reply; ++ void *timer_data = NULL; ++ ++ ValkeyModule_Assert(ValkeyModule_StopTimer(ctx, d->id, &timer_data) == VALKEYMODULE_OK); ++ ValkeyModule_Assert(timer_data == d); ++ ++ reply = ValkeyModule_Call(ctx, "INCR", "s", d->keyname); ++ if (reply != NULL) ++ ValkeyModule_FreeCallReply(reply); ++ ValkeyModule_FreeString(ctx, d->keyname); ++ ValkeyModule_Free(d); ++} ++ + int test_createtimer(ValkeyModuleCtx *ctx, ValkeyModuleString **argv, int argc) + { + if (argc != 3) { +@@ -84,6 +107,30 @@ int test_stoptimer(ValkeyModuleCtx *ctx, ValkeyModuleString **argv, int argc) + return VALKEYMODULE_OK; + } + ++int test_selfstoptimer(ValkeyModuleCtx *ctx, ValkeyModuleString **argv, int argc) ++{ ++ if (argc != 3) { ++ ValkeyModule_WrongArity(ctx); ++ return VALKEYMODULE_OK; ++ } ++ ++ long long period; ++ if (ValkeyModule_StringToLongLong(argv[1], &period) == VALKEYMODULE_ERR) { ++ ValkeyModule_ReplyWithError(ctx, "Invalid time specified."); ++ return VALKEYMODULE_OK; ++ } ++ ++ ValkeyModuleString *keyname = argv[2]; ++ ValkeyModule_RetainString(ctx, keyname); ++ ++ selfstopTimerData *d = ValkeyModule_Alloc(sizeof(*d)); ++ d->keyname = keyname; ++ d->id = ValkeyModule_CreateTimer(ctx, period, selfstop_timer_callback, d); ++ ValkeyModule_ReplyWithLongLong(ctx, d->id); ++ ++ return VALKEYMODULE_OK; ++} ++ + + int ValkeyModule_OnLoad(ValkeyModuleCtx *ctx, ValkeyModuleString **argv, int argc) { + VALKEYMODULE_NOT_USED(argv); +@@ -97,6 +144,8 @@ int ValkeyModule_OnLoad(ValkeyModuleCtx *ctx, ValkeyModuleString **argv, int arg + return VALKEYMODULE_ERR; + if (ValkeyModule_CreateCommand(ctx,"test.stoptimer", test_stoptimer,"",0,0,0) == VALKEYMODULE_ERR) + return VALKEYMODULE_ERR; ++ if (ValkeyModule_CreateCommand(ctx,"test.selfstoptimer", test_selfstoptimer,"",0,0,0) == VALKEYMODULE_ERR) ++ return VALKEYMODULE_ERR; + + return VALKEYMODULE_OK; + } +diff --git a/tests/unit/moduleapi/timer.tcl b/tests/unit/moduleapi/timer.tcl +index 4e9dd0f0966..40835fbbf8b 100644 +--- a/tests/unit/moduleapi/timer.tcl ++++ b/tests/unit/moduleapi/timer.tcl +@@ -56,6 +56,24 @@ start_server {tags {"modules"}} { + assert_equal {} [r test.gettimer $id] + } + ++ test {RM_StopTimer: stopping the currently firing timer is safe} { ++ # Regression for double-free when a timer callback calls ++ # ValkeyModule_StopTimer() on its own timer ID. ++ r set "timer-selfstop-key" 0 ++ set id [r test.selfstoptimer 10 timer-selfstop-key] ++ ++ wait_for_condition 50 100 { ++ [r get timer-selfstop-key] == 1 ++ } else { ++ fail "Self-stopping timer callback did not run" ++ } ++ ++ # Timer was freed by StopTimer inside the callback; id must be gone ++ # and the server must still be responsive (no double free crash). ++ assert_equal {} [r test.gettimer $id] ++ assert_equal {PONG} [r ping] ++ } ++ + test "Module can be unloaded when timer was finished" { + r set "timer-incr-key" 0 + r test.createtimer 500 timer-incr-key diff -Nru valkey-8.1.1+dfsg1/debian/patches/series valkey-8.1.1+dfsg1/debian/patches/series --- valkey-8.1.1+dfsg1/debian/patches/series 2026-03-29 08:34:29.000000000 +0000 +++ valkey-8.1.1+dfsg1/debian/patches/series 2026-10-24 15:19:26.000000000 +0000 @@ -10,3 +10,10 @@ CVE-2025-49844_CVE-2025-46817_CVE-2025-46818_CVE-2025-46819.patch CVE-2025-67733.patch CVE-2026-21863.patch +CVE-2026-56684.patch +CVE-2026-63639.patch +CVE-2026-82631.patch +CVE-2026-82677.patch +CVE-2026-23479.patch +CVE-2026-23631.patch +CVE-2026-25243.patch