diff options
| -rw-r--r-- | CHANGELOG.md | 4 | ||||
| -rw-r--r-- | asan.supp | 18 | ||||
| -rw-r--r-- | msan.supp | 14 | ||||
| -rw-r--r-- | scripts/test.lua | 17 | ||||
| -rw-r--r-- | src/zencore/include/zencore/memory/fmalloc.h | 11 | ||||
| -rw-r--r-- | src/zencore/xmake.lua | 4 | ||||
| -rw-r--r-- | src/zenserver/xmake.lua | 18 | ||||
| -rw-r--r-- | thirdparty/xmake.lua | 1 | ||||
| -rw-r--r-- | tsan.supp | 49 | ||||
| -rw-r--r-- | xmake.lua | 187 |
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") @@ -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 @@ -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 --)"} } } |