Which sanitizer should I use?
Decision guide for choosing between AddressSanitizer, UBSan, ThreadSanitizer, MemorySanitizer, and LeakSanitizer.
Quick answer
| I want to find... | Use |
|---|---|
| Memory corruption (overflow, use-after-free) | ASan |
| Undefined behavior (overflow, null deref) | UBSan |
| Data races between threads | TSan |
| Uninitialized memory reads | MSan |
| Memory leaks | LSan (or ASan, which includes it) |
| Memory corruption and UB together | ASan + UBSan (compatible) |
Decision flow
Is the bug related to threads / concurrency?
→ Yes → ThreadSanitizer (-fsanitize=thread)
→ No → continue
Is the bug related to reading garbage / uninitialized values?
→ Yes → MemorySanitizer (-fsanitize=memory, Clang only)
→ No → continue
Are you seeing crashes, corruption, or wrong data from pointer operations?
→ Yes → AddressSanitizer (-fsanitize=address)
→ No → continue
Are you chasing subtle logic bugs, integer overflow, or UB?
→ Yes → UBSan (-fsanitize=undefined)
→ No → continue
Are you just checking for memory leaks?
→ Yes → LeakSanitizer (-fsanitize=leak)
AddressSanitizer (ASan)
Detects: heap overflow, stack overflow, use-after-free, use-after-return, double-free, heap use-after-scope, memory leaks (via LSan).
Overhead: ~2× memory, ~2× speed — usable in CI.
Compatibility: GCC + Clang. Incompatible with TSan.
clang++ -fsanitize=address -g -O1 -o app main.cpp
./app
# Reports: exact file/line/allocation stack for every errorWhen to run: Always in debug builds and CI. If you're not running ASan, you have undetected memory bugs.
UndefinedBehaviorSanitizer (UBSan)
Detects: signed integer overflow, null pointer dereference, misaligned access, invalid enum, out-of-bounds (static arrays), type-confusion casts, VLA overflows.
Overhead: Minimal (~5-10% speed).
Best practice: Combine with ASan — they're fully compatible:
clang++ -fsanitize=address,undefined -fno-sanitize-recover=all -g -O1 -o app main.cpp-fno-sanitize-recover=all makes UBSan abort instead of continuing — surfaces bugs instead of silently continuing past them.
ThreadSanitizer (TSan)
Detects: data races (concurrent read+write without synchronization), lock ordering issues, use-after-free in multi-threaded code.
Overhead: ~5-15× memory, ~5-15× speed — run in dedicated CI job.
Important: Incompatible with ASan and MSan. Run in a separate job.
clang++ -fsanitize=thread -g -O1 -o app main.cppWhen to run: Any time you add or modify concurrency code.
MemorySanitizer (MSan)
Detects: reads from uninitialized memory (stack variables, malloc'd memory).
Limitation (important): All linked code must be MSan-instrumented. Uninstrumented glibc/libstdc++ calls produce false positives. Requires building libc++ and libstdc++ with MSan, or using suppressions.
Overhead: ~3× memory, ~3× speed.
Clang only. GCC does not support MSan.
clang++ -fsanitize=memory -fPIE -pie -g -O1 -o app main.cppUse MSan when ASan and UBSan pass but you still have mysterious wrong-value bugs.
LeakSanitizer (LSan)
Detects: memory leaks at program exit.
Already included in ASan — runs automatically when program exits cleanly. Run standalone for faster check-only mode:
clang++ -fsanitize=leak -g -o app main.cpp
LSAN_OPTIONS=report_objects=1 ./appRunning in CI
Run sanitizers in separate jobs — they're mutually exclusive except ASan+UBSan:
# .github/workflows/sanitizers.yml
jobs:
asan-ubsan:
steps:
- run: cmake -B build -DCMAKE_CXX_FLAGS="-fsanitize=address,undefined -fno-sanitize-recover=all"
- run: cmake --build build && ctest --test-dir build
tsan:
steps:
- run: cmake -B build -DCMAKE_CXX_FLAGS="-fsanitize=thread"
- run: cmake --build build && ctest --test-dir buildSanitizer compatibility matrix
| ASan | UBSan | TSan | MSan | LSan | |
|---|---|---|---|---|---|
| ASan | — | ✅ | ❌ | ❌ | ✅ (built-in) |
| UBSan | ✅ | — | ⚠️ limited | ❌ | ✅ |
| TSan | ❌ | ⚠️ limited | — | ❌ | ❌ |
| MSan | ❌ | ❌ | ❌ | — | ❌ |
CMake sanitizer presets
The cleanest way to enable sanitizers without polluting your main build: use CMake presets or a dedicated toolchain file.
// CMakePresets.json
{
"version": 3,
"configurePresets": [
{
"name": "asan",
"inherits": "debug",
"cacheVariables": {
"CMAKE_CXX_FLAGS": "-fsanitize=address,undefined -fno-sanitize-recover=all -g -O1 -fno-omit-frame-pointer"
}
},
{
"name": "tsan",
"inherits": "debug",
"cacheVariables": {
"CMAKE_CXX_FLAGS": "-fsanitize=thread -g -O1"
}
},
{
"name": "msan",
"inherits": "debug",
"cacheVariables": {
"CMAKE_CXX_FLAGS": "-fsanitize=memory -fPIE -pie -g -O1",
"CMAKE_EXE_LINKER_FLAGS": "-pie"
}
}
]
}cmake --preset asan && cmake --build build/asan && ctest --test-dir build/asanOr via a simple CMake variable:
# In CMakeLists.txt — controlled from command line
option(ENABLE_ASAN "Enable AddressSanitizer" OFF)
option(ENABLE_TSAN "Enable ThreadSanitizer" OFF)
if(ENABLE_ASAN)
target_compile_options(myapp PRIVATE -fsanitize=address,undefined
-fno-sanitize-recover=all -fno-omit-frame-pointer)
target_link_options(myapp PRIVATE -fsanitize=address,undefined)
endif()cmake -B build -DENABLE_ASAN=ON && cmake --build buildRuntime options — tuning sanitizer behavior
Sanitizers are controlled at runtime via environment variables:
# ASan: show full allocation stacks, abort on first error
ASAN_OPTIONS=abort_on_error=1:halt_on_error=1:print_stats=1 ./myapp
# ASan: detect ODR violations (duplicate symbol definitions)
ASAN_OPTIONS=detect_odr_violation=2 ./myapp
# UBSan: print backtrace, abort instead of continuing
UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 ./myapp
# TSan: show full race report
TSAN_OPTIONS=second_deadlock_stack=1:history_size=7 ./myapp
# LSan standalone (when run without ASan)
LSAN_OPTIONS=report_objects=1:max_leaks=100 ./myappReading ASan output
==12345==ERROR: AddressSanitizer: heap-use-after-free on address 0x602000000010 at pc ...
READ of size 4 at 0x602000000010 thread T0
#0 0x40169a in process_item src/processor.cpp:42
#1 0x4017bb in run_loop src/main.cpp:87
0x602000000010 is located 0 bytes inside of 4-byte region [0x602000000010,0x602000000014)
freed by thread T0 here:
#0 in operator delete src/processor.cpp:30
previously allocated by thread T0 here:
#0 in operator new src/processor.cpp:18How to read it:
- First block: where the bad access happened (file + line)
- Second block: where the freed memory came from
- Third block: where it was originally allocated
This gives you the complete lifetime: allocate → free → bad read/write. The three stack traces together pinpoint the bug immediately.
Common findings by sanitizer
| Sanitizer | What it usually finds |
|---|---|
| ASan | Accessing vector element after push_back invalidated iterators; returning pointer to local variable; double-delete in custom allocator |
| UBSan | Signed integer overflow in loop index (int i = INT_MAX; i++); calling virtual method on null pointer; accessing union member after storing different type |
| TSan | Two threads reading/writing a std::vector without a lock; incrementing a shared counter without atomic; publisher-subscriber with unsynchronized flag |
| MSan | Using struct member before zeroing the struct; reading from a malloc'd buffer before writing to it |