Join our Newsletter — 33% off our NHI Course
Home› Guides› Sanitisers Guide: ASan, UBSan, MSan and TSan in…
Guide Application Security

Sanitisers Guide: ASan, UBSan, MSan and TSan in Testing and CI

← All guides
By NHI Mgmt Group, edited by Lalit Choda Updated 27 October 2026 13 min read
On this page

Memory corruption, undefined behaviour and data races in C and C++ code often run silently in tests and only surface as crashes or exploitable bugs in production. Compiler sanitisers are runtime instrumentation that makes these bugs fail loudly, with a stack trace, at the moment they happen. This guide explains what AddressSanitizer (ASan), UndefinedBehaviorSanitizer (UBSan), MemorySanitizer (MSan) and ThreadSanitizer (TSan) each detect, what they cost, which can be combined, how to run them in CI and with fuzzing, how they apply to unsafe Rust, and how to triage what they find. It is written for C, C++ and Rust developers, build engineers and the application security teams who set test requirements for native code.

Key takeaways

  • Each sanitiser finds a different bug class. ASan for out-of-bounds and use-after-free, UBSan for undefined behaviour such as signed overflow and bad shifts, MSan for reads of uninitialised memory, TSan for data races.
  • They are test tools, not hardening. Clang states that ASan and TSan are not meant for production executables; for UBSan in security-sensitive production builds it recommends the minimal runtime or trap mode instead of the full runtime.
  • Not every combination is allowed. GCC documents that ASan cannot be combined with TSan or HWASan, and TSan cannot be combined with ASan or LeakSanitizer, so plan separate build variants.
  • Sanitisers only see executed code. Their value depends on test and fuzzing coverage, which is why OSS-Fuzz builds fuzz targets with ASan and UBSan by default.
  • MSan and TSan need everything instrumented. Uninstrumented libraries cause false reports or missed races, so budget for rebuilding dependencies.

What sanitisers are and where they fit

A sanitiser is a compiler pass plus a runtime library. The compiler inserts checks around memory accesses, arithmetic or synchronisation, and the runtime keeps shadow state (for example which bytes are addressable or initialised) and prints a report when a check fails. Because detection happens at the exact faulting instruction, a sanitiser report points at the bug rather than at a crash many calls later.

Sanitisers sit between static analysis and production hardening. Static analysis can flag possible flaws without running code but produces false positives; sanitisers have very few false positives for the bugs they cover but only see the paths that tests exercise. Production hardening such as _FORTIFY_SOURCE, stack protectors and control flow integrity runs in shipped binaries and reduces exploitability. See the Compiler and Toolchain Hardening Guide for those options and the C and C++ Secure Coding Guide for the coding practices that prevent these bugs in the first place.

The four main sanitisers compared

SanitiserFlag (Clang, GCC)DetectsDocumented cost (Clang)Key constraint
AddressSanitizer-fsanitize=addressOut-of-bounds access to heap, stack and globals; use-after-free; use-after-return; use-after-scope; double and invalid free; leaks (via LeakSanitizer)Typical slowdown 2x; more real memory, stack use up to 3x; maps 16+ TB of virtual address space on 64-bitNo static linking of executables; not for production
UndefinedBehaviorSanitizer-fsanitize=undefined plus named checksSigned integer overflow, invalid shifts, null use, out-of-bounds indexing with static bounds, misalignment, wrong dynamic type (vptr), wrong function pointer typeClang describes the checks as small runtime cost with no impact on address space layout or ABIFull runtime is for testing; use minimal runtime or trap mode in production
MemorySanitizer-fsanitize=memory (Clang)Use of uninitialised memory in branches, pointer use and across calls; use after destructionTypical slowdown 3x; 2x memory, 3x with origin tracking; origin tracking adds 1.5x to 2x slowdownAll code must be instrumented, including dependencies; Linux, NetBSD and FreeBSD
ThreadSanitizer-fsanitize=threadData races; also lock inversion, thread leaks and mutex misuseTypical slowdown about 5x to 15x; memory about 5x to 10x64-bit only in practice; implies PIE; no static libc or libstdc++; Clang marks it beta

