Lesson 3 of 6
The compiler cannot tell which pointers are live at run time, so it says nothing about use-after-free, double frees, or most out-of-bounds access (lesson 11.1). The sanitizers watch the program as it runs and report the exact operation and line.
They are built into GCC and Clang. There is nothing to install.
AddressSanitizer
g++ -std=c++20 -Wall -Wextra -fsanitize=address -g main.cpp -o main
-g matters: without it you get addresses instead of line numbers.
Take the off-by-one from lesson 11.2:
#include <iostream>
int sum_range(const int* values, int count) {
int total = 0;
for (int i = 0; i <= count; ++i) { // BUG: <= reads one past the end
total += values[i];
}
return total;
}
int main() {
int* data = new int[5];
for (int i = 0; i < 5; ++i) {
data[i] = i + 1;
}
std::cout << "sum: " << sum_range(data, 5) << "\n";
delete[] data;
return 0;
}
Run normally, it prints sum: 15 and exits 0. Built with ASan, it stops at the bad read:
=================================================================
==1645==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x503000000054
READ of size 4 at 0x503000000054 thread T0
#0 0x... in sum_range(int const*, int) buggy.cpp:6
#1 0x... in main buggy.cpp:17
0x503000000054 is located 0 bytes after 20-byte region [0x503000000040,0x503000000054)
allocated by thread T0 here:
#0 0x... in operator new[](unsigned long)
#1 0x... in main buggy.cpp:12
SUMMARY: AddressSanitizer: heap-buffer-overflow buggy.cpp:6 in sum_range(int const*, int)
Read the four things it tells you:
- What:
heap-buffer-overflow, aREAD of size 4— reading oneint. - Where:
buggy.cpp:6, insidesum_range, called frommainat line 17. - What memory:
0 bytes aftera 20-byte region — immediately past a five-intarray. - Whose memory: allocated at
buggy.cpp:12, which is thenew int[5].
That is the allocation site and the bad access site. Finding this by reading code takes minutes; ASan takes one run.
What ASan finds
- Heap buffer overflow (reading or writing past
newed memory) - Stack buffer overflow (past a local array)
- Global buffer overflow
- Use-after-free
- Use-after-return (lesson 8.5's dangling pointer)
- Double free and mismatched
new/delete[] - Memory leaks, at exit
Every one of those is a bug from Module 8 that produces no compiler warning.
UndefinedBehaviorSanitizer
g++ -std=c++20 -fsanitize=undefined -g main.cpp -o main
UBSan covers a different set: arithmetic and type problems rather than memory.
#include <climits>
#include <iostream>
int main() {
int x = INT_MAX;
std::cout << "before\n";
x = x + 1; // signed overflow: undefined
std::cout << "after: " << x << "\n";
return 0;
}
Built with UBSan, running it reports:
ub.cpp:7:7: runtime error: signed integer overflow: 2147483647 + 1 cannot be represented in type 'int'
before
after: -2147483648
Note two things. The message names the exact expression and the exact type. And the program
keeps running — UBSan reports by default rather than aborting, which is why after: still
prints. Add -fno-sanitize-recover=all to make it stop at the first finding.
What UBSan finds:
- Signed integer overflow (lesson 3.3)
- Division by zero
- Shifting by more than the width of the type
- Null pointer dereference
- Misaligned access
- Casting a value outside the range of the destination type
Use them together
g++ -std=c++20 -Wall -Wextra -Werror -fsanitize=address,undefined -g main.cpp -o main_test
They compose. This is the build your tests should run under.
The one that does not compose is ThreadSanitizer (-fsanitize=thread, Module 21), which
conflicts with ASan and needs its own build.
The costs
| | Slowdown | Memory | |---|---|---| | ASan | roughly 2× | roughly 3× | | UBSan | ~20% | small |
Irrelevant for a test suite; unacceptable in production. The rule: run your tests under sanitizers, ship without them.
They also only find bugs on the paths you actually execute. A sanitizer build that never runs the buggy code path reports nothing. That makes them a strong argument for having tests (Module 19) — the sanitizers are only as thorough as your coverage.
Leak detection
ASan reports leaks at exit:
#include <iostream>
int main() {
int* leaked = new int[100];
leaked[0] = 1;
std::cout << "exiting without delete[]\n";
return 0; // deliberately no delete[]
}
Under ASan that produces:
exiting without delete[]
=================================================================
==2001==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
SUMMARY: AddressSanitizer: 400 byte(s) leaked in 1 allocation(s).
Compare that with lesson 8.4, where making a leak visible needed a custom operator new. ASan
does it with a compiler flag, and names the allocation line.
Reading a report
Every report has the same shape:
- The error type —
heap-buffer-overflow,heap-use-after-free,detected memory leaks. - The operation —
READ of size 4,WRITE of size 8. - The stack at the moment of the error — read top to bottom, most recent first.
- The history of that memory — where it was allocated, and freed if it was.
The fourth part is the one people skip and the one that usually solves it. "Allocated at line 12, freed at line 20, read at line 25" tells you the whole story.
Try it
Write a program with a deliberate use-after-free and run it three ways: plain, under ASan, and
under -fsanitize=undefined. Note which one catches it. Then add a leak and see what ASan
reports at exit.
Checkpoint
- Sanitizers watch a running program and report what the compiler cannot see.
- ASan: buffer overflows, use-after-free, use-after-return, double free, leaks.
- UBSan: signed overflow, division by zero, bad shifts, null dereference, bad casts.
-fsanitize=address,undefinedcomposes; ThreadSanitizer needs its own build.- Always add
-g, or you get addresses instead of line numbers. - UBSan reports and continues by default;
-fno-sanitize-recover=allmakes it stop. - Roughly 2× slower — run tests under them, ship without them.
- They only see the paths you execute, which is an argument for having tests.
Questions about this lesson
Log in to ask a question.