# ThreadSanitizer options and suppression patterns for Zen # # This is a mixed-format file: # # 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=, 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 # ignored # # xmake passes the file via TSAN_OPTIONS=suppressions= 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=, # 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 # to each bucket slot, including this shared global. Multiple threads concurrently # destroying empty EASTL hash_maps all write NULL to gpEmptyBucketArray[0], which # TSAN reports as a race. This is benign: the slot is always NULL and writing NULL # to it has no observable effect. race:eastl::hashtable*DoFreeNodes* # UE::Trace's GetUid() uses a racy static uint32 cache (Uid = Uid ? Uid : Initialize()) # as a fast path to avoid re-entering Initialize(). The actual initialization is done via # a thread-safe static (Uid_ThreadSafeInit) inside Initialize(), so the worst case is # redundant calls to Initialize() which always returns the same value. race:*Fields::GetUid* # TRACE_CPU_SCOPE generates a function-local `static int32 scope_id` that is lazily # initialized without synchronization (if (0 == scope_id) scope_id = ScopeNew(...)). # Same benign pattern as GetUid: the worst case is redundant calls to ScopeNew() which # always returns the same value for a given scope name. race:*$trace_scope_id*