Contact
Evil Genius SocietyHomeLearnForumLeaderboardSocialsMessages
← Back to module

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, a READ of size 4 — reading one int.
  • Where: buggy.cpp:6, inside sum_range, called from main at line 17.
  • What memory: 0 bytes after a 20-byte region — immediately past a five-int array.
  • Whose memory: allocated at buggy.cpp:12, which is the new 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:

  1. The error type — heap-buffer-overflow, heap-use-after-free, detected memory leaks.
  2. The operation — READ of size 4, WRITE of size 8.
  3. The stack at the moment of the error — read top to bottom, most recent first.
  4. 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,undefined composes; 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=all makes 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.