Related tools fill gaps. HWAddressSanitizer (-fsanitize=hwaddress) uses pointer tag bits to detect memory errors with lower overhead and GCC documents it mainly for AArch64. LeakSanitizer is built into ASan on Linux and can run standalone. On Windows, MSVC supports AddressSanitizer with /fsanitize=address from Visual Studio 2019 version 16.9 for x86 and x64 on Windows 10 and later; Microsoft lists thread, leak, memory, undefined and hwaddress sanitisers only as possible future additions, and states that ASan should not be used in production.

What each sanitiser maps to

Mapping sanitisers to weakness classes helps decide which variants a project needs. Memory-unsafe code that parses untrusted input needs ASan and UBSan at minimum; multi-threaded services need TSan; code that handles secrets or builds network messages from structs benefits from MSan, because uninitialised bytes can leak memory contents.

Weakness class (CWE)Primary sanitiserNotes
Out-of-bounds write and read (CWE-787, CWE-125)ASanHeap, stack and global redzones; UBSan bounds covers statically known array bounds
Use after free (CWE-416), double free (CWE-415)ASanFreed memory is quarantined so later access is caught
Integer overflow or wraparound (CWE-190)UBSansigned-integer-overflow is in the undefined group; unsigned wraparound needs -fsanitize=unsigned-integer-overflow or the integer group and is often intended behaviour
NULL pointer dereference (CWE-476)UBSan null; ASanUBSan also flags creating a null reference
Use of uninitialised variable (CWE-457)MSanReports only when the value affects behaviour, not on every copy
Race condition (CWE-362)TSanOnly races on executed interleavings; TSan has an optional adaptive delay mode to expose more
Memory leak (CWE-401)LeakSanitizerOn by default with ASan on Linux; matters for long-running services and denial of service

Build variants and combinations

Because some sanitisers cannot share a build, most projects run a small matrix of build variants. GCC states that -fsanitize=address cannot be combined with -fsanitize=thread or -fsanitize=hwaddress, and that -fsanitize=thread cannot be combined with -fsanitize=address or -fsanitize=leak. MSan is a Clang feature and needs its own fully instrumented build. ASan and UBSan combine well and are the usual default pair.

VariantFlagsWhen to run
ASan plus UBSan-fsanitize=address,undefined -fno-omit-frame-pointer -g -O1Every pull request: unit and integration tests, and fuzzing
TSan-fsanitize=thread -g -O1Nightly or on changes to concurrent code; long-running stress tests
MSan-fsanitize=memory -fsanitize-memory-track-origins -g -O1 with instrumented dependenciesNightly or weekly once dependencies are rebuilt; fuzzing of parsers
Production UBSan subset-fsanitize=<chosen checks> -fsanitize-trap=all or -fsanitize-minimal-runtimeShipped builds where a trap on undefined behaviour is preferable to continuing
  • Optimisation level: Clang recommends -O1 or higher for reasonable performance, -fno-omit-frame-pointer for better stack traces with ASan, and -g for file and line information.
  • Link with the compiler driver: Clang notes that the final link step must use clang, not ld, so the runtime is linked.
  • Fail fast in CI: by default UBSan reports and continues. Use -fno-sanitize-recover=... for the checks you gate on, so a report fails the test run. GCC documents that the runtime default halt_on_error is 1 for ASan and 0 for TSan and UBSan.
  • Check the feature in code: __has_feature(address_sanitizer) lets test code adapt, for example to skip a test that deliberately exhausts memory.
  • Windows specifics: MSVC ASan is not compatible with edit-and-continue (/ZI), incremental linking or /RTC, and Microsoft notes /ZI is enabled by default for new CMake projects.

Runtime options and reports

Each runtime reads an environment variable: ASAN_OPTIONS, UBSAN_OPTIONS, MSAN_OPTIONS, TSAN_OPTIONS and LSAN_OPTIONS. Setting help=1 lists the available flags. Options worth standardising in CI:

  • ASan: detect_leaks=1 (default on Linux, opt-in on macOS), detect_stack_use_after_return=1 (already enabled on Linux per Clang), and check_initialization_order=1 for C++ static initialisation order bugs (not supported on macOS).
  • UBSan: print_stacktrace=1 for symbolised traces, which Clang notes needs -g, -fno-sanitize-merge, -fno-omit-frame-pointer and llvm-symbolizer on the path; log_path to write reports to files your CI collects.
  • TSan: halt_on_error (default false), exitcode (default 66), history_size to keep more access history for clearer reports, and detect_deadlocks (default true).
  • Symbolisation: keep llvm-symbolizer available in the test image; unsymbolised reports slow triage badly.

