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.
This commit is contained in:
Ozan Tezcan 2026-04-14 08:47:04 +03:00 committed by YaacovHazan
parent 8cdf9391da
commit 837ca7f8f4
2 changed files with 82 additions and 0 deletions

View file

@ -2251,6 +2251,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();

View file

@ -1878,3 +1878,80 @@ 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 repl-diskless-load yes
$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"
}
}
}
}