Use-After-Free (memory safety) in scripting engine during FULLSYNC
Description
Summary:
- The commit fixes a use-after-free vulnerability in Redis replication when a full synchronization (FULLSYNC) happens while a timed-out script is still running on a replica. The scripting engine could be freed during FULLSYNC (due to emptyData/scriptingReset) while a script was still executing, leading to a use-after-free and potential memory corruption or crash.
- The fix adds a guard at the start of readSyncBulkPayload to delay processing of the FULLSYNC payload if a long-running command (script) is yielding, ensuring the scripting engine is not freed while a script is still using it.
- This is a real vulnerability fix (CVE-2026-23631). The change is not a mere dependency bump; it addresses a race between replication and the scripting engine. The repository shows the patch in replication.c and accompanying integration tests to validate the scenario.
Impact:
- Affected versions: Redis 8.6.x prior to 8.6.2 (i.e., versions that include the vulnerable behavior but not this fix).
- Vulnerability type: Use-After-Free in the scripting engine during FULLSYNC on replicas.
- Severity: HIGH (memory-safety issue with potential crash or memory corruption under exploitation conditions).
- Affected scenario: A replica running a long-running or timed-out script experiences FULLSYNC, during which the scripting engine could be freed while still in use by the script.
Root cause analysis:
- FULLSYNC processing could free the functions engine (scripting engine) as part of the sync flow. If a long-running script was still executing, this could result in a use-after-free.
- The fix ensures that readSyncBulkPayload returns early when inside a yielding long command, effectively delaying FULLSYNC processing until the script completes.
Verification rationale:
- The commit includes a guarded check in readSyncBulkPayload to skip processing when isInsideYieldingLongCommand() is true, preventing the premature freeing of the scripting engine during FULLSYNC.
- The accompanying integration test extends replication scenarios to explicitly cover the case where a long-running script is present during fullsync, verifying that the replica remains responsive post-fullsync once the script ends.
- The CVE identifier provided in the commit (CVE-2026-23631) aligns with a memory-safety use-after-free scenario.
Proof of Concept
Proof-of-concept (PoC) to reproduce/use the vulnerability before the fix:
Prerequisites:
- Two Redis instances running with replication: master on port 6379 and replica on port 6380.
- Version: any 8.6.x prior to 8.6.2.
- Sufficient data backlog to trigger a FULLSYNC (e.g., large data insertions on master).
- A script that occupies the replica for a period (timed-out or long-running) to create a yielding long command on the replica.
Steps:
1) Start master and replica with replication enabled:
- Master: redis-server --port 6379
- Replica: redis-server --port 6380 --replicaof 127.0.0.1 6379
2) On the replica, start a blocking Lua script (long-running) to simulate a timed-out script:
- redis-cli -p 6380 eval "while true do end" 0 &
- Note: The script runs indefinitely and will be considered a long/yielding operation.
3) Trigger a FULLSYNC on the replica by rapidly filling the master backlog, forcing a resync:
- On master, add a burst of keys: for i in {1..100000}; do redis-cli -p 6379 -n 0 set key:$i value:$i; done
- Alternatively, configure a small client-output-buffer-limit on the replica to trigger a FULLSYNC when the backlog exceeds the threshold (as in the test: repl-diskless-sync yes, repl-diskless-sync-delay 0, and small replica backlog per config).
4) Observe behavior during FULLSYNC:
- Before the fix: the long-running script on the replica could cause a use-after-free of the scripting engine when FULLSYNC loads the RDB, potentially leading to a crash or memory corruption.
- With the fix: readSyncBulkPayload short-circuits while isInsideYieldingLongCommand() is true, delaying FULLSYNC processing until the script finishes, avoiding the use-after-free.
5) End state:
- Kill the blocking script on the replica:
redis-cli -p 6380 SCRIPT KILL
- Check replication health:
redis-cli -p 6380 PING # should return PONG
Notes:
- The repository’s test addition mirrors this scenario by testing both Lua script and Redis Function paths on the replica during an imminent FULLSYNC, asserting that the replica remains in BUSY state during the long script and recovers afterward.
- The PoC above is a practical approximation to illustrate the race; exact timings may vary by environment and configuration.
Commit Details
Author: Ozan Tezcan
Date: 2026-04-14 05:47 UTC
Message:
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.
Triage Assessment
Vulnerability Type: Use-After-Free
Confidence: HIGH
Reasoning:
Commit fixes a use-after-free vulnerability that occurs when a full sync happens while a timed-out script is still running on a replica. By delaying fullsync processing until the script finishes, it prevents freeing the scripting engine while it is in use. This directly mitigates a memory-safety issue with security implications (CVE-2026-23631).
Verification Assessment
Vulnerability Type: Use-After-Free (memory safety) in scripting engine during FULLSYNC
Confidence: HIGH
Affected Versions: < 8.6.2
Code Diff
diff --git a/src/replication.c b/src/replication.c
index 6726cff19f0..44d81ba51fb 100644
--- a/src/replication.c
+++ b/src/replication.c
@@ -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();
diff --git a/tests/integration/replication.tcl b/tests/integration/replication.tcl
index b3a03a2f9f0..b3020ab5f47 100644
--- a/tests/integration/replication.tcl
+++ b/tests/integration/replication.tcl
@@ -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"
+ }
+ }
+ }
+}