C++ Module 6 – References, pointers and memory
Finding memory bugs with AddressSanitizer and UBSan
Build C++ with AddressSanitizer and UBSan, read a report from the bad access back to the allocation, and see which sanitizers each platform offers.
What you will learn
- Build programs with AddressSanitizer and UndefinedBehaviorSanitizer
- Read a sanitizer report from the error line to the allocation site
- Identify which sanitizers Windows, macOS and Linux support
Before you start
What you’ll learn · Before you start
What you will learn
- Build programs with AddressSanitizer and UndefinedBehaviorSanitizer
- Read a sanitizer report from the error line to the allocation site
- Identify which sanitizers Windows, macOS and Linux support
Before you start
On this page
A sanitizer is a compiler option that builds extra checks into your program. AddressSanitizer (ASan) watches
every memory access and stops the program at the first one that touches memory it should not: past the end of an
array, after a delete, or after a variable’s scope has ended. UndefinedBehaviorSanitizer (UBSan) checks for
other undefined behaviour, such as signed overflow, a misaligned pointer or an oversized shift. Build with
-fsanitize=address,undefined -g -fno-omit-frame-pointer, run the program as usual, and a bug that used to crash
somewhere else, or not at all, produces a report naming the faulty line and where the memory came from.
Sanitizers turn the silent bugs of object lifetime and undefined behaviour into loud ones, and they work with both GCC and Clang. This lesson builds them, reads one report line by line, shows five more memory errors and three UBSan checks, maps which sanitizers exist on Windows, macOS and Linux, and measures what a sanitized build costs.
Building with sanitizers
Every flag of the recommended command has a job:
-fsanitize=addressadds a check before each load and store, and links a runtime library that replacesnew,delete,mallocandfree, so that it knows which memory is allocated and which was freed.-fsanitize=undefinedadds UBSan’s checks, a group that includes signed overflow, shifts, misaligned pointers, null pointers and array indexes whose bounds the compiler knows.-gkeeps the debug information that turns addresses in the report into function names, files and lines.-fno-omit-frame-pointerkeeps the frame pointer, so that the stack traces in reports are complete.
Give the flags when compiling and when linking; with one clang++ or g++ command that does both, that is
automatic. Clang’s documentation suggests -O1 or higher for reasonable speed; without it the program only runs
slower.
How AddressSanitizer knows
ASan keeps a shadow memory: one shadow byte describes 8 bytes of your program’s memory. A shadow byte of 00
means all 8 bytes may be used, a value from 01 to 07 means only that many of the first bytes may, and other values
mark memory that must not be touched, each kind with its own code. The runtime surrounds every allocation with
redzones, poisoned bytes on both sides, and keeps freed memory poisoned for a while instead of handing it out at
once, in a queue called the quarantine. Each instrumented access looks up its shadow byte first. That is why ASan
finds every overflow that lands in a redzone and uses of memory that was freed recently, and why it can miss an access
that jumps far past the redzone into another live block, or a use of memory freed long ago and handed out again.
Reading a report, line by line
reset sets one element too many, a classic off-by-one:
#include <print>
// Sets scores[0] to scores[count] to zero: one element more than the caller has.
void reset(int* scores, int count) {
for (int i = 0; i <= count; ++i) scores[i] = 0;
}
int main() {
int* scores = new int[5];
reset(scores, 5);
std::println("reset done");
delete[] scores;
} from session import run
# The whole AddressSanitizer report for one bug, built with the flags this lesson recommends.
run("""
clang++ -std=c++23 -g -fsanitize=address,undefined -fno-omit-frame-pointer overflow.cpp -o overflow
./overflow
""", max_lines=70, full=True) Output
$ clang++ -std=c++23 -g -fsanitize=address,undefined -fno-omit-frame-pointer overflow.cpp -o overflow
$ ./overflow
=================================================================
==…==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x… at pc 0x… bp 0x… sp 0x…
WRITE of size 4 at 0x… thread T0
#0 0x… in reset(int*, int) overflow.cpp:5:48
#1 0x… in main overflow.cpp:10:5
#2 0x… in start (/usr/lib/dyld:arm64e+0x204e0)
0x… is located 0 bytes after 20-byte region [0x…,0x…)
allocated by thread T0 here:
#0 0x… in _Znam (/Library/Developer/CommandLineTools/usr/lib/clang/21/lib/darwin/libclang_rt.asan_osx_dynamic.dylib:arm64e+0x50268)
#1 0x… in main overflow.cpp:9:19
#2 0x… in start (/usr/lib/dyld:arm64e+0x204e0)
SUMMARY: AddressSanitizer: heap-buffer-overflow overflow.cpp:5:48 in reset(int*, int)
Shadow bytes around the buggy address:
0x…: fa fa 00 00 00 00 fa fa 00 00 00 00 fa fa 00 00
0x…: 00 00 fa fa 00 00 00 00 fa fa 00 00 00 00 fa fa
0x…: 00 00 00 00 fa fa 00 00 00 00 fa fa 00 00 00 00
0x…: fa fa 00 00 00 00 fa fa 00 00 00 00 fa fa 00 00
0x…: 00 00 fa fa 00 00 00 00 fa fa 00 00 00 00 fa fa
=>0x…: 00 00[04]fa fa fa fa fa fa fa fa fa fa fa fa fa
0x…: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x…: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x…: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x…: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x…: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
==…==ABORTING
[exit status 134]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 overflow_session.py
The report reads from top to bottom like this:
- The error.
heap-buffer-overflow: an access just outside memory that came from the heap, here fromnew[]. The process number and addresses, shown as==…==and0x…on this page, change on every run. - The access.
WRITE of size 4: the program wrote anint. The stack trace below says where: frame#0isresetatoverflow.cpp:5:48, line 5, column 48, called bymainat line 10. The first frame in your own code is almost always the line to look at. - The memory.
located 0 bytes after 20-byte region: the write landed right after a block of 20 bytes, which is 5ints, so the index was 5, one past the last valid index 4. - The origin.
allocated by thread T0 here: the block came fromoperator new[](its mangled name,_Znam) called on line 9. For a use after free, afreed bystack appears here too. - The summary, one line to search for in logs, then the shadow bytes around the address. In the marked row,
00 00cover the first 16 bytes of the block,[04]says only 4 of the next 8 bytes belong to it, which is exactly where the write went, andfais the heap redzone. The legend below decodes every value. ABORTING: ASan stops the program at the first error, by design, with exit status 134 here.
How AddressSanitizer describes the block that reset() overflows
Text description of the diagram
The figure shows the memory around the 20-byte block that new int[5] returned in overflow.cpp, in five rows of 8 bytes. Next to each row is the shadow byte that AddressSanitizer keeps for those 8 bytes.
- The 8 bytes before the block are a redzone. Shadow byte fa: the heap redzone, which must not be touched.
- Bytes 0 to 7 of the block hold scores[0] and scores[1]. Shadow byte 00: all 8 bytes may be used.
- Bytes 8 to 15 hold scores[2] and scores[3]. Shadow byte 00.
- Bytes 16 to 23 hold scores[4] in the first 4 bytes; the other 4 belong to the redzone. Shadow byte 04: only the first 4 bytes may be used.
- The 8 bytes after that are redzone again. Shadow byte fa.
An arrow marks offset 20, the first byte after scores[4]: the place where reset() writes scores[5]. Its row's shadow byte, 04, says only bytes 16 to 19 are usable, so AddressSanitizer reports a heap-buffer-overflow 0 bytes after the 20-byte region.
Most of the time, steps 1 to 4 are all you need: what kind of access, which line, how far outside, and whose memory.
Five more memory errors
bugs.cpp holds four more classic mistakes; the command-line argument chooses which one runs:
#include <print>
#include <string_view>
void use_after_free() {
int* marks = new int{90};
delete marks;
std::println("marks = {}", *marks); // the int was deleted on the line above
}
void use_after_scope() {
int* best = nullptr;
{
int score = 87;
best = &score;
} // score's lifetime ends here
std::println("best = {}", *best);
}
void double_free() {
int* marks = new int{90};
delete marks;
delete marks; // the same pointer, deleted again
}
void stack_overflow() {
int last_five[5] = {1, 2, 3, 4, 5};
int total = 0;
for (int i = 0; i <= 5; ++i) total += last_five[i];
std::println("total = {}", total);
}
int main(int argc, char* argv[]) {
std::string_view bug = argc > 1 ? argv[1] : "";
if (bug == "use-after-free") use_after_free();
else if (bug == "use-after-scope") use_after_scope();
else if (bug == "double-free") double_free();
else if (bug == "stack-overflow") stack_overflow();
else std::println("usage: bugs use-after-free | use-after-scope | double-free | stack-overflow");
} from session import run
# Four more bugs from bugs.cpp, one per run, with the report shortened to its key lines.
run("""
clang++ -std=c++23 -g -fsanitize=address,undefined -fno-omit-frame-pointer bugs.cpp -o bugs
./bugs use-after-free
./bugs use-after-scope
./bugs double-free
./bugs stack-overflow
""") Output
$ clang++ -std=c++23 -g -fsanitize=address,undefined -fno-omit-frame-pointer bugs.cpp -o bugs
$ ./bugs use-after-free
==…==ERROR: AddressSanitizer: heap-use-after-free on address 0x…
READ of size 4 at 0x… thread T0
#8 in use_after_free() bugs.cpp:7
#9 in main bugs.cpp:34
0x… is located 0 bytes inside of 4-byte region [0x…,0x…)
freed by thread T0 here:
#1 in use_after_free() bugs.cpp:6
#2 in main bugs.cpp:34
previously allocated by thread T0 here:
#1 in use_after_free() bugs.cpp:5
#2 in main bugs.cpp:34
SUMMARY: AddressSanitizer: heap-use-after-free
[48 more lines of the report left out]
==…==ABORTING
[exit status 134]
$ ./bugs use-after-scope
==…==ERROR: AddressSanitizer: stack-use-after-scope on address 0x…
READ of size 4 at 0x… thread T0
#8 in use_after_scope() bugs.cpp:16
#9 in main bugs.cpp:35
Address 0x… is located in stack of thread T0 at offset 32 in frame
#0 in use_after_scope() bugs.cpp:10
SUMMARY: AddressSanitizer: stack-use-after-scope
[47 more lines of the report left out]
==…==ABORTING
[exit status 134]
$ ./bugs double-free
==…==ERROR: AddressSanitizer: attempting double-free on 0x… in thread T0:
#1 in double_free() bugs.cpp:22
#2 in main bugs.cpp:36
0x… is located 0 bytes inside of 4-byte region [0x…,0x…)
freed by thread T0 here:
#1 in double_free() bugs.cpp:21
#2 in main bugs.cpp:36
previously allocated by thread T0 here:
#1 in double_free() bugs.cpp:20
#2 in main bugs.cpp:36
SUMMARY: AddressSanitizer: double-free
[10 more lines of the report left out]
==…==ABORTING
[exit status 134]
$ ./bugs stack-overflow
bugs.cpp:28:43: runtime error: index 5 out of bounds for type 'int[5]'
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior bugs.cpp:28:43
==…==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x…
READ of size 4 at 0x… thread T0
#0 in stack_overflow() bugs.cpp:28
#1 in main bugs.cpp:37
Address 0x… is located in stack of thread T0 at offset 52 in frame
#0 in stack_overflow() bugs.cpp:25
SUMMARY: AddressSanitizer: stack-buffer-overflow
[40 more lines of the report left out]
==…==ABORTING
[exit status 134]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 bugs_session.py
These reports are shortened to their key lines. Reading each one:
- heap-use-after-free: the read on line 7, the
deleteon line 6 that freed the memory, and thenewon line 5. Frames#0to#7are insidestd::println, which made the actual read; the first frame inbugs.cppnames your line. - stack-use-after-scope:
scorelived in the inner block, and line 16 reads it throughbestafter the block ended. ASan poisons a local variable’s memory when its scope ends, which both GCC and Clang do by default with-fsanitize=address. - double-free: line 22 deletes memory that line 21 had already deleted; the report shows both
deletes and the allocation. - stack-buffer-overflow: reading
last_five[5]from a 5-element local array. Here UBSan spoke first, because the array’s bound is known at compile time (index 5 out of bounds for type 'int[5]'), and then ASan stopped the read.
A fifth kind, container-overflow, appears in the exercise below: libc++, the library of Apple clang, marks the
spare capacity of a std::vector as unusable, so reading a reserved but unused element is reported even though the
memory is allocated.
The error names you will meet most, what each one means and its usual cause:
heap-buffer-overflow: an access just outside a heap block. Usually an off-by-one index, or a wrong size passed tonew[].stack-buffer-overflow: an access just outside a local array. Usually an off-by-one, orstrcpyinto a buffer that is too small.global-buffer-overflow: an access just outside a global array, often through an index read from input that nobody checked.heap-use-after-free: an access to freed heap memory, through a dangling pointer, a reference into a vector that reallocated, or a view of a string that was destroyed.stack-use-after-scope: an access to a local variable after its block ended, through a pointer or reference kept too long.double-freeandbad-free: adeleteof memory twice, or of memory thatnewnever returned, usually because two owners share one raw pointer.container-overflow: an access to a vector’s capacity beyond its size, typically indexing afterreserveinstead of adding elements withpush_back.
Three UBSan checks
AddressSanitizer only checks memory. This program has two bugs it ignores: it reads a 4-byte Header at an address
that is not a multiple of 4, and it shifts a 32-bit value by 32:
#include <cstdint>
#include <print>
struct Header {
std::uint32_t length;
};
// A message as it arrived: a type byte, then a 4-byte length (7), then the rest.
alignas(4) unsigned char packet[8] = {0xFF, 7, 0, 0, 0, 0, 0, 0};
// Treats bytes as a Header: only correct if they start at a multiple of 4.
std::uint32_t length_of(const unsigned char* bytes) {
return reinterpret_cast<const Header*>(bytes)->length;
}
// A mask with the low `bits` bits set: only correct for bits from 0 to 31.
std::uint32_t mask_of(int bits) {
return (1u << bits) - 1;
}
int main() {
std::println("length = {}", length_of(packet + 1)); // packet + 1 is not 4-byte aligned
std::println("mask = {:#x}", mask_of(32));
std::println("main ends");
} from session import run
# Two kinds of undefined behaviour that AddressSanitizer does not look for, built with UBSan.
run("""
clang++ -std=c++23 -O2 ub.cpp -o ub && ./ub
clang++ -std=c++23 -g -fsanitize=undefined ub.cpp -o ub && ./ub
clang++ -std=c++23 -g -fsanitize=undefined -fno-sanitize-recover=undefined ub.cpp -o ub && ./ub
""") Output
$ clang++ -std=c++23 -O2 ub.cpp -o ub && ./ub
length = 7
mask = 0x7
main ends
$ clang++ -std=c++23 -g -fsanitize=undefined ub.cpp -o ub && ./ub
ub.cpp:13:52: runtime error: member access within misaligned address 0x… for type 'const Header', which requires 4 byte alignment
0x…: note: pointer points here
00 00 00 ff 07 00 00 00 00 00 00 …
^
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior ub.cpp:13:52
ub.cpp:13:52: runtime error: load of misaligned address 0x… for type 'const std::uint32_t' (aka 'const unsigned int'), which requires 4 byte alignment
0x…: note: pointer points here
00 00 00 ff 07 00 00 00 00 00 00 …
^
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior ub.cpp:13:52
length = 7
ub.cpp:18:16: runtime error: shift exponent 32 is too large for 32-bit type 'unsigned int'
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior ub.cpp:18:16
mask = 0x0
main ends
$ clang++ -std=c++23 -g -fsanitize=undefined -fno-sanitize-recover=undefined ub.cpp -o ub && ./ub
ub.cpp:13:52: runtime error: member access within misaligned address 0x… for type 'const Header', which requires 4 byte alignment
0x…: note: pointer points here
00 00 00 ff 07 00 00 00 00 00 00 …
^
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior ub.cpp:13:52
Abort trap: 6
[exit status 134]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 ub_session.py
The plain build prints length = 7, because the arm64 processor of this Mac happens to allow the misaligned read, and
mask = 0x7, a value that has nothing to do with the shift: the optimiser saw that a shift by 32 is undefined and
dropped the computation, so the slot meant for the mask still held the 7 of the line before. The UBSan build, which
really shifts, prints 0x0. A processor that faults on misaligned loads would stop the first read too. UBSan names
both problems: the member access and the load on line 13,
with the bytes around the address and a caret under it, and the shift on line 18. The fix for the first is the one
from the memory layout lesson: copy the bytes with std::memcpy, which has no alignment requirement and no aliasing
problem; the fix for the second is to treat 32 separately.
Notice what happened after each report: the program went on and printed main ends, with exit status 0. Most UBSan
checks recover by default, so a test suite would pass. -fno-sanitize-recover=undefined (or =all) makes the first
report fatal, as the last build shows, and belongs in every test or CI build.
Which sanitizers exist where
- AddressSanitizer (memory errors): Linux with GCC and Clang, macOS arm64 with Apple clang, and Windows with
MSVC’s
/fsanitize=addresson x86 and x64. - UndefinedBehaviorSanitizer (other undefined behaviour): Linux and macOS arm64; not MSVC.
- LeakSanitizer (memory never freed): Linux, where it is on by default in ASan builds; not on macOS arm64 or MSVC.
- ThreadSanitizer (data races): Linux and macOS arm64; not MSVC.
- MemorySanitizer (reads of uninitialised memory): Clang only, on Linux, NetBSD and FreeBSD.
The Linux entries and the platform lists come from the Clang and GCC documentation, and GCC does not offer MemorySanitizer at all. Microsoft’s documentation limits MSVC to AddressSanitizer, on x86 and x64. For macOS, the compilers of this Mac answer for themselves:
#include <print>
int main() {
int* scores = new int[100]; // never deleted: a leak
scores[0] = 42;
std::println("first score {}", scores[0]);
} from session import run
# Which sanitizers this Mac's compilers offer: macOS 26 on arm64, Apple clang 21 and Homebrew GCC 16.
run("""
clang++ -std=c++23 -g -fsanitize=address leak.cpp -o leak && ASAN_OPTIONS=detect_leaks=1 ./leak
clang++ -std=c++23 -fsanitize=leak leak.cpp -o leak
clang++ -std=c++23 -fsanitize=memory leak.cpp -o leak
clang++ -std=c++23 -fsanitize=thread leak.cpp -o leak && ./leak
g++-16 -std=c++23 -fsanitize=undefined leak.cpp -o leak && ./leak
g++-16 -std=c++23 -fsanitize=memory leak.cpp -o leak
g++-16 -std=c++23 -fsanitize=thread -c leak.cpp -o leak.o && g++-16 -fsanitize=thread leak.o -o leak
""", max_lines=3) Output
$ clang++ -std=c++23 -g -fsanitize=address leak.cpp -o leak && ASAN_OPTIONS=detect_leaks=1 ./leak
==…==AddressSanitizer: detect_leaks is not supported on this platform.
Abort trap: 6
[exit status 134]
$ clang++ -std=c++23 -fsanitize=leak leak.cpp -o leak
clang++: error: unsupported option '-fsanitize=leak' for target 'arm64-apple-darwin25.6.0'
[exit status 1]
$ clang++ -std=c++23 -fsanitize=memory leak.cpp -o leak
clang++: error: unsupported option '-fsanitize=memory' for target 'arm64-apple-darwin25.6.0'
[exit status 1]
$ clang++ -std=c++23 -fsanitize=thread leak.cpp -o leak && ./leak
first score 42
$ g++-16 -std=c++23 -fsanitize=undefined leak.cpp -o leak && ./leak
first score 42
$ g++-16 -std=c++23 -fsanitize=memory leak.cpp -o leak
g++-16: error: unrecognized argument to '-fsanitize=' option: 'memory'
[exit status 1]
$ g++-16 -std=c++23 -fsanitize=thread -c leak.cpp -o leak.o && g++-16 -fsanitize=thread leak.o -o leak
Undefined symbols for architecture arm64:
"___tsan_func_entry", referenced from:
std::to_chars(char*, char*, unsigned char, int) in leak.o
[152 more lines]
[exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 platform_session.py
Apple clang offers ASan, UBSan and ThreadSanitizer, but refuses -fsanitize=leak and -fsanitize=memory, and its
ASan runtime rejects detect_leaks=1, although Clang’s own documentation lists macOS as supported. Homebrew’s GCC
builds UBSan programs, and ASan ones, but has no ThreadSanitizer library on macOS, so the link fails. To find leaks,
run the tests on Linux, where LeakSanitizer is part of every ASan build; ThreadSanitizer cannot be combined with
AddressSanitizer in one build.
Online judges
LeetCode’s help centre says its C++ judge compiles with Clang at -O2, using libstdc++ from GCC, with
AddressSanitizer enabled. A judge result that starts with AddressSanitizer: heap-buffer-overflow is therefore
exactly the report of this lesson: look for the first frame in your solution, and for how far outside the region
the access landed.
What a sanitized build costs
Clang’s documentation puts the typical slowdown of AddressSanitizer at 2x, with more memory used, especially for
programs that make many small allocations. Here is one measurement on this Mac, a loop over 20 million ints,
built at the same optimisation level without and with both sanitizers:
#include <chrono>
#include <numeric>
#include <print>
#include <vector>
int main() {
std::vector<int> values(20'000'000);
std::iota(values.begin(), values.end(), 0);
auto start = std::chrono::steady_clock::now();
long long total = 0;
for (int round = 0; round < 10; ++round)
for (std::size_t i = 0; i < values.size(); ++i) total += values[i] % 7;
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - start).count();
std::println("total {}, {} ms", total, ms);
} from session import run
# The same loop, built without and with AddressSanitizer and UBSan, at the same optimisation level.
run("""
clang++ -std=c++23 -O1 cost.cpp -o cost && ./cost
clang++ -std=c++23 -O1 -g -fsanitize=address,undefined -fno-omit-frame-pointer cost.cpp -o cost-checked && ./cost-checked
""") Output
$ clang++ -std=c++23 -O1 cost.cpp -o cost && ./cost total 599999970, 261 ms $ clang++ -std=c++23 -O1 -g -fsanitize=address,undefined -fno-omit-frame-pointer cost.cpp -o cost-checked && ./cost-checked total 599999970, 767 ms
This output changes from run to run: Run times change from run to run with whatever else the computer is doing, so only the shape of the output is fixed.
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 cost_session.py
The sanitized loop is much slower, because every element access now checks its shadow byte and every addition its overflow. That cost is why sanitizers belong in debug builds, tests and continuous integration, not in timing runs or releases. A sensible setup:
- A test build with
-fsanitize=address,undefined -fno-sanitize-recover=all -g -fno-omit-frame-pointer, run by every test suite. - On Linux, a separate ThreadSanitizer build for code with threads, and LeakSanitizer through ASan.
- For releases, the cheap checks of a hardened standard library, which are designed for production; Clang’s documentation describes its full UBSan runtime as meant for testing, and offers a minimal runtime or trap mode for production builds.
- Sanitizers only check the code paths that run, so pair them with tests that reach the edge cases.
Your turn: name the bug from its report
drill.cpp holds six new bugs. Read the code, then answer the questions below: each one shows the shortened report
of one run, and asks what the bug is and where to fix it. The full session follows the questions, so that you can
check every report against its run.
#include <cstring>
#include <print>
#include <string>
#include <string_view>
#include <vector>
int counts[4];
void save_name() {
char name[8];
std::strcpy(name, "Priyanka");
std::println("saved {}", name);
}
void count_grade(int grade) {
++counts[grade];
}
std::string_view welcome() {
std::string text = "Welcome to the C++ memory lab, batch 7";
return text;
}
int fifth_reading() {
std::vector<int> readings;
readings.reserve(10);
readings.push_back(42);
return readings[4];
}
int average(int total, int count) {
return total / count;
}
int factorial(int n) {
int result = 1;
for (int i = 2; i <= n; ++i) result *= i;
return result;
}
int main(int argc, char* argv[]) {
std::string_view which = argc > 1 ? argv[1] : "";
if (which == "1") save_name();
if (which == "2") count_grade(4);
if (which == "3") std::println("{}", welcome());
if (which == "4") std::println("{}", fifth_reading());
if (which == "5") std::println("{}", average(0, 0));
if (which == "6") std::println("{}", factorial(13));
} Read six reports
6 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
from session import run
# Six bugs from drill.cpp, one per run, built with AddressSanitizer and UBSan.
run("""
clang++ -std=c++23 -g -fsanitize=address,undefined -fno-omit-frame-pointer drill.cpp -o drill
./drill 1
./drill 2
./drill 3
./drill 4
./drill 5
./drill 6
""") Output
$ clang++ -std=c++23 -g -fsanitize=address,undefined -fno-omit-frame-pointer drill.cpp -o drill
drill.cpp:11:5: warning: 'strcpy' will always overflow; destination buffer has size 8, but the source string has length 9 (including NUL byte) [-Wfortify-source]
11 | std::strcpy(name, "Priyanka");
| ^
drill.cpp:21:12: warning: address of stack memory associated with local variable 'text' returned [-Wreturn-stack-address]
21 | return text;
| ^~~~
2 warnings generated.
$ ./drill 1
==…==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x…
WRITE of size 9 at 0x… thread T0
#1 in save_name() drill.cpp:11
#2 in main drill.cpp:43
Address 0x… is located in stack of thread T0 at offset 40 in frame
#0 in save_name() drill.cpp:9
SUMMARY: AddressSanitizer: stack-buffer-overflow
[40 more lines of the report left out]
==…==ABORTING
[exit status 134]
$ ./drill 2
==…==ERROR: AddressSanitizer: global-buffer-overflow on address 0x…
READ of size 4 at 0x… thread T0
#0 in count_grade(int) drill.cpp:16
#1 in main drill.cpp:44
0x… is located 0 bytes after global variable 'counts' defined in 'drill.cpp:7' (0x…) of size 16
SUMMARY: AddressSanitizer: global-buffer-overflow
[34 more lines of the report left out]
==…==ABORTING
[exit status 134]
$ ./drill 3
==…==ERROR: AddressSanitizer: heap-use-after-free on address 0x…
READ of size 38 at 0x… thread T0
#28 in main drill.cpp:45
0x… is located 0 bytes inside of 40-byte region [0x…,0x…)
freed by thread T0 here:
#2 in welcome() drill.cpp:22
#3 in main drill.cpp:45
previously allocated by thread T0 here:
#4 in welcome() drill.cpp:20
#5 in main drill.cpp:45
SUMMARY: AddressSanitizer: heap-use-after-free
[72 more lines of the report left out]
==…==ABORTING
[exit status 134]
$ ./drill 4
==…==ERROR: AddressSanitizer: container-overflow on address 0x…
READ of size 4 at 0x… thread T0
#0 in fifth_reading() drill.cpp:28
#1 in main drill.cpp:46
0x… is located 16 bytes inside of 40-byte region [0x…,0x…)
allocated by thread T0 here:
#9 in fifth_reading() drill.cpp:26
#10 in main drill.cpp:46
SUMMARY: AddressSanitizer: container-overflow
[47 more lines of the report left out]
==…==ABORTING
[exit status 134]
$ ./drill 5
drill.cpp:32:18: runtime error: division by zero
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior drill.cpp:32:18
0
$ ./drill 6
drill.cpp:37:41: runtime error: signed integer overflow: 479001600 * 13 cannot be represented in type 'int'
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior drill.cpp:37:41
1932053504
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 drill_session.py
Before the runs, notice the build: Clang warned about two of the six bugs while compiling (-Wfortify-source for the
strcpy and -Wreturn-stack-address for the returned view). Warnings are the cheapest sanitizer of all; read them
first.
Interview questions
Fresher · What does AddressSanitizer do, and what does it cost?
AddressSanitizer is a compiler feature, -fsanitize=address in GCC and Clang and /fsanitize=address in MSVC,
that adds a check to every memory access and replaces the allocator. It keeps shadow memory that records which bytes
may be used, puts poisoned redzones around each allocation and keeps freed memory poisoned for a while, so the first
out-of-bounds access, use after free, use after scope or double free stops the program with a report: the kind of
error, the faulty line, and where the memory was allocated and freed. The price is speed and memory. Clang’s documentation gives a typical
slowdown of 2x, and memory use grows, most for small allocations. So it belongs in debug and test builds and CI,
not in releases or timing runs, and it only finds bugs on the code paths that actually run.
Key takeaways
- Build with
-fsanitize=address,undefined -g -fno-omit-frame-pointerfor tests, and add-fno-sanitize-recover=allso that every report fails the run. - Read a report in four steps: the error kind, the access and the first frame in your code, how far outside which region it landed, and where that memory was allocated or freed.
- ASan finds out-of-bounds accesses on the heap, the stack and globals, uses after free and after scope, double frees and, with libc++, container overflows. UBSan finds signed overflow, bad shifts, misaligned pointers, division by zero and known-bound array indexes.
- MSVC offers only AddressSanitizer, on x86 and x64. On macOS arm64, Apple clang has ASan, UBSan and ThreadSanitizer but no LeakSanitizer or MemorySanitizer, so run leak checks on Linux.
- Sanitized builds run several times slower and use more memory: use them for debugging and CI, never for timing.
References
- AddressSanitizer (Clang documentation) (LLVM Project)
- UndefinedBehaviorSanitizer (Clang documentation) (LLVM Project)
- LeakSanitizer (Clang documentation) (LLVM Project)
- MemorySanitizer (Clang documentation) (LLVM Project)
- ThreadSanitizer (Clang documentation) (LLVM Project)
- Program Instrumentation Options (GCC) (GNU Project)
- AddressSanitizer (Microsoft C++ documentation) (Microsoft)
- AddressSanitizer shadow bytes (Microsoft C++ documentation) (Microsoft)
- AddressSanitizer: A Fast Address Sanity Checker (USENIX ATC 2012) (USENIX Association)
- What are the environments for the programming languages? (LeetCode help centre, archived copy) (LeetCode)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress