Sanitizers

The undefined behaviour in lesson 34 is invisible until it breaks. GCC can compile the program with extra checks that turn the invisible into a report: AddressSanitizer watches memory, UndefinedBehaviorSanitizer watches arithmetic, and LeakSanitizer watches allocations. The checks cost run time, so they are a debugging build, not the release build.

AddressSanitizer

AddressSanitizer places a red zone around every heap block and checks each access. The loop below writes five ints into a block for four, so the last write lands in the red zone. The report names the function, the file and the line of the write, and the line of the allocation.

#include <stdio.h> #include <stdlib.h> int main(void) { int *values = malloc(4 * sizeof *values); if (values == NULL) { return 1; } for (int i = 0; i <= 4; i++) { values[i] = i; } printf("%d\n", values[4]); free(values); return 0; }

gcc -fsanitize=address -g -Wall -Wextra overflow.c -o overflow
./overflow 2>&1 | head -n 14

=================================================================
==745==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x7affeee60020 at pc 0x5e89d6e4b281 bp 0x7ffffde3c3b0 sp 0x7ffffde3c3a0
WRITE of size 4 at 0x7affeee60020 thread T0

#0 0x5e89d6e4b280 in main /tmp/c3/L35/overflow.c:12
#1 0x7edfef950153 in libc_start_main_stage2 src/env/__libc_start_main.c:95

0x7affeee60020 is located 0 bytes after 16-byte region [0x7affeee60010,0x7affeee60020)
allocated by thread T0 here:
#0 0x7edfef338a1d in malloc (/usr/lib/libasan.so.8+0xeda1d) (BuildId: 49cdec61ea4a7e68d1b3321b11e733d1f2124ac9)
#1 0x5e89d6e4b216 in main /tmp/c3/L35/overflow.c:6
#2 0x7edfef950153 in libc_start_main_stage2 src/env/__libc_start_main.c:95

The process id and the addresses change on every run; the file, the line and the type of error do not. The last line of the report, which head cut off here, is SUMMARY: AddressSanitizer: heap-buffer-overflow followed by the same source location.

UndefinedBehaviorSanitizer

UBSan instruments arithmetic and reports the first undefined operation. By default it continues with the wrapped result; with -fno-sanitize-recover=all it stops the program immediately, so a test run fails as soon as the bug happens.

#include <limits.h> #include <stdio.h> int main(void) { int value = INT_MAX; value = value + 1; printf("%d\n", value); return 0; }

gcc -fsanitize=undefined -fno-sanitize-recover=all -g -Wall -Wextra ub.c -o ub
./ub; echo "status: $?"

ub.c:8:11: runtime error: signed integer overflow: 2147483647 + 1 cannot be represented in type 'int'
status: 1

There is no printf output because the program stopped at the faulty assignment. The status 1 is what a test runner or a continuous integration job reads, which is why the no-recover flag is the usual choice for tests.

LeakSanitizer

AddressSanitizer includes LeakSanitizer, which reports blocks that were allocated and never freed when the program exits. The leak from lesson 32 is invisible without it, and here it becomes a report with the allocation site:

#include <stdio.h> #include <stdlib.h> int main(void) { int *value = malloc(sizeof *value); if (value == NULL) { return 1; } *value = 7; printf("%d\n", *value); return 0; }

gcc -fsanitize=address -g -Wall -Wextra leak.c -o leak_asan
./leak_asan 2>&1 | head -n 12

7

=================================================================
==774==ERROR: LeakSanitizer: detected memory leaks

Direct leak of 4 byte(s) in 1 object(s) allocated from:
#0 0x7138e772da1d in malloc (/usr/lib/libasan.so.8+0xeda1d) (BuildId: 49cdec61ea4a7e68d1b3321b11e733d1f2124ac9)
#1 0x57a7764b81f6 in main /tmp/c3/L35/leak.c:6
#2 0x7138e7d45153 in libc_start_main_stage2 src/env/__libc_start_main.c:95

SUMMARY: AddressSanitizer: 4 byte(s) leaked in 1 allocation(s).

The program printed its value and exited with status 1, because the leak checker runs at exit and turns the leak into a failure. The report names the four bytes and the line of the allocation, which is all a developer needs to add the missing free.

The same bug without flags

The leak program from the previous section compiles and runs without a single warning when the sanitizer is not enabled. That is the problem the sanitizers solve: the same code, the same output, and no sign that four bytes were lost.

#include <stdio.h> #include <stdlib.h> int main(void) { int *value = malloc(sizeof *value); if (value == NULL) { return 1; } *value = 7; printf("%d\n", *value); return 0; }

gcc -Wall -Wextra leak.c -o leak_plain
./leak_plain; echo "status: $?"

7
status: 0

The normal build still has its uses: it is faster and it is what the users run. The practical setup is two builds from one source tree, one with the flags for tests and one without for release, which is what the build lessons later in the course automate.

A double free report

The double free from lesson 31 crashes without a message in the normal build. With AddressSanitizer the library catches the second call and explains that the block is already free, with both call sites in the stack:

#include <stdio.h> #include <stdlib.h> int main(void) { int *value = malloc(sizeof *value); if (value == NULL) { return 1; } *value = 3; free(value); free(value); return 0; }

gcc -fsanitize=address -g -Wall -Wextra double.c -o double_asan
./double_asan 2>&1 | head -n 10

=================================================================
==773==ERROR: AddressSanitizer: attempting double-free on 0x78c206c20010 in thread T0:
#0 0x7ca2071008ad (/usr/lib/libasan.so.8+0xec8ad) (BuildId: 49cdec61ea4a7e68d1b3321b11e733d1f2124ac9)
#1 0x5d6f3968a241 in main /tmp/c3/L35/double.c:13
#2 0x7ca207719153 in libc_start_main_stage2 src/env/__libc_start_main.c:95

0x78c206c20010 is located 0 bytes inside of 4-byte region [0x78c206c20010,0x78c206c20014)
freed by thread T0 here:
#0 0x7ca2071008ad (/usr/lib/libasan.so.8+0xec8ad) (BuildId: 49cdec61ea4a7e68d1b3321b11e733d1f2124ac9)
#1 0x5d6f3968a235 in main /tmp/c3/L35/double.c:12

The first stack ends at line 13, the second free, and the second stack ends at line 12, the free that released the block. The compiler also warns about the pattern at build time, as lesson 31 showed; the sanitizer adds the runtime proof and the exact address.

Exercises

  1. Compile the heap overflow program with -fsanitize=address,undefined and read which sanitizer reports first.
  2. Add a use-after-free to the leak program and compare the AddressSanitizer report with the compile-time warning.
  3. Build a program of your own twice, with and without -fsanitize=address, and compare the run time of a loop that allocates a thousand small blocks.

Questions

A new set of questions in a new order every time. Choose one answer for each, then check.

  1. What does AddressSanitizer check?

    Answer

    Memory accesses around heap and stack objects

    ASan places red zones around objects and checks each access, so an out-of-bounds write is reported at the line that makes it.

  2. What does -fno-sanitize-recover=all change about UBSan?

    Answer

    It stops the program at the first report

    Recovery is on by default for UBSan; the flag makes the run fail as soon as an undefined operation happens.

  3. When does LeakSanitizer report a block?

    Answer

    At program exit

    The leak checker runs at exit and reports blocks that are still allocated with no remaining reference.

  4. Why are sanitizer builds not the release build?

    Answer

    They add run-time checks and slow the program

    The checks cost time and memory, which is acceptable in tests and not in the build users run.

  5. This program is built with gcc -std=c17 -fsanitize=address -g -Wall -Wextra overflow.c -o overflow. What error does AddressSanitizer report? #include <stdio.h> #include <stdlib.h> int main(void) { int *values = malloc(4 * sizeof *values); if (values == NULL) return 1; for (int i = 0; i <= 4; i++) values[i] = i; printf("%d\n", values[4]); free(values); return 0; }

    Answer

    heap-buffer-overflow

    The last write lands just past the sixteen-byte heap block, inside the red zone that ASan monitors.

  6. This program is built with gcc -std=c17 -fsanitize=undefined -fno-sanitize-recover=all -g -Wall -Wextra ub.c -o ub and run. What exit status does the shell report? #include <limits.h> #include <stdio.h> int main(void) { int value = INT_MAX; value = value + 1; printf("%d\n", value); return 0; }

    Answer

    1

    The program stops at the faulty assignment before printing, and the sanitizer runtime exits with status 1.

  7. This program is built as leak and run with ./leak; echo "status: $?". What does the shell print? #include <stdio.h> #include <stdlib.h> int main(void) { int *value = malloc(sizeof *value); if (value == NULL) return 1; *value = 7; printf("%d\n", *value); return 0; }

    Answer

    7 / status: 0

    The normal build prints the value and exits successfully; the lost four bytes leave no trace.

  8. This program is built with gcc -std=c17 -fsanitize=address -g -Wall -Wextra leak.c -o leak_asan. What does LeakSanitizer report? #include <stdio.h> #include <stdlib.h> int main(void) { int *value = malloc(sizeof *value); if (value == NULL) return 1; *value = 7; printf("%d\n", *value); return 0; }

    Answer

    Direct leak of 4 byte(s) in 1 object(s)

    One int is four bytes on this platform, and the report names the allocation site.

  9. In the AddressSanitizer report for a double free, which call appears at the top of the first stack?

    Answer

    The second free

    The first stack starts at the second free, while the second stack shows the free that released the block.

  10. What is the practical setup for sanitizers in a project?

    Answer

    Two builds from one source, one with sanitizers for tests

    The checked build runs the tests, and the plain build is what ships; both come from the same sources.

The code on this page was run with gcc (Alpine 15.2.0) 15.2.0 with the AddressSanitizer and UndefinedBehaviorSanitizer runtimes on Alpine Linux 3.24.3 (x86_64) in a Docker container.

Article author: Arthur Isaev

Banner image