Sanitizers Deep-Dive: ASan, UBSan, TSan, MSan in C++
Master AddressSanitizer, UBSan, TSan, and MSan in C++.
20+ years shipping performance-critical C and C++ systems. Lessons pulled from things that broke in production.
- ✓Basic knowledge of C++ syntax and compilation
- ✓Understanding of pointers and dynamic memory allocation
- ✓Familiarity with multithreading concepts (for TSan)
- ✓A compiler supporting sanitizers (GCC 4.8+ or Clang 3.1+)
- AddressSanitizer (ASan) detects buffer overflows, use-after-free, and memory leaks.
- UndefinedBehaviorSanitizer (UBSan) catches integer overflow, null pointer dereference, and other UB.
- ThreadSanitizer (TSan) detects data races in multithreaded code.
- MemorySanitizer (MSan) identifies reads of uninitialized memory.
- Enable with compiler flags like -fsanitize=address and link with -fsanitize=address.
Think of sanitizers as a security camera system for your code. ASan watches for memory misuse like a guard checking IDs at a door. UBSan is a rulebook enforcer that flags when you break the rules. TSan monitors conversations between threads to prevent miscommunication. MSan checks if you're using data before it's properly set, like reading a blank page.
| Chrome | Firefox | Safari | Edge |
|---|---|---|---|
| ✓ | ✓ | ✓ | ✓ |
Memory bugs, undefined behavior, data races, and uninitialized memory are the silent killers of C++ applications. They lurk in production, causing crashes, security vulnerabilities, and unpredictable behavior. Traditional debugging tools often fail to catch these issues until they manifest in catastrophic ways. Enter the sanitizers: AddressSanitizer (ASan), UndefinedBehaviorSanitizer (UBSan), ThreadSanitizer (TSan), and MemorySanitizer (MSan). These compiler-integrated tools instrument your code at compile time to detect a wide range of bugs during runtime. They are fast, precise, and have become essential in modern C++ development. In this deep-dive, you'll learn how to use each sanitizer, understand their output, and integrate them into your build and CI pipelines. We'll cover real-world scenarios, common pitfalls, and best practices. By the end, you'll be equipped to write safer, more reliable C++ code.
What Are Sanitizers?
Sanitizers are runtime tools integrated into compilers like GCC and Clang that instrument code to detect specific classes of bugs. They work by adding checks at compile time that verify memory accesses, arithmetic operations, thread synchronization, and memory initialization. Each sanitizer focuses on a different category: AddressSanitizer (ASan) for memory errors, UndefinedBehaviorSanitizer (UBSan) for undefined behavior, ThreadSanitizer (TSan) for data races, and MemorySanitizer (MSan) for uninitialized memory reads. They are designed to be fast (typically 2x slowdown) and provide detailed error reports including stack traces. Sanitizers are not a replacement for unit tests but complement them by catching bugs that are hard to test for. They are especially valuable in C++ where manual memory management and low-level operations are common.
AddressSanitizer (ASan): Detecting Memory Errors
AddressSanitizer is the most widely used sanitizer. It detects heap buffer overflows, stack buffer overflows, global buffer overflows, use-after-free, use-after-return, double free, and memory leaks. ASan works by shadow memory: it maps every 8 bytes of application memory to one shadow byte that encodes whether the memory is accessible. On each memory access, the compiler inserts a check against the shadow memory. If the check fails, ASan reports an error with a stack trace. To use ASan, compile with -fsanitize=address and link with the same flag. For C++ programs, also add -fsanitize=address -fno-omit-frame-pointer for better stack traces. Example: detecting a use-after-free.
UndefinedBehaviorSanitizer (UBSan): Catching UB
Undefined behavior (UB) is a notorious source of bugs in C++. The C++ standard leaves many operations undefined, meaning the compiler can assume they never happen and optimize accordingly. UBSan instruments code to detect common UB at runtime, such as integer overflow, null pointer dereference, misaligned access, invalid shifts, and more. To enable UBSan, compile with -fsanitize=undefined. You can also enable specific checks like -fsanitize=integer or -fsanitize=null. UBSan reports the exact location and type of UB. Example: integer overflow.
ThreadSanitizer (TSan): Detecting Data Races
Data races occur when two threads access the same memory location without synchronization, and at least one access is a write. TSan instruments every memory access and tracks the happens-before relationship using vector clocks. When it detects a race, it reports the two conflicting accesses and the threads involved. To use TSan, compile with -fsanitize=thread and link with -lpthread. TSan requires all code to be compiled with the flag, including libraries. Example: a simple data race.
MemorySanitizer (MSan): Uninitialized Memory Reads
Reading uninitialized memory leads to undefined behavior. MSan detects when a program reads memory that has not been initialized. It works by tracking the initialization state of every bit of memory using shadow memory. When an uninitialized value is used in a way that affects program behavior (e.g., branching, output), MSan reports an error. To use MSan, compile with -fsanitize=memory -fsanitize-memory-track-origins (for origin tracking). MSan requires all code to be compiled with the flag, including libraries. Example: reading an uninitialized local variable.
Combining Sanitizers and Best Practices
Sanitizers can be combined, but not all combinations work together. ASan and UBSan can be used together: -fsanitize=address,undefined. TSan and ASan are incompatible because both instrument memory accesses. MSan also conflicts with ASan and TSan. For maximum coverage, run separate builds with different sanitizers. Best practices: 1) Always compile with -g -fno-omit-frame-pointer for better stack traces. 2) Use sanitizers in your CI pipeline on every commit. 3) For large codebases, start with ASan and UBSan as they have lower overhead. 4) Use suppression files to ignore known false positives. 5) Combine with fuzzing for automated bug discovery. Example: combining ASan and UBSan.
The Phantom Crash: A Use-After-Free in a Web Server
- Always run sanitizers in test and staging environments.
- Use ASan to catch use-after-free and buffer overflows early.
- Sanitizers can find bugs that are nearly impossible to reproduce manually.
- Integrate sanitizers into CI to prevent regressions.
- Combine sanitizers with fuzzing for maximum coverage.
g++ -fsanitize=address -g -o test test.cpp./test| File | Command / Code | Purpose |
|---|---|---|
| sanitizer_intro.cpp | int main() { | What Are Sanitizers? |
| asan_use_after_free.cpp | int main() { | AddressSanitizer (ASan) |
| ubsan_integer_overflow.cpp | int main() { | UndefinedBehaviorSanitizer (UBSan) |
| tsan_data_race.cpp | int counter = 0; | ThreadSanitizer (TSan) |
| msan_uninitialized.cpp | int main() { | MemorySanitizer (MSan) |
| combined_sanitizers.cpp | int main() { | Combining Sanitizers and Best Practices |
Key takeaways
Common mistakes to avoid
5 patternsForgetting to link with -fsanitize=address when compiling with ASan
Using TSan without compiling all code (including libraries) with -fsanitize=thread
Ignoring UBSan warnings about signed integer overflow
Running sanitizers in release mode without debug symbols
Assuming sanitizers catch all bugs
Interview Questions on This Topic
What is AddressSanitizer and how does it detect heap buffer overflows?
Frequently Asked Questions
20+ years shipping performance-critical C and C++ systems. Lessons pulled from things that broke in production.
That's C++ Advanced. Mark it forged?
3 min read · try the examples if you haven't