Sanitisers and fuzzing

Fuzzing generates the inputs; sanitisers turn silent corruption into a crash the fuzzer can record. OSS-Fuzz, Google's continuous fuzzing service for open source, lets a project list address, memory and undefined in its project.yaml, and uses ASan and UBSan by default. Its documentation recommends MSan but does not enable it by default because uninstrumented system dependencies are likely to cause false positives; projects that opt in must build all their libraries with MemorySanitizer flags, and can mark a sanitiser as experimental while they fix build problems.

  1. Write fuzz targets for every parser of untrusted input: file formats, network protocols, configuration and deserialisation code.
  2. Build each target with ASan plus UBSan first, then add MSan once dependencies are instrumented.
  3. Keep a regression corpus: every crashing input becomes a unit test run under the same sanitiser in CI.
  4. Deduplicate by stack: group crashes by the top frames of the sanitiser report before filing bugs.

See the Fuzzing Guide for harness design, coverage and corpus management.

Sanitisers in Rust

Safe Rust prevents most memory errors at compile time, but unsafe blocks, FFI calls into C and C++ and some concurrency primitives can still introduce them. The Rust Unstable Book documents sanitiser support behind the unstable -Zsanitizer flag, so it needs a nightly toolchain. It separates sanitisers intended for testing or fuzzing (AddressSanitizer, HWAddressSanitizer, LeakSanitizer, MemorySanitizer, RealtimeSanitizer, ThreadSanitizer) from those that may also be used in production (ControlFlowIntegrity, KernelControlFlowIntegrity, MemTagSanitizer, SafeStack, ShadowCallStack and DataFlowSanitizer). Its examples build with RUSTFLAGS=-Zsanitizer=address and cargo build -Zbuild-std --target x86_64-unknown-linux-gnu, and each sanitiser lists its supported targets. MemorySanitizer in Rust has the same rule as in C: C and C++ dependencies must be rebuilt with -fsanitize=memory or reports will be false positives. See the Rust Security Guide and the Memory-Safe Languages Guide.

Triage, suppressions and ignore lists

A sanitiser report should be treated as a real bug until shown otherwise. False reports exist but mostly come from configuration: uninstrumented code under MSan or TSan, custom allocators ASan does not understand, or synchronisation TSan cannot see. Each runtime provides scoped ways to silence what is genuinely not a bug.

  • Runtime suppression files: ASan supports suppressions only for external libraries (types such as interceptor_via_lib and odr_violation); UBSan suppressions name a check and a location and only apply to recoverable checks; TSan supports types such as race, race_top, mutex, thread, deadlock and called_from_lib; leak suppressions go in LSAN_OPTIONS.
  • Function attributes: __attribute__((no_sanitize("address"))) or a specific check such as no_sanitize("signed-integer-overflow") removes instrumentation from one function.
  • Ignore lists: the sanitiser special case list can exclude source files and functions at compile time. Clang warns that for TSan, ignored code is not instrumented at all, which can itself cause false positives and missing stack frames, unlike no_sanitize("thread"), which keeps some instrumentation.
  • Govern suppressions like code: every entry needs a comment with the reason and an owner, and the suppression files should be reviewed when dependencies change. A suppression added to make CI green is a recorded, unfixed bug.

When a report is real, classify it by the sanitiser's bug type and attacker reachability, then set severity using your bug bar: a heap overflow reachable from network input is a different class from a signed overflow in a test utility. See the Vulnerability Severity and Bug Bar Guide for Development Teams.

Rolling sanitisers out

  1. Pilot on one component: pick a library that parses untrusted input, add the ASan plus UBSan variant and run the existing tests. Expect a first wave of real bugs; fix them before widening.
  2. Report only, then gate: run the variant as a non-blocking CI job for two or three weeks, triage everything it finds, then make it blocking for new reports.
  3. Widen by component: add the variant to each native component in turn, starting with internet-facing and privileged code.
  4. Add TSan and MSan on a schedule: nightly jobs first, with an owner for each, because their reports take longer to triage.
  5. Roll back safely: if a variant blocks releases because of a toolchain problem, switch that job back to non-blocking with a dated ticket rather than deleting it.

Measuring sanitiser coverage

  • Variant coverage: share of native components with ASan plus UBSan in pull request CI, and with TSan and MSan on a schedule.
  • Executed code coverage under sanitisers: line or edge coverage of the sanitised test and fuzz runs, because unexecuted code is unchecked code.
  • Open sanitiser findings by type and age: heap-buffer-overflow and use-after-free reports should be fixed within the bug bar deadline for memory corruption.
  • Suppression count and age: a growing suppression file is a growing backlog.
  • Escapes: memory safety bugs found in production or by external researchers in code paths that sanitised tests already covered point to gaps in inputs or options.

Framework mapping

FrameworkReferenceRelevance
NIST SP 800-53 Rev. 5SA-11(8) Dynamic Code AnalysisSanitised test and fuzz runs are dynamic analysis of the developer's code
NIST SP 800-53 Rev. 5SA-11 Developer Testing and Evaluation; SA-15 Development Process, Standards, and ToolsSanitiser build variants and CI gates as defined test tooling
NIST SP 800-53 Rev. 5SI-16 Memory ProtectionProduction hardening complements, but is not replaced by, sanitiser testing
NIST CSF 2.0PR.PS-06Secure software development practices integrated and monitored
MITRE CWECWE-787, CWE-125, CWE-416, CWE-190, CWE-457, CWE-362Weakness classes sanitisers detect at run time

Common failure patterns

  • Shipping sanitiser builds: ASan and TSan runtimes are not designed for security-sensitive production use, and Microsoft and Clang both say so. Keep sanitised builds in test.
  • Running sanitisers on thin tests: a clean ASan run over unit tests that never touch the parser proves little. Pair sanitisers with fuzzing of untrusted input paths.
  • Letting UBSan recover silently: reports scroll past in logs while the job passes. Gate with -fno-sanitize-recover on the checks that matter.
  • MSan without instrumented dependencies: teams see false reports from libraries, conclude MSan is noisy and drop it. Rebuild dependencies first or leave MSan out.
  • Blanket ignore lists: excluding whole directories to quieten TSan or ASan hides real bugs in exactly the hardest code.
  • One variant only: running ASan alone misses races and uninitialised reads; the cheap fix is a nightly TSan job.

Practitioner checklist

  • Add an ASan plus UBSan build variant to pull request CI for all C, C++ and unsafe Rust components.
  • Add scheduled TSan runs for concurrent code and MSan runs once dependencies are instrumented.
  • Build with -O1 -g -fno-omit-frame-pointer and keep llvm-symbolizer in test images.
  • Make sanitiser reports fail the build with -fno-sanitize-recover and halt_on_error where appropriate.
  • Fuzz every parser of untrusted input under sanitisers and keep crashing inputs as regression tests.
  • Keep sanitiser runtimes out of release builds; use UBSan trap mode or the minimal runtime only for chosen checks.
  • Require a reason and owner for every suppression and ignore list entry, and review them on dependency updates.
  • Track sanitised coverage, open findings by type and age, and suppression growth.

Standards and references

Related NHI Mgmt Group resources: Fuzzing Guide · C and C++ Secure Coding Guide · Compiler and Toolchain Hardening Guide · Memory-Safe Languages Guide

Free weekly newsletter

Subscribe to the NHI & AI Identity Journal

The latest on NHI and Agentic AI security – articles, research, breaches, news and events every week.

Bonus 33% off our NHI Course when you subscribe.

NHIMG Editorial Note
Researched and written by the NHI Mgmt Group editorial team using AI tools, under the editorial direction of Lalit Choda. Facts are drawn from the sources listed on this page and checked by our automated quality process. Last updated 27 October 2026. Spotted an error? Use our contact page.
NHI Mgmt Group, the independent authority on Non-Human Identity, IAM and Agentic AI security. nhimg.org