Control: tags -1 + patch

Hi Maintainers,

In Debian forky 5:8.0.6-3, the 0015-CVE-2026-92925.patch patch file adding
`tests/unit/cluster/hostnames.tcl` calls `build_cluster_bus_header`, which
was missing from `tests/support/cluster_util.tcl`. This causes the test
"PING with hostname extension missing null terminator is rejected" to fail.

I have updated the patch to include the missing `build_cluster_bus_header`
procedure in `tests/support/cluster_util.tcl`. Attached is the updated
patch.

Thanks!
Hurman
From: Sergei Georgiev <[email protected]>
Date: Fri, 5 Jun 2026 11:56:05 +0300
Subject: [PATCH] Reject cluster bus PING extensions with missing null
 terminator (#15263)

The cluster bus PING/PONG/MEET packet parser validated extension padding
and total length but never checked that string-carrying extensions are
properly null-terminated, allowing a crafted packet to trigger
out-of-bounds reads when the payload is later consumed as a C string.

1. **Null-termination check for hostname and human-nodename extensions
(`cluster_legacy.c`)**

Added a check inside the existing extension-validation loop in
`clusterProcessPacket`: for `CLUSTERMSG_EXT_TYPE_HOSTNAME` and
`CLUSTERMSG_EXT_TYPE_HUMAN_NODENAME` extension types, it verifies that
the data portion is non-empty (`datalen > 0`) and that the last byte is
`''`. Packets failing this check are rejected with a warning log and
an early return, the same way other malformed-extension cases are
handled.

2. **Test (`hostnames.tcl`)**

A new test exercises the rejection path by constructing a raw
cluster-bus PING packet with a 32-byte hostname extension that contains
no `''`, sending it directly to a node's bus port, and verifying the
packet is dropped (warning logged, hostname not updated in `CLUSTER
NODES`). Two helper procs (`build_cluster_bus_ping` and
`build_hostname_extension`) build the binary packet from scratch in Tcl,
allowing fine-grained control over extension contents without needing a
modified Redis sender.

Origin: upstream, https://github.com/redis/redis/commit/37894faeea11e2db28b9fc2af378a762d2c36523.patch
---
 src/cluster_legacy.c             |   40 +++++++++++++++++++-
 tests/support/cluster_util.tcl   |   34 +++++++++++++++++
 tests/unit/cluster/hostnames.tcl |   78 +++++++++++++++++++++++++++++++++++++++
 3 files changed, 150 insertions(+), 2 deletions(-)

Index: redis-8.0.6/src/cluster_legacy.c
===================================================================
--- redis-8.0.6.orig/src/cluster_legacy.c
+++ redis-8.0.6/src/cluster_legacy.c
@@ -220,7 +220,7 @@ sds auxShardIdGetter(clusterNode *n, sds
 }
 
 int auxShardIdPresent(clusterNode *n) {
-    return strlen(n->shard_id);
+    return strnlen(n->shard_id, CLUSTER_NAMELEN);
 }
 
 int auxHumanNodenameSetter(clusterNode *n, void *value, int length) {
@@ -2782,7 +2782,7 @@ int clusterProcessPacket(clusterLink *li
                     return 1;
                 }
                 uint16_t extlen = getPingExtLength(ext);
-                if (extlen % 8 != 0) {
+                if (extlen < sizeof(clusterMsgPingExt) || extlen % 8 != 0) {
                     serverLog(LL_WARNING, "Received a %s packet without proper padding (%d bytes)",
                         clusterGetMessageTypeString(type), (int) extlen);
                     return 1;
@@ -2795,6 +2795,42 @@ int clusterProcessPacket(clusterLink *li
                         (unsigned long long) totlen);
                     return 1;
                 }
+                uint16_t exttype = ntohs(ext->type);
+                uint32_t datalen = extlen - sizeof(clusterMsgPingExt);
+                if (exttype == CLUSTERMSG_EXT_TYPE_HOSTNAME ||
+                    exttype == CLUSTERMSG_EXT_TYPE_HUMAN_NODENAME) {
+                    char *str = (char *) ext->ext;
+                    if (datalen == 0 || str[datalen - 1] != '\0') {
+                        serverLog(LL_WARNING,
+                            "Received %s packet with missing null terminator in extension type %d",
+                            clusterGetMessageTypeString(type), exttype);
+                        return 1;
+                    }
+                } else if (exttype == CLUSTERMSG_EXT_TYPE_FORGOTTEN_NODE) {
+                    if (datalen < sizeof(clusterMsgPingExtForgottenNode)) {
+                        serverLog(LL_WARNING,
+                            "Received %s packet with truncated extension type %d",
+                            clusterGetMessageTypeString(type), exttype);
+                        return 1;
+                    }
+                } else if (exttype == CLUSTERMSG_EXT_TYPE_SHARDID) {
+                    char *str = (char *) ext->ext;
+                    if (datalen < sizeof(clusterMsgPingExtShardId) ||
+                        verifyClusterNodeId(str, CLUSTER_NAMELEN) != C_OK)
+                    {
+                        serverLog(LL_WARNING,
+                            "Received %s packet with invalid shard id in extension type %d",
+                            clusterGetMessageTypeString(type), exttype);
+                        return 1;
+                    }
+                } else if (exttype == CLUSTERMSG_EXT_TYPE_INTERNALSECRET) {
+                    if (datalen < sizeof(clusterMsgPingExtInternalSecret)) {
+                        serverLog(LL_WARNING,
+                            "Received %s packet with truncated extension type %d",
+                            clusterGetMessageTypeString(type), exttype);
+                        return 1;
+                    }
+                }
                 explen += extlen;
                 ext = getNextPingExt(ext);
             }
Index: redis-8.0.6/tests/unit/cluster/hostnames.tcl
===================================================================
--- redis-8.0.6.orig/tests/unit/cluster/hostnames.tcl
+++ redis-8.0.6/tests/unit/cluster/hostnames.tcl
@@ -16,6 +16,36 @@ proc get_slot_field {slot_output shard_i
     return [lindex [lindex [lindex $slot_output $shard_id] $node_id] $attrib_id]
 }
 
+proc build_cluster_bus_ping {sender_name sender_port sender_cport extensions_data} {
+    set CLUSTERMSG_TYPE_PING 0
+    set CLUSTERMSG_FLAG0_EXT_DATA 0x04
+    set CLUSTER_NODE_MASTER 1
+
+    set num_extensions [expr {[string length $extensions_data] > 0 ? 1 : 0}]
+    set mflags0 [expr {$num_extensions > 0 ? $CLUSTERMSG_FLAG0_EXT_DATA : 0}]
+    set totlen [expr {2256 + [string length $extensions_data]}]
+
+    set hdr [build_cluster_bus_header $sender_name $sender_port $sender_cport \
+        $CLUSTERMSG_TYPE_PING $totlen $num_extensions $CLUSTER_NODE_MASTER $mflags0]
+    append hdr $extensions_data
+    return $hdr
+}
+
+proc build_hostname_extension {hostname_bytes} {
+    set ext_header_size 8
+    set total_ext_len [expr {$ext_header_size + [string length $hostname_bytes]}]
+    set padded_len [expr {(($total_ext_len + 7) / 8) * 8}]
+    set padding_len [expr {$padded_len - $total_ext_len}]
+
+    set ext ""
+    append ext [binary format I $padded_len]
+    append ext [binary format S 0]
+    append ext [binary format S 0]
+    append ext $hostname_bytes
+    append ext [string repeat \x00 $padding_len]
+    return $ext
+}
+
 # Start a cluster with 3 masters and 4 replicas.
 # These tests rely on specific node ordering, so make sure no node fails over.
 start_cluster 3 4 {tags {external:skip cluster} overrides {cluster-replica-no-failover yes}} {
@@ -227,4 +257,52 @@ test "Test hostname validation" {
     # Note this isn't a valid hostname, but it passes our internal validation
     R 0 config set cluster-announce-hostname "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-."
 }
+
+test "PING with hostname extension missing null terminator is rejected" {
+    for {set j 0} {$j < [llength $::servers]} {incr j} {
+        R $j config set cluster-announce-hostname ""
+    }
+    wait_for_condition 50 100 {
+        [are_hostnames_propagated ""] eq 1
+    } else {
+        fail "hostnames were not cleared"
+    }
+
+    set node1_id [R 1 CLUSTER MYID]
+    set target_host [srv 0 host]
+    set target_bus_port [expr {[srv 0 port] + 10000}]
+    set sender_port [srv -1 port]
+    set sender_cport [expr {$sender_port + 10000}]
+
+    # Freeze node 1 so its real PINGs cannot overwrite the injected hostname.
+    set node1_pid [srv -1 pid]
+    pause_process $node1_pid
+
+    set payload_len 32
+    set bad_hostname [string repeat A $payload_len]
+    set bad_ext [build_hostname_extension $bad_hostname]
+    set bad_packet [build_cluster_bus_ping $node1_id $sender_port $sender_cport $bad_ext]
+
+    # Record the log position before injecting the bad packet.
+    set loglines [count_log_lines 0]
+
+    if {$::tls} {
+        set fd [::tls::socket \
+            -cafile "$::tlsdir/ca.crt" \
+            -certfile "$::tlsdir/client.crt" \
+            -keyfile "$::tlsdir/client.key" \
+            $target_host $target_bus_port]
+    } else {
+        set fd [socket $target_host $target_bus_port]
+    }
+    fconfigure $fd -translation binary -buffering full
+    puts -nonewline $fd $bad_packet
+    flush $fd
+
+    # Verify the server logged the proper rejection message.
+    wait_for_log_messages 0 {"*missing null terminator*"} $loglines 50 100
+
+    close $fd
+    resume_process $node1_pid
+}
 }
Index: redis-8.0.6/tests/support/cluster_util.tcl
===================================================================
--- redis-8.0.6.orig/tests/support/cluster_util.tcl
+++ redis-8.0.6/tests/support/cluster_util.tcl
@@ -227,3 +227,37 @@ proc check_cluster_node_mark {flag ref_n
     }
     fail "Unable to find instance id in cluster nodes. ID: $instance_id_to_check"
 }
+
+proc build_cluster_bus_header {sender_name sender_port sender_cport msg_type totlen {num_extensions 0} {flags 0} {mflags0 0}} {
+     set CLUSTER_NAMELEN 40
+    set NET_IP_STR_LEN 46
+
+    set sender_padded [binary format a${CLUSTER_NAMELEN} $sender_name]
+    set myslots [string repeat \x00 [expr {16384/8}]]
+    set slaveof [string repeat \x00 $CLUSTER_NAMELEN]
+    set myip [string repeat \x00 $NET_IP_STR_LEN]
+    set notused1 [string repeat \x00 30]
+
+    set hdr ""
+    append hdr "RCmb"
+    append hdr [binary format I $totlen]
+    append hdr [binary format S 1]                  ;# ver
+    append hdr [binary format S $sender_port]       ;# port
+    append hdr [binary format S $msg_type]          ;# type
+    append hdr [binary format S 0]                  ;# count
+    append hdr [binary format W 1]                  ;# currentEpoch
+    append hdr [binary format W 2]                  ;# configEpoch
+    append hdr [binary format W 0]                  ;# offset
+    append hdr $sender_padded                       ;# sender
+    append hdr $myslots                             ;# myslots
+    append hdr $slaveof                             ;# slaveof
+    append hdr $myip                                ;# myip
+    append hdr [binary format S $num_extensions]    ;# extensions
+    append hdr $notused1                            ;# notused1
+    append hdr [binary format S 0]                  ;# pport
+    append hdr [binary format S $sender_cport]      ;# cport
+    append hdr [binary format S $flags]             ;# flags
+    append hdr [binary format c 0]                  ;# state
+    append hdr [binary format ccc $mflags0 0 0]     ;# mflags
+    return $hdr
+}
\ No newline at end of file

Reply via email to