From e965fd1b420e5fe1ff572fb24b65b70d8b0b84cc Mon Sep 17 00:00:00 2001 From: "debing.sun" Date: Tue, 9 Jun 2026 22:01:51 +0800 Subject: [PATCH 1/7] test --- deps/Makefile | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/deps/Makefile b/deps/Makefile index 7ca6de4c26e..8c1cc1e621d 100644 --- a/deps/Makefile +++ b/deps/Makefile @@ -16,6 +16,21 @@ DEPS_CFLAGS := $(CFLAGS) DEPS_LDFLAGS := $(LDFLAGS) CLANG := $(findstring clang,$(shell sh -c '$(CC) --version | head -1')) +# Make sure the dependencies honor SANITIZER as well. Otherwise top-level +# sanitized Redis builds can end up linking unsanitized vendored libraries. +ifeq ($(SANITIZER),address) + DEPS_CFLAGS+=-fsanitize=address -fno-sanitize-recover=all -fno-omit-frame-pointer + DEPS_LDFLAGS+=-fsanitize=address +else +ifeq ($(SANITIZER),undefined) + DEPS_CFLAGS+=-fsanitize=undefined -fno-sanitize-recover=all -fno-omit-frame-pointer + DEPS_LDFLAGS+=-fsanitize=undefined +else +ifeq ($(SANITIZER),thread) + DEPS_CFLAGS+=-fsanitize=thread -fno-sanitize-recover=all -fno-omit-frame-pointer + DEPS_LDFLAGS+=-fsanitize=thread +else + # MSan looks for errors related to uninitialized memory. # Make sure to build the dependencies with MSan as it needs all the code to be instrumented. # A library could be used to initialize memory but if it's not build with --fsanitize=memory then @@ -28,6 +43,9 @@ else $(error "MemorySanitizer needs to be compiled and linked with clang. Please use CC=clang") endif endif +endif +endif +endif default: @echo "Explicit target required" From b2b43bc79e871b6622c00aa000ff4ac0e35ae81c Mon Sep 17 00:00:00 2001 From: "debing.sun" Date: Wed, 10 Jun 2026 10:46:19 +0800 Subject: [PATCH 2/7] test --- src/Makefile | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Makefile b/src/Makefile index 8939f89e07c..df83255eecc 100644 --- a/src/Makefile +++ b/src/Makefile @@ -26,7 +26,11 @@ CLANG := $(findstring clang,$(shell sh -c '$(CC) --version | head -1')) # explicitly without any defaults added, pass the OPT variable instead. OPTIMIZATION?=-O3 ENABLE_LTO?= +# Don't enable LTO under a sanitizer build: clang's LTO emits per-TU ASan +# module destructors that the GNU bfd linker fails to resolve ("defined in +# discarded section"), and LTO offers no benefit while sanitizing anyway. ifeq ($(OPTIMIZATION),-O3) +ifeq ($(SANITIZER),) ifeq (clang,$(CLANG)) ENABLE_LTO=-flto else @@ -34,6 +38,7 @@ ifeq ($(OPTIMIZATION),-O3) endif OPTIMIZATION+=$(ENABLE_LTO) endif +endif ifneq ($(OPTIMIZATION),-O0) OPTIMIZATION+=-fno-omit-frame-pointer endif From 1f4a1b011e2fdbfc51b2d855a51521a97f033fbb Mon Sep 17 00:00:00 2001 From: "debing.sun" Date: Wed, 10 Jun 2026 11:31:33 +0800 Subject: [PATCH 3/7] test --- modules/vector-sets/Makefile | 9 +++++++++ src/Makefile | 10 ++++++++++ src/module.c | 9 ++++++++- 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/modules/vector-sets/Makefile b/modules/vector-sets/Makefile index f8c05c9bc87..248f6820fe0 100644 --- a/modules/vector-sets/Makefile +++ b/modules/vector-sets/Makefile @@ -1,12 +1,21 @@ # Compiler settings CC = cc +CLANG := $(findstring clang,$(shell $(CC) --version 2>/dev/null | head -1)) + ifdef SANITIZER ifeq ($(SANITIZER),address) SAN=-fsanitize=address else ifeq ($(SANITIZER),undefined) SAN=-fsanitize=undefined + # The Redis module API calls every RedisModule_* symbol through a + # type-erased (void*-cast) function pointer, which is ABI-safe but trips + # clang UBSan's -fsanitize=function check. Disable that sub-check (the + # option/check is clang-only; gcc has no `function` sanitizer for C). + ifeq ($(CLANG),clang) + SAN+=-fno-sanitize=function + endif else ifeq ($(SANITIZER),thread) SAN=-fsanitize=thread diff --git a/src/Makefile b/src/Makefile index df83255eecc..b5cf7952812 100644 --- a/src/Makefile +++ b/src/Makefile @@ -348,6 +348,16 @@ ifneq ($(SKIP_VEC_SETS),yes) vpath %.c ../modules/vector-sets REDIS_VEC_SETS_OBJ=hnsw.o vset.o vset_config.o FINAL_CFLAGS+=-DINCLUDE_VEC_SETS=1 +# Vector sets are built into the server but use the Redis module API +# (redismodule.h), which calls every RedisModule_* symbol through a type-erased +# (void*-cast) function pointer. That is ABI-safe but trips clang UBSan's +# -fsanitize=function check, so disable that sub-check for these objects only +# (the check is clang-only; gcc has no C `function` sanitizer). +ifeq ($(SANITIZER),undefined) +ifeq ($(CLANG),clang) +$(REDIS_VEC_SETS_OBJ): FINAL_CFLAGS += -fno-sanitize=function +endif +endif endif ifndef V diff --git a/src/module.c b/src/module.c index eb203ce8d69..25e24376cfc 100644 --- a/src/module.c +++ b/src/module.c @@ -13390,7 +13390,14 @@ int moduleLoad(const char *path, void **module_argv, int module_argc, int is_loa } /* Load a module by its 'onload' callback and initialize it. On success C_OK is returned, otherwise - * C_ERR is returned. */ + * C_ERR is returned. + * + * The module's RedisModule_OnLoad is resolved via dlsym and called through a + * type-erased pointer (int (*)(void *, void **, int)), which does not match its + * real signature (int (RedisModuleCtx *, RedisModuleString **, int)). This is + * intentional and ABI-safe, but trips UBSan's -fsanitize=function check, so we + * disable it for this function only. */ +REDIS_NO_SANITIZE("function") int moduleOnLoad(int (*onload)(void *, void **, int), const char *path, void *handle, void **module_argv, int module_argc, int is_loadex) { RedisModuleCtx ctx; moduleCreateContext(&ctx, NULL, REDISMODULE_CTX_TEMP_CLIENT); /* We pass NULL since we don't have a module yet. */ From c53773e6ba604b6c825c7e96ada2eac68decb5b3 Mon Sep 17 00:00:00 2001 From: "debing.sun" Date: Wed, 10 Jun 2026 11:36:19 +0800 Subject: [PATCH 4/7] test --- src/module.c | 1 - 1 file changed, 1 deletion(-) diff --git a/src/module.c b/src/module.c index 25e24376cfc..7445b12f66a 100644 --- a/src/module.c +++ b/src/module.c @@ -13397,7 +13397,6 @@ int moduleLoad(const char *path, void **module_argv, int module_argc, int is_loa * real signature (int (RedisModuleCtx *, RedisModuleString **, int)). This is * intentional and ABI-safe, but trips UBSan's -fsanitize=function check, so we * disable it for this function only. */ -REDIS_NO_SANITIZE("function") int moduleOnLoad(int (*onload)(void *, void **, int), const char *path, void *handle, void **module_argv, int module_argc, int is_loadex) { RedisModuleCtx ctx; moduleCreateContext(&ctx, NULL, REDISMODULE_CTX_TEMP_CLIENT); /* We pass NULL since we don't have a module yet. */ From 175d2c35f26baca006315bce71f5f24a266d8cf2 Mon Sep 17 00:00:00 2001 From: "debing.sun" Date: Wed, 10 Jun 2026 11:54:40 +0800 Subject: [PATCH 5/7] test --- deps/Makefile | 7 +++++++ deps/lua/src/lua_cmsgpack.c | 26 ++++++++++++++------------ 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/deps/Makefile b/deps/Makefile index 8c1cc1e621d..48a6747194a 100644 --- a/deps/Makefile +++ b/deps/Makefile @@ -133,6 +133,13 @@ endif LUA_CFLAGS+= -Wall -DLUA_ANSI -DENABLE_CJSON_GLOBAL -DREDIS_STATIC='' -DLUA_USE_MKSTEMP $(DEPS_CFLAGS) LUA_LDFLAGS+= $(DEPS_LDFLAGS) +# Lua's table/index code intentionally casts possibly out-of-range lua_Numbers +# to int (it then validates via a round-trip comparison, e.g. luaH_get), which +# is benign but trips UBSan's float-cast-overflow check and aborts under +# -fno-sanitize-recover. Disable that sub-check for Lua only. +ifeq ($(SANITIZER),undefined) +LUA_CFLAGS+= -fno-sanitize=float-cast-overflow +endif ifeq ($(LUA_DEBUG),yes) LUA_CFLAGS+= -O0 -g -DLUA_USE_APICHECK else diff --git a/deps/lua/src/lua_cmsgpack.c b/deps/lua/src/lua_cmsgpack.c index 5f8929d454d..39831dcacf4 100644 --- a/deps/lua/src/lua_cmsgpack.c +++ b/deps/lua/src/lua_cmsgpack.c @@ -628,10 +628,11 @@ void mp_decode_to_lua_type(lua_State *L, mp_cur *c) { case 0xd2: /* int 32 */ mp_cur_need(c,5); lua_pushinteger(L, - ((int32_t)c->p[1] << 24) | - ((int32_t)c->p[2] << 16) | - ((int32_t)c->p[3] << 8) | - (int32_t)c->p[4]); + (int32_t)( + ((uint32_t)c->p[1] << 24) | + ((uint32_t)c->p[2] << 16) | + ((uint32_t)c->p[3] << 8) | + (uint32_t)c->p[4])); mp_cur_consume(c,5); break; case 0xcf: /* uint 64 */ @@ -654,14 +655,15 @@ void mp_decode_to_lua_type(lua_State *L, mp_cur *c) { #else lua_pushinteger(L, #endif - ((int64_t)c->p[1] << 56) | - ((int64_t)c->p[2] << 48) | - ((int64_t)c->p[3] << 40) | - ((int64_t)c->p[4] << 32) | - ((int64_t)c->p[5] << 24) | - ((int64_t)c->p[6] << 16) | - ((int64_t)c->p[7] << 8) | - (int64_t)c->p[8]); + (int64_t)( + ((uint64_t)c->p[1] << 56) | + ((uint64_t)c->p[2] << 48) | + ((uint64_t)c->p[3] << 40) | + ((uint64_t)c->p[4] << 32) | + ((uint64_t)c->p[5] << 24) | + ((uint64_t)c->p[6] << 16) | + ((uint64_t)c->p[7] << 8) | + (uint64_t)c->p[8])); mp_cur_consume(c,9); break; case 0xc0: /* nil */ From 230b2261fedddf35e96e9fec27b38b27bb4da212 Mon Sep 17 00:00:00 2001 From: "debing.sun" Date: Wed, 10 Jun 2026 14:13:28 +0800 Subject: [PATCH 6/7] test --- tests/support/benchmark.tcl | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/tests/support/benchmark.tcl b/tests/support/benchmark.tcl index 156b20556e3..6cfdd6d431e 100644 --- a/tests/support/benchmark.tcl +++ b/tests/support/benchmark.tcl @@ -11,22 +11,32 @@ proc redisbenchmark_tls_config {testsdir} { } } +# When running under ThreadSanitizer, redis-benchmark must load the same +# suppressions file as the server (it is exec'd separately and otherwise gets +# no TSAN_OPTIONS). Returns an `env`-prefix list, or empty when not under tsan. +proc redisbenchmark_tsan_prefix {} { + if {$::tsan} { + return [list /usr/bin/env "TSAN_OPTIONS=allocator_may_return_null=1,detect_deadlocks=0,suppressions=src/tsan.sup"] + } + return {} +} + proc redisbenchmark {host port {opts {}}} { - set cmd [list src/redis-benchmark -h $host -p $port] + set cmd [list {*}[redisbenchmark_tsan_prefix] src/redis-benchmark -h $host -p $port] lappend cmd {*}[redisbenchmark_tls_config "tests"] lappend cmd {*}$opts return $cmd } proc redisbenchmarkuri {host port {opts {}}} { - set cmd [list src/redis-benchmark -u redis://$host:$port] + set cmd [list {*}[redisbenchmark_tsan_prefix] src/redis-benchmark -u redis://$host:$port] lappend cmd {*}[redisbenchmark_tls_config "tests"] lappend cmd {*}$opts return $cmd } proc redisbenchmarkuriuserpass {host port user pass {opts {}}} { - set cmd [list src/redis-benchmark -u redis://$user:$pass@$host:$port] + set cmd [list {*}[redisbenchmark_tsan_prefix] src/redis-benchmark -u redis://$user:$pass@$host:$port] lappend cmd {*}[redisbenchmark_tls_config "tests"] lappend cmd {*}$opts return $cmd From 0bddc721b815f561efac01dfc28bcd9bbf3ead48 Mon Sep 17 00:00:00 2001 From: "debing.sun" Date: Wed, 10 Jun 2026 17:55:55 +0800 Subject: [PATCH 7/7] test --- src/tsan.sup | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/tsan.sup b/src/tsan.sup index 904ea6b33ff..8382e735374 100644 --- a/src/tsan.sup +++ b/src/tsan.sup @@ -7,6 +7,16 @@ signal:printCrashReport # https://github.com/jemalloc/jemalloc/issues/2621 race:malloc_mutex_trylock_final +# redis-benchmark with --threads shares one hdr_histogram across worker threads: +# they record latencies via hdr_record_value_atomic (atomic increments to the +# counts/min/max/total) while thread 0's showThroughput reads it with non-atomic +# accessors (hdr_mean) and periodically resets current_sec via hdr_reset, to +# print live throughput. The displayed stats are approximate by design, so these +# races are benign and not worth per-bucket synchronization. hdr_histogram is +# only accessed concurrently by redis-benchmark; the server uses it from the +# main thread only. The final benchmark report runs after all workers stopped. +race:hdr_histogram.c + # A race can happen on conn->last_errno if replica client is reading/writing # data in IO thread and main thread is calling connAddrPeerName for some reason # (f.e genRedisInfoString/roleCommand...).