mirror of
https://github.com/hanzoai/redis.git
synced 2026-10-30 18:25:12 +00:00
## Summary
Add overflow checks for attacker-controlled `uint32_t` length fields in
`clusterProcessPacket()` before adding them to `explen` (`uint32_t`).
Without these checks, crafted PUBLISH/PUBLISHSHARD and MODULE cluster
bus messages can wrap `explen` to a small value, bypassing the `totlen
!= explen` validation and causing heap out-of-bounds reads or OOM aborts
in the processing path.
## Problem
In `clusterProcessPacket()`, the expected packet length for PUBLISH and
MODULE messages is computed by summing struct overhead with
variable-length fields read from the packet header via `ntohl()`:
```c
// PUBLISH (line 2841-2846)
explen += sizeof(clusterMsgDataPublish) - 8 +
ntohl(hdr->data.publish.msg.channel_len) +
ntohl(hdr->data.publish.msg.message_len);
// MODULE (line 2855-2858)
explen += sizeof(clusterMsgModule) - 3 + ntohl(hdr->data.module.msg.len);
```
Both `channel_len + message_len` (PUBLISH) and `len` (MODULE) are
`uint32_t` values from the network packet. Their addition to `explen`
can overflow `uint32_t`, wrapping to a small value that matches
`totlen`, passing the size validation at line 2864.
**Example (PUBLISH):** With `channel_len = 0x80000000` and `message_len
= 0x80000000`, their sum overflows to `0`, making `explen` equal to just
the base struct size. An attacker sets `totlen` to match. The validation
passes, and the processing path at line 3273 calls
`createStringObject()` with the original 2GB lengths, reading far past
the received buffer.
**Attack vector:** Reachable via the cluster bus port (default: data
port + 10000), which does not require authentication by default.
## Fix
Extract `ntohl` values into local variables and check each addition
against `UINT32_MAX` before performing it. Reject the packet with a
warning log if overflow is detected. The computed `explen` is identical
to the original for all non-overflowing (legitimate) inputs.
---------
Signed-off-by: Sebastien Tardif <[email protected]>
Co-authored-by: debing.sun <[email protected]>
302 lines
10 KiB
Tcl
302 lines
10 KiB
Tcl
#
|
|
# Copyright (c) 2009-Present, Redis Ltd.
|
|
# All rights reserved.
|
|
#
|
|
# Copyright (c) 2024-present, Valkey contributors.
|
|
# All rights reserved.
|
|
#
|
|
# Licensed under your choice of (a) the Redis Source Available License 2.0
|
|
# (RSALv2); or (b) the Server Side Public License v1 (SSPLv1); or (c) the
|
|
# GNU Affero General Public License v3 (AGPLv3).
|
|
#
|
|
# Portions of this file are available under BSD3 terms; see REDISCONTRIBUTIONS for more information.
|
|
#
|
|
|
|
# Cluster helper functions
|
|
# Normalize cluster slots configuration by sorting replicas by node ID
|
|
proc normalize_cluster_slots {slots_config} {
|
|
set normalized {}
|
|
foreach slot_range $slots_config {
|
|
if {[llength $slot_range] <= 3} {
|
|
lappend normalized $slot_range
|
|
} else {
|
|
# Sort replicas (index 3+) by node ID, keep start/end/master unchanged
|
|
set replicas [lrange $slot_range 3 end]
|
|
set sorted_replicas [lsort -index 2 $replicas]
|
|
lappend normalized [concat [lrange $slot_range 0 2] $sorted_replicas]
|
|
}
|
|
}
|
|
return $normalized
|
|
}
|
|
|
|
# Check if cluster configuration is consistent.
|
|
proc cluster_config_consistent {} {
|
|
for {set j 0} {$j < [llength $::servers]} {incr j} {
|
|
if {$j == 0} {
|
|
set base_cfg [R $j cluster slots]
|
|
set base_secret [R $j debug internal_secret]
|
|
set normalized_base_cfg [normalize_cluster_slots $base_cfg]
|
|
} else {
|
|
set cfg [R $j cluster slots]
|
|
set secret [R $j debug internal_secret]
|
|
set normalized_cfg [normalize_cluster_slots $cfg]
|
|
if {$normalized_cfg != $normalized_base_cfg || $secret != $base_secret} {
|
|
return 0
|
|
}
|
|
}
|
|
}
|
|
|
|
return 1
|
|
}
|
|
|
|
# Check if cluster size is consistent.
|
|
proc cluster_size_consistent {cluster_size} {
|
|
for {set j 0} {$j < $cluster_size} {incr j} {
|
|
if {[CI $j cluster_known_nodes] ne $cluster_size} {
|
|
return 0
|
|
}
|
|
}
|
|
return 1
|
|
}
|
|
|
|
# Wait for cluster configuration to propagate and be consistent across nodes.
|
|
proc wait_for_cluster_propagation {} {
|
|
wait_for_condition 50 100 {
|
|
[cluster_config_consistent] eq 1
|
|
} else {
|
|
fail "cluster config did not reach a consistent state"
|
|
}
|
|
}
|
|
|
|
# Wait for cluster size to be consistent across nodes.
|
|
proc wait_for_cluster_size {cluster_size} {
|
|
wait_for_condition 1000 50 {
|
|
[cluster_size_consistent $cluster_size] eq 1
|
|
} else {
|
|
fail "cluster size did not reach a consistent size $cluster_size"
|
|
}
|
|
}
|
|
|
|
# Check that cluster nodes agree about "state", or raise an error.
|
|
proc wait_for_cluster_state {state} {
|
|
for {set j 0} {$j < [llength $::servers]} {incr j} {
|
|
wait_for_condition 100 50 {
|
|
[CI $j cluster_state] eq $state
|
|
} else {
|
|
fail "Cluster node $j cluster_state:[CI $j cluster_state]"
|
|
}
|
|
}
|
|
}
|
|
|
|
# Default slot allocation for clusters, each master has a continuous block
|
|
# and approximately equal number of slots.
|
|
proc continuous_slot_allocation {masters} {
|
|
set avg [expr double(16384) / $masters]
|
|
set slot_start 0
|
|
for {set j 0} {$j < $masters} {incr j} {
|
|
set slot_end [expr int(ceil(($j + 1) * $avg) - 1)]
|
|
R $j cluster addslotsrange $slot_start $slot_end
|
|
set slot_start [expr $slot_end + 1]
|
|
}
|
|
}
|
|
|
|
# Setup method to be executed to configure the cluster before the
|
|
# tests run.
|
|
proc cluster_setup {masters node_count slot_allocator code} {
|
|
# Have all nodes meet
|
|
if {$::tls} {
|
|
set tls_cluster [lindex [R 0 CONFIG GET tls-cluster] 1]
|
|
}
|
|
if {$::tls && !$tls_cluster} {
|
|
for {set i 1} {$i < $node_count} {incr i} {
|
|
R 0 CLUSTER MEET [srv -$i host] [srv -$i pport]
|
|
}
|
|
} else {
|
|
for {set i 1} {$i < $node_count} {incr i} {
|
|
R 0 CLUSTER MEET [srv -$i host] [srv -$i port]
|
|
}
|
|
}
|
|
|
|
$slot_allocator $masters
|
|
|
|
wait_for_cluster_propagation
|
|
|
|
# Setup master/replica relationships
|
|
for {set i 0} {$i < $masters} {incr i} {
|
|
set nodeid [R $i CLUSTER MYID]
|
|
for {set j [expr $i + $masters]} {$j < $node_count} {incr j $masters} {
|
|
R $j CLUSTER REPLICATE $nodeid
|
|
}
|
|
}
|
|
|
|
wait_for_cluster_propagation
|
|
wait_for_cluster_state "ok"
|
|
|
|
uplevel 1 $code
|
|
}
|
|
|
|
# Start a cluster with the given number of masters and replicas. Replicas
|
|
# will be allocated to masters by round robin.
|
|
proc start_cluster {masters replicas options code {slot_allocator continuous_slot_allocation}} {
|
|
set ::cluster_master_nodes $masters
|
|
set ::cluster_replica_nodes $replicas
|
|
set node_count [expr $masters + $replicas]
|
|
|
|
# Set the final code to be the tests + cluster setup
|
|
set code [list cluster_setup $masters $node_count $slot_allocator $code]
|
|
|
|
# Configure the starting of multiple servers. Set cluster node timeout
|
|
# aggressively since many tests depend on ping/pong messages.
|
|
set cluster_options [list overrides [list cluster-enabled yes cluster-ping-interval 100 cluster-node-timeout 3000 cluster-slot-stats-enabled yes]]
|
|
set options [concat $cluster_options $options]
|
|
|
|
# Cluster mode only supports a single database, so before executing the tests
|
|
# it needs to be configured correctly and needs to be reset after the tests.
|
|
set old_singledb $::singledb
|
|
set ::singledb 1
|
|
start_multiple_servers $node_count $options $code
|
|
set ::singledb $old_singledb
|
|
}
|
|
|
|
# Test node for flag.
|
|
proc cluster_has_flag {node flag} {
|
|
expr {[lsearch -exact [dict get $node flags] $flag] != -1}
|
|
}
|
|
|
|
# Returns the parsed "myself" node entry as a dictionary.
|
|
proc cluster_get_myself id {
|
|
set nodes [get_cluster_nodes $id]
|
|
foreach n $nodes {
|
|
if {[cluster_has_flag $n myself]} {return $n}
|
|
}
|
|
return {}
|
|
}
|
|
|
|
# Returns a parsed CLUSTER NODES output as a list of dictionaries.
|
|
proc get_cluster_nodes id {
|
|
set lines [split [R $id cluster nodes] "\r\n"]
|
|
set nodes {}
|
|
foreach l $lines {
|
|
set l [string trim $l]
|
|
if {$l eq {}} continue
|
|
set args [split $l]
|
|
set node [dict create \
|
|
id [lindex $args 0] \
|
|
addr [lindex $args 1] \
|
|
flags [split [lindex $args 2] ,] \
|
|
slaveof [lindex $args 3] \
|
|
ping_sent [lindex $args 4] \
|
|
pong_recv [lindex $args 5] \
|
|
config_epoch [lindex $args 6] \
|
|
linkstate [lindex $args 7] \
|
|
slots [lrange $args 8 end] \
|
|
]
|
|
lappend nodes $node
|
|
}
|
|
return $nodes
|
|
}
|
|
|
|
# Returns 1 if no node knows node_id, 0 if any node knows it.
|
|
proc node_is_forgotten {node_id} {
|
|
for {set j 0} {$j < [llength $::servers]} {incr j} {
|
|
set cluster_nodes [R $j CLUSTER NODES]
|
|
if { [string match "*$node_id*" $cluster_nodes] } {
|
|
return 0
|
|
}
|
|
}
|
|
return 1
|
|
}
|
|
|
|
# Isolate a node from the cluster and give it a new nodeid
|
|
proc isolate_node {id} {
|
|
set node_id [R $id CLUSTER MYID]
|
|
R $id CLUSTER RESET HARD
|
|
# Here we additionally test that CLUSTER FORGET propagates to all nodes.
|
|
set other_id [expr $id == 0 ? 1 : 0]
|
|
R $other_id CLUSTER FORGET $node_id
|
|
wait_for_condition 50 100 {
|
|
[node_is_forgotten $node_id]
|
|
} else {
|
|
fail "CLUSTER FORGET was not propagated to all nodes"
|
|
}
|
|
}
|
|
|
|
# Check if cluster's view of hostnames is consistent
|
|
proc are_hostnames_propagated {match_string} {
|
|
for {set j 0} {$j < [llength $::servers]} {incr j} {
|
|
set cfg [R $j cluster slots]
|
|
foreach node $cfg {
|
|
for {set i 2} {$i < [llength $node]} {incr i} {
|
|
if {! [string match $match_string [lindex [lindex [lindex $node $i] 3] 1]] } {
|
|
return 0
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return 1
|
|
}
|
|
|
|
proc wait_node_marked_fail {ref_node_index instance_id_to_check} {
|
|
wait_for_condition 1000 50 {
|
|
[check_cluster_node_mark fail $ref_node_index $instance_id_to_check]
|
|
} else {
|
|
fail "Replica node never marked as FAIL ('fail')"
|
|
}
|
|
}
|
|
|
|
proc wait_node_marked_pfail {ref_node_index instance_id_to_check} {
|
|
wait_for_condition 1000 50 {
|
|
[check_cluster_node_mark fail\? $ref_node_index $instance_id_to_check]
|
|
} else {
|
|
fail "Replica node never marked as PFAIL ('fail?')"
|
|
}
|
|
}
|
|
|
|
proc check_cluster_node_mark {flag ref_node_index instance_id_to_check} {
|
|
set nodes [get_cluster_nodes $ref_node_index]
|
|
|
|
foreach n $nodes {
|
|
if {[dict get $n id] eq $instance_id_to_check} {
|
|
return [cluster_has_flag $n $flag]
|
|
}
|
|
}
|
|
fail "Unable to find instance id in cluster nodes. ID: $instance_id_to_check"
|
|
}
|
|
|
|
# Build the 2256-byte cluster bus header (CLUSTERMSG_MIN_LEN) common to all
|
|
# message types. Only the type field and the data that follows differ.
|
|
proc build_cluster_bus_header {sender_name sender_port sender_cport msg_type totlen} {
|
|
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 0] ;# 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 0] ;# flags
|
|
append hdr [binary format c 0] ;# state
|
|
append hdr [binary format ccc 0 0 0] ;# mflags
|
|
|
|
return $hdr
|
|
}
|