Skip to content
C++

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 threadsTSan
Uninitialized memory readsMSan
Memory leaksLSan (or ASan, which includes it)
Memory corruption and UB togetherASan + 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.

bash
clang++ -fsanitize=address -g -O1 -o app main.cpp
./app
# Reports: exact file/line/allocation stack for every error

When 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:

bash
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.

bash
clang++ -fsanitize=thread -g -O1 -o app main.cpp

When 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.

bash
clang++ -fsanitize=memory -fPIE -pie -g -O1 -o app main.cpp

Use 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:

bash
clang++ -fsanitize=leak -g -o app main.cpp
LSAN_OPTIONS=report_objects=1 ./app

Running in CI

Run sanitizers in separate jobs — they're mutually exclusive except ASan+UBSan:

yaml
# .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 build

Sanitizer compatibility matrix

ASanUBSanTSanMSanLSan
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.

json
// 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"
      }
    }
  ]
}
bash
cmake --preset asan && cmake --build build/asan && ctest --test-dir build/asan

Or via a simple CMake variable:

cmake
# 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()
bash
cmake -B build -DENABLE_ASAN=ON && cmake --build build

Runtime options — tuning sanitizer behavior

Sanitizers are controlled at runtime via environment variables:

bash
# 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 ./myapp

Reading ASan output

cpp
==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:18

How 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

SanitizerWhat it usually finds
ASanAccessing vector element after push_back invalidated iterators; returning pointer to local variable; double-delete in custom allocator
UBSanSigned integer overflow in loop index (int i = INT_MAX; i++); calling virtual method on null pointer; accessing union member after storing different type
TSanTwo threads reading/writing a std::vector without a lock; incrementing a shared counter without atomic; publisher-subscriber with unsynchronized flag
MSanUsing struct member before zeroing the struct; reading from a malloc'd buffer before writing to it