C++ Sanitizers: ASan, TSan, UBSan, and MSan Explained
이 글의 핵심
Clang/GCC sanitizers for C++: AddressSanitizer, ThreadSanitizer, UBSan, and CI examples. Catch buffer overflows, UAF, races, and UB with compile flags and examples.
Introduction
Sanitizers are compiler-assisted tools that detect memory errors, data races, and undefined behavior at runtime. They are faster than Valgrind for many workflows and provide accurate stack traces so you can fix bugs quickly.
1. Sanitizer overview
Main sanitizers
# AddressSanitizer (ASan) — memory errors
g++ -fsanitize=address -g program.cpp -o program
# ThreadSanitizer (TSan) — data races
g++ -fsanitize=thread -g program.cpp -o program
# UndefinedBehaviorSanitizer (UBSan) — undefined behavior
g++ -fsanitize=undefined -g program.cpp -o program
# MemorySanitizer (MSan) — uninitialized memory
g++ -fsanitize=memory -g program.cpp -o program
# LeakSanitizer (LSan) — leaks (often bundled with ASan)
g++ -fsanitize=leak -g program.cpp -o program
Comparison
| Sanitizer | Detects | Overhead | Pairing |
|---|---|---|---|
| ASan | Memory errors | ~2× | ASan + UBSan |
| TSan | Data races | ~5–15× | Alone |
| UBSan | Undefined behavior | ~1.2× | ASan + UBSan |
| MSan | Uninitialized reads | ~3× | Alone |
| LSan | Leaks | Low | Often with ASan |
2. AddressSanitizer (ASan)
Buffer overflow
// bug1.cpp
#include <iostream>
int main() {
int arr[10];
// Bug: out of bounds
for (int i = 0; i <= 10; ++i) {
arr[i] = i;
}
return 0;
}
Build and run:
$ g++ -fsanitize=address -g bug1.cpp -o bug1
$ ./bug1
=================================================================
==12345==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7ffc...
WRITE of size 4 at 0x7ffc....thread T0
#0 0x....in main bug1.cpp:8
#1 0x....in __libc_start_main
Address 0x7ffc....is located in stack of thread T0 at offset 40 in frame
#0 0x....in main bug1.cpp:4
Use-after-free
// bug2.cpp
#include <iostream>
int main() {
int* ptr = new int(42);
delete ptr;
// Bug: use after free
std::cout << *ptr << std::endl;
return 0;
}
ASan output:
$ g++ -fsanitize=address -g bug2.cpp -o bug2
$ ./bug2
=================================================================
==12346==ERROR: AddressSanitizer: heap-use-after-free on address 0x602...
READ of size 4 at 0x602....thread T0
#0 0x....in main bug2.cpp:7
0x602....is located 0 bytes inside of 4-byte region [0x602..., 0x602...)
freed by thread T0 here:
#0 0x....in operator delete(void*)
#1 0x....in main bug2.cpp:5
Memory leak
// bug3.cpp
#include <iostream>
int main() {
// Bug: leak
int* leak = new int(42);
return 0; // missing delete
}
ASan output:
$ g++ -fsanitize=address -g bug3.cpp -o bug3
$ ./bug3
=================================================================
==12347==ERROR: LeakSanitizer: detected memory leaks
Direct leak of 4 byte(s) in 1 object(s) allocated from:
#0 0x....in operator new(unsigned long)
#1 0x....in main bug3.cpp:5
3. ThreadSanitizer (TSan)
Data race
// race.cpp
#include <thread>
#include <iostream>
int counter = 0;
void increment() {
for (int i = 0; i < 100000; ++i) {
++counter; // data race!
}
}
int main() {
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
std::cout << "counter: " << counter << std::endl;
return 0;
}
TSan output:
$ g++ -fsanitize=thread -g race.cpp -o race
$ ./race
==================
WARNING: ThreadSanitizer: data race (pid=12348)
Write of size 4 at 0x....by thread T1:
#0 increment() race.cpp:7
Previous write of size 4 at 0x....by thread T2:
#0 increment() race.cpp:7
Fix:
#include <atomic>
#include <thread>
#include <iostream>
std::atomic<int> counter{0};
void increment() {
for (int i = 0; i < 100000; ++i) {
++counter; // atomic
}
}
int main() {
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
std::cout << "counter: " << counter << std::endl; // 200000
return 0;
}
4. UndefinedBehaviorSanitizer (UBSan)
Undefined behavior
// ub.cpp
#include <iostream>
int main() {
// Bug 1: division by zero
int x = 0;
int y = 5 / x;
// Bug 2: signed overflow
int max = 2147483647;
int overflow = max + 1;
// Bug 3: null pointer dereference
int* ptr = nullptr;
int value = *ptr;
// Bug 4: out-of-bounds array index
int arr[10];
int index = 15;
int val = arr[index];
return 0;
}
UBSan output:
$ g++ -fsanitize=undefined -g ub.cpp -o ub
$ ./ub
ub.cpp:6:15: runtime error: division by zero
ub.cpp:10:23: runtime error: signed integer overflow: 2147483647 + 1 cannot be represented in type 'int'
ub.cpp:14:17: runtime error: load of null pointer of type 'int'
ub.cpp:19:17: runtime error: index 15 out of bounds for type 'int [10]'
5. CI/CD integration
CMake
# CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
project(MyProject)
set(CMAKE_CXX_STANDARD 17)
option(ENABLE_ASAN "Enable AddressSanitizer" OFF)
option(ENABLE_TSAN "Enable ThreadSanitizer" OFF)
option(ENABLE_UBSAN "Enable UBSanitizer" OFF)
if(ENABLE_ASAN)
add_compile_options(-fsanitize=address -fno-omit-frame-pointer)
add_link_options(-fsanitize=address)
endif()
if(ENABLE_TSAN)
add_compile_options(-fsanitize=thread)
add_link_options(-fsanitize=thread)
endif()
if(ENABLE_UBSAN)
add_compile_options(-fsanitize=undefined)
add_link_options(-fsanitize=undefined)
endif()
add_executable(myapp main.cpp)
Build:
# ASan
cmake -DENABLE_ASAN=ON ..
make
# TSan
cmake -DENABLE_TSAN=ON ..
make
# UBSan
cmake -DENABLE_UBSAN=ON ..
make
GitHub Actions
# .github/workflows/sanitizers.yml
name: Sanitizers
on: [push, pull_request]
jobs:
asan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build with ASan
run: |
g++ -fsanitize=address -g src/*.cpp -o test_asan
- name: Run Tests
run: ./test_asan
tsan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build with TSan
run: |
g++ -fsanitize=thread -g src/*.cpp -o test_tsan
- name: Run Tests
run: ./test_tsan
ubsan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build with UBSan
run: |
g++ -fsanitize=undefined -g src/*.cpp -o test_ubsan
- name: Run Tests
run: ./test_ubsan
6. Common issues
Issue 1: Performance overhead
// Typical overhead:
// - ASan: ~2×
// - TSan: ~5–15×
// - UBSan: ~1.2×
// ✅ Dev/test only
// ✅ Strip from release builds
Issue 2: Compatibility
# ❌ ASan + TSan together (not supported this way)
g++ -fsanitize=address,thread program.cpp # error
# ✅ Separate builds
g++ -fsanitize=address program.cpp # ASan
g++ -fsanitize=thread program.cpp # TSan
# ✅ ASan + UBSan is OK
g++ -fsanitize=address,undefined program.cpp
Issue 3: False positives / intentional patterns
// Suppress with attribute
__attribute__((no_sanitize("address")))
void intentionalOverflow() {
// ...
}
// Or suppression file asan.supp
ASAN_OPTIONS=suppressions=asan.supp ./program
Issue 4: Debug symbols
# ❌ No -g (weaker stacks)
g++ -fsanitize=address program.cpp
# ✅ Add -g
g++ -fsanitize=address -g program.cpp
# ✅ Lower optimization for clearer traces
g++ -fsanitize=address -g -O1 program.cpp
7. Bug-hunting example
// buggy_program.cpp
#include <iostream>
#include <vector>
#include <thread>
class DataProcessor {
std::vector<int> data;
int counter = 0;
public:
void leakyFunction() {
int* leak = new int(42);
// missing delete
}
void bufferOverflow() {
int arr[10];
for (int i = 0; i <= 10; ++i) {
arr[i] = i;
}
}
void useAfterFree() {
int* ptr = new int(42);
delete ptr;
std::cout << *ptr << std::endl;
}
void dataRace() {
auto increment = [this]() {
for (int i = 0; i < 100000; ++i) {
++counter; // no synchronization
}
};
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
}
void integerOverflow() {
int max = 2147483647;
int overflow = max + 1;
std::cout << overflow << std::endl;
}
};
int main() {
DataProcessor processor;
return 0;
}
Test script:
#!/bin/bash
echo "=== AddressSanitizer ==="
g++ -fsanitize=address -g buggy_program.cpp -o test_asan
./test_asan
echo "=== ThreadSanitizer ==="
g++ -fsanitize=thread -g buggy_program.cpp -o test_tsan
./test_tsan
echo "=== UBSanitizer ==="
g++ -fsanitize=undefined -g buggy_program.cpp -o test_ubsan
./test_ubsan
8. Sanitizer options
Environment variables
# ASan
export ASAN_OPTIONS=detect_leaks=1:halt_on_error=0
# TSan
export TSAN_OPTIONS=history_size=7:halt_on_error=0
# UBSan
export UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=0
./program
Useful ASan options
ASAN_OPTIONS=detect_leaks=1
ASAN_OPTIONS=detect_stack_use_after_return=1
ASAN_OPTIONS=detect_container_overflow=1
ASAN_OPTIONS=log_path=asan.log
ASAN_OPTIONS=detect_leaks=1:log_path=asan.log:halt_on_error=0 ./program
Summary
Key takeaways
- ASan: Memory errors (buffers, UAF, leaks)
- TSan: Data races
- UBSan: Undefined behavior (overflow, divide-by-zero)
- MSan: Uninitialized memory
- LSan: Leaks (often with ASan)
- Cost: Dev/test only (~2–15×)
Choosing a sanitizer
| Bug type | Sanitizer | When |
|---|---|---|
| Buffer overflow | ASan | Always in dev |
| Leaks | ASan / LSan | Always |
| Use-after-free | ASan | Always |
| Data race | TSan | Multithreaded code |
| Integer UB | UBSan | Arithmetic-heavy code |
| Uninit reads | MSan | Complex initialization |
Practical tips
- Local dev: ASan + UBSan
- Threads: separate TSan build
- CI: automate sanitizer builds
- Release: no sanitizers
Next steps
Related posts
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. Clang/GCC sanitizers for C++: AddressSanitizer, ThreadSanitizer, UBSan, and CI examples. Catch buffer overflows, UAF, ra… 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ Heap Corruption | ‘힙 손상’ 가이드
- C++ 메모리 누수 완벽 가이드 | 5가지 원인과 해결 방법 (Valgrind/ASan)
- C++ Use After Free | ‘해제 후 사용’ 가이드
이 글에서 다루는 키워드 (관련 검색어)
C++, sanitizers, debugging, memory, tools 등으로 검색하시면 이 글이 도움이 됩니다.