aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md4
-rw-r--r--asan.supp18
-rw-r--r--msan.supp14
-rw-r--r--scripts/test.lua17
-rw-r--r--src/zencore/include/zencore/memory/fmalloc.h11
-rw-r--r--src/zencore/xmake.lua4
-rw-r--r--src/zenserver/xmake.lua18
-rw-r--r--thirdparty/xmake.lua1
-rw-r--r--tsan.supp49
-rw-r--r--xmake.lua187
10 files changed, 269 insertions, 54 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 365f88822..36a211d47 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,6 +15,10 @@
- Improvement: Updated asio to 1.38.0
- Improvement: Updated fmt to 1.12.1
- Improvement: Fixed issue where oplog upload could create blocks larger than the max limit (64Mb)
+- Improvement: Add easy access options for sanitizers with `xmake config` and `xmake test` as options
+ - `--msan=[y|n]` Enable MemorySanitizer (Linux only, requires all deps instrumented)
+ - `--asan=[y|n]` Enable AddressSanitizer (disables mimalloc and sentry)
+ - `--tsan=[y|n]` Enable ThreadSanitizer (Linux/Mac only)
- Bugfix: Fixed sentry-native build to allow LTO on Windows
- Bugfix: Minor test stability fixes (flaky hash collisions, per-thread RNG seeds)
diff --git a/asan.supp b/asan.supp
new file mode 100644
index 000000000..e784c3fb8
--- /dev/null
+++ b/asan.supp
@@ -0,0 +1,18 @@
+# AddressSanitizer options for Zen
+#
+# IMPORTANT: There is no native "options from file" mechanism for ASAN on any
+# platform or compiler. The sanitizer.options rule in xmake.lua parses this
+# file and sets ASAN_OPTIONS directly. If you run a binary without xmake you
+# must set ASAN_OPTIONS manually, e.g.:
+# Linux/Mac: ASAN_OPTIONS="new_delete_type_mismatch=0" ./zenserver
+# Windows: set ASAN_OPTIONS=new_delete_type_mismatch=0
+#
+# Format:
+# key=value ASAN runtime option
+# # comment line (the whole line must start with #)
+# <blank> ignored
+
+# EASTL's allocator stores a size_t header in every allocation, but the
+# deallocation path passes the element size without the header. ASAN reports
+# this as a new/delete type mismatch. This is a false positive.
+new_delete_type_mismatch=0
diff --git a/msan.supp b/msan.supp
new file mode 100644
index 000000000..1131a7915
--- /dev/null
+++ b/msan.supp
@@ -0,0 +1,14 @@
+# MemorySanitizer options for Zen
+#
+# IMPORTANT: This file is NOT passed directly to the MSAN runtime. The
+# sanitizer.msan_options rule in xmake.lua parses it and constructs
+# MSAN_OPTIONS. If you run a binary directly (not via xmake run / xmake test)
+# you must set MSAN_OPTIONS manually.
+#
+# MSAN has no native suppression file mechanism; only key=value options are
+# supported here.
+#
+# Format:
+# key=value MSAN runtime option; xmake sets it directly in MSAN_OPTIONS
+# # comment line (the whole line must start with #)
+# <blank> ignored
diff --git a/scripts/test.lua b/scripts/test.lua
index df1218ce8..3c18225fb 100644
--- a/scripts/test.lua
+++ b/scripts/test.lua
@@ -86,11 +86,22 @@ function main()
arch = "x86_64"
end
+ local want_asan = option.get("asan") == true
+ local have_asan = config.get("asan") == true
+ local want_tsan = option.get("tsan") == true
+ local have_tsan = config.get("tsan") == true
+ local want_msan = option.get("msan") == true
+ local have_msan = config.get("msan") == true
+
-- Only reconfigure if current config doesn't already match
- if config.get("mode") ~= "debug" or config.get("plat") ~= plat or config.get("arch") ~= arch then
+ if config.get("mode") ~= "debug" or config.get("plat") ~= plat or config.get("arch") ~= arch
+ or want_asan ~= have_asan or want_tsan ~= have_tsan or want_msan ~= have_msan then
local toolchain_flag = config.get("toolchain") and ("--toolchain=" .. config.get("toolchain")) or ""
local sdk_flag = config.get("sdk") and ("--sdk=" .. config.get("sdk")) or ""
- os.exec("xmake config -y -c -m debug -p %s -a %s %s %s", plat, arch, toolchain_flag, sdk_flag)
+ local asan_flag = want_asan and "--asan=y" or ""
+ local tsan_flag = want_tsan and "--tsan=y" or ""
+ local msan_flag = want_msan and "--msan=y" or ""
+ os.exec("xmake config -y -c -m debug -p %s -a %s %s %s %s %s %s", plat, arch, toolchain_flag, sdk_flag, asan_flag, tsan_flag, msan_flag)
end
-- Build targets we're going to run
@@ -104,7 +115,7 @@ function main()
local use_junit_reporting = option.get("junit")
local use_noskip = option.get("noskip")
- local use_verbose = option.get("verbose")
+ local use_verbose = option.get("output")
local repeat_count = tonumber(option.get("repeat")) or 1
local extra_args = option.get("arguments") or {}
local junit_report_files = {}
diff --git a/src/zencore/include/zencore/memory/fmalloc.h b/src/zencore/include/zencore/memory/fmalloc.h
index 0c183cfd0..c50a9729c 100644
--- a/src/zencore/include/zencore/memory/fmalloc.h
+++ b/src/zencore/include/zencore/memory/fmalloc.h
@@ -9,6 +9,7 @@
// Detect if any sanitizers are enabled.
#if defined(__has_feature)
+// Clang: query each sanitizer individually
# if __has_feature(address_sanitizer)
# define ZEN_ADDRESS_SANITIZER 1
# endif
@@ -21,8 +22,14 @@
# if __has_feature(leak_sanitizer)
# define ZEN_LEAK_SANITIZER 1
# endif
-#elif defined(__SANITIZE_ADDRESS__) // For Windows
-# define ZEN_ADDRESS_SANITIZER 1
+#else
+// MSVC and GCC: check predefined macros set by the compiler when sanitizers are active
+# if defined(__SANITIZE_ADDRESS__) // MSVC (ASAN only), GCC
+# define ZEN_ADDRESS_SANITIZER 1
+# endif
+# if defined(__SANITIZE_THREAD__) // GCC
+# define ZEN_THREAD_SANITIZER 1
+# endif
#endif
#if !defined(ZEN_ADDRESS_SANITIZER)
diff --git a/src/zencore/xmake.lua b/src/zencore/xmake.lua
index 171f4c533..b08975df1 100644
--- a/src/zencore/xmake.lua
+++ b/src/zencore/xmake.lua
@@ -21,7 +21,7 @@ target('zencore')
add_deps("rpmalloc")
end
- if has_config("zenmimalloc") then
+ if has_config("zenmimalloc") and not use_asan then
add_packages("mimalloc")
end
@@ -47,7 +47,7 @@ target('zencore')
{public=true}
)
- if has_config("zensentry") then
+ if has_config("zensentry") and not use_asan then
add_packages("sentry-native")
if is_os("windows") then
diff --git a/src/zenserver/xmake.lua b/src/zenserver/xmake.lua
index 394745211..b619c5548 100644
--- a/src/zenserver/xmake.lua
+++ b/src/zenserver/xmake.lua
@@ -39,11 +39,11 @@ target("zenserver")
add_packages("oidctoken")
add_packages("nomad")
- if has_config("zenmimalloc") then
+ if has_config("zenmimalloc") and not use_asan then
add_packages("mimalloc")
end
- if has_config("zensentry") then
+ if has_config("zensentry") and not use_asan then
add_packages("sentry-native")
end
@@ -142,11 +142,19 @@ target("zenserver")
table.insert(args, "--detach=false")
+ -- On Windows the ASAN runtime is a DLL whose directory must be on PATH;
+ -- runenvs.make collects that (and any other Windows package run envs).
+ local addenvs, setenvs
+ if is_host("windows") then
+ import("private.action.run.runenvs")
+ addenvs, setenvs = runenvs.make(target)
+ end
+
-- debugging?
if option.get("debug") then
- debugger.run(targetfile, args, {curdir = rundir})
+ debugger.run(targetfile, args, {curdir = rundir, addenvs = addenvs, setenvs = setenvs})
else
- os.execv(targetfile, args, {curdir = rundir, detach = option.get("detach")})
+ os.execv(targetfile, args, {curdir = rundir, detach = option.get("detach"), addenvs = addenvs, setenvs = setenvs})
end
end)
@@ -177,7 +185,7 @@ target("zenserver")
end
end
- if has_config("zensentry") then
+ if has_config("zensentry") and not target:policy("build.sanitizer.address") then
local pkg = target:pkg("sentry-native")
if pkg then
local installdir = pkg:installdir()
diff --git a/thirdparty/xmake.lua b/thirdparty/xmake.lua
index 8bf4512db..196f184ae 100644
--- a/thirdparty/xmake.lua
+++ b/thirdparty/xmake.lua
@@ -38,6 +38,7 @@ target('rpmalloc')
set_languages("c17", "cxx20")
if is_os("windows") and not (get_config("toolchain") or ""):find("clang") then
add_cflags("/experimental:c11atomics", {force=true, tools="cl"})
+ add_cflags("/wd5105", {tools="cl"})
end
add_defines("RPMALLOC_FIRST_CLASS_HEAPS=1", "ENABLE_STATISTICS=1", "ENABLE_OVERRIDE=0")
add_files("rpmalloc/rpmalloc.c")
diff --git a/tsan.supp b/tsan.supp
index 93398da67..6dfe4e393 100644
--- a/tsan.supp
+++ b/tsan.supp
@@ -1,17 +1,42 @@
-# TSAN suppression / options file for zenserver
+# ThreadSanitizer options and suppression patterns for Zen
#
-# Usage:
-# TSAN_OPTIONS="detect_deadlocks=0 suppressions=$(pwd)/tsan.supp" ./zenserver
+# This is a mixed-format file:
#
-# NOTE: detect_deadlocks=0 is required because the GC's LockState() acquires shared
-# lock scopes on every named cache bucket (m_IndexLock) and every oplog
-# (GcReferenceLocker) simultaneously. With enough buckets/projects/oplogs this
-# easily exceeds TSAN's hard per-thread limit of 128 simultaneously-held locks
-# (all_locks_with_contexts_[128] in sanitizer_deadlock_detector.h:67), causing a
-# CHECK abort. This is a known TSAN limitation, not a real deadlock risk.
-# The long-term fix is to replace the N per-bucket shared-lock pattern in
-# ZenCacheStore::LockState / ProjectStore::LockState with a single coarser
-# "GC epoch" RwLock at the disk-layer / project-store level.
+# key=value TSAN runtime option; parsed by xmake's sanitizer.options
+# rule and set directly in TSAN_OPTIONS. When this file is also
+# passed as suppressions=<path>, the TSAN runtime silently skips
+# these lines (they don't match the type:pattern format).
+# race:... race condition suppression pattern
+# mutex:... mutex suppression pattern
+# signal:... signal suppression pattern
+# thread:... thread suppression pattern
+# # comment line
+# <blank> ignored
+#
+# xmake passes the file via TSAN_OPTIONS=suppressions=<path> when both the
+# compiler is Clang or GCC (not MSVC/clang-cl) and the platform is Linux or
+# macOS. This is handled by the sanitizer.options rule in xmake.lua.
+#
+# If you run a binary directly (not via xmake run / xmake test) set
+# TSAN_OPTIONS manually, e.g.:
+# TSAN_OPTIONS="detect_deadlocks=0:suppressions=$(pwd)/tsan.supp" ./zenserver
+
+# Required because GC's LockState() acquires shared lock scopes on every named
+# cache bucket (m_IndexLock) and every oplog (GcReferenceLocker) simultaneously.
+# With enough buckets/projects/oplogs this easily exceeds TSAN's hard per-thread
+# limit of 128 simultaneously-held locks, causing a CHECK abort. This is a known
+# TSAN limitation, not a real deadlock risk. The long-term fix is to replace the
+# N per-bucket shared-lock pattern in ZenCacheStore::LockState /
+# ProjectStore::LockState with a single coarser "GC epoch" RwLock.
+#
+# NOTE: this line will produce a TSAN warning along the lines of:
+# WARNING: failed to parse suppression 'detect_deadlocks=0'
+# This is expected and harmless. xmake's sanitizer.options rule parses
+# key=value lines from this file and injects them into TSAN_OPTIONS directly.
+# When the file is subsequently passed to the TSAN runtime as suppressions=<path>,
+# the runtime does not understand key=value syntax, prints the warning, and skips
+# the line. The option is already active via TSAN_OPTIONS; nothing is lost.
+detect_deadlocks=0
# EASTL's hashtable uses a global gpEmptyBucketArray[2] sentinel shared by all
# empty hash tables (mnBucketCount == 1). DoFreeNodes unconditionally writes NULL
diff --git a/xmake.lua b/xmake.lua
index b33d75444..2f5017fdd 100644
--- a/xmake.lua
+++ b/xmake.lua
@@ -19,6 +19,13 @@ end
set_allowedmodes("debug", "release")
add_rules("mode.debug", "mode.release")
+-- Disable xmake's run.autobuild (on by default in xmake 3.x). Auto-rebuild
+-- before run is dangerous: if the current config differs from the build that
+-- produced the binary (e.g. ASAN was toggled), xmake can rebuild with wrong
+-- flags, causing linker errors (e.g. MSVC annotate_string mismatch). Users
+-- are expected to run `xmake build` explicitly before `xmake run`.
+set_policy("run.autobuild", false)
+
if is_plat("windows") then
if false then
-- DLL runtime
@@ -37,44 +44,161 @@ if is_plat("windows") then
end
end
---------------------------------------------------------------------------
-- Sanitizers
--
-- https://xmake.io/api/description/builtin-policies.html#build-sanitizer-address
--
--- When using sanitizers, it may be necessary to change some configuration
--- options. In particular, you may want to use `--zensentry=no` to disable
--- Sentry support as it may not be compatible with some sanitizers. Also,
--- it may be necessary to disable mimalloc by using `--zenmimalloc=no`.
-
--- AddressSanitizer is supported on Windows (MSVC 2019+), Linux, and MacOS
+-- Each sanitizer has a .supp file (asan.supp, tsan.supp, msan.supp). A single
+-- sanitizer.options rule handles all three in one before_run callback.
+--
+-- asan.supp and msan.supp contain key=value runtime options (no native
+-- "options from file" mechanism exists for either sanitizer on any platform).
+--
+-- tsan.supp is a mixed-format file: key=value runtime options (parsed by xmake
+-- and set in TSAN_OPTIONS) plus native TSAN suppression patterns (race:...,
+-- mutex:...) that are passed via TSAN_OPTIONS=suppressions=<path> on Linux/Mac.
+-- The TSAN runtime silently skips key=value lines when parsing the suppression
+-- file (they don't match type:pattern; TSAN emits a verbose-level warning).
+--
+-- IMPORTANT: The env vars are set by xmake's before_run hook. If you run a
+-- binary directly (not via xmake run / xmake test) you must set them manually.
-local use_asan = false -- Automatically disables Sentry when set to true
-set_policy("build.sanitizer.address", use_asan)
+--------------------------------------------------------------------------
--- ThreadSanitizer, MemorySanitizer, LeakSanitizer, and UndefinedBehaviorSanitizer
--- are supported on Linux and MacOS only.
+-- AddressSanitizer is supported on Windows (MSVC 2019+), Linux, and MacOS.
+-- Enable with: `xmake config --asan=y` or `xmake test --asan`
+-- (automatically disables mimalloc and sentry, which are incompatible with ASAN)
--
--- You can enable these by editing the xmake.lua directly, or by passing the
--- appropriate flags on the command line:
+-- Options live in asan.supp. For manual runs:
+-- Linux/Mac: ASAN_OPTIONS="new_delete_type_mismatch=0" ./zenserver
+-- Windows: set ASAN_OPTIONS=new_delete_type_mismatch=0
+
+option("asan")
+ set_default(false)
+ set_showmenu(true)
+ set_description("Enable AddressSanitizer (disables mimalloc and sentry)")
+option_end()
+
+-- Global flag: read by sub-target xmake.lua files (like enable_unity)
+use_asan = has_config("asan")
+
+if use_asan then
+ set_policy("build.sanitizer.address", true)
+ -- On Windows/MSVC, explicitly define __SANITIZE_ADDRESS__ so that
+ -- IntelliSense in VS sees it (XmakeDefines -> PreprocessorDefinitions).
+ -- MSVC sets it implicitly at compile time via /fsanitize=address, but
+ -- vsxmake IntelliSense needs it in the explicit defines list.
+ -- Not needed on Clang/GCC: they set it implicitly and adding it via -D
+ -- would cause a macro-redefinition warning (error with -Werror).
+ if is_os("windows") then
+ add_defines("__SANITIZE_ADDRESS__=1")
+ end
+end
+
+--------------------------------------------------------------------------
+
+-- ThreadSanitizer is supported on Linux and MacOS only.
+-- Enable with: `xmake config --tsan=y` or `xmake test --tsan`
--
--- `xmake --policies=build.sanitizer.thread:y` for ThreadSanitizer,
--- `xmake --policies=build.sanitizer.memory:y` for MemorySanitizer, etc.
+-- Key=value options and suppression patterns both live in tsan.supp. For
+-- manual runs: TSAN_OPTIONS="detect_deadlocks=0:suppressions=$(pwd)/tsan.supp"
+
+option("tsan")
+ set_default(false)
+ set_showmenu(true)
+ set_description("Enable ThreadSanitizer (Linux/Mac only)")
+option_end()
+
+use_tsan = has_config("tsan")
+
+if use_tsan then
+ if is_os("windows") then
+ raise("TSAN is not supported on Windows. Use Linux or macOS.")
+ end
+ set_policy("build.sanitizer.thread", true)
+end
+
+--------------------------------------------------------------------------
--- When using TSAN you will want to also use the suppression tile to silence
--- known benign races. You do this by ensuring the the TSAN_OPTIONS environment
--- vriable is set to something like `TSAN_OPTIONS="suppressions=$(projectdir)/tsan.supp"`
+-- MemorySanitizer is supported on Linux only. In practice it rarely works
+-- because all dependencies must also be compiled with MSan instrumentation.
+-- Enable with: `xmake config --msan=y` or `xmake test --msan`
--
--- `prompt> TSAN_OPTIONS="detect_deadlocks=0 suppressions=$(projectdir)/tsan.supp" xmake run zenserver`
+-- Options live in msan.supp. For manual runs: set MSAN_OPTIONS manually.
+
+option("msan")
+ set_default(false)
+ set_showmenu(true)
+ set_description("Enable MemorySanitizer (Linux only, requires all deps instrumented)")
+option_end()
+
+use_msan = has_config("msan")
+
+if use_msan then
+ set_policy("build.sanitizer.memory", true)
+end
+
+--------------------------------------------------------------------------
+
+-- Single rule handles all three sanitizer env vars. parse_supp_opts is defined
+-- once inside before_run (scripting scope) where io is a valid global.
+
+rule("sanitizer.options")
+ before_run(function(target)
+ -- Parses key=value lines from a .supp file. Blank lines and lines whose
+ -- first non-whitespace character is '#' are ignored. Suppression-pattern
+ -- lines (race:..., mutex:...) don't match ^[%w_]+= and are skipped.
+ local function parse_supp_opts(filepath)
+ local opts = {}
+ local content = os.isfile(filepath) and io.readfile(filepath) or ""
+ for line in content:gmatch("[^\r\n]+") do
+ line = line:match("^%s*(.-)%s*$")
+ if line ~= "" and not line:match("^#") and line:match("^[%w_]+=") then
+ table.insert(opts, line)
+ end
+ end
+ return opts
+ end
+
+ if target:policy("build.sanitizer.address") and not os.getenv("ASAN_OPTIONS") then
+ local opts = parse_supp_opts(path.join(os.projectdir(), "asan.supp"))
+ if #opts > 0 then
+ os.setenv("ASAN_OPTIONS", table.concat(opts, ":"))
+ end
+ end
+
+ if target:policy("build.sanitizer.thread") and not os.getenv("TSAN_OPTIONS") then
+ local suppfile = path.join(os.projectdir(), "tsan.supp")
+ local opts = parse_supp_opts(suppfile)
+ -- Also pass the suppression patterns via the native file mechanism when
+ -- both conditions hold:
+ -- 1. Not clang-cl: file path support requires the LLVM/GCC TSAN runtime.
+ -- 2. Platform is Linux or macOS: TSAN is unavailable on Windows
+ -- (config-time raise() above prevents reaching here on Windows).
+ local tc_name = get_config("toolchain") or ""
+ local is_not_clang_cl = tc_name ~= "clang-cl"
+ if os.isfile(suppfile) and is_not_clang_cl
+ and (target:is_plat("linux") or target:is_plat("macosx")) then
+ table.insert(opts, "suppressions=" .. suppfile)
+ end
+ if #opts > 0 then
+ os.setenv("TSAN_OPTIONS", table.concat(opts, ":"))
+ end
+ end
+
+ if target:policy("build.sanitizer.memory") and not os.getenv("MSAN_OPTIONS") then
+ local opts = parse_supp_opts(path.join(os.projectdir(), "msan.supp"))
+ if #opts > 0 then
+ os.setenv("MSAN_OPTIONS", table.concat(opts, ":"))
+ end
+ end
+ end)
+rule_end()
+add_rules("sanitizer.options")
---set_policy("build.sanitizer.thread", true)
--set_policy("build.sanitizer.leak", true)
--set_policy("build.sanitizer.undefined", true)
--- In practice, this does not work because of the difficulty of compiling
--- dependencies with MemorySanitizer.
---set_policy("build.sanitizer.memory", true)
-
--------------------------------------------------------------------------
-- Dependencies
@@ -259,16 +383,16 @@ end
option("zensentry")
set_default(true)
set_showmenu(true)
- set_description("Enables Sentry support")
+ set_description("Enables Sentry support (incompatible with ASAN)")
option_end()
-add_define_by_config("ZEN_USE_SENTRY", "zensentry")
+add_defines("ZEN_USE_SENTRY=" .. ((has_config("zensentry") and not use_asan) and "1" or "0"))
option("zenmimalloc")
- set_default(not use_asan)
+ set_default(true)
set_showmenu(true)
- set_description("Use MiMalloc for faster memory management")
+ set_description("Use MiMalloc for faster memory management (incompatible with ASAN)")
option_end()
-add_define_by_config("ZEN_USE_MIMALLOC", "zenmimalloc")
+add_defines("ZEN_USE_MIMALLOC=" .. ((has_config("zenmimalloc") and not use_asan) and "1" or "0"))
option("zenrpmalloc")
set_default(true)
@@ -489,7 +613,10 @@ task("test")
{'j', "junit", "k", nil, "Enable junit report output"},
{'n', "noskip", "k", nil, "Run skipped tests (passes --no-skip to doctest)"},
{nil, "repeat", "kv", nil, "Repeat tests N times (stops on first failure)"},
- {'v', "verbose", "k", nil, "Route child process output to stdout (zenserver-test)"},
+ {'V', "output", "k", nil, "Route child process output to stdout (zenserver-test)"},
+ {nil, "asan", "k", nil, "Enable AddressSanitizer (disables mimalloc and sentry)"},
+ {nil, "tsan", "k", nil, "Enable ThreadSanitizer (Linux/Mac only)"},
+ {nil, "msan", "k", nil, "Enable MemorySanitizer (Linux only, requires all deps instrumented)"},
{nil, "arguments", "vs", nil, "Extra arguments passed to test runners (after --)"}
}
}