From d95a5a0990c557d716b0158c824f5e1b2e2d31a5 Mon Sep 17 00:00:00 2001 From: "chenyangyang.cy" Date: Mon, 26 Jan 2026 11:37:47 +0800 Subject: [PATCH 1/3] fix slab bug --- src/slabapi.c | 17 ++++++++++++++-- tests/tairhash.tcl | 51 +++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 65 insertions(+), 3 deletions(-) diff --git a/src/slabapi.c b/src/slabapi.c index 9693e49..e6c1c99 100644 --- a/src/slabapi.c +++ b/src/slabapi.c @@ -212,7 +212,14 @@ void slab_expireDelete(tairhash_zskiplist *zsl, RedisModuleString *key, long lon if (update_findNode) { // update min value int smallest_subscript = slab_minExpireTimeIndex(find_slab); - find_node->expire_min = find_slab->expires[smallest_subscript], find_node->key_min = find_slab->keys[smallest_subscript]; + long long new_expire_min = find_slab->expires[smallest_subscript]; + RedisModuleString *new_key_min = find_slab->keys[smallest_subscript]; + if (new_expire_min != find_node->expire_min || RedisModule_StringCompare(new_key_min, find_node->key_min) != 0) { + Slab *slab = find_node->slab; + int ret = tairhash_zslDelete(zsl, find_node->key_min, find_node->expire_min); + assert(ret == 1); + find_node = tairhash_zslInsertNode(zsl, slab, new_key_min, new_expire_min); + } } slab_mergeIfNeed(zsl, find_node); // if need merge return; @@ -268,7 +275,13 @@ void slab_deleteSlabExpire(tairhash_zskiplist *zsl, tairhash_zskiplistNode *zsl_ } } slab->num_keys = effective_num; - zsl_node->expire_min = slab->expires[min_index], zsl_node->key_min = slab->keys[min_index]; + long long new_expire_min = slab->expires[min_index]; + RedisModuleString *new_key_min = slab->keys[min_index]; + if (new_expire_min != zsl_node->expire_min || RedisModule_StringCompare(new_key_min, zsl_node->key_min) != 0) { + int ret = tairhash_zslDelete(zsl, zsl_node->key_min, zsl_node->expire_min); + assert(ret == 1); + zsl_node = tairhash_zslInsertNode(zsl, slab, new_key_min, new_expire_min); + } slab_mergeIfNeed(zsl, zsl_node); return; } diff --git a/tests/tairhash.tcl b/tests/tairhash.tcl index 6832fe9..18362f9 100755 --- a/tests/tairhash.tcl +++ b/tests/tairhash.tcl @@ -2083,4 +2083,53 @@ start_server {tags {"tairhash"} overrides {bind 0.0.0.0}} { # } } } -} \ No newline at end of file +} + +start_server {tags {"slab_bug_reproduce"}} { + test "Test Slab consistency after extensive delete" { + r module load $testmodule + + set key "test_slab_key" + # 1. Fill multiple slabs (Slab size is 512) + # Insert 1500 items. Should create ~3-4 slabs. + for {set i 0} {$i < 1500} {incr i} { + r exhset $key field_$i val ex $i + } + + # 2. Delete items from the beginning (Smallest expires/keys) + # This forces the First Slab's Min Value to increase repeatedly. + # This is where the potential order invariant violation happens. + for {set i 0} {$i < 1000} {incr i} { + r exhdel $key field_$i + } + + # 3. Verify the remaining items are accessible. + # If the First Slab's Min increased but 'leapfrogged' the Second Slab, + # the Second Slab might become unreachable or data could be corrupted. + for {set i 1000} {$i < 1500} {incr i} { + assert_equal [r exhget $key field_$i] "val" + } + + assert_equal [r exhlen $key] 500 + } + + test "Test Slab Split and Merge random operations" { + r del $key + # Random operations to trigger split/merge and potential race/logic errors + for {set i 0} {$i < 2000} {incr i} { + set action [expr {int(rand()*10)}] + set f_id [expr {int(rand()*1000)}] + set expire [expr {int(rand()*100000)}] + + if {$action < 7} { + # 70% Set + r exhset $key f_$f_id val ex $expire + } else { + # 30% Del + r exhdel $key f_$f_id + } + } + # Just ensure it didn't crash + r ping + } +} From a5488b9197a8742ce3d48d060e471ef818bd4c6e Mon Sep 17 00:00:00 2001 From: "chenyangyang.cy" Date: Mon, 26 Jan 2026 13:45:49 +0800 Subject: [PATCH 2/3] fix UAF in scan mode --- src/scan_algorithm.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/scan_algorithm.c b/src/scan_algorithm.c index 5892dab..21ecba8 100644 --- a/src/scan_algorithm.c +++ b/src/scan_algorithm.c @@ -239,6 +239,7 @@ void passiveExpire(RedisModuleCtx *ctx, int dbid, RedisModuleString *key) { start_index = 0; ln = tair_hash_obj->expire_index->header->level[0].forward; while (ln && keys_per_loop) { + m_zskiplistNode *next = ln->level[0].forward; field = ln->member; if (fieldExpireIfNeeded(ctx, dbid, key, tair_hash_obj, field, 0)) { g_expire_algorithm.stat_passive_expired_field[dbid]++; @@ -250,7 +251,7 @@ void passiveExpire(RedisModuleCtx *ctx, int dbid, RedisModuleString *key) { } else { break; } - ln = ln->level[0].forward; + ln = next; } if (may_delkey) { @@ -265,7 +266,6 @@ void passiveExpire(RedisModuleCtx *ctx, int dbid, RedisModuleString *key) { } if (start_index) { - m_zslDeleteRangeByRank(tair_hash_obj->expire_index, 1, start_index); delEmptyTairHashIfNeeded(ctx, NULL, key, tair_hash_obj); } m_listDelNode(keys, node); From af3103569ca3c279da83c68863a379932e924152 Mon Sep 17 00:00:00 2001 From: "chenyangyang.cy" Date: Mon, 26 Jan 2026 14:35:25 +0800 Subject: [PATCH 3/3] Add more test --- tests/tairhash.tcl | 98 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/tests/tairhash.tcl b/tests/tairhash.tcl index 18362f9..5a62ca9 100755 --- a/tests/tairhash.tcl +++ b/tests/tairhash.tcl @@ -2132,4 +2132,102 @@ start_server {tags {"slab_bug_reproduce"}} { # Just ensure it didn't crash r ping } + + test "Test Slab churn with updates and deletes" { + set key "test_slab_churn" + r del $key + + # insert enough items to span multiple slabs + for {set i 0} {$i < 1200} {incr i} { + r exhset $key field_$i val ex 10 + } + + # update half of them to a later expire, delete some + for {set i 0} {$i < 600} {incr i} { + r exhset $key field_$i val ex 20 + } + for {set i 600} {$i < 900} {incr i} { + r exhdel $key field_$i + } + + assert_equal 900 [r exhlen $key] + + # verify remaining fields exist + for {set i 0} {$i < 600} {incr i} { + assert_equal "val" [r exhget $key field_$i] + } + for {set i 900} {$i < 1200} {incr i} { + assert_equal "val" [r exhget $key field_$i] + } + } + + test "Test Slab delete non-existing fields" { + set key "test_slab_del_missing" + r del $key + for {set i 0} {$i < 300} {incr i} { + r exhset $key field_$i val ex 5 + } + + # delete existing and non-existing fields mixed + for {set i 0} {$i < 600} {incr i} { + r exhdel $key field_$i + } + + assert_equal 0 [r exhlen $key] + assert_equal 0 [r exists $key] + } + + test "Test Slab expire boundary and fast TTL" { + set key "test_slab_fast_ttl" + r del $key + + # very small ttl + r exhset $key field1 val PX 1 + r exhset $key field2 val PX 2 + r exhset $key field3 val EX 1 + + after 3000 + + assert_equal 0 [r exhlen $key] + assert_equal "" [r exhget $key field1] + assert_equal "" [r exhget $key field2] + assert_equal "" [r exhget $key field3] + } + + test "Test Slab same field repeated set/del" { + set key "test_slab_repeat" + r del $key + + for {set i 0} {$i < 500} {incr i} { + r exhset $key field val ex [expr {($i % 3) + 1}] + if {$i % 2 == 0} { + r exhdel $key field + } + } + + # final set to ensure field exists + r exhset $key field val ex 2 + assert_equal 1 [r exhexists $key field] + } + + test "Test Slab scan during expirations" { + set key "test_slab_scan" + r del $key + + for {set i 0} {$i < 800} {incr i} { + r exhset $key field_$i val ex 1 + } + + after 1500 + + # scan should not crash even if most fields expired + set cur 0 + while 1 { + set res [r exhscan $key $cur] + set cur [lindex $res 0] + if {$cur == 0} break + } + + assert_equal 0 [r exhlen $key] + } }