[Date Prev][Date Next] [Thread Prev][Thread Next] [Date Index] [Thread Index]

Bug#1110319: unblock: redict/7.3.5+ds-1



Package: release.debian.org
Severity: normal
X-Debbugs-Cc: redict@packages.debian.org
Control: affects -1 + src:redict
User: release.debian.org@packages.debian.org
Usertags: unblock

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA512

Please unblock package redict

[ Reason ]
RC bugs:
* Fix CVE-2025-21605 (Closes: #1104011)
* Fix CVE-2025-48367 (Closes: #1108980)
* Fix CVE-2025-32023 (Closes: #1108977)
* Fix CVE-2025-27151 (Closes: #1106823)
and an important bug:
* Fix CVE-2025-49112 (Closes: #1107212)

[ Impact ]
Redict won't be in Trixie.

[ Tests ]
Upstream's tests are extensive and continue to pass.

[ Risks ]
Leaf package. Same/similar fixes are already applied for redis and
valkey packages.

[ Checklist ]
  [x] all changes are documented in the d/changelog
  [x] I reviewed all changes and I approve them
  [x] attach debdiff against the package in testing

[ Other info ]
debdiff might be hard to digest because it contains a new upstream
version; summary included below. Upstream only publishes security fixes,
there are no new features that could potentially break anything.

Upstream changes:
( https://codeberg.org/redict/redict/compare/7.3.2...7.3.5 )
* d73a7eac9b Limiting output buffer for unauthenticated client (CVE-2025-21605)  
* b75220c7e1 tests: Fix redict test command 
* f7cd2acc2a Retry accept(2) on transient errors (CVE-2025-48367)
* 682ea40774 Fix CVE-2025-32023
* and various version number bumps

Debian changes:
* Patch to fix CVE-2025-27151
  https://salsa.debian.org/redict-team/redict/-/blob/debian/7.3.5+ds-1/debian/patches/0006-CVE-2025-27151.patch
* Patch to fix CVE-2025-49112
  https://salsa.debian.org/redict-team/redict/-/blob/debian/7.3.5+ds-1/debian/patches/0007-CVE-2025-49112.patch

unblock redict/7.3.5+ds-1


-----BEGIN PGP SIGNATURE-----

iQJHBAEBCgAxFiEESl/RzRFQh8wD3DXB1ZeJcgbF8H8FAmiPE0MTHG1heXRoYW1A
ZGViaWFuLm9yZwAKCRDVl4lyBsXwf/hBD/4rWzwyFdrvuu1zFR9oGAYNjzGbOYi+
tG6iUsKkCcGyt/Ug2iE4mDpiaQOpdRIXd1RgkvchZ3ffT9Cs+EPO7r9+OgOeQMxX
qx9j+S4ehnDs+0g5IQpAtsRuKV9ajEFJeMxGVxC0XUZlEUKfok9TJBLw1oW8jeDA
7Us0V+kSDCrWgeDCpIErA48ZihP9hRG0GzChD30S57pLy61Y7zBd2s5Mrg+ovk+z
xTKp2qqch5Rsmw5N6TvkkrZ+eiRPPmoLp5xNA/Pm3JEl2lA6bxBlzdYWcv5IUNX4
nydEZYVm21OTnBCvEOgKNijuXhshwJwa6tDSNuosoFL59CYUZbIRiFfPN+s8yl9y
7ulbnY0TGAiIggt50tr0UcySTCcvCJnBx/1uZD4q9Cu+EWTSo6lXgmOg3Xv90Wyh
l0OTnrcOQgOnWEWxP/Wcw/E33IYy6A5x2CP+CmLQgHjqNCy8bMh61Z3dvVtHWc+E
/TBa6eUVZpmRqbNSXKvH2UZ+yjpIczvlfULZqY9D1p0I/8TeyEHZ0adBaUPFEq/u
v7Jd9njqmn8AWOLUpglP55plbnQUOMu2neF3+AsuSPIxYxM8IzKVj3hM90K0w1z9
trS0t7ebkl5Qujhbzj2vOzCeO4mXmLqyQ/iC4bp4JS/iwqo52/M8eHHQ3dezizAB
4nZV1sKuzjgpvg==
=XHBu
-----END PGP SIGNATURE-----
diff -Nru redict-7.3.2+ds/debian/changelog redict-7.3.5+ds/debian/changelog
--- redict-7.3.2+ds/debian/changelog	2025-01-08 21:10:54.000000000 +0800
+++ redict-7.3.5+ds/debian/changelog	2025-08-03 14:22:26.000000000 +0800
@@ -1,3 +1,14 @@
+redict (7.3.5+ds-1) unstable; urgency=medium
+
+  * New upstream version 7.3.5
+    * Contains fix for CVE-2025-21605 (Closes: #1104011)
+    * Contains fix for CVE-2025-48367 (Closes: #1108980)
+    * Contains fix for CVE-2025-32023 (Closes: #1108977)
+  * Add patch to fix CVE-2025-27151 (Closes: #1106823)
+  * Add patch to fix CVE-2025-49112 (Closes: #1107212)
+
+ -- Maytham Alsudany <maytham@debian.org>  Sun, 03 Aug 2025 14:22:26 +0800
+
 redict (7.3.2+ds-1) unstable; urgency=medium
 
   * New upstream version 7.3.2
diff -Nru redict-7.3.2+ds/debian/gbp.conf redict-7.3.5+ds/debian/gbp.conf
--- redict-7.3.2+ds/debian/gbp.conf	2024-10-13 08:27:29.000000000 +0800
+++ redict-7.3.5+ds/debian/gbp.conf	2025-08-03 14:18:23.000000000 +0800
@@ -1,3 +1,4 @@
 [DEFAULT]
 debian-branch=debian/latest
 upstream-branch=upstream
+ignore-branch=True
diff -Nru redict-7.3.2+ds/debian/patches/0006-CVE-2025-27151.patch redict-7.3.5+ds/debian/patches/0006-CVE-2025-27151.patch
--- redict-7.3.2+ds/debian/patches/0006-CVE-2025-27151.patch	1970-01-01 08:00:00.000000000 +0800
+++ redict-7.3.5+ds/debian/patches/0006-CVE-2025-27151.patch	2025-08-03 14:22:26.000000000 +0800
@@ -0,0 +1,26 @@
+From: YaacovHazan <yaacov.hazan@redis.com>
+From: fossdd <fossdd@pwned.life>
+Acked-by: Maytham Alsudany <maytham@debian.org>
+Subject: [PATCH] Check length of AOF file name in redict-check-aof (CVE-2025-27151)
+Applied-Upstream: https://codeberg.org/redict/redict/commit/40aa98db1d6601d30154ff078705dcfe1c4c7708
+Bug-Debian: https://bugs.debian.org/1106823
+
+Adapted from https://github.com/redis/redis/commit/643b5db235cb82508e72f11c7b4bbfc7dc39be56
+
+Ensure that the length of the input file name does not exceed PATH_MAX
+
+--- a/src/redict-check-aof.c
++++ b/src/redict-check-aof.c
+@@ -534,6 +534,12 @@ int redict_check_aof_main(int argc, char **argv) {
+         goto invalid_args;
+     }
+
++    /* Check if filepath is longer than PATH_MAX */
++    if (strlen(filepath) > PATH_MAX) {
++        printf("Error: filepath is too long (exceeds PATH_MAX)\n");
++        goto invalid_args;
++    }
++
+     /* In the glibc implementation dirname may modify their argument. */
+     memcpy(temp_filepath, filepath, strlen(filepath) + 1);
+     dirpath = dirname(temp_filepath);
diff -Nru redict-7.3.2+ds/debian/patches/0007-CVE-2025-49112.patch redict-7.3.5+ds/debian/patches/0007-CVE-2025-49112.patch
--- redict-7.3.2+ds/debian/patches/0007-CVE-2025-49112.patch	1970-01-01 08:00:00.000000000 +0800
+++ redict-7.3.5+ds/debian/patches/0007-CVE-2025-49112.patch	2025-08-03 14:22:26.000000000 +0800
@@ -0,0 +1,31 @@
+From: Zeroday BYTE <github@zerodaysec.org>
+From: Maytham Alsudany <maytham@debian.org>
+Subject: [PATCH] Fix unsigned difference expression compared to zero (#2101)
+Forwarded: no
+Bug-Debian: https://bugs.debian.org/1107212
+
+Adapted from https://github.com/valkey-io/valkey/commit/374718b2a365ca69f715d542709b7d71540b1387
+
+Fix the issue need to ensure that the subtraction `prev->size -
+prev->used` does not underflow. This can be achieved by explicitly
+checking that `prev->used` is less than `prev->size` before performing
+the subtraction. This approach avoids relying on unsigned arithmetic and
+ensures the logic is clear and robust.
+
+The specific changes are:
+1. Replace the condition `prev->size - prev->used > 0` with `prev->used
+< prev->size`.
+2. This change ensures that the logic checks whether there is remaining
+space in the buffer without risking underflow.
+
+--- a/src/networking.c
++++ b/src/networking.c
+@@ -756,7 +756,7 @@
+      * - It has enough room already allocated
+      * - And not too large (avoid large memmove) */
+     if (ln->prev != NULL && (prev = listNodeValue(ln->prev)) &&
+-        prev->size - prev->used > 0)
++        prev->used < prev->size)
+     {
+         size_t len_to_copy = prev->size - prev->used;
+         if (len_to_copy > length)
diff -Nru redict-7.3.2+ds/debian/patches/series redict-7.3.5+ds/debian/patches/series
--- redict-7.3.2+ds/debian/patches/series	2024-10-13 08:27:29.000000000 +0800
+++ redict-7.3.5+ds/debian/patches/series	2025-08-03 14:22:26.000000000 +0800
@@ -4,3 +4,5 @@
 0003-Add-support-for-USE_SYSTEM_JEMALLOC-flag.patch
 0004-Add-support-for-USE_SYSTEM_HIREDICT-flag.patch
 0005-Fix-hiredict-imports.patch
+0006-CVE-2025-27151.patch
+0007-CVE-2025-49112.patch
diff -Nru redict-7.3.2+ds/src/anet.c redict-7.3.5+ds/src/anet.c
--- redict-7.3.2+ds/src/anet.c	2025-01-08 17:09:07.000000000 +0800
+++ redict-7.3.5+ds/src/anet.c	2025-07-14 16:50:50.000000000 +0800
@@ -596,6 +596,37 @@
     return s;
 }
 
+/* For some error cases indicates transient errors and accept can be retried
+ * in order to serve other pending connections. This function should be called with the last errno,
+ * right after anetTcpaccept or anetUnixAccept returned an error in order to retry them. */
+int anetRetryAcceptOnError(int err) {
+    /* This is a transient error which can happen, for example, when
+     * a client initiates a TCP handshake (SYN),
+     * the server receives and queues it in the pending connections queue (the SYN queue),
+     * but before accept() is called, the connection is aborted.
+     * in such cases we can continue accepting other connections. ß*/
+    if (err == ECONNABORTED)
+        return 1;
+
+#if defined(__linux__)
+    /* https://www.man7.org/linux/man-pages/man2/accept4.2 suggests that:
+     * Linux accept() (and accept4()) passes already-pending network
+       errors on the new socket as an error code from accept().  This
+       behavior differs from other BSD socket implementations.  For
+       reliable operation the application should detect the network
+       errors defined for the protocol after accept() and treat them like
+       EAGAIN by retrying.  In the case of TCP/IP, these are ENETDOWN,
+       EPROTO, ENOPROTOOPT, EHOSTDOWN, ENONET, EHOSTUNREACH, EOPNOTSUPP,
+       and ENETUNREACH. */
+    if (err == ENETDOWN || err == EPROTO || err == ENOPROTOOPT ||
+        err == EHOSTDOWN || err == ENONET || err == EHOSTUNREACH ||
+        err == EOPNOTSUPP || err == ENETUNREACH) {
+        return 1;
+    }
+#endif
+    return 0;
+}
+
 /* Accept a connection and also make sure the socket is non-blocking, and CLOEXEC.
  * returns the new socket FD, or -1 on error. */
 static int anetGenericAccept(char *err, int s, struct sockaddr *sa, socklen_t *len) {
diff -Nru redict-7.3.2+ds/src/anet.h redict-7.3.5+ds/src/anet.h
--- redict-7.3.2+ds/src/anet.h	2025-01-08 17:09:07.000000000 +0800
+++ redict-7.3.5+ds/src/anet.h	2025-07-14 16:50:50.000000000 +0800
@@ -50,5 +50,6 @@
 int anetSetSockMarkId(char *err, int fd, uint32_t id);
 int anetGetError(int fd);
 int anetIsFifo(char *filepath);
+int anetRetryAcceptOnError(int err);
 
 #endif
diff -Nru redict-7.3.2+ds/src/cluster_legacy.c redict-7.3.5+ds/src/cluster_legacy.c
--- redict-7.3.2+ds/src/cluster_legacy.c	2025-01-08 17:09:07.000000000 +0800
+++ redict-7.3.5+ds/src/cluster_legacy.c	2025-07-14 16:50:50.000000000 +0800
@@ -800,9 +800,9 @@
 void deriveAnnouncedPorts(int *announced_tcp_port, int *announced_tls_port,
                           int *announced_cport) {
     /* Config overriding announced ports. */
-    *announced_tcp_port = server.cluster_announce_port ? 
+    *announced_tcp_port = server.cluster_announce_port ?
                           server.cluster_announce_port : server.port;
-    *announced_tls_port = server.cluster_announce_tls_port ? 
+    *announced_tls_port = server.cluster_announce_tls_port ?
                           server.cluster_announce_tls_port : server.tls_port;
     /* Derive cluster bus port. */
     if (server.cluster_announce_bus_port) {
@@ -891,7 +891,7 @@
     } else if (!new && (sdslen(node->human_nodename) == 0)) {
         return;
     }
-    
+
     if (new) {
         node->human_nodename = sdscpy(node->human_nodename, new);
     } else if (sdslen(node->human_nodename) != 0) {
@@ -1031,7 +1031,7 @@
         serverLog(LL_WARNING, "Failed listening on port %u (cluster), aborting.", listener->port);
         exit(1);
     }
-    
+
     if (createSocketAcceptHandler(&server.clistener, clusterAcceptHandler) != C_OK) {
         serverPanic("Unrecoverable error creating Redict Cluster socket accept handler.");
     }
@@ -1237,6 +1237,7 @@
     while(max--) {
         cfd = anetTcpAccept(server.neterr, fd, cip, sizeof(cip), &cport);
         if (cfd == ANET_ERR) {
+            if (anetRetryAcceptOnError(errno)) continue;
             if (errno != EWOULDBLOCK)
                 serverLog(LL_VERBOSE,
                     "Error accepting cluster node: %s", server.neterr);
@@ -2027,7 +2028,7 @@
 char *getCorruptedNodeIdByteString(clusterMsgDataGossip *gossip_msg) {
     const int num_bytes = CLUSTER_NAMELEN + 8;
     /* Allocate enough room for 4 chars per byte + null terminator */
-    char *byte_string = (char*) zmalloc((num_bytes*4) + 1); 
+    char *byte_string = (char*) zmalloc((num_bytes*4) + 1);
     const char *name_ptr = gossip_msg->nodename;
 
     /* Ensure we won't print beyond the bounds of the message */
@@ -2255,7 +2256,7 @@
     if (node->link) freeClusterLink(node->link);
     node->flags &= ~CLUSTER_NODE_NOADDR;
     serverLog(LL_NOTICE,"Address updated for node %.40s (%s), now %s:%d",
-        node->name, node->human_nodename, node->ip, getNodeDefaultClientPort(node)); 
+        node->name, node->human_nodename, node->ip, getNodeDefaultClientPort(node));
 
     /* Check if this is our master and we have to change the
      * replication target as well. */
@@ -2436,7 +2437,7 @@
  * The ping/pong/meet messages support arbitrary extensions to add additional
  * metadata to the messages that are sent between the various nodes in the
  * cluster. The extensions take the form:
- * [ Header length + type (8 bytes) ] 
+ * [ Header length + type (8 bytes) ]
  * [ Extension information (Arbitrary length, but must be 8 byte padded) ]
  */
 
@@ -2451,7 +2452,7 @@
 static clusterMsgPingExt *getInitialPingExt(clusterMsg *hdr, int count) {
     clusterMsgPingExt *initial = (clusterMsgPingExt*) &(hdr->data.ping.gossip[count]);
     return initial;
-} 
+}
 
 /* Given a current ping extension, returns the start of the next extension. May return
  * an invalid address if there are no further ping extensions. */
@@ -2533,7 +2534,7 @@
             /* Populate human_nodename */
             clusterMsgPingExtHumanNodename *ext = preparePingExt(cursor, CLUSTERMSG_EXT_TYPE_HUMAN_NODENAME, getHumanNodenamePingExtSize());
             memcpy(ext->human_nodename, myself->human_nodename, sdslen(myself->human_nodename));
-        
+
             /* Move the write cursor */
             cursor = nextPingExt(cursor);
         }
@@ -2974,7 +2975,7 @@
                         if (sender->configEpoch > senderConfigEpoch) {
                             serverLog(LL_NOTICE,
                                     "Ignore stale message from %.40s (%s) in shard %.40s;"
-                                    " gossip config epoch: %llu, current config epoch: %llu", 
+                                    " gossip config epoch: %llu, current config epoch: %llu",
                                     sender->name,
                                     sender->human_nodename,
                                     sender->shard_id,
@@ -3758,7 +3759,7 @@
 
     decrRefCount(channel);
     decrRefCount(message);
-    
+
     return msgblock;
 }
 
@@ -4119,13 +4120,13 @@
     }
     lastlog_time = time(NULL);
     serverLog(LL_NOTICE,"Currently unable to failover: %s", msg);
-    
+
     int cur_vote = server.cluster->failover_auth_count;
     int cur_quorum = (server.cluster->size / 2) + 1;
     /* Emits a log when an election is in progress and waiting for votes or when the failover attempt expired. */
     if (reason == CLUSTER_CANT_FAILOVER_WAITING_VOTES || reason == CLUSTER_CANT_FAILOVER_EXPIRED) {
         serverLog(LL_NOTICE, "Needed quorum: %d. Number of votes received so far: %d", cur_quorum, cur_vote);
-    } 
+    }
 }
 
 /* This function implements the final part of automatic and manual failovers,
@@ -4638,7 +4639,7 @@
          */
         if(clusterNodeCronHandleReconnect(node, handshake_timeout, now)) continue;
     }
-    dictReleaseIterator(di); 
+    dictReleaseIterator(di);
 
     /* Ping some random node 1 time every 10 iterations, so that we usually ping
      * one random node every second. */
@@ -4724,7 +4725,7 @@
          * received PONG is older than half the cluster timeout, send
          * a new ping now, to ensure all the nodes are pinged without
          * a too big delay. */
-        mstime_t ping_interval = server.cluster_ping_interval ? 
+        mstime_t ping_interval = server.cluster_ping_interval ?
             server.cluster_ping_interval : server.cluster_node_timeout/2;
         if (node->link &&
             node->ping_sent == 0 &&
@@ -4766,7 +4767,7 @@
                 node->flags |= CLUSTER_NODE_PFAIL;
                 update_state = 1;
                 if (clusterNodeIsMaster(myself) && server.cluster->size == 1) {
-                    markNodeAsFailingIfNeeded(node);                    
+                    markNodeAsFailingIfNeeded(node);
                 } else {
                     serverLog(LL_DEBUG,"*** NODE %.40s possibly failing", node->name);
                 }
@@ -5417,7 +5418,7 @@
  * include all the known nodes in the representation, including nodes in
  * the HANDSHAKE state.
  *
- * Setting tls_primary to 1 to put TLS port in the main <ip>:<port> 
+ * Setting tls_primary to 1 to put TLS port in the main <ip>:<port>
  * field and put TCP port in aux field, instead of the opposite way.
  *
  * The representation obtained using this function is used for the output
@@ -5568,7 +5569,7 @@
     for (j = 0; j < CLUSTER_SLOTS; j++) {
         if (slots[j]) {
             int retval;
-                
+
             /* If this slot was set as importing we can clear this
              * state as now we are the real owner of the slot. */
             if (server.cluster->importing_slots_from[j])
diff -Nru redict-7.3.2+ds/src/hyperloglog.c redict-7.3.5+ds/src/hyperloglog.c
--- redict-7.3.2+ds/src/hyperloglog.c	2025-01-08 17:09:07.000000000 +0800
+++ redict-7.3.5+ds/src/hyperloglog.c	2025-07-14 16:50:50.000000000 +0800
@@ -562,7 +562,8 @@
     sds sparse = o->ptr, dense;
     struct hllhdr *hdr, *oldhdr = (struct hllhdr*)sparse;
     int idx = 0, runlen, regval;
-    uint8_t *p = (uint8_t*)sparse, *end = p+sdslen(sparse);
+    uint8_t *p = (uint8_t *)sparse, *end = p + sdslen(sparse);
+    int valid = 1;
 
     /* If the representation is already the right one return ASAP. */
     hdr = (struct hllhdr*) sparse;
@@ -582,18 +583,29 @@
     while(p < end) {
         if (HLL_SPARSE_IS_ZERO(p)) {
             runlen = HLL_SPARSE_ZERO_LEN(p);
+            if ((runlen + idx) > HLL_REGISTERS) { /* Overflow. */
+                valid = 0;
+                break;
+            }
             idx += runlen;
             p++;
         } else if (HLL_SPARSE_IS_XZERO(p)) {
             runlen = HLL_SPARSE_XZERO_LEN(p);
+            if ((runlen + idx) > HLL_REGISTERS) { /* Overflow. */
+                valid = 0;
+                break;
+            }
             idx += runlen;
             p += 2;
         } else {
             runlen = HLL_SPARSE_VAL_LEN(p);
             regval = HLL_SPARSE_VAL_VALUE(p);
-            if ((runlen + idx) > HLL_REGISTERS) break; /* Overflow. */
-            while(runlen--) {
-                HLL_DENSE_SET_REGISTER(hdr->registers,idx,regval);
+            if ((runlen + idx) > HLL_REGISTERS) { /* Overflow. */
+                valid = 0;
+                break;
+            }
+            while (runlen--) {
+                HLL_DENSE_SET_REGISTER(hdr->registers, idx, regval);
                 idx++;
             }
             p++;
@@ -602,7 +614,7 @@
 
     /* If the sparse representation was valid, we expect to find idx
      * set to HLL_REGISTERS. */
-    if (idx != HLL_REGISTERS) {
+    if (!valid || idx != HLL_REGISTERS) {
         sdsfree(dense);
         return C_ERR;
     }
@@ -898,28 +910,41 @@
 /* Compute the register histogram in the sparse representation. */
 void hllSparseRegHisto(uint8_t *sparse, int sparselen, int *invalid, int* reghisto) {
     int idx = 0, runlen, regval;
-    uint8_t *end = sparse+sparselen, *p = sparse;
+    uint8_t *end = sparse + sparselen, *p = sparse;
+    int valid = 1;
 
     while(p < end) {
         if (HLL_SPARSE_IS_ZERO(p)) {
             runlen = HLL_SPARSE_ZERO_LEN(p);
+            if ((runlen + idx) > HLL_REGISTERS) { /* Overflow. */
+                valid = 0;
+                break;
+            }
             idx += runlen;
             reghisto[0] += runlen;
             p++;
         } else if (HLL_SPARSE_IS_XZERO(p)) {
             runlen = HLL_SPARSE_XZERO_LEN(p);
+            if ((runlen + idx) > HLL_REGISTERS) { /* Overflow. */
+                valid = 0;
+                break;
+            }
             idx += runlen;
             reghisto[0] += runlen;
             p += 2;
         } else {
             runlen = HLL_SPARSE_VAL_LEN(p);
             regval = HLL_SPARSE_VAL_VALUE(p);
+            if ((runlen + idx) > HLL_REGISTERS) { /* Overflow. */
+                valid = 0;
+                break;
+            }
             idx += runlen;
             reghisto[regval] += runlen;
             p++;
         }
     }
-    if (idx != HLL_REGISTERS && invalid) *invalid = 1;
+    if ((!valid || idx != HLL_REGISTERS) && invalid) *invalid = 1;
 }
 
 /* ========================= HyperLogLog Count ==============================
@@ -1067,30 +1092,42 @@
     } else {
         uint8_t *p = hll->ptr, *end = p + sdslen(hll->ptr);
         long runlen, regval;
+        int valid = 1;
 
         p += HLL_HDR_SIZE;
         i = 0;
         while(p < end) {
             if (HLL_SPARSE_IS_ZERO(p)) {
                 runlen = HLL_SPARSE_ZERO_LEN(p);
+                if ((runlen + i) > HLL_REGISTERS) { /* Overflow. */
+                    valid = 0;
+                    break;
+                }
                 i += runlen;
                 p++;
             } else if (HLL_SPARSE_IS_XZERO(p)) {
                 runlen = HLL_SPARSE_XZERO_LEN(p);
+                if ((runlen + i) > HLL_REGISTERS) { /* Overflow. */
+                    valid = 0;
+                    break;
+                }
                 i += runlen;
                 p += 2;
             } else {
                 runlen = HLL_SPARSE_VAL_LEN(p);
                 regval = HLL_SPARSE_VAL_VALUE(p);
-                if ((runlen + i) > HLL_REGISTERS) break; /* Overflow. */
-                while(runlen--) {
+                if ((runlen + i) > HLL_REGISTERS) { /* Overflow. */
+                    valid = 0;
+                    break;
+                }
+                while (runlen--) {
                     if (regval > max[i]) max[i] = regval;
                     i++;
                 }
                 p++;
             }
         }
-        if (i != HLL_REGISTERS) return C_ERR;
+        if (!valid || i != HLL_REGISTERS) return C_ERR;
     }
     return C_OK;
 }
diff -Nru redict-7.3.2+ds/src/networking.c redict-7.3.5+ds/src/networking.c
--- redict-7.3.2+ds/src/networking.c	2025-01-08 17:09:07.000000000 +0800
+++ redict-7.3.5+ds/src/networking.c	2025-07-14 16:50:50.000000000 +0800
@@ -3877,6 +3877,11 @@
     int soft = 0, hard = 0, class;
     unsigned long used_mem = getClientOutputBufferMemoryUsage(c);
 
+    /* For unauthenticated clients the output buffer is limited to prevent
+     * them from abusing it by not reading the replies */
+    if (used_mem > 1024 && authRequired(c))
+        return 1;
+
     class = getClientType(c);
     /* For the purpose of output buffer limiting, masters are handled
      * like normal clients. */
diff -Nru redict-7.3.2+ds/src/socket.c redict-7.3.5+ds/src/socket.c
--- redict-7.3.2+ds/src/socket.c	2025-01-08 17:09:07.000000000 +0800
+++ redict-7.3.5+ds/src/socket.c	2025-07-14 16:50:50.000000000 +0800
@@ -297,6 +297,7 @@
     while(max--) {
         cfd = anetTcpAccept(server.neterr, fd, cip, sizeof(cip), &cport);
         if (cfd == ANET_ERR) {
+            if (anetRetryAcceptOnError(errno)) continue;
             if (errno != EWOULDBLOCK)
                 serverLog(LL_WARNING,
                     "Accepting client connection: %s", server.neterr);
diff -Nru redict-7.3.2+ds/src/tls.c redict-7.3.5+ds/src/tls.c
--- redict-7.3.2+ds/src/tls.c	2025-01-08 17:09:07.000000000 +0800
+++ redict-7.3.5+ds/src/tls.c	2025-07-14 16:50:50.000000000 +0800
@@ -754,6 +754,7 @@
     while(max--) {
         cfd = anetTcpAccept(server.neterr, fd, cip, sizeof(cip), &cport);
         if (cfd == ANET_ERR) {
+            if (anetRetryAcceptOnError(errno)) continue;
             if (errno != EWOULDBLOCK)
                 serverLog(LL_WARNING,
                     "Accepting client connection: %s", server.neterr);
diff -Nru redict-7.3.2+ds/src/unix.c redict-7.3.5+ds/src/unix.c
--- redict-7.3.2+ds/src/unix.c	2025-01-08 17:09:07.000000000 +0800
+++ redict-7.3.5+ds/src/unix.c	2025-07-14 16:50:50.000000000 +0800
@@ -82,6 +82,7 @@
     while(max--) {
         cfd = anetUnixAccept(server.neterr, fd);
         if (cfd == ANET_ERR) {
+            if (anetRetryAcceptOnError(errno)) continue;
             if (errno != EWOULDBLOCK)
                 serverLog(LL_WARNING,
                     "Accepting client connection: %s", server.neterr);
diff -Nru redict-7.3.2+ds/src/version.h redict-7.3.5+ds/src/version.h
--- redict-7.3.2+ds/src/version.h	2025-01-08 17:09:07.000000000 +0800
+++ redict-7.3.5+ds/src/version.h	2025-07-14 16:50:50.000000000 +0800
@@ -4,5 +4,5 @@
 // SPDX-License-Identifier: BSD-3-Clause
 // SPDX-License-Identifier: LGPL-3.0-only
 
-#define REDICT_VERSION "7.3.2"
-#define REDICT_VERSION_NUM 0x00070302
+#define REDICT_VERSION "7.3.5"
+#define REDICT_VERSION_NUM 0x00070305
diff -Nru redict-7.3.2+ds/tests/unit/auth.tcl redict-7.3.5+ds/tests/unit/auth.tcl
--- redict-7.3.2+ds/tests/unit/auth.tcl	2025-01-08 17:09:07.000000000 +0800
+++ redict-7.3.5+ds/tests/unit/auth.tcl	2025-07-14 16:50:50.000000000 +0800
@@ -51,6 +51,24 @@
         assert_match {*unauthenticated bulk length*} $e
         $rr close
     }
+
+    test {For unauthenticated clients output buffer is limited} {
+        set rr [redict [srv "host"] [srv "port"] 1 $::tls]
+        $rr SET x 5
+        catch {[$rr read]} e
+        assert_match {*NOAUTH Authentication required*} $e
+
+        # Fill the output buffer in a loop without reading it and make
+        # sure the client disconnected.
+        # Considering the socket eat some of the replies, we are testing
+        # that such client can't consume more than few MB's.
+        catch {
+            for {set j 0} {$j < 1000000} {incr j} {
+                    $rr SET x 5
+            }
+        } e
+        assert_match {I/O error reading reply} $e
+    }
 }
 
 start_server {tags {"auth_binary_password external:skip"}} {
@@ -82,7 +100,7 @@
             # Verify replica is not able to sync with master
             wait_for_log_messages 0 {"*Unable to AUTH to MASTER*"} $loglines 1000 10
             assert_equal {down} [s 0 master_link_status]
-            
+
             # Test replica with the correct masterauth
             $slave config set masterauth "abc\x00def"
             wait_for_condition 50 100 {
diff -Nru redict-7.3.2+ds/tests/unit/hyperloglog.tcl redict-7.3.5+ds/tests/unit/hyperloglog.tcl
--- redict-7.3.2+ds/tests/unit/hyperloglog.tcl	2025-01-08 17:09:07.000000000 +0800
+++ redict-7.3.5+ds/tests/unit/hyperloglog.tcl	2025-07-14 16:50:50.000000000 +0800
@@ -5,6 +5,32 @@
 # SPDX-License-Identifier: LGPL-3.0-only
 
 start_server {tags {"hll"}} {
+    test {CVE-2025-32023: Sparse HLL XZERO overflow triggers crash} {
+        # Build a valid HLL header for sparse encoding
+        set hll [binary format cccc 72 89 76 76] ; # "HYLL"
+        append hll [binary format cccc 1 0 0 0] ; # encoding=sparse, 3 reserved
+        append hll [binary format cccccccc 0 0 0 0 0 0 0 0] ; # cached cardinality
+
+        # We need alternating XZERO and ZERO opcodes to build up a large enough
+        # HyperLogLog value that will overflow the runlength.
+        # 0x7f 0xff is the XZERO opcode that sets the index to 16384.
+        # 0x3f is the ZERO opcode that sets the index to 256.
+        # We repeat this pattern at least 33554432 times to overflow the runlength,
+        # 50331648 is just 33554432 * 1.5, which is just a round number.
+        append hll [string repeat [binary format ccc 0x7f 0xff 0x3f] 50331648]
+
+        # We need an actual value at the end to trigger the out of bounds write
+        append hll [binary format c 0x80]
+
+        # Set the raw value into the key
+        r set hll_overflow $hll
+        r pfadd hll_merge_source hi
+
+        # Test pfadd and pfmerge, these used to crash but now error
+        assert_error {*INVALIDOBJ*} {r pfmerge fail_target hll_overflow hll_merge_source}
+        assert_error {*INVALIDOBJ*} {r pfadd hll_overflow foo}
+    } {} {large-memory}
+
     test {HyperLogLog self test passes} {
         catch {r pfselftest} e
         set e

Reply to: