C++ Runtime Checking: AddressSanitizer and ThreadSanitizer
Why Static Analysis Is Not Enough
Memory bugs in C++ are categorized as “undefined behavior” — the program may appear to work correctly for months, then corrupt data or crash under specific allocation patterns, load levels, or OS scheduling decisions. Static analysis catches some patterns, but many bugs only manifest at runtime.
AddressSanitizer (ASan) instruments every memory access. It maintains shadow memory tracking which bytes are valid, and reports the exact location of:
- Heap buffer overflows
- Stack buffer overflows
- Use-after-free
- Use-after-scope (returning reference to local)
- Double-free
ThreadSanitizer (TSan) tracks thread operations — reads, writes, locks, and atomics — and reports data races: two threads accessing the same memory without synchronization where at least one thread writes.
Both sanitizers are available in GCC and Clang. MSVC supports /fsanitize=address from Visual Studio 2019 16.9+.
AddressSanitizer (ASan)
Build Flags
# GCC or Clang
g++ -std=c++20 \
-fsanitize=address \ # enable ASan
-fsanitize=undefined \ # also enable UBSan (compatible with ASan)
-fno-omit-frame-pointer \ # better stack traces
-g \ # source line numbers in reports
-O1 \ # some optimization, but preserve frames
-o my_program my_program.cpp
# CMake — add to debug config
cmake -DCMAKE_CXX_FLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer" \
-DCMAKE_BUILD_TYPE=RelWithDebInfo ..
Use-After-Free Example
// uaf_example.cpp
#include <iostream>
int main() {
int* p = new int(42);
delete p;
// Access after free — undefined behavior
std::cout << *p << "\n"; // ASan catches this immediately
return 0;
}
$ g++ -fsanitize=address -fno-omit-frame-pointer -g -o uaf uaf_example.cpp
$ ./uaf
==12345==ERROR: AddressSanitizer: heap-use-after-free on address 0x602000000010
READ of size 4 at 0x602000000010 thread T0
#0 0x... in main uaf_example.cpp:8
...
0x602000000010 is located 0 bytes inside of 4-byte region [0x602000000010,0x602000000014)
freed by thread T0 here:
#0 0x... in operator delete(void*)
#1 0x... in main uaf_example.cpp:5
ASan tells you the exact line of the use-after-free AND the exact line where the memory was freed. Compare this to debugging a crash dump with no sanitizer — you would see a corrupted stack or wrong value with no indication of when the memory was freed.
Heap Buffer Overflow
#include <vector>
int main() {
std::vector<int> v = {1, 2, 3};
// Write one element past the end
v[3] = 99; // ASan: heap-buffer-overflow on write
return 0;
}
ERROR: AddressSanitizer: heap-buffer-overflow
WRITE of size 4 at address 0x... thread T0
#0 0x... in main overflow.cpp:5
Without ASan, this may silently corrupt adjacent heap metadata and crash later — completely unrelated to the actual bug site.
Stack Buffer Overflow
void process(const char* input) {
char buffer[8];
strcpy(buffer, input); // overflow if input > 7 chars + null
}
int main() {
process("this is way too long for the buffer");
return 0;
}
ERROR: AddressSanitizer: stack-buffer-overflow
WRITE of size 35 at address 0x... thread T0
#0 0x... in process stack_overflow.cpp:3
Memory Leak Detection (LSan)
LeakSanitizer is bundled with ASan on most platforms:
#include <memory>
int main() {
int* leaked = new int[100]; // never deleted
// program exits without freeing 'leaked'
}
# LSan runs automatically with ASan on Linux
$ ASAN_OPTIONS=detect_leaks=1 ./my_program
ERROR: LeakSanitizer: detected memory leaks
Direct leak of 400 byte(s) in 1 object(s) allocated from:
#0 0x... in operator new[](unsigned long)
#1 0x... in main leak.cpp:4
ThreadSanitizer (TSan)
Build Flags
# TSan is separate from ASan — do NOT combine them
g++ -std=c++20 \
-fsanitize=thread \
-fno-omit-frame-pointer \
-g \
-O1 \
-o my_program my_program.cpp
Data Race Example
#include <thread>
#include <iostream>
int counter = 0; // shared variable, no synchronization
void increment() {
for (int i = 0; i < 100000; i++) {
counter++; // read-modify-write without lock — data race
}
}
int main() {
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
std::cout << counter << '\n'; // might print anything
}
$ g++ -fsanitize=thread -fno-omit-frame-pointer -g -o race race.cpp
$ ./race
WARNING: ThreadSanitizer: data race (pid=12345)
Write of size 4 at 0x... by thread T2:
#0 increment() race.cpp:7
Previous write of size 4 at 0x... by thread T1:
#0 increment() race.cpp:7
Thread T1 (tid=..., running):
#0 increment() race.cpp:7
#1 main race.cpp:14
Thread T2 (tid=..., started):
...
TSan identifies:
- Which threads are racing
- Which memory address they’re racing on
- Whether each access is a read or write
- The exact source line of each access
Correct Version
#include <thread>
#include <atomic>
std::atomic<int> counter{0}; // atomic — no race
void increment() {
for (int i = 0; i < 100000; i++) {
counter.fetch_add(1, std::memory_order_relaxed);
}
}
Or with a mutex:
#include <thread>
#include <mutex>
int counter = 0;
std::mutex mtx;
void increment() {
for (int i = 0; i < 100000; i++) {
std::lock_guard<std::mutex> lock(mtx);
counter++;
}
}
CI Integration
Run separate sanitizer jobs in your CI pipeline:
GitHub Actions
# .github/workflows/sanitizers.yml
name: Sanitizers
on: [push, pull_request]
jobs:
asan:
name: AddressSanitizer
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build with ASan
run: |
cmake -B build-asan \
-DCMAKE_CXX_FLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer" \
-DCMAKE_BUILD_TYPE=RelWithDebInfo
cmake --build build-asan
- name: Run tests
run: |
cd build-asan
ASAN_OPTIONS=detect_leaks=1 ctest --output-on-failure
tsan:
name: ThreadSanitizer
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build with TSan
run: |
cmake -B build-tsan \
-DCMAKE_CXX_FLAGS="-fsanitize=thread -fno-omit-frame-pointer" \
-DCMAKE_BUILD_TYPE=RelWithDebInfo
cmake --build build-tsan
- name: Run tests
run: ctest --test-dir build-tsan --output-on-failure
CMake Presets
// CMakePresets.json
{
"configurePresets": [
{
"name": "asan",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "RelWithDebInfo",
"CMAKE_CXX_FLAGS": "-fsanitize=address,undefined -fno-omit-frame-pointer"
}
},
{
"name": "tsan",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "RelWithDebInfo",
"CMAKE_CXX_FLAGS": "-fsanitize=thread -fno-omit-frame-pointer"
}
}
]
}
Suppression Files
Not every sanitizer report represents a real bug. Third-party libraries with known benign races, or code paths that are logically safe but trigger false positives, can be suppressed:
# tsan.supp — TSan suppression file
race:libcrypto:CRYPTO_atomic_add # known-safe OpenSSL atomics
race:my_legacy_lib::LegacyGlobal::init # single-init pattern, actually safe
TSAN_OPTIONS="suppressions=tsan.supp" ./my_program
Use suppressions sparingly. Every suppression is a potential real bug being hidden. Prefer fixing the underlying issue.
What Each Sanitizer Finds
| Sanitizer | What it catches | Overhead |
|---|---|---|
| ASan | Heap/stack buffer overflow, use-after-free, double-free, use-after-scope | ~2x CPU, ~2-3x memory |
| TSan | Data races between threads | ~5-15x CPU, ~5-10x memory |
| UBSan | Integer overflow, null dereference, bad casts, alignment violations | ~minimal |
| LSan | Memory leaks (bundled with ASan) | Minimal extra |
Recommended combination: run ASan + UBSan together (they’re compatible), and TSan in a separate job.
Key Takeaways
- ASan catches memory corruption at the exact access site — not where the crash eventually manifests
- TSan detects data races that may only appear under specific scheduling — not reproducible otherwise
- Never ship sanitized binaries to production in performance-sensitive code — the overhead is 2-15x
- CI integration is the highest-value use: fail the build when a sanitizer finds a bug, before it reaches production
- ASan + UBSan can run together; TSan requires a separate build (don’t mix them)
- Use suppression files sparingly for known-safe third-party patterns — not as a way to silence real bugs
- LeakSanitizer (LSan) is bundled with ASan on Linux — catches memory leaks without Valgrind’s overhead
Frequently Asked Questions (FAQ)
Q. When would I use this in practice?
A. Catch C++ memory bugs and data races with AddressSanitizer and ThreadSanitizer.
Q. What should I read before this?
A. Follow the previous article or related articles links at the bottom of each post to learn in sequence. See the C++ series index for the full picture.
Q. Where can I study this more deeply?
A. Check cppreference and the relevant library’s official documentation. The reference links at the end of the article are also worth using.
Related Articles (Internal Links)
Other articles related to this topic.
- C++ Sanitizers | ASan·TSan으로 메모리 버그·data race 자동 탐지
- C++ CI/CD 파이프라인: GitHub Actions를 이용한 멀티 OS 자동 빌드·테스트 가이드
- C++ Segmentation fault | core dump
Keywords Covered in This Article (Related Search Terms)
This article covers C++, AddressSanitizer, ThreadSanitizer, memory safety, data race, ASan, TSan.