Sanitizers are compiler-based tools that instrument your code at compile time and detect bugs at runtime. They are the single most effective bug-finding tool for C++ code — faster and more comprehensive than Valgrind, and used by every major C++ project (Chrome, LLVM, Firefox, Linux kernel test suites).
Key insight: Sanitizers catch bugs at RUNTIME. You need tests that exercise the code paths. A sanitizer with no test coverage catches nothing.
| Sanitizer | Flag | Catches | Overhead | GCC | Clang |
|---|---|---|---|---|---|
| ASan (AddressSanitizer) | -fsanitize=address |
Memory errors | ~2x | ✓ | ✓ |
| UBSan (UndefinedBehaviorSanitizer) | -fsanitize=undefined |
Undefined behavior | ~minimal | ✓ | ✓ |
| TSan (ThreadSanitizer) | -fsanitize=thread |
Data races | ~5-15x | ✓ | ✓ |
| MSan (MemorySanitizer) | -fsanitize=memory |
Uninitialized reads | ~3x | ✗ | ✓ |
| LSan (LeakSanitizer) | Included in ASan | Memory leaks | ~minimal | ✓ | ✓ |
Critical constraint: ASan and TSan cannot be combined (different runtimes). Run them as separate CI jobs.
This is fundamentally different from Valgrind, which uses binary translation (no recompilation needed, but 10-50x slower).
malloc‘d / new‘d bufferdelete / freefree/delete on the same pointer twiceg++ -std=c++2a -fsanitize=address -fno-omit-frame-pointer -g -O1 source.cpp -o binary
-fno-omit-frame-pointer: Ensures accurate stack traces-g: Debug info for line numbers in reports-O1: Recommended optimization level (catches more than -O0, less noise than -O2)export ASAN_OPTIONS="detect_stack_use_after_return=1:detect_leaks=1:halt_on_error=0"
Key options:
- detect_stack_use_after_return=1 — enables use-after-return detection (extra overhead)
- detect_leaks=1 — enable leak checking (default on Linux)
- halt_on_error=0 — continue after first error (find multiple bugs in one run)
- malloc_context_size=30 — deeper stack traces for allocations
- suppressions=/path/to/asan.supp — suppress known false positives
ASan maps every 8 bytes of application memory to 1 byte of “shadow memory”.
The shadow byte encodes which of the 8 bytes are accessible. Each malloc adds
“redzones” around the allocation (poisoned shadow bytes). Each free poisons
the entire region and adds it to a quarantine (delayed reuse).
INT_MAX + 1nullptr__builtin_unreachable()g++ -std=c++2a -fsanitize=undefined -g source.cpp -o binary
-fsanitize=signed-integer-overflow
-fsanitize=null
-fsanitize=alignment
-fsanitize=shift
-fsanitize=float-cast-overflow
-fsanitize=vla-bound
export UBSAN_OPTIONS="print_stacktrace=1:halt_on_error=1"
print_stacktrace=1 — include stack trace in error reportshalt_on_error=1 — abort on first UB (default is to continue)UBSan can be combined with ASan:
g++ -fsanitize=address,undefined -fno-omit-frame-pointer -g source.cpp
g++ -std=c++2a -fsanitize=thread -g -O1 source.cpp -o binary -pthread
IMPORTANT: Must compile ALL source files with -fsanitize=thread, including libraries.
Mixing instrumented and non-instrumented code produces false positives.
export TSAN_OPTIONS="second_deadlock_stack=1:halt_on_error=0:history_size=7"
second_deadlock_stack=1 — show both stacks in deadlock reportshalt_on_error=0 — continue after finding a racehistory_size=7 — more memory for race detection (0-7, higher = more memory)suppressions=/path/to/tsan.supp — suppress known races# tsan.supp
race:third_party_lib::SomeFunction
race:legacy_module.cpp
deadlock:known_safe_lock_pattern
MSan detects use of uninitialized memory. Not available in GCC.
clang++ -fsanitize=memory -fno-omit-frame-pointer -g source.cpp -o binary
MSan requires ALL linked libraries to be instrumented. This makes it the hardest sanitizer to deploy (you need to build libc++, etc. with MSan).
LSan is integrated into ASan and runs at program exit:
g++ -fsanitize=address source.cpp -o binary
ASAN_OPTIONS=detect_leaks=1 ./binary
Or standalone (Clang only):
clang++ -fsanitize=leak source.cpp -o binary
When you have false positives or third-party code you can’t fix:
ASan suppression file (asan.supp):
interceptor_via_fun:third_party_function
leak:ThirdPartyLib::Allocate
TSan suppression file (tsan.supp):
race:ThirdPartyLib*
mutex:AnnotateMutex
UBSan suppression file — use function attribute instead:
__attribute__((no_sanitize("undefined")))
void legacy_function_with_known_ub() { ... }
Apply at runtime:
ASAN_OPTIONS=suppressions=asan.supp ./binary
TSAN_OPTIONS=suppressions=tsan.supp ./binary
Best practice: run each sanitizer as a separate CI job:
# .github/workflows/sanitizers.yml
jobs:
asan:
runs-on: ubuntu-latest
steps:
- run: cmake -DCMAKE_CXX_FLAGS="-fsanitize=address -fno-omit-frame-pointer" ..
- run: make && ctest
ubsan:
runs-on: ubuntu-latest
steps:
- run: cmake -DCMAKE_CXX_FLAGS="-fsanitize=undefined" ..
- run: make && ctest
tsan:
runs-on: ubuntu-latest
steps:
- run: cmake -DCMAKE_CXX_FLAGS="-fsanitize=thread" ..
- run: make && ctest
| Feature | Sanitizers | Valgrind |
|---|---|---|
| Speed | 2-15x overhead | 10-50x overhead |
| Recompilation needed | Yes | No |
| Memory errors | ASan | Memcheck |
| Thread errors | TSan | Helgrind/DRD |
| UB detection | UBSan | Limited |
| Uninitialized reads | MSan (Clang) | Memcheck |
| Platform | Linux, macOS, others | Linux primarily |
| False positives | Very rare | Occasional |
| CI suitability | Excellent (fast) | Poor (slow) |
Rule of thumb: Use sanitizers for CI, Valgrind for one-off deep analysis of existing binaries you can’t recompile.
Running ROS nodes under sanitizers:
# Build your ROS package with sanitizers
catkin_make -DCMAKE_CXX_FLAGS="-fsanitize=address -fno-omit-frame-pointer -g"
# Run a node with ASan
ASAN_OPTIONS="detect_leaks=0:halt_on_error=0" rosrun my_package my_node
# Suppress noise from ROS libraries
ASAN_OPTIONS="suppressions=$HOME/ros_asan.supp" rosrun my_package my_node
Common suppressions needed for ROS:
# ros_asan.supp
leak:ros::init
leak:ros::NodeHandle
leak:pluginlib
For TSan with ROS, many races in ROS infrastructure are benign but noisy. Use a suppression file and focus on YOUR code’s races.
Sanitizers have found critical bugs in major projects:
-fwrapv or cast to unsigned.# ASan (memory bugs)
g++ -std=c++2a -fsanitize=address -fno-omit-frame-pointer -g -O1 src.cpp -o bin_asan
# UBSan (undefined behavior)
g++ -std=c++2a -fsanitize=undefined -g src.cpp -o bin_ubsan
# TSan (data races) — needs -pthread
g++ -std=c++2a -fsanitize=thread -g -O1 src.cpp -o bin_tsan -pthread
# ASan + UBSan combined
g++ -std=c++2a -fsanitize=address,undefined -fno-omit-frame-pointer -g src.cpp -o bin_combo
# Run with options
ASAN_OPTIONS="detect_leaks=1:halt_on_error=0" ./bin_asan
UBSAN_OPTIONS="print_stacktrace=1:halt_on_error=1" ./bin_ubsan
TSAN_OPTIONS="second_deadlock_stack=1" ./bin_tsan
| Exercise | Sanitizer | Focus |
|---|---|---|
| ex01 | ASan | 8 classic memory bugs |
| ex02 | UBSan | 8 undefined behavior patterns |
| ex03 | TSan | 5 data races + deadlock |
| ex04 | All three | Realistic mini-project, find & fix |
| ex05 | Detective | Real-world-like subtle bugs |
| puzzle01 | TSan | False positive + suppression |
| puzzle02 | None | What sanitizers CAN’T catch |