From 1abd489d0716cbfd68fb1c39e0d1a0bba2dc6fc5 Mon Sep 17 00:00:00 2001 From: Alessio Attilio Date: Tue, 24 Mar 2026 03:22:58 +0100 Subject: [PATCH] Skip memory prefetch during loading to avoid crash in dictEmpty callback (#14848) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #14838 ## Summary Fix a crash in `prefetchCommands()` that occurs during replica full sync when the replica has existing data that needs to be emptied. ## Problem Description During `emptyData()` → `kvstoreEmpty()` → `dictEmpty()` → `_dictClear()`, the first hash table is cleared and `d->ht_table[0]` is set to NULL via `_dictReset`. Then while clearing the second hash table, every 65536 buckets it invokes `replicationEmptyDbCallback()` → `processEventsWhileBlocked()` → `readQueryFromClient()` → `prefetchCommands()`. At this point, `dictSize() > 0` still holds (because the second hash table isn't fully cleared yet), but `ht_table[0]` is already NULL. The prefetch code assumed `ht_table[0]` is always valid when `dictSize() > 0`, leading to a crash. ## Solution 1. **Skip prefetch during loading**: Added a `server.loading` check at the top of `prefetchCommands()` to return early. During RDB loading, the main dictionary is being rebuilt, so prefetching keys from it is useless anyway. 2. **Add defensive assertion**: Added `serverAssert(batch->current_dicts[i]->ht_table[0])` in `initBatchInfo()` to catch any future cases where `ht_table[0]` is NULL while `dictSize() > 0` (which should only happen mid-`dictEmpty` via `_dictReset`). --------- Co-authored-by: kairosci Co-authored-by: debing.sun Co-authored-by: Yuan Wang --- src/memory_prefetch.c | 7 +++++- tests/integration/replication.tcl | 40 +++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/src/memory_prefetch.c b/src/memory_prefetch.c index 8ecdc12b4f..46810147fe 100644 --- a/src/memory_prefetch.c +++ b/src/memory_prefetch.c @@ -165,6 +165,11 @@ static void initBatchInfo(dict **dicts, GetValueDataFunc func) { info->state = PREFETCH_DONE; continue; } + + /* We skip prefetch during loading, so ht_table[0] should never be NULL + * when dictSize() > 0 (which only happens mid-dictEmpty via _dictReset). */ + serverAssert(batch->current_dicts[i]->ht_table[0]); + info->ht_idx = HT_IDX_INVALID; info->current_entry = NULL; info->current_kv = NULL; @@ -334,7 +339,7 @@ int determinePrefetchCount(int len) { * 3. Prefetch the keys and values for all commands in the current batch from * the main dictionaries. */ void prefetchCommands(void) { - if (!batch) return; + if (!batch || server.loading) return; /* Prefetch argv's for all pending commands */ for (size_t i = 0; i < batch->pcmd_count; i++) { diff --git a/tests/integration/replication.tcl b/tests/integration/replication.tcl index 1805a3d524..51959e4f48 100644 --- a/tests/integration/replication.tcl +++ b/tests/integration/replication.tcl @@ -1832,3 +1832,43 @@ start_server {tags {"repl external:skip"}} { } } } + +start_server {tags {"repl external:skip"}} { + set master [srv 0 client] + set master_host [srv 0 host] + set master_port [srv 0 port] + + start_server {overrides {io-threads 2}} { + set slave [srv 0 client] + + test {prefetchCommands handles NULL argv and keys during RDB replication with IO threads} { + # Enable diskless sync to trigger RDB streaming during replication + $master config set repl-diskless-sync yes + $master config set repl-diskless-sync-delay 0 + + # Populate keys in the format key:$i with 128-byte values. + $slave debug populate 700000 key 128 + + # Force a full resync by resetting the slave. + set rd [redis_deferring_client 0] + $rd slaveof $master_host $master_port + + # Create a large pipeline command. + set batch_size 1000 + set buf "" + for {set i 0} {$i < $batch_size} {incr i} { + append buf [format_command get key:1] + } + + # Continuously send pipelined commands so that the replica processes + # and prefetches them while it is emptying old data during full sync. + set start_time [clock milliseconds] + while {[clock milliseconds] - $start_time < 5000} { + $rd write $buf + $rd flush + if {[s 0 master_link_status] eq "up"} break + } + $rd close + } + } +}