Version in base suite: 8.0.2-3+deb13u2 Base version: redis_8.0.2-3+deb13u2 Target version: redis_8.0.2-3+deb13u3 Base file: /srv/ftp-master.debian.org/ftp/pool/main/r/redis/redis_8.0.2-3+deb13u2.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/r/redis/redis_8.0.2-3+deb13u3.dsc changelog | 39 + patches/0013-CVE-2026-21863.patch | 15 patches/0014-CVE-2026-25243.patch | 268 ++++++++ patches/0015-CVE-2026-23631.patch | 114 +++ patches/0016-CVE-2026-23479.patch | 73 ++ patches/0017-CVE-2026-66373.patch | 91 ++ patches/0018-CVE-2026-81934.patch | 49 + patches/0019-Fix-ACL-key-checks-for-SORT-GEORADIUS-XREAD.patch | 315 ++++++++++ patches/0020-Fix-ACL-OOB-for-wrong-arity-KEYNUM-commands.patch | 189 ++++++ patches/0021-Check-slotinfo-range-when-loading-RDB.patch | 59 + patches/0022-Fix-use-after-free-in-handleClientsBlockedOnKey.patch | 110 +++ patches/0023-Fix-HGETEX-out-of-bounds-read-when-FIELDS-lacks-numfields.patch | 61 + patches/0024-Fix-MurmurHash64A-overflow-in-HyperLogLog.patch | 63 ++ patches/series | 11 14 files changed, 1444 insertions(+), 13 deletions(-) dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmpeiaqh_qz/redis_8.0.2-3+deb13u2.dsc: no acceptable signature found dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmpeiaqh_qz/redis_8.0.2-3+deb13u3.dsc: no acceptable signature found diff -Nru redis-8.0.2/debian/changelog redis-8.0.2/debian/changelog --- redis-8.0.2/debian/changelog 2026-05-13 04:00:00.000000000 +0000 +++ redis-8.0.2/debian/changelog 2026-09-14 16:52:10.000000000 +0000 @@ -1,3 +1,42 @@ +redis (5:8.0.2-3+deb13u3) trixie-security; urgency=high + + * Non-maintainer upload by the Security Team. + * CVE-2026-25243: Invalid memory access in RESTORE. The RESTORE + command did not properly validate serialized values; an + authenticated attacker able to run RESTORE could supply a crafted + payload triggering invalid memory access and possibly remote code + execution. (Closes: #1147421) + * CVE-2026-23631: Lua use-after-free on replicas. An authenticated + attacker could exploit the master-replica synchronization mechanism + to trigger a use-after-free on replicas where replica-read-only is + disabled, potentially leading to remote code execution. + (Closes: #1147421) + * CVE-2026-23479: Use-after-free in the unblock client flow. The error + return from processCommandAndResetClient was not handled when re- + executing a blocked command, allowing an authenticated attacker to + trigger a use-after-free and possibly remote code execution. + (Closes: #1147421) + * CVE-2026-66373: Double free via RESTORE of a stream whose NACK is + shared by several consumers, an incomplete fix for CVE-2026-25243; + deleting both consumers with XGROUP DELCONSUMER could lead to remote + code execution. (Closes: #1147422) + * CVE-2026-81934: Use-after-free in tlsProcessPendingData() when + handling the TLS pending-data list. A remote unauthenticated + attacker may be able to execute arbitrary code with the privileges + of the server. (Closes: #1147423) + * Some important fixes upstream shipped as security fixes without CVE: + - From 8.2.9: ACL key-permission bypass in SORT, + GEORADIUS/GEORADIUSBYMEMBER and XREAD/XREADGROUP, out-of-bounds argv + access during ACL key extraction for wrong-arity KEYNUM commands, + out-of-range SLOT_INFO slot id in RDB loading causing memory corruption, + and a use-after-free in handleClientsBlockedOnKey when reprocessing a + command evicts another client blocked on the same key. + - From 8.0.5: out-of-bounds argv read and crash in HGETEX when the + FIELDS option lacks its numfields argument, and an integer overflow in + the HyperLogLog MurmurHash64A with entries over 2GB. + + -- Aron Xu Tue, 15 Sep 2026 00:52:10 +0800 + redis (5:8.0.2-3+deb13u2) trixie-security; urgency=high * CVE-2025-67733: RESP protocol injection via Lua error_reply. A user diff -Nru redis-8.0.2/debian/patches/0013-CVE-2026-21863.patch redis-8.0.2/debian/patches/0013-CVE-2026-21863.patch --- redis-8.0.2/debian/patches/0013-CVE-2026-21863.patch 2026-05-13 04:00:00.000000000 +0000 +++ redis-8.0.2/debian/patches/0013-CVE-2026-21863.patch 2026-09-14 16:52:10.000000000 +0000 @@ -7,20 +7,9 @@ Signed-off-by: Roshan Khatri Co-authored-by: Madelyn Olson -[Backport from Valkey upstream commit +Backport from Valkey upstream commit: https://github.com/valkey-io/valkey/commit/416939303d2550aefff73ac180f41b84c12ba6c0 - (no Redis-upstream commit exists for this CVE; Valkey is a fork of Redis - with closely shared cluster code).] - -[Adapted for redis-8.0.2: - - The validation lives inline in clusterProcessPacket() rather than in - a separate clusterIsValidPacket(); on a malformed packet redis-8.0.2 - returns 1 (drop packet, keep the link) instead of 0. - - The local `extlen` in this block is `uint16_t` in redis-8.0.2, not - `uint32_t`; surrounding context lines were updated accordingly. - - The serverLog() messages, the two new checks (gossip count and - extension header bounds) and the new test file - tests/unit/cluster/packet.tcl are byte-identical to upstream.] +Reviewed-by: Aron Xu --- src/cluster_legacy.c | 21 +++++++ tests/unit/cluster/packet.tcl | 113 ++++++++++++++++++++++++++++++++++ diff -Nru redis-8.0.2/debian/patches/0014-CVE-2026-25243.patch redis-8.0.2/debian/patches/0014-CVE-2026-25243.patch --- redis-8.0.2/debian/patches/0014-CVE-2026-25243.patch 1970-01-01 00:00:00.000000000 +0000 +++ redis-8.0.2/debian/patches/0014-CVE-2026-25243.patch 2026-09-14 16:52:10.000000000 +0000 @@ -0,0 +1,268 @@ +From defbf45b9adbbfb21a394786b7092d064a8c9b49 Mon Sep 17 00:00:00 2001 +From: Sergei Georgiev +Date: Thu, 27 Nov 2025 11:41:11 +0200 +Subject: [PATCH] Invalid Memory Access in Redis RESTORE Command + (CVE-2026-25243) + +(cherry picked from commit defbf45b9adbbfb21a394786b7092d064a8c9b49) +--- + modules/vector-sets/vset.c | 46 +++++++++++++++-- + src/rdb.c | 6 +-- + src/sds.c | 9 +++- + src/zipmap.c | 7 +++ + tests/integration/corrupt-dump.tcl | 83 ++++++++++++++++++++++++++++++ + tests/unit/dump.tcl | 15 ++++++ + 6 files changed, 157 insertions(+), 9 deletions(-) + +diff --git a/modules/vector-sets/vset.c b/modules/vector-sets/vset.c +index 33b65070d..41d3922dc 100644 +--- a/modules/vector-sets/vset.c ++++ b/modules/vector-sets/vset.c +@@ -1767,14 +1767,50 @@ void *VectorSetRdbLoad(RedisModuleIO *rdb, int encver) { + uint32_t input_dim = RedisModule_LoadUnsigned(rdb); + if (RedisModule_IsIOError(rdb)) goto ioerr; + uint32_t output_dim = dim; +- size_t matrix_size = sizeof(float) * input_dim * output_dim; + +- vset->proj_matrix = RedisModule_Alloc(matrix_size); +- vset->proj_input_size = input_dim; ++ /* Sanity check dimensions to avoid absurd / degenerate matrices. */ ++ if (input_dim == 0 || output_dim == 0) { ++ RedisModule_LogIOError(rdb, "warning", ++ "Invalid projection matrix dimensions: input_dim=%u, output_dim=%u", ++ (unsigned)input_dim, (unsigned)output_dim); ++ goto ioerr; ++ } + +- // Load projection matrix as a binary blob +- char *matrix_blob = RedisModule_LoadStringBuffer(rdb, NULL); ++ /* Check for overflow in matrix_size = sizeof(float) * input_dim * output_dim. */ ++ #if SIZE_MAX == UINT32_MAX ++ if ((size_t)output_dim > SIZE_MAX / sizeof(float)) { ++ RedisModule_LogIOError(rdb, "warning", ++ "Projection matrix size overflow (output_dim too large): input_dim=%u, output_dim=%u", ++ (unsigned)input_dim, (unsigned)output_dim); ++ goto ioerr; ++ } ++ #endif ++ ++ size_t max_input = SIZE_MAX / (sizeof(float) * (size_t)output_dim); ++ if ((size_t)input_dim > max_input) { ++ RedisModule_LogIOError(rdb, "warning", ++ "Projection matrix size overflow: input_dim=%u, output_dim=%u", ++ (unsigned)input_dim, (unsigned)output_dim); ++ goto ioerr; ++ } ++ ++ size_t matrix_size = sizeof(float) * (size_t)input_dim * (size_t)output_dim; ++ ++ /* Load projection matrix as a binary blob and validate length. */ ++ size_t blob_len = 0; ++ char *matrix_blob = RedisModule_LoadStringBuffer(rdb, &blob_len); + if (matrix_blob == NULL) goto ioerr; ++ ++ if (blob_len != matrix_size) { ++ RedisModule_LogIOError(rdb, "warning", ++ "Mismatching projection matrix length: expected=%zu, got=%zu", ++ matrix_size, blob_len); ++ RedisModule_Free(matrix_blob); ++ goto ioerr; ++ } ++ ++ vset->proj_matrix = RedisModule_Alloc(matrix_size); ++ vset->proj_input_size = input_dim; + memcpy(vset->proj_matrix, matrix_blob, matrix_size); + RedisModule_Free(matrix_blob); + } +diff --git a/src/rdb.c b/src/rdb.c +index 2128d1329..860deaf3f 100644 +--- a/src/rdb.c ++++ b/src/rdb.c +@@ -2571,11 +2571,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); +@@ -3066,7 +3067,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/sds.c b/src/sds.c +index 7f1429c46..edd0495aa 100644 +--- a/src/sds.c ++++ b/src/sds.c +@@ -90,7 +90,14 @@ sds _sdsnewlen(const void *init, size_t initlen, int trymalloc) { + unsigned char *fp; /* flags pointer. */ + size_t usable; + +- assert(initlen + hdrlen + 1 > initlen); /* Catch size_t overflow */ ++ if (trymalloc) { ++ /* protect against size_t overflow */ ++ if (initlen + hdrlen + 1 <= initlen) ++ return NULL; ++ } else { ++ assert(initlen + hdrlen + 1 > initlen); /* Catch size_t overflow */ ++ } ++ + sh = trymalloc? + s_trymalloc_usable(hdrlen+initlen+1, &usable) : + s_malloc_usable(hdrlen+initlen+1, &usable); +diff --git a/src/zipmap.c b/src/zipmap.c +index 51c64ca81..e3981d810 100644 +--- a/src/zipmap.c ++++ b/src/zipmap.c +@@ -387,6 +387,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 */ + +@@ -402,6 +406,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/integration/corrupt-dump.tcl b/tests/integration/corrupt-dump.tcl +index ee044c71a..6017723e0 100644 +--- a/tests/integration/corrupt-dump.tcl ++++ b/tests/integration/corrupt-dump.tcl +@@ -941,6 +941,89 @@ test {corrupt payload: fuzzer findings - vector sets with wrong encoding} { + } + } + ++test {corrupt payload: zipmap - element wouldn't fit in listpack} { ++ # Redis converts legacy zipmap encoded hashes to listpacks. ++ # This test creates a zipmap entry with a 1GB value which cannot ++ # fit into a listpack and verifies that RESTORE fails. ++ ++ start_server [list overrides [list loglevel verbose use-exit-on-panic yes crash-memcheck-enabled no proto-max-bulk-len 2147483648 client-query-buffer-limit 2147483648]] { ++ proc zipmap_encode_len {len} { ++ if {$len < 254} { ++ return [binary format c $len] ++ } else { ++ return [binary format ci 254 $len] ++ } ++ } ++ r config set sanitize-dump-payload no ++ ++ # Generates Zipmap with 1GB value - should fail lpSafeToAdd check ++ set val_len [expr {1024 * 1024 * 1024 + 1}] ++ ++ # Zipmap has 1 element ++ set zm [binary format c 1] ++ # Field is 1 byte long ++ append zm [zipmap_encode_len 1] ++ append zm "k" ++ # Value is 1GB long ++ append zm [zipmap_encode_len $val_len] ++ append zm [binary format c 0] ++ append zm [string repeat "A" $val_len] ++ # ZIPMAP_END marker ++ append zm [binary format c 255] ++ # Prepend RDB header ++ set zm_len [string length $zm] ++ set rdb_len [binary format cI 0x80 $zm_len] ++ set dump [binary format c 9] ++ append dump $rdb_len ++ append dump $zm ++ append dump [binary format s 9] ++ append dump [binary format w 0] ++ ++ catch {r RESTORE _hash 0 $dump} err ++ assert_match "*Bad data format*" $err ++ } ++} {} {large-memory} ++ ++test {corrupt payload: zipmap - 5 bytes length encoding for a small field} { ++ start_server [list overrides [list loglevel verbose use-exit-on-panic yes crash-memcheck-enabled no]] { ++ catch { ++ r restore key 0 "\x09\x11\x01\xfe\x04\x00\x00\x00\x01\x00\xff\x00\x04\x00\x76\x61\x6c\x31\xff\x09\x00\xf9\xd5\xa4\xf7\x7d\x00\x3f\x1b" ++ } err ++ assert_match "*Bad data format*" $err ++ verify_log_message 0 "*integrity check failed*" 0 ++ } ++} ++ ++test {corrupt payload: zipmap - 5 bytes length encoding for a small value} { ++ start_server [list overrides [list loglevel verbose use-exit-on-panic yes crash-memcheck-enabled no]] { ++ catch { ++ r restore key 0 "\x09\x0e\x01\x01\x6b\xfe\x04\x00\x00\x00\x00\x76\x61\x6c\x31\xff\x09\x00\xd0\xf9\xe4\x1d\xe4\xfb\x11\x4c" ++ } err ++ assert_match "*Bad data format*" $err ++ verify_log_message 0 "*integrity check failed*" 0 ++ } ++} ++ ++test {corrupt payload: zipmap - 5 bytes length encoding and a huge field} { ++ start_server [list overrides [list loglevel verbose use-exit-on-panic yes crash-memcheck-enabled no] ] { ++ catch { ++ r restore key 0 "\x09\x41\x15\x02\x04\x6b\x65\x79\x31\x04\x00\x76\x61\x6c\x31\xfe\x04\x00\x00\x00\xfe\xff\xff\xff\xfd\x00\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42\xff\x09\x00\x54\x2f\x0a\xca\x4e\x5c\x49\x9f" ++ } err ++ assert_match "*Bad data format*" $err ++ verify_log_message 0 "*integrity check failed*" 0 ++ } ++} ++ ++test {corrupt payload: stream - duplicated consumer PEL entry} { ++ start_server [list overrides [list loglevel verbose use-exit-on-panic yes crash-memcheck-enabled no] ] { ++ catch { ++ r restore key 0 "\x15\x01\x10\x00\x00\x01\x9b\x0d\x56\xa9\xb7\x00\x00\x00\x00\x00\x00\x00\x00\xc3\x39\x40\x42\x15\x42\x00\x00\x00\x11\x00\x02\x01\x00\x01\x01\x01\x86\x66\x69\x65\x6c\x64\x31\x07\x00\x01\x40\x0f\x0a\x00\x01\x86\x76\x61\x6c\x75\x65\x31\x07\x04\x20\x0b\x02\xcd\xd9\x02\xe0\x01\x22\x01\x32\x07\x80\x1a\x04\x32\x07\x06\x01\xff\x02\x81\x00\x00\x01\x9b\x0d\x56\xb7\x90\x00\x81\x00\x00\x01\x9b\x0d\x56\xa9\xb7\x00\x00\x00\x02\x01\x07\x6d\x79\x67\x72\x6f\x75\x70\x81\x00\x00\x01\x9b\x0d\x56\xb7\x90\x00\x02\x02\x00\x00\x01\x9b\x0d\x56\xa9\xb7\x00\x00\x00\x00\x00\x00\x00\x00\x80\xd9\x56\x0d\x9b\x01\x00\x00\x01\x00\x00\x01\x9b\x0d\x56\xb7\x90\x00\x00\x00\x00\x00\x00\x00\x00\x80\xd9\x56\x0d\x9b\x01\x00\x00\x01\x01\x09\x63\x6f\x6e\x73\x75\x6d\x65\x72\x31\x80\xd9\x56\x0d\x9b\x01\x00\x00\x80\xd9\x56\x0d\x9b\x01\x00\x00\x02\x00\x00\x01\x9b\x0d\x56\xa9\xb7\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x9b\x0d\x56\xa9\xb7\x00\x00\x00\x00\x00\x00\x00\x00\x0c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x09\x00\x4b\xe0\x99\x30\x67\x4d\xe5\x87" ++ } err ++ assert_match "*Bad data format*" $err ++ verify_log_message 0 "*Duplicated consumer PEL entry*" 0 ++ } ++} ++ + + } ;# tags + +diff --git a/tests/unit/dump.tcl b/tests/unit/dump.tcl +index 0cd62804a..de2a7873f 100644 +--- a/tests/unit/dump.tcl ++++ b/tests/unit/dump.tcl +@@ -140,6 +140,21 @@ start_server {tags {"dump"}} { + close_replication_stream $repl + } {} {needs:repl} + ++ test {RESTORE fail with invalid payload size} { ++ r debug set-skip-checksum-validation 1 ++ # Payload with mismatched size: claims 0xFFFFFFFFFFFFFFF7 bytes (max uint64 - 8) but provides no data ++ # \x00 = String type ++ # \x81 = 64-bit length marker ++ # \xFF\xFF\xFF\xFF\xFF\xFF\xFF\xF7 = 18446744073709551607 in big-endian ++ # \x0c\x00 = RDB version ++ # \x00... = fake CRC64 ++ set encoded "\x00\x81\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xF7\x0c\x00\x00\x00\x00\x00\x00\x00\x00\x00" ++ r del test ++ catch {r restore test 0 $encoded} e ++ r debug set-skip-checksum-validation 0 ++ set e ++ } {*Bad data format*} {needs:debug} ++ + test {DUMP of non existing key returns nil} { + r dump nonexisting_key + } {} diff -Nru redis-8.0.2/debian/patches/0015-CVE-2026-23631.patch redis-8.0.2/debian/patches/0015-CVE-2026-23631.patch --- redis-8.0.2/debian/patches/0015-CVE-2026-23631.patch 1970-01-01 00:00:00.000000000 +0000 +++ redis-8.0.2/debian/patches/0015-CVE-2026-23631.patch 2026-09-14 16:52:10.000000000 +0000 @@ -0,0 +1,114 @@ +From 5b2a5044d4413113c5213c7a2d5f455186d7d75c Mon Sep 17 00:00:00 2001 +From: Ozan Tezcan +Date: Tue, 14 Apr 2026 08:47:04 +0300 +Subject: [PATCH] Fix use-after-free when fullsync happens while replica is + running a timed out script (CVE-2026-23631) + +Fullsync triggers emptyData and scriptingReset which free the scripting/function engine. If a timed out script is still running on the replica, this causes a use-after-free. Delay fullsync processing in readSyncBulkPayload until the script finishes. + +(cherry picked from commit 5b2a5044d4413113c5213c7a2d5f455186d7d75c) +--- + src/replication.c | 5 ++ + tests/integration/replication.tcl | 76 +++++++++++++++++++++++++++++++ + 2 files changed, 81 insertions(+) + +diff --git a/src/replication.c b/src/replication.c +index 5fab23b1c..489032aed 100644 +--- a/src/replication.c ++++ b/src/replication.c +@@ -2022,6 +2022,11 @@ void replicationAttachToNewMaster(void) { + /* Asynchronously read the SYNC payload we receive from a master */ + #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 --git a/tests/integration/replication.tcl b/tests/integration/replication.tcl +index 10e7a1f25..de8e6bf2e 100644 +--- a/tests/integration/replication.tcl ++++ b/tests/integration/replication.tcl +@@ -1659,3 +1659,79 @@ start_server {tags {"repl external:skip"}} { + } + } + } ++ ++# Fullsync should not free the functions lib ctx while the replica has ++# a timed out function that is still running. ++foreach type {script function} { ++ start_server {tags {"repl external:skip"}} { ++ start_server {} { ++ set master [srv -1 client] ++ set master_host [srv -1 host] ++ set master_port [srv -1 port] ++ set replica [srv 0 client] ++ ++ test "Fullsync should not free scripting engine on a replica while a $type is running" { ++ $master config set repl-diskless-sync yes ++ $master config set repl-diskless-sync-delay 0 ++ # Set small client output buffer limit to trigger fullsync quickly ++ $master config set client-output-buffer-limit "replica 1k 1k 0" ++ $replica config set busy-reply-threshold 1 ;# script timeout in 1 ms ++ ++ # Load function ++ if {$type eq "function"} { ++ $master function load replace {#!lua name=blocklib ++ redis.register_function{ ++ function_name='blockfunc', ++ callback=function() while true do end end, ++ flags={'no-writes'} ++ } ++ } ++ } ++ ++ # Start replication ++ $replica replicaof $master_host $master_port ++ wait_for_sync $replica ++ ++ # Run the blocking script on replica ++ set rd [redis_deferring_client] ++ if {$type eq "script"} { ++ $rd eval {while true do end} 0 ++ } else { ++ $rd fcall_ro blockfunc 0 ++ } ++ ++ # Verify replica replies with BUSY ++ wait_for_condition 50 100 { ++ [catch {$replica ping} e] == 1 && [string match {*BUSY*} $e] ++ } else { ++ fail "$type didn't become busy" ++ } ++ ++ # Fills client output buffer and triggers fullsync ++ populate 5 bigkey 1000000 -1 ++ wait_for_condition 50 100 { ++ [s -1 sync_full] >= 2 ++ } else { ++ fail "Fullsync was not triggered" ++ } ++ ++ # Verify replica is still running the function ++ after 1000 ++ catch {$replica ping} e ++ assert_match {*BUSY*} $e "replica should still reply with BUSY" ++ ++ if {$type eq "script"} { ++ $replica script kill ++ } else { ++ $replica function kill ++ } ++ ++ # Verify replica is responsive again ++ catch {$rd read} result ++ $rd close ++ wait_for_sync $replica ++ assert_equal [$replica ping] "PONG" ++ } ++ } ++ } ++} diff -Nru redis-8.0.2/debian/patches/0016-CVE-2026-23479.patch redis-8.0.2/debian/patches/0016-CVE-2026-23479.patch --- redis-8.0.2/debian/patches/0016-CVE-2026-23479.patch 1970-01-01 00:00:00.000000000 +0000 +++ redis-8.0.2/debian/patches/0016-CVE-2026-23479.patch 2026-09-14 16:52:10.000000000 +0000 @@ -0,0 +1,73 @@ +From 252f4bd1370d25ee5627475a81ac8ba3c567be40 Mon Sep 17 00:00:00 2001 +From: "debing.sun" +Date: Mon, 29 Dec 2025 16:20:41 +0800 +Subject: [PATCH] Fix use-after-free when evicting blocked client during + unblock (CVE-2026-23479) + +When re-executing a pending command after unblocking, check the return value +of `processCommandAndResetClient` and exit if needed. + +(cherry picked from commit 252f4bd1370d25ee5627475a81ac8ba3c567be40) +--- + src/blocked.c | 8 +++++++- + tests/unit/client-eviction.tcl | 29 +++++++++++++++++++++++++++++ + 2 files changed, 36 insertions(+), 1 deletion(-) + +diff --git a/src/blocked.c b/src/blocked.c +index 4d3df4403..4d65f56ec 100644 +--- a/src/blocked.c ++++ b/src/blocked.c +@@ -657,7 +657,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->flags & CLIENT_BLOCKED)) { + if (c->flags & CLIENT_MODULE) { + moduleCallCommandUnblockedHandler(c); +diff --git a/tests/unit/client-eviction.tcl b/tests/unit/client-eviction.tcl +index 3caaf9bd4..62e42580c 100644 +--- a/tests/unit/client-eviction.tcl ++++ b/tests/unit/client-eviction.tcl +@@ -607,5 +607,34 @@ start_server {} { + } + } + ++start_server {} { ++ r flushall ++ r client no-evict on ++ r config set maxmemory-clients 0 ++ ++ test "Verify blocked client eviction during unblock does not cause use-after-free" { ++ # Create a deferring client that will be blocked on stream ++ # Use a long stream name to make client memory usage exceed 200000 bytes ++ set rd [redis_deferring_client] ++ $rd XREAD BLOCK 0 STREAMS mystream stream_[string repeat x 200000] $ $ ++ ++ # Wait for the client to be blocked ++ wait_for_condition 50 100 { ++ [s blocked_clients] eq {1} ++ } else { ++ fail "Client was not blocked" ++ } ++ ++ # Now lower MAXMEMORY-CLIENTS to a low value and use ++ # XADD to unblock the blocked client, triggering eviction. ++ r MULTI ++ r CONFIG SET MAXMEMORY-CLIENTS 100000 ;# Put in MULTI to defer blocked client eviction until after EXEC ++ r XADD mystream * field val ++ r EXEC ++ r PING ++ $rd close ++ } ++} ++ + } ;# tags + diff -Nru redis-8.0.2/debian/patches/0017-CVE-2026-66373.patch redis-8.0.2/debian/patches/0017-CVE-2026-66373.patch --- redis-8.0.2/debian/patches/0017-CVE-2026-66373.patch 1970-01-01 00:00:00.000000000 +0000 +++ redis-8.0.2/debian/patches/0017-CVE-2026-66373.patch 2026-09-14 16:52:10.000000000 +0000 @@ -0,0 +1,91 @@ +From d8a624453db3ef39bf40f3e73c25aceee9fd8b76 Mon Sep 17 00:00:00 2001 +From: sggeorgiev +Date: Thu, 23 Apr 2026 15:46:48 +0300 +Subject: [PATCH] Reject corrupt stream RDB with shared NACK across consumers + (#15081) + +**Summary** + +Detects and rejects corrupt stream RDB payloads where the same NACK +(pending entry) is referenced by more than one consumer, which violates +a stream data-structure. + +**Changes** + +- **`rdbLoadObject` (stream consumer PEL loading)**: Added a guard that +checks `nack->consumer != NULL` before assigning the consumer pointer. +When a second consumer's PEL references a NACK that was already claimed +by a prior consumer, the loader now reports a corrupt RDB error and +aborts instead of silently overwriting the pointer. Without this check, +two consumers share the same `streamNACK`, and freeing the first +consumer's PEL leaves the second with a dangling pointer. +- **`corrupt-dump.tcl`**: Added a regression test that crafts a stream +with two consumers (`consumerA`, `consumerB`) whose PELs both reference +the same entry (`1-0`). The `RESTORE` command is expected to fail with +`"Bad data format"`, and the server must remain responsive (`PING` +succeeds). + +**Benefits** + +- **Fail-fast on corrupt data**: The invariant violation is caught at +load time with a clear diagnostic message rather than manifesting as a +crash later during normal operation. +- **Regression coverage**: The crafted payload in the test ensures this +class of corruption is permanently guarded against. + +(cherry picked from commit d8a624453db3ef39bf40f3e73c25aceee9fd8b76) +--- + src/rdb.c | 8 ++++++++ + tests/integration/corrupt-dump.tcl | 17 ++++++++++++++++- + 2 files changed, 24 insertions(+), 1 deletion(-) + +diff --git a/src/rdb.c b/src/rdb.c +index 860deaf3f..e6545d944 100644 +--- a/src/rdb.c ++++ b/src/rdb.c +@@ -3058,6 +3058,14 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) + } + streamNACK *nack = result; + ++ /* If the NACK already has a consumer assigned, the ++ * payload is corrupt — each global PEL entry must be ++ * claimed by exactly one consumer. */ ++ if (nack->consumer != NULL) { ++ rdbReportCorruptRDB("Stream consumer PEL entry already has a consumer assigned"); ++ decrRefCount(o); ++ return NULL; ++ } + /* 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. */ +diff --git a/tests/integration/corrupt-dump.tcl b/tests/integration/corrupt-dump.tcl +index 6017723e0..58bb52ea4 100644 +--- a/tests/integration/corrupt-dump.tcl ++++ b/tests/integration/corrupt-dump.tcl +@@ -1020,10 +1020,25 @@ test {corrupt payload: stream - duplicated consumer PEL entry} { + r restore key 0 "\x15\x01\x10\x00\x00\x01\x9b\x0d\x56\xa9\xb7\x00\x00\x00\x00\x00\x00\x00\x00\xc3\x39\x40\x42\x15\x42\x00\x00\x00\x11\x00\x02\x01\x00\x01\x01\x01\x86\x66\x69\x65\x6c\x64\x31\x07\x00\x01\x40\x0f\x0a\x00\x01\x86\x76\x61\x6c\x75\x65\x31\x07\x04\x20\x0b\x02\xcd\xd9\x02\xe0\x01\x22\x01\x32\x07\x80\x1a\x04\x32\x07\x06\x01\xff\x02\x81\x00\x00\x01\x9b\x0d\x56\xb7\x90\x00\x81\x00\x00\x01\x9b\x0d\x56\xa9\xb7\x00\x00\x00\x02\x01\x07\x6d\x79\x67\x72\x6f\x75\x70\x81\x00\x00\x01\x9b\x0d\x56\xb7\x90\x00\x02\x02\x00\x00\x01\x9b\x0d\x56\xa9\xb7\x00\x00\x00\x00\x00\x00\x00\x00\x80\xd9\x56\x0d\x9b\x01\x00\x00\x01\x00\x00\x01\x9b\x0d\x56\xb7\x90\x00\x00\x00\x00\x00\x00\x00\x00\x80\xd9\x56\x0d\x9b\x01\x00\x00\x01\x01\x09\x63\x6f\x6e\x73\x75\x6d\x65\x72\x31\x80\xd9\x56\x0d\x9b\x01\x00\x00\x80\xd9\x56\x0d\x9b\x01\x00\x00\x02\x00\x00\x01\x9b\x0d\x56\xa9\xb7\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x9b\x0d\x56\xa9\xb7\x00\x00\x00\x00\x00\x00\x00\x00\x0c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x09\x00\x4b\xe0\x99\x30\x67\x4d\xe5\x87" + } err + assert_match "*Bad data format*" $err +- verify_log_message 0 "*Duplicated consumer PEL entry*" 0 ++ verify_log_message 0 "*Stream consumer PEL entry already has a consumer assigned*" 0 + } + } + ++test {corrupt payload: stream with NACK shared between two consumers} { ++ start_server [list overrides [list loglevel verbose use-exit-on-panic yes crash-memcheck-enabled no]] { ++ r debug set-skip-checksum-validation 1 ++ # Payload: stream with entry 1-0, one consumer group (mygroup), ++ # two consumers whose PELs both reference 1-0 (shared NACK). ++ # XACK on one consumer frees the NACK, leaving a dangling ++ # pointer in the other consumer's PEL (use-after-free). ++ catch {r RESTORE mystream 0 "\x1a\x01\x10\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x1d\x1d\x00\x00\x00\x0a\x00\x01\x01\x00\x01\x01\x01\x81\x6b\x02\x00\x01\x02\x01\x00\x01\x00\x01\x81\x76\x02\x04\x01\xff\x01\x01\x00\x01\x00\x00\x00\x01\x01\x07\x6d\x79\x67\x72\x6f\x75\x70\x01\x00\x01\x01\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x01\x64\x42\xb9\x9d\x01\x00\x00\x01\x02\x09\x63\x6f\x6e\x73\x75\x6d\x65\x72\x41\x01\x64\x42\xb9\x9d\x01\x00\x00\x01\x64\x42\xb9\x9d\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x09\x63\x6f\x6e\x73\x75\x6d\x65\x72\x42\x01\x64\x42\xb9\x9d\x01\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\x01\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x40\x64\x40\x64\x00\x00\x00\x0c\x00\xe7\x12\xf7\xcc\x25\xd5\x0e\x44"} err ++ catch {r XACK mystream mygroup 1-0} _ ++ catch {r XREADGROUP GROUP mygroup consumerA COUNT 10 STREAMS mystream 0} _ ++ catch {r DEL mystream} _ ++ assert_match "*Bad data format*" $err ++ r ping ++ } ++} + + } ;# tags + diff -Nru redis-8.0.2/debian/patches/0018-CVE-2026-81934.patch redis-8.0.2/debian/patches/0018-CVE-2026-81934.patch --- redis-8.0.2/debian/patches/0018-CVE-2026-81934.patch 1970-01-01 00:00:00.000000000 +0000 +++ redis-8.0.2/debian/patches/0018-CVE-2026-81934.patch 2026-09-14 16:52:10.000000000 +0000 @@ -0,0 +1,49 @@ +From 0678e5663aa87ef0bbe62837219a4c85f6f5c93c Mon Sep 17 00:00:00 2001 +From: Sergei Georgiev +Date: Tue, 9 Jun 2026 14:22:50 +0300 +Subject: [PATCH] Fix use-after-free in tlsProcessPendingData() pending-list + iteration (#1391) +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +`tlsProcessPendingData()` iterates `pending_list` using a `listIter`, which pre-caches the `next` node pointer on every `listNext()` call. This cached pointer can dangle and be dereferenced after the node it points to has been freed, causing a use-after-free and a server crash (SIGSEGV). + +The issue occurs because `tlsHandleEvent()` runs the connection's read handler, which can execute a command (e.g. `CLIENT KILL`) that closes a *different* pending TLS connection. That close path goes through `freeClient()` → `connClose()` → `connTLSClose()`, which calls `listDelNode()` and frees the victim connection's `pending_list` node. If the iterator's cached `next` pointer referenced that node, the following `listNext()` reads freed memory. The `listNext()` contract only permits removing the *current* node, not arbitrary other nodes. + +Replace the `listIter`-based iteration with a detach-from-head, bounded drain so that no list node pointer is ever held across a handler call: + +- Re-read `listFirst()` on each iteration instead of relying on a pre-cached `next` pointer +- Detach the head via `tlsPendingRemove()` *before* calling `tlsHandleEvent()`, so the loop always makes forward progress +- Semantics are preserved: in the common case each connection is handled exactly once per cycle, in order + +(cherry picked from commit 98ff29b2828bf3245167b416ee23e6797f551a37) +(cherry picked from commit 0678e5663aa87ef0bbe62837219a4c85f6f5c93c) +--- + src/tls.c | 9 ++++----- + 1 file changed, 4 insertions(+), 5 deletions(-) + +diff --git a/src/tls.c b/src/tls.c +index a0733a4b6..0fa468cfc 100644 +--- a/src/tls.c ++++ b/src/tls.c +@@ -1098,15 +1098,14 @@ static int tlsHasPendingData(struct aeEventLoop *el) { + } + + static int tlsProcessPendingData(struct aeEventLoop *el) { +- listIter li; +- listNode *ln; +- + list *pending_list = el->privdata[1]; + if (!pending_list) return 0; + int processed = listLength(pending_list); +- listRewind(pending_list,&li); +- while((ln = listNext(&li))) { ++ for (int i = 0; i < processed; i++) { ++ listNode *ln = listFirst(pending_list); ++ if (!ln) break; + tls_connection *conn = listNodeValue(ln); ++ tlsPendingRemove(conn); + tlsHandleEvent(conn, AE_READABLE); + } + return processed; diff -Nru redis-8.0.2/debian/patches/0019-Fix-ACL-key-checks-for-SORT-GEORADIUS-XREAD.patch redis-8.0.2/debian/patches/0019-Fix-ACL-key-checks-for-SORT-GEORADIUS-XREAD.patch --- redis-8.0.2/debian/patches/0019-Fix-ACL-key-checks-for-SORT-GEORADIUS-XREAD.patch 1970-01-01 00:00:00.000000000 +0000 +++ redis-8.0.2/debian/patches/0019-Fix-ACL-key-checks-for-SORT-GEORADIUS-XREAD.patch 2026-09-14 16:52:10.000000000 +0000 @@ -0,0 +1,315 @@ +From 00ae4906c2c079c351bd63fd3fd42bd0c4b2c8d0 Mon Sep 17 00:00:00 2001 +From: Sergei Georgiev +Date: Sat, 18 Jul 2026 10:21:19 +0300 +Subject: [PATCH] Fix ACL key checks for SORT, GEORADIUS/GEORADIUSBYMEMBER, and + XREAD/XREADGROUP (#15478) +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Several commands resolved ACL keys from a keyspec that didn't match the +key the command actually operates on, allowing a permission bypass: + +- **`GEORADIUS`/`GEORADIUSBYMEMBER`**: the `STORE`/`STOREDIST` keyspec +matches the **first** occurrence of the keyword, but the command uses +**last-wins** semantics and writes to the **last** key. So `GEORADIUS +... STORE STORE ` passed ACL while writing to the +forbidden key (same for `STOREDIST` and both commands). + +- **`SORT`**: `STORE` also uses last-wins semantics. The `STORE` keyspec +is already `unknown`, so ACL falls back to `sortGetKeys()` — but that +parser didn't skip the `STORE` argument. An earlier `STORE` value that +looks like an option keyword (`BY`/`GET`/`LIMIT`) derailed the scan, so +a permitted (or non-existent) first key masked a forbidden last key that +the write actually landed on. + +- **`XREAD`/`XREADGROUP`**: the stream keys are the args after the +`STREAMS` keyword, whose position is found by a static keyspec. A +consumer/group named `STREAMS`, or options before `GROUP`/`STREAMS`, +could shift the real `STREAMS` token so ACL validated the wrong argument +and the read hit a forbidden key. + +Make ACL resolve keys via each command's `getKeys` function, which +returns the effective keys with proper flags: + +- **`GEORADIUS`/`GEORADIUSBYMEMBER`**: mark the `STORE`/`STOREDIST` +keyspecs `INCOMPLETE` so ACL falls back to `georadiusGetKeys()`, which +returns the last key (`RO|ACCESS` source, `OW|UPDATE` store). +- **`SORT`**: no keyspec change needed — the `STORE` keyspec is already +`unknown`, so ACL already delegates to `sortGetKeys()`. Fixed the parser +to skip the `STORE` argument (`i++`) so it isn't re-parsed as an option +keyword; the last `STORE` still wins. +- **`XREAD`/`XREADGROUP`**: mark the keyspecs `INCOMPLETE` and fall back +to `xreadGetKeys()`, which now skips option arguments (`COUNT`, +`MAXCOUNT`, `MAXSIZE`, `CLAIM`, `GROUP`, …) to locate the real `STREAMS` +token and tags the resolved keys `RO|ACCESS`. + +Adds `acl-v2` tests for duplicate `STORE`/`STOREDIST` +(`GEORADIUS`/`GEORADIUSBYMEMBER`, `SORT`) and for the +`XREAD`/`XREADGROUP` `STREAMS`-keyword confusion. + +(cherry picked from commit ce2f04c3fd72978ab7c841a8074621363c11eee0) +(cherry picked from commit 00ae4906c2c079c351bd63fd3fd42bd0c4b2c8d0) +--- + src/commands.def | 8 +++--- + src/commands/georadius.json | 8 ++++-- + src/commands/georadiusbymember.json | 8 ++++-- + src/commands/xread.json | 4 ++- + src/commands/xreadgroup.json | 4 ++- + src/db.c | 23 +++++++++------- + tests/unit/acl-v2.tcl | 42 +++++++++++++++++++++++++++++ + 7 files changed, 77 insertions(+), 20 deletions(-) + +diff --git a/src/commands.def b/src/commands.def +index 5a7545e32..1f594a7eb 100644 +--- a/src/commands.def ++++ b/src/commands.def +@@ -2853,7 +2853,7 @@ commandHistory GEORADIUS_History[] = { + #ifndef SKIP_CMD_KEY_SPECS_TABLE + /* GEORADIUS key specs */ + keySpec GEORADIUS_Keyspecs[3] = { +-{NULL,CMD_KEY_RO|CMD_KEY_ACCESS,KSPEC_BS_INDEX,.bs.index={1},KSPEC_FK_RANGE,.fk.range={0,1,0}},{NULL,CMD_KEY_OW|CMD_KEY_UPDATE,KSPEC_BS_KEYWORD,.bs.keyword={"STORE",6},KSPEC_FK_RANGE,.fk.range={0,1,0}},{NULL,CMD_KEY_OW|CMD_KEY_UPDATE,KSPEC_BS_KEYWORD,.bs.keyword={"STOREDIST",6},KSPEC_FK_RANGE,.fk.range={0,1,0}} ++{NULL,CMD_KEY_RO|CMD_KEY_ACCESS,KSPEC_BS_INDEX,.bs.index={1},KSPEC_FK_RANGE,.fk.range={0,1,0}},{"Incomplete because duplicate STORE options use last-wins; fall back to georadiusGetKeys",CMD_KEY_OW|CMD_KEY_UPDATE|CMD_KEY_INCOMPLETE,KSPEC_BS_KEYWORD,.bs.keyword={"STORE",6},KSPEC_FK_RANGE,.fk.range={0,1,0}},{"Incomplete because duplicate STOREDIST options use last-wins; fall back to georadiusGetKeys",CMD_KEY_OW|CMD_KEY_UPDATE|CMD_KEY_INCOMPLETE,KSPEC_BS_KEYWORD,.bs.keyword={"STOREDIST",6},KSPEC_FK_RANGE,.fk.range={0,1,0}} + }; + #endif + +@@ -2916,7 +2916,7 @@ commandHistory GEORADIUSBYMEMBER_History[] = { + #ifndef SKIP_CMD_KEY_SPECS_TABLE + /* GEORADIUSBYMEMBER key specs */ + keySpec GEORADIUSBYMEMBER_Keyspecs[3] = { +-{NULL,CMD_KEY_RO|CMD_KEY_ACCESS,KSPEC_BS_INDEX,.bs.index={1},KSPEC_FK_RANGE,.fk.range={0,1,0}},{NULL,CMD_KEY_OW|CMD_KEY_UPDATE,KSPEC_BS_KEYWORD,.bs.keyword={"STORE",5},KSPEC_FK_RANGE,.fk.range={0,1,0}},{NULL,CMD_KEY_OW|CMD_KEY_UPDATE,KSPEC_BS_KEYWORD,.bs.keyword={"STOREDIST",5},KSPEC_FK_RANGE,.fk.range={0,1,0}} ++{NULL,CMD_KEY_RO|CMD_KEY_ACCESS,KSPEC_BS_INDEX,.bs.index={1},KSPEC_FK_RANGE,.fk.range={0,1,0}},{"Incomplete because duplicate STORE options use last-wins; fall back to georadiusGetKeys",CMD_KEY_OW|CMD_KEY_UPDATE|CMD_KEY_INCOMPLETE,KSPEC_BS_KEYWORD,.bs.keyword={"STORE",5},KSPEC_FK_RANGE,.fk.range={0,1,0}},{"Incomplete because duplicate STOREDIST options use last-wins; fall back to georadiusGetKeys",CMD_KEY_OW|CMD_KEY_UPDATE|CMD_KEY_INCOMPLETE,KSPEC_BS_KEYWORD,.bs.keyword={"STOREDIST",5},KSPEC_FK_RANGE,.fk.range={0,1,0}} + }; + #endif + +@@ -10238,7 +10238,7 @@ struct COMMAND_ARG XRANGE_Args[] = { + #ifndef SKIP_CMD_KEY_SPECS_TABLE + /* XREAD key specs */ + keySpec XREAD_Keyspecs[1] = { +-{NULL,CMD_KEY_RO|CMD_KEY_ACCESS,KSPEC_BS_KEYWORD,.bs.keyword={"STREAMS",1},KSPEC_FK_RANGE,.fk.range={-1,1,2}} ++{"Incomplete because a stream key named STREAMS (or options before it) can shift the STREAMS keyword; fall back to xreadGetKeys",CMD_KEY_RO|CMD_KEY_ACCESS|CMD_KEY_INCOMPLETE,KSPEC_BS_KEYWORD,.bs.keyword={"STREAMS",1},KSPEC_FK_RANGE,.fk.range={-1,1,2}} + }; + #endif + +@@ -10270,7 +10270,7 @@ struct COMMAND_ARG XREAD_Args[] = { + #ifndef SKIP_CMD_KEY_SPECS_TABLE + /* XREADGROUP key specs */ + keySpec XREADGROUP_Keyspecs[1] = { +-{NULL,CMD_KEY_RO|CMD_KEY_ACCESS,KSPEC_BS_KEYWORD,.bs.keyword={"STREAMS",4},KSPEC_FK_RANGE,.fk.range={-1,1,2}} ++{"Incomplete because a consumer/group named STREAMS (or options before GROUP) can shift the STREAMS keyword; fall back to xreadGetKeys",CMD_KEY_RO|CMD_KEY_ACCESS|CMD_KEY_INCOMPLETE,KSPEC_BS_KEYWORD,.bs.keyword={"STREAMS",4},KSPEC_FK_RANGE,.fk.range={-1,1,2}} + }; + #endif + +diff --git a/src/commands/georadius.json b/src/commands/georadius.json +index 6ced9049c..834f84ac9 100644 +--- a/src/commands/georadius.json ++++ b/src/commands/georadius.json +@@ -49,9 +49,11 @@ + } + }, + { ++ "notes": "Incomplete because duplicate STORE options use last-wins; fall back to georadiusGetKeys", + "flags": [ + "OW", +- "UPDATE" ++ "UPDATE", ++ "INCOMPLETE" + ], + "begin_search": { + "keyword": { +@@ -68,9 +70,11 @@ + } + }, + { ++ "notes": "Incomplete because duplicate STOREDIST options use last-wins; fall back to georadiusGetKeys", + "flags": [ + "OW", +- "UPDATE" ++ "UPDATE", ++ "INCOMPLETE" + ], + "begin_search": { + "keyword": { +diff --git a/src/commands/georadiusbymember.json b/src/commands/georadiusbymember.json +index 6102a1b16..e6198702b 100644 +--- a/src/commands/georadiusbymember.json ++++ b/src/commands/georadiusbymember.json +@@ -49,9 +49,11 @@ + } + }, + { ++ "notes": "Incomplete because duplicate STORE options use last-wins; fall back to georadiusGetKeys", + "flags": [ + "OW", +- "UPDATE" ++ "UPDATE", ++ "INCOMPLETE" + ], + "begin_search": { + "keyword": { +@@ -68,9 +70,11 @@ + } + }, + { ++ "notes": "Incomplete because duplicate STOREDIST options use last-wins; fall back to georadiusGetKeys", + "flags": [ + "OW", +- "UPDATE" ++ "UPDATE", ++ "INCOMPLETE" + ], + "begin_search": { + "keyword": { +diff --git a/src/commands/xread.json b/src/commands/xread.json +index 95e22c494..5b94df724 100644 +--- a/src/commands/xread.json ++++ b/src/commands/xread.json +@@ -15,9 +15,11 @@ + ], + "key_specs": [ + { ++ "notes": "Incomplete because a stream key named STREAMS (or options before it) can shift the STREAMS keyword; fall back to xreadGetKeys", + "flags": [ + "RO", +- "ACCESS" ++ "ACCESS", ++ "INCOMPLETE" + ], + "begin_search": { + "keyword": { +diff --git a/src/commands/xreadgroup.json b/src/commands/xreadgroup.json +index 93e45a877..3020c3af9 100644 +--- a/src/commands/xreadgroup.json ++++ b/src/commands/xreadgroup.json +@@ -16,9 +16,11 @@ + ], + "key_specs": [ + { ++ "notes": "Incomplete because a consumer/group named STREAMS (or options before GROUP) can shift the STREAMS keyword; fall back to xreadGetKeys", + "flags": [ + "RO", +- "ACCESS" ++ "ACCESS", ++ "INCOMPLETE" + ], + "begin_search": { + "keyword": { +diff --git a/src/db.c b/src/db.c +index 508c342fb..59ecf706a 100644 +--- a/src/db.c ++++ b/src/db.c +@@ -2902,12 +2902,12 @@ int sortGetKeys(struct redisCommand *cmd, robj **argv, int argc, getKeysResult * + i += skiplist[j].skip; + break; + } else if (!strcasecmp(argv[i]->ptr,"store") && i+1 < argc) { +- /* Note: we don't increment "num" here and continue the loop +- * to be sure to process the *last* "STORE" option if multiple +- * ones are provided. This is same behavior as SORT. */ ++ /* Don't increment "num" so the *last* STORE option wins if ++ * several are given (same behavior as SORT). */ + found_store = 1; + keys[num].pos = i+1; /* */ + keys[num].flags = CMD_KEY_OW | CMD_KEY_UPDATE; ++ i++; /* Skip the store argument so it isn't re-parsed as an option keyword. */ + break; + } + } +@@ -2972,8 +2972,10 @@ int migrateGetKeys(struct redisCommand *cmd, robj **argv, int argc, getKeysResul + * GEORADIUS key x y radius unit [WITHDIST] [WITHHASH] [WITHCOORD] [ASC|DESC] + * [COUNT count] [STORE key|STOREDIST key] + * GEORADIUSBYMEMBER key member radius unit ... options ... +- * +- * This command has a fully defined keyspec, so returning flags isn't needed. */ ++ * ++ * STORE/STOREDIST keyspecs are marked incomplete because duplicate options ++ * use last-wins semantics (same as georadiusGeneric). ACL and other callers ++ * of getKeysFromCommandWithSpecs fall back here. */ + int georadiusGetKeys(struct redisCommand *cmd, robj **argv, int argc, getKeysResult *result) { + int i, num; + keyReference *keys; +@@ -3002,10 +3004,10 @@ int georadiusGetKeys(struct redisCommand *cmd, robj **argv, int argc, getKeysRes + + /* Add all key positions to keys[] */ + keys[0].pos = 1; +- keys[0].flags = 0; +- if(num > 1) { ++ keys[0].flags = CMD_KEY_RO | CMD_KEY_ACCESS; ++ if (num > 1) { + keys[1].pos = stored_key; +- keys[1].flags = 0; ++ keys[1].flags = CMD_KEY_OW | CMD_KEY_UPDATE; + } + result->numkeys = num; + return num; +@@ -3014,7 +3016,8 @@ int georadiusGetKeys(struct redisCommand *cmd, robj **argv, int argc, getKeysRes + /* XREAD [BLOCK ] [COUNT ] [GROUP ] + * STREAMS key_1 key_2 ... key_N ID_1 ID_2 ... ID_N + * +- * This command has a fully defined keyspec, so returning flags isn't needed. */ ++ * The keyspec is incomplete, so callers fall back to this function to parse ++ * the options and locate the real STREAMS token. */ + int xreadGetKeys(struct redisCommand *cmd, robj **argv, int argc, getKeysResult *result) { + int i, num = 0; + keyReference *keys; +@@ -3055,7 +3058,7 @@ int xreadGetKeys(struct redisCommand *cmd, robj **argv, int argc, getKeysResult + keys = getKeysPrepareResult(result, num); + for (i = streams_pos+1; i < argc-num; i++) { + keys[i-streams_pos-1].pos = i; +- keys[i-streams_pos-1].flags = 0; ++ keys[i-streams_pos-1].flags = CMD_KEY_RO | CMD_KEY_ACCESS; + } + result->numkeys = num; + return num; +diff --git a/tests/unit/acl-v2.tcl b/tests/unit/acl-v2.tcl +index b233118dd..bc7cf45c6 100644 +--- a/tests/unit/acl-v2.tcl ++++ b/tests/unit/acl-v2.tcl +@@ -397,6 +397,48 @@ start_server {tags {"acl external:skip"}} { + assert_match {*has no permissions to access the 'write1' key*} [r ACL DRYRUN command-test GEORADIUS write1 longitude latitude radius M STORE write2] + } + ++ test {Test GEORADIUS duplicate STORE is checked against the last key} { ++ r ACL setuser command-test +@all %R~read* %W~write* %RW~rw* ++ ++ # Duplicate STORE/STOREDIST uses last-wins semantics (same as ++ # georadiusGeneric), so ACL must validate the last key. Otherwise a ++ # permitted first key would mask a forbidden second key. ++ assert_equal "OK" [r ACL DRYRUN command-test GEORADIUS read longitude latitude radius M STORE read1 STORE write2] ++ assert_match {*has no permissions to access the 'read2' key*} [r ACL DRYRUN command-test GEORADIUS read longitude latitude radius M STORE write1 STORE read2] ++ assert_match {*has no permissions to access the 'read2' key*} [r ACL DRYRUN command-test GEORADIUS read longitude latitude radius M STOREDIST write1 STOREDIST read2] ++ ++ assert_equal "OK" [r ACL DRYRUN command-test GEORADIUSBYMEMBER read member radius M STORE read1 STORE write2] ++ assert_match {*has no permissions to access the 'read2' key*} [r ACL DRYRUN command-test GEORADIUSBYMEMBER read member radius M STORE write1 STORE read2] ++ assert_match {*has no permissions to access the 'read2' key*} [r ACL DRYRUN command-test GEORADIUSBYMEMBER read member radius M STOREDIST write1 STOREDIST read2] ++ } ++ ++ test {Test SORT duplicate STORE is checked against the last key} { ++ r ACL setuser command-test +@all %R~read* %W~write* %RW~rw* ++ ++ # Duplicate STORE uses last-wins semantics, so ACL must validate the ++ # last key. The earlier STORE argument can look like an option keyword ++ # (BY/GET/LIMIT) and must be skipped so a permitted first key doesn't ++ # mask a forbidden last key. ++ assert_equal "OK" [r ACL DRYRUN command-test SORT read STORE by STORE write2] ++ assert_match {*has no permissions to access the 'read2' key*} [r ACL DRYRUN command-test SORT read STORE by STORE read2] ++ assert_match {*has no permissions to access the 'read2' key*} [r ACL DRYRUN command-test SORT read STORE get STORE read2] ++ assert_match {*has no permissions to access the 'read2' key*} [r ACL DRYRUN command-test SORT read STORE limit STORE read2] ++ } ++ ++ test {Test XREADGROUP STREAMS keyword is not confused with the consumer name} { ++ r ACL setuser command-test +@all %R~read* %W~write* %RW~rw* ++ ++ # A consumer literally named "STREAMS" must not be mistaken for the ++ # STREAMS option, otherwise ACL validates the wrong argument. ++ assert_equal "OK" [r ACL DRYRUN command-test XREADGROUP NOACK GROUP g STREAMS STREAMS read2 >] ++ assert_match {*has no permissions to access the 'write2' key*} [r ACL DRYRUN command-test XREADGROUP NOACK GROUP g STREAMS STREAMS write2 >] ++ assert_match {*has no permissions to access the 'write2' key*} [r ACL DRYRUN command-test XREADGROUP COUNT 1 GROUP g STREAMS STREAMS write2 >] ++ ++ # Sanity: a normal consumer name still validates the real stream key. ++ assert_equal "OK" [r ACL DRYRUN command-test XREADGROUP GROUP g consumer STREAMS read2 >] ++ assert_match {*has no permissions to access the 'write2' key*} [r ACL DRYRUN command-test XREADGROUP GROUP g consumer STREAMS write2 >] ++ } ++ + # Existence test commands are not marked as access since they are the result + # of a lot of write commands. We therefore make the claim they can be executed + # when either READ or WRITE flags are provided. diff -Nru redis-8.0.2/debian/patches/0020-Fix-ACL-OOB-for-wrong-arity-KEYNUM-commands.patch redis-8.0.2/debian/patches/0020-Fix-ACL-OOB-for-wrong-arity-KEYNUM-commands.patch --- redis-8.0.2/debian/patches/0020-Fix-ACL-OOB-for-wrong-arity-KEYNUM-commands.patch 1970-01-01 00:00:00.000000000 +0000 +++ redis-8.0.2/debian/patches/0020-Fix-ACL-OOB-for-wrong-arity-KEYNUM-commands.patch 2026-09-14 16:52:10.000000000 +0000 @@ -0,0 +1,189 @@ +From a8eec43d9a6b5512e248c9c2b481ca485e56d19d Mon Sep 17 00:00:00 2001 +From: Zijie Zhao +Date: Wed, 18 Mar 2026 20:30:56 -0500 +Subject: [PATCH] Fix ACL OOB for wrong-arity KEYNUM commands (#14847) + +`luaRedisAclCheckCmdPermissionsCommand` and +`RM_ACLCheckCommandPermissions` now call `commandCheckArity()` to check +command arity before calling `ACLCheckAllUserCommandPerm`, matching the +behavior of `processCommand`, `scriptCall`, and `RM_Call`. Without this, +KEYNUM keyspec commands like EVAL with wrong arity cause out-of-bounds +argv access during key extraction. + +Also fix KEYNUM index calculation (`first + keynumidx`) and add a bounds +check in genericGetKeys(). + +Add scripting and module ACL tests for wrong-arity `EVAL` to lock in the +non-crashing behavior. + +Fixes #14843 + +(cherry picked from commit c4d74587b55383354f53a5c61bd313ccf7867f4b) +(cherry picked from commit 1812c6d02038d2dc7bdb9b2ed5e5f1ed1b1838cf) +(cherry picked from commit a8eec43d9a6b5512e248c9c2b481ca485e56d19d) +Reviewed-by: Aron Xu +--- + src/db.c | 9 +++++++-- + src/module.c | 8 +++++++- + src/script_lua.c | 3 +++ + src/server.c | 10 ++++------ + src/server.h | 2 +- + tests/unit/moduleapi/aclcheck.tcl | 5 +++++ + tests/unit/scripting.tcl | 12 ++++++++++++ + 7 files changed, 39 insertions(+), 10 deletions(-) + +diff --git a/src/db.c b/src/db.c +index 59ecf706a..c279f9edd 100644 +--- a/src/db.c ++++ b/src/db.c +@@ -2486,10 +2486,11 @@ int getKeysUsingKeySpecs(struct redisCommand *cmd, robj **argv, int argc, int se + } else if (spec->find_keys_type == KSPEC_FK_KEYNUM) { + step = spec->fk.keynum.keystep; + long long numkeys; +- if (spec->fk.keynum.keynumidx >= argc) ++ long keynumidx = first + spec->fk.keynum.keynumidx; ++ if (keynumidx >= argc || keynumidx < 0) + goto invalid_spec; + +- sds keynum_str = argv[first + spec->fk.keynum.keynumidx]->ptr; ++ sds keynum_str = argv[keynumidx]->ptr; + if (!string2ll(keynum_str,sdslen(keynum_str),&numkeys) || numkeys < 0) { + /* Unable to parse the numkeys argument or it was invalid */ + goto invalid_spec; +@@ -2770,6 +2771,10 @@ int genericGetKeys(int storeKeyOfs, int keyCountOfs, int firstKeyOfs, int keySte + int i, num; + keyReference *keys; + ++ if (keyCountOfs >= argc) { ++ result->numkeys = 0; ++ return 0; ++ } + num = atoi(argv[keyCountOfs]->ptr); + /* Sanity check. Don't return any key if the command is going to + * reply with syntax error. (no input keys). */ +diff --git a/src/module.c b/src/module.c +index 07fee9218..cff183a8f 100644 +--- a/src/module.c ++++ b/src/module.c +@@ -6476,7 +6476,7 @@ RedisModuleCallReply *RM_Call(RedisModuleCtx *ctx, const char *cmdname, const ch + reply = callReplyCreateError(err, ctx); + goto cleanup; + } +- if (!commandCheckArity(c, error_as_call_replies? &err : NULL)) { ++ if (!commandCheckArity(c->cmd, c->argc, error_as_call_replies? &err : NULL)) { + errno = EINVAL; + if (error_as_call_replies) + reply = callReplyCreateError(err, ctx); +@@ -9811,6 +9811,7 @@ RedisModuleUser *RM_GetModuleUserFromUserName(RedisModuleString *name) { + * REDISMODULE_ERR is returned and errno is set to the following values: + * + * * ENOENT: Specified command does not exist. ++ * * EINVAL: Invalid number of arguments for the specified command. + * * EACCES: Command cannot be executed, according to ACL rules + */ + int RM_ACLCheckCommandPermissions(RedisModuleUser *user, RedisModuleString **argv, int argc) { +@@ -9823,6 +9824,11 @@ int RM_ACLCheckCommandPermissions(RedisModuleUser *user, RedisModuleString **arg + return REDISMODULE_ERR; + } + ++ if (!commandCheckArity(cmd, argc, NULL)) { ++ errno = EINVAL; ++ return REDISMODULE_ERR; ++ } ++ + if (ACLCheckAllUserCommandPerm(user->user, cmd, argv, argc, &keyidxptr) != ACL_OK) { + errno = EACCES; + return REDISMODULE_ERR; +diff --git a/src/script_lua.c b/src/script_lua.c +index a541ec755..746ef124b 100644 +--- a/src/script_lua.c ++++ b/src/script_lua.c +@@ -1123,6 +1123,9 @@ static int luaRedisAclCheckCmdPermissionsCommand(lua_State *lua) { + if ((cmd = lookupCommand(argv, argc)) == NULL) { + luaPushError(lua, "Invalid command passed to redis.acl_check_cmd()"); + raise_error = 1; ++ } else if (!commandCheckArity(cmd, argc, NULL)) { ++ luaPushError(lua, "Wrong number of args for redis.acl_check_cmd()"); ++ raise_error = 1; + } else { + int keyidxptr; + if (ACLCheckAllUserCommandPerm(rctx->original_client->user, cmd, argv, argc, &keyidxptr) != ACL_OK) { +diff --git a/src/server.c b/src/server.c +index fd1180af2..dbff08b01 100644 +--- a/src/server.c ++++ b/src/server.c +@@ -3943,13 +3943,11 @@ int commandCheckExistence(client *c, sds *err) { + + /* Check if c->argc is valid for c->cmd, fills `err` with details in case it isn't. + * Return 1 if valid. */ +-int commandCheckArity(client *c, sds *err) { +- if ((c->cmd->arity > 0 && c->cmd->arity != c->argc) || +- (c->argc < -c->cmd->arity)) +- { ++int commandCheckArity(struct redisCommand *cmd, int argc, sds *err) { ++ if ((cmd->arity > 0 && cmd->arity != argc) || (argc < -cmd->arity)) { + if (err) { + *err = sdsnew(NULL); +- *err = sdscatprintf(*err, "wrong number of arguments for '%s' command", c->cmd->fullname); ++ *err = sdscatprintf(*err, "wrong number of arguments for '%s' command", cmd->fullname); + } + return 0; + } +@@ -4042,7 +4040,7 @@ int processCommand(client *c) { + rejectCommandSds(c, err); + return C_OK; + } +- if (!commandCheckArity(c, &err)) { ++ if (!commandCheckArity(c->cmd, c->argc, &err)) { + rejectCommandSds(c, err); + return C_OK; + } +diff --git a/src/server.h b/src/server.h +index 17c7099b4..544bb14c5 100644 +--- a/src/server.h ++++ b/src/server.h +@@ -3273,7 +3273,7 @@ struct redisCommand *lookupCommandByCStringLogic(dict *commands, const char *s); + struct redisCommand *lookupCommandByCString(const char *s); + struct redisCommand *lookupCommandOrOriginal(robj **argv, int argc); + int commandCheckExistence(client *c, sds *err); +-int commandCheckArity(client *c, sds *err); ++int commandCheckArity(struct redisCommand *cmd, int argc, sds *err); + void startCommandExecution(void); + int incrCommandStatsOnError(struct redisCommand *cmd, int flags); + void call(client *c, int flags); +diff --git a/tests/unit/moduleapi/aclcheck.tcl b/tests/unit/moduleapi/aclcheck.tcl +index cf89ea52e..5d9667cf6 100644 +--- a/tests/unit/moduleapi/aclcheck.tcl ++++ b/tests/unit/moduleapi/aclcheck.tcl +@@ -17,6 +17,11 @@ start_server {tags {"modules acl"}} { + assert {[dict get $entry context] eq {module}} + assert {[dict get $entry object] eq {set}} + assert {[dict get $entry reason] eq {command}} ++ ++ # Wrong command arity must fail safely (no crash on KEYNUM keyspec path) ++ r acl setuser default on nopass resetkeys ~restricted:* +@all ++ catch {r aclcheck.rm_call.check.cmd eval script} e ++ assert_match {*DENIED*} $e + } + + test {test module check acl for key prefix permission} { +diff --git a/tests/unit/scripting.tcl b/tests/unit/scripting.tcl +index 70189d70b..08e7e0843 100644 +--- a/tests/unit/scripting.tcl ++++ b/tests/unit/scripting.tcl +@@ -2603,3 +2603,15 @@ start_server {tags {"scripting external:skip large-memory"}} { + } + } + } ++ ++start_server {tags {"scripting"}} { ++ test "Wrong arity EVAL in acl_check_cmd returns error not crash" { ++ r acl setuser bob on {>123} {+@scripting} {+set} {~x*} ++ assert_equal [r auth bob 123] {OK} ++ # Must be the first Lua call in this server instance ++ catch {run_script { ++ return redis.acl_check_cmd('eval','script') ++ } 0} e ++ assert_match {*Wrong number of args*} $e ++ } ++} diff -Nru redis-8.0.2/debian/patches/0021-Check-slotinfo-range-when-loading-RDB.patch redis-8.0.2/debian/patches/0021-Check-slotinfo-range-when-loading-RDB.patch --- redis-8.0.2/debian/patches/0021-Check-slotinfo-range-when-loading-RDB.patch 1970-01-01 00:00:00.000000000 +0000 +++ redis-8.0.2/debian/patches/0021-Check-slotinfo-range-when-loading-RDB.patch 2026-09-14 16:52:10.000000000 +0000 @@ -0,0 +1,59 @@ +From b1030a2f66311d5a03c58ebdeb5ade77daf74fe5 Mon Sep 17 00:00:00 2001 +From: Mincho Paskalev +Date: Tue, 21 Jul 2026 15:34:32 +0300 +Subject: [PATCH] Check slotinfo range when loading RDB (#1541) +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Redis has a memory corruption bug in the RDB loader’s SLOT_INFO +handling. A malicious RDB can provide invalid slot metadata that makes +Redis use a slot id outside the normal cluster slot range while +preparing per-slot dictionaries during load. +With the right heap layout, that out-of-bounds slot lookup can be made +to land on attacker-controlled data that was also loaded from the RDB. +Redis then treats that data as a real dictionary structure + +The attacker can uses this to build a fake dictionary and fake callback +table in memory, then reaches code execution as the Redis process. The +cluster-bus INTERNALSECRET issue is used to get an internal Redis +connection and make the target replicate from an attacker-controlled +fake master. The fake master then serves the malicious SLOT_INFO RDB. +The actual RCE primitive is the RDB-loader bug. +Source review shows the same SLOT_INFO loader pattern in Redis 7.4.x +through 8.8-m03; I verified full RCE on 8.6.2. + +The issue is that Redis trusts SLOT_INFO metadata from the RDB too much. +During RDB load, Redis reads slot_id, slot_size, and expires_slot_size +from the file, then passes the slot id directly into kvstore dictionary +expansion. + +Just check the slot_id is in range. + +(cherry picked from commit e92a526973b660071f51078b81ee17aeab359f84) +(cherry picked from commit b1030a2f66311d5a03c58ebdeb5ade77daf74fe5) +--- + src/rdb.c | 10 ++++++++++ + 1 file changed, 10 insertions(+) + +diff --git a/src/rdb.c b/src/rdb.c +index e6545d944..cf23d5c1b 100644 +--- a/src/rdb.c ++++ b/src/rdb.c +@@ -3442,6 +3442,16 @@ int rdbLoadRioWithLoadingCtx(rio *rdb, int rdbflags, rdbSaveInfo *rsi, rdbLoadin + if (!server.cluster_enabled) { + continue; /* Ignore gracefully. */ + } ++ /* slot_id comes straight from the RDB and is used as an index into the ++ * per-slot kvstore dictionaries. A malformed RDB can supply a value ++ * outside the valid slot range, which would be truncated to a negative or ++ * out-of-range int index inside the kvstore layer and cause an ++ * out-of-bounds access. Reject such records as corrupt before expanding. */ ++ if (slot_id >= (uint64_t)kvstoreNumDicts(db->keys)) { ++ rdbReportCorruptRDB("SLOT_INFO slot id %llu is out of range (max %d)", ++ (unsigned long long)slot_id, kvstoreNumDicts(db->keys)); ++ return C_ERR; ++ } + /* In cluster mode we resize individual slot specific dictionaries based on the number of keys that slot holds. */ + kvstoreDictExpand(db->keys, slot_id, slot_size); + kvstoreDictExpand(db->expires, slot_id, expires_slot_size); diff -Nru redis-8.0.2/debian/patches/0022-Fix-use-after-free-in-handleClientsBlockedOnKey.patch redis-8.0.2/debian/patches/0022-Fix-use-after-free-in-handleClientsBlockedOnKey.patch --- redis-8.0.2/debian/patches/0022-Fix-use-after-free-in-handleClientsBlockedOnKey.patch 1970-01-01 00:00:00.000000000 +0000 +++ redis-8.0.2/debian/patches/0022-Fix-use-after-free-in-handleClientsBlockedOnKey.patch 2026-09-14 16:52:10.000000000 +0000 @@ -0,0 +1,110 @@ +From 009324be81f7105313ce6586ed4807f03c62c7fe Mon Sep 17 00:00:00 2001 +From: Yuan Wang +Date: Thu, 6 Aug 2026 16:52:31 +0800 +Subject: [PATCH] Fix use-after-free in handleClientsBlockedOnKey (#15594) + +Fixes https://github.com/redis/redis/issues/15562. + +handleClientsBlockedOnKey() used a list iterator while reprocessing clients +blocked on the same key. Reprocessing one client may trigger client eviction, +which can remove another blocked client and free the node cached by the +iterator, resulting in a use-after-free. + +Replace the iterator with a bounded head-to-tail traversal. Before reprocessing +a client, move its node to the tail so the next waiter remains at the head while +preserving FIFO order. Since reprocessing may remove all blocked clients and +free the list, look up the list again before each iteration. + +(cherry picked from commit a8edcfc98c50bb01c850e5dab3998ce57807144d) +(cherry picked from commit 009324be81f7105313ce6586ed4807f03c62c7fe) +--- + src/blocked.c | 21 ++++++++++++----- + tests/unit/client-eviction.tcl | 41 ++++++++++++++++++++++++++++++++++ + 2 files changed, 57 insertions(+), 5 deletions(-) + +diff --git a/src/blocked.c b/src/blocked.c +index 4d65f56ec..b04fec3d2 100644 +--- a/src/blocked.c ++++ b/src/blocked.c +@@ -560,14 +560,25 @@ static void handleClientsBlockedOnKey(readyList *rl) { + + if (de) { + list *clients = dictGetVal(de); +- listNode *ln; +- listIter li; +- listRewind(clients,&li); +- + /* 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--) { ++ while (count-- > 0) { ++ /* Processing the previous client may have removed all blocked ++ * clients and freed the list, so look it up again each time. */ ++ de = dictFind(rl->db->blocking_keys, rl->key); ++ if (!de) break; ++ list *clients = dictGetVal(de); ++ listNode *ln = listFirst(clients); ++ serverAssert(ln); ++ ++ /* Rotate before reprocessing because unblocking may remove this ++ * client, and command reprocessing may evict other blocked clients ++ * and free the list. If the client remains blocked or blocks again, ++ * keeping it at the tail lets us advance to the next client while ++ * preserving the order of the other waiters. */ ++ listRotateHeadToTail(clients); ++ + client *receiver = listNodeValue(ln); + robj *o = lookupKeyReadWithFlags(rl->db, rl->key, LOOKUP_NOEFFECTS); + /* 1. In case new key was added/touched we need to verify it satisfy the +diff --git a/tests/unit/client-eviction.tcl b/tests/unit/client-eviction.tcl +index 62e42580c..24c8a5c66 100644 +--- a/tests/unit/client-eviction.tcl ++++ b/tests/unit/client-eviction.tcl +@@ -636,5 +636,46 @@ start_server {} { + } + } + ++start_server {} { ++ test "Evicting the next client while serving blocked clients is safe" { ++ r flushall ++ r client no-evict on ++ r config set maxmemory-clients 0 ++ ++ set rd1 [redis_deferring_client] ++ set rd2 [redis_deferring_client] ++ $rd2 CLIENT SETNAME client-to-evict ++ assert_equal OK [$rd2 read] ++ ++ # Block two clients on the same key in a known order. ++ $rd1 BLPOP mylist 0 ++ wait_for_blocked_clients_count 1 ++ $rd2 BLPOP mylist 0 ++ wait_for_blocked_clients_count 2 ++ ++ # Queue a large request behind the second client's blocking command so ++ # it will be selected for eviction while the first client is processed. ++ $rd2 get [string repeat Q [kb 2048]] ++ wait_for_condition 50 100 { ++ [client_field client-to-evict tot-mem] > [mb 1] ++ } else { ++ fail "Failed to increase the second blocked client's memory usage" ++ } ++ ++ # Apply the client-memory limit and make the key ready in one transaction. ++ # Eviction then runs while rd1 is being reprocessed, selecting rd2 while ++ # it is still in the blocked-client list. ++ r MULTI ++ r CONFIG SET MAXMEMORY-CLIENTS [kb 128] ++ r LPUSH mylist x ++ r EXEC ++ ++ assert_equal {mylist x} [$rd1 read] ++ assert {![client_exists client-to-evict]} ;# rd2 is evicted ++ $rd1 close ++ $rd2 close ++ } ++} ++ + } ;# tags + diff -Nru redis-8.0.2/debian/patches/0023-Fix-HGETEX-out-of-bounds-read-when-FIELDS-lacks-numfields.patch redis-8.0.2/debian/patches/0023-Fix-HGETEX-out-of-bounds-read-when-FIELDS-lacks-numfields.patch --- redis-8.0.2/debian/patches/0023-Fix-HGETEX-out-of-bounds-read-when-FIELDS-lacks-numfields.patch 1970-01-01 00:00:00.000000000 +0000 +++ redis-8.0.2/debian/patches/0023-Fix-HGETEX-out-of-bounds-read-when-FIELDS-lacks-numfields.patch 2026-09-14 16:52:10.000000000 +0000 @@ -0,0 +1,61 @@ +From 9342dda5717738e050116d4093f1202d56cb2286 Mon Sep 17 00:00:00 2001 +From: sggeorgiev +Date: Sun, 24 Aug 2025 21:20:52 +0300 +Subject: [PATCH] Fix HGETEX out-of-bounds read when FIELDS option missing + numfields argument + +When the HGETEX command is used with the FIELDS option but without the required +numfields argument, the server would attempt to access an out-of-bounds argv index. + +This PR adds a check to ensure numfields is present before accessing it, +returning an error if it is missing. Also includes a test case to cover this scenario. + +(cherry picked from commit 9342dda5717738e050116d4093f1202d56cb2286) +--- + src/t_hash.c | 6 ++++++ + tests/unit/type/hash-field-expire.tcl | 8 +++++--- + 2 files changed, 11 insertions(+), 3 deletions(-) + +diff --git a/src/t_hash.c b/src/t_hash.c +index b7d899368..c5b8284ab 100644 +--- a/src/t_hash.c ++++ b/src/t_hash.c +@@ -2812,6 +2812,12 @@ void hgetexCommand(client *c) { + num_fields_pos += 1; + } + ++ /* Check if we have enough arguments */ ++ if (num_fields_pos >= c->argc) { ++ addReplyErrorArity(c); ++ return; ++ } ++ + if (strcasecmp(c->argv[num_fields_pos - 1]->ptr, "FIELDS") != 0) { + addReplyError(c, "Mandatory argument FIELDS is missing or not at the right position"); + return; +diff --git a/tests/unit/type/hash-field-expire.tcl b/tests/unit/type/hash-field-expire.tcl +index d1afb9c15..976fd551d 100644 +--- a/tests/unit/type/hash-field-expire.tcl ++++ b/tests/unit/type/hash-field-expire.tcl +@@ -886,9 +886,9 @@ start_server {tags {"external:skip needs:debug"}} { + assert_error "*wrong number of arguments*" {r HGETEX h1 FIELDS} + assert_error "*wrong number of arguments*" {r HGETEX h1 FIELDS 0} + assert_error "*wrong number of arguments*" {r HGETEX h1 FIELDS 1} +- assert_error "*argument FIELDS is missing*" {r HGETEX h1 XFIELDX 1 a} +- assert_error "*argument FIELDS is missing*" {r HGETEX h1 PXAT 1 1} +- assert_error "*argument FIELDS is missing*" {r HGETEX h1 PERSIST 1 FIELDS 1 a} ++ assert_error "*wrong number of arguments*" {r HGETEX h1 PXAT 1 1} ++ assert_error "*Mandatory argument FIELDS*" {r HGETEX h1 XFIELDX 1 a} ++ assert_error "*Mandatory argument FIELDS*" {r HGETEX h1 PERSIST 1 FIELDS 1 a} + assert_error "*must match the number of arguments*" {r HGETEX h1 FIELDS 2 a} + assert_error "*Number of fields must be a positive integer*" {r HGETEX h1 FIELDS 0 a} + assert_error "*Number of fields must be a positive integer*" {r HGETEX h1 FIELDS -1 a} +@@ -907,6 +907,8 @@ start_server {tags {"external:skip needs:debug"}} { + assert_error "*invalid expire time*" {r HGETEX h1 EXAT [expr (1<<46) + 100 ] FIELDS 1 a} + assert_error "*invalid expire time*" {r HGETEX h1 PX [expr (1<<46) - [clock milliseconds] + 100 ] FIELDS 1 a} + assert_error "*invalid expire time*" {r HGETEX h1 PXAT [expr (1<<46) + 100 ] FIELDS 1 a} ++ assert_error "*wrong number of arguments*" {r HGETEX missingkey EX 100 FIELDS} ++ assert_error "*wrong number of arguments*" {r EVAL "return redis.call('HGETEX', 'missingkey', 'EX', '100', 'FIELDS')" 0} + } + + test "HGETEX - get without setting ttl ($type)" { diff -Nru redis-8.0.2/debian/patches/0024-Fix-MurmurHash64A-overflow-in-HyperLogLog.patch redis-8.0.2/debian/patches/0024-Fix-MurmurHash64A-overflow-in-HyperLogLog.patch --- redis-8.0.2/debian/patches/0024-Fix-MurmurHash64A-overflow-in-HyperLogLog.patch 1970-01-01 00:00:00.000000000 +0000 +++ redis-8.0.2/debian/patches/0024-Fix-MurmurHash64A-overflow-in-HyperLogLog.patch 2026-09-14 16:52:10.000000000 +0000 @@ -0,0 +1,63 @@ +From 53666e9884f0bd71ee43a005f69ac625377b752a Mon Sep 17 00:00:00 2001 +From: "debing.sun" +Date: Wed, 13 Aug 2025 16:09:44 +0800 +Subject: [PATCH] Fix MurmurHash64A overflow in HyperLogLog with 2GB+ entries + +The MurmurHash64A function in hyperloglog.c used an int parameter for length, +causing integer overflow when processing PFADD entries larger than 2GB. +This could lead to server crashes. + +Changed the len parameter from int to size_t to properly handle +large inputs up to SIZE_MAX in HyperLogLog operations. +Refer to the implementation in facebook/mcrouter@2dbee3d/mcrouter/lib/fbi/hash.c#L54 + +(cherry picked from commit 53666e9884f0bd71ee43a005f69ac625377b752a) +--- + src/hyperloglog.c | 2 +- + tests/support/util.tcl | 2 +- + tests/unit/hyperloglog.tcl | 8 ++++++++ + 3 files changed, 10 insertions(+), 2 deletions(-) + +diff --git a/src/hyperloglog.c b/src/hyperloglog.c +index d6a87822d..f52df29e2 100644 +--- a/src/hyperloglog.c ++++ b/src/hyperloglog.c +@@ -393,7 +393,7 @@ static int simd_enabled = 1; + * It was modified for Redis in order to provide the same result in + * big and little endian archs (endian neutral). */ + REDIS_NO_SANITIZE("alignment") +-uint64_t MurmurHash64A (const void * key, int len, unsigned int seed) { ++uint64_t MurmurHash64A (const void * key, size_t len, unsigned int seed) { + const uint64_t m = 0xc6a4a7935bd1e995; + const int r = 47; + uint64_t h = seed ^ (len * m); +diff --git a/tests/support/util.tcl b/tests/support/util.tcl +index 0d7d88516..e293c0573 100644 +--- a/tests/support/util.tcl ++++ b/tests/support/util.tcl +@@ -1069,7 +1069,7 @@ proc get_nonloopback_client {} { + } + + # The following functions and variables are used only when running large-memory +-# tests. We avoid defining them when not running large-memory tests because the ++# tests. We avoid defining them when not running large-memory tests because the + # global variables takes up lots of memory. + proc init_large_mem_vars {} { + if {![info exists ::str500]} { +diff --git a/tests/unit/hyperloglog.tcl b/tests/unit/hyperloglog.tcl +index 76c0a8d8d..fa7c3a5d0 100644 +--- a/tests/unit/hyperloglog.tcl ++++ b/tests/unit/hyperloglog.tcl +@@ -359,4 +359,12 @@ start_server {tags {"hll"}} { + r pfadd hll 1 2 3 + assert {[r getrange hll 15 15] eq "\x80"} + } ++ ++ test {PFADD with 2GB entry should not crash server due to overflow in MurmurHash64A} { ++ r config set proto-max-bulk-len 3221225472 ++ r config set client-query-buffer-limit 3221225472 ++ r write "*3\r\n\$5\r\nPFADD\r\n\$3\r\nhll\r\n" ++ write_big_bulk 2147483648; ++ r ping ++ } {PONG} {large-memory} + } diff -Nru redis-8.0.2/debian/patches/series redis-8.0.2/debian/patches/series --- redis-8.0.2/debian/patches/series 2026-05-13 04:00:00.000000000 +0000 +++ redis-8.0.2/debian/patches/series 2026-09-14 16:52:10.000000000 +0000 @@ -12,3 +12,14 @@ 0011-CVE-2025-46817.patch 0012-CVE-2025-67733.patch 0013-CVE-2026-21863.patch +0014-CVE-2026-25243.patch +0015-CVE-2026-23631.patch +0016-CVE-2026-23479.patch +0017-CVE-2026-66373.patch +0018-CVE-2026-81934.patch +0019-Fix-ACL-key-checks-for-SORT-GEORADIUS-XREAD.patch +0020-Fix-ACL-OOB-for-wrong-arity-KEYNUM-commands.patch +0021-Check-slotinfo-range-when-loading-RDB.patch +0022-Fix-use-after-free-in-handleClientsBlockedOnKey.patch +0023-Fix-HGETEX-out-of-bounds-read-when-FIELDS-lacks-numfields.patch +0024-Fix-MurmurHash64A-overflow-in-HyperLogLog.patch