Reading AddressSanitizer Reports
Decode the reports the sandbox prints for heap-buffer-overflow, use-after-free, and leaks (which line failed, where the memory was allocated and freed), then fix a broken program using only its report.
A Report Is a Form, Not a Wall
Since chapter 1 this course has been handing you sanitizer reports and telling you what they said. That was a loan, and this lesson pays it back. What arrives on standard error when a program goes wrong looks like a wall of hexadecimal, and the first instinct is to scroll past it to the one line that names your file. Resist that, because the wall is a form, and it has the same fields filled in every time: what kind of mistake this was, what the program was doing at that instant, where in your code it happened, and which block of memory it happened to. Learn the fields once and every future report is a lookup rather than a puzzle. The address digits, the pc and bp and sp values, and the shadow-byte table at the bottom are for people debugging the sanitizer itself; you can read every report in this course without them.
The next program is wrong on purpose, and the mistake is chapter 4 lesson 1's fencepost moved to the heap.
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int count = 0;
if (scanf("%d", &count) != 1 || count <= 0)
{
printf("expected a positive count\n");
return 0;
}
int *values = malloc(count * sizeof *values);
if (values == NULL)
{
printf("could not allocate\n");
return 0;
}
for (int i = 0; i <= count; ++i)
{
values[i] = i * 10;
}
printf("last is %d\n", values[count - 1]);
free(values);
return 0;
}
The loop says i <= count where it should say i < count, so on a block of four it writes a fifth element. gcc compiles it without a word, and that silence is worth a note: lesson 1's stack version drew a -Warray-bounds warning because the bound was a literal the compiler could reason about, while here the bound arrives from scanf and there is nothing left to reason about. Given 4, the program never reaches its printf.
=================================================================
==14==ERROR: AddressSanitizer: heap-buffer-overflow on address 0xfc1f8fbe0020 at pc 0x000000400b6c bp 0xffffe5cdcf30 sp 0xffffe5cdcf48
WRITE of size 4 at 0xfc1f8fbe0020 thread T0
#0 0x000000400b68 in main /tmp/blocks.c:24
0xfc1f8fbe0020 is located 0 bytes after 16-byte region [0xfc1f8fbe0010,0xfc1f8fbe0020)
allocated by thread T0 here:
#0 0xffff91124488 in malloc (/usr/local/lib64/libasan.so.8+0xd4488)
#1 0x000000400ab0 in main /tmp/blocks.c:14
SUMMARY: AddressSanitizer: heap-buffer-overflow /tmp/blocks.c:24 in main
Take the fields in order. The ERROR line names the kind of mistake, heap-buffer-overflow, and that name alone tells you which of a small number of stories you are in: something on the heap was touched outside the block it belongs to. ==14== is the process id and carries no meaning for you. The next line, WRITE of size 4, is what the program was doing: not reading, writing, and moving four bytes, which on this platform is exactly one int. Read those two lines together and you already know you are looking for a line that assigns to one element of an int block.
Under that comes the first stack trace, which is where it happened, and it has one reading rule: start at #0 and go down until you reach the first frame naming a file you wrote. Here #0 is already main /tmp/blocks.c:24, and line 24 is values[i] = i * 10;. That rule matters because #0 is often not yours. Look at the second trace, the one under allocated by thread T0 here: its #0 is malloc inside libasan.so, which is the sanitizer's own code, and your line is #1, main /tmp/blocks.c:14. Frames above your own are the library you called into, and they are never the bug.
Between the two traces sits the line that does the real work, and it is the one beginners skip. is located 0 bytes after 16-byte region [0xfc1f8fbe0010,0xfc1f8fbe0020) states the relationship between the address you touched and a block the sanitizer knows about. The block is 16 bytes, an int is 4, so this is a block of four elements with valid indices 0 through 3, which confirms it is the block you think it is. The bad address is 0 bytes after the end, meaning it is the very first byte past the block, which is element 4. A loop reaching element 4 of a four-element block is i <= count, and you have the bug without opening a debugger. When the figure is not zero, divide: a real run of the same program indexing values[count + 2] reported READ of size 4 at an address located 8 bytes after 16-byte region, and 8 divided by 4 is 2, so the access was two elements past the end, at index 6. Finally, allocated by names where the memory came from, line 14 here, and in a use-after-free or double-free report a freed by thread T0 here stack appears alongside it naming where it was released. The SUMMARY line at the bottom repeats the kind and your location, and is the one line worth reading first when you only want to know where to look.
Every Report in Your Collection
You have now met the whole set that a course this size runs into. This is a recap rather than a re-demonstration, and the point of the list is that the first word of a report narrows the search before you read anything else.
SEGV on unknown address 0x000000000000(chapters 1 and 3): something dereferenced a null pointer. You produced it three ways:scanf("%d", age)with the&left off, a plain null dereference, and a function returning the address of a local, which at-O2handed back a null pointer.stack-overflow(chapter 3): the call stack ran out of room, which in practice means a recursion with no base case or one that never reaches it.stack-use-after-scope(chapter 3): a pointer to an object in an inner block was used after that block closed.stack-buffer-overflow(chapter 4 lesson 1): an array with automatic storage duration was indexed outside its bounds. Its report names the offending variable inside the frame, which the heap version cannot do.heap-buffer-overflow(this lesson): amallocblock was indexed outside its bounds. Instead of a variable name you get the block's size and your distance from it.heap-use-after-free(chapter 4 lesson 3): a block was used afterfree. The report carries three stacks: the access, thefreed by, and thepreviously allocated by.attempting double-free(chapter 4 lesson 3): the same block was passed tofreetwice, and again you get both the earlier free and the allocation.LeakSanitizer: detected memory leaks(chapter 4 lesson 4): a live block had no pointer left to it when the program ended.
One structural difference cuts across that list and decides how you read the output. Everything above the last entry is a memory access error: the sanitizer stops the program at the offending instruction, so the run ends there and any output the program had not printed yet never appears. A leak is the opposite, reported only after the program has run to completion and exited normally, which is why a leaking program prints all of its expected output and still fails.
Reading One You Have Not Seen Before
Anatomy is not the skill. The skill is the sequence you run when a report you did not expect appears, so here is the sequence on a second program. The next program is wrong on purpose, and this time it is not the loop.
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int count = 0;
if (scanf("%d", &count) != 1 || count <= 0)
{
printf("expected a positive count\n");
return 0;
}
int *squares = malloc(count);
if (squares == NULL)
{
printf("could not allocate\n");
return 0;
}
for (int i = 0; i < count; ++i)
{
squares[i] = i * i;
}
printf("last square is %d\n", squares[count - 1]);
free(squares);
return 0;
}
Given 4, that produces a report whose headline is word for word the one you just read.
=================================================================
==14==ERROR: AddressSanitizer: heap-buffer-overflow on address 0xfc1faafe0014 at pc 0x000000400b6c bp 0xffffd5d2bf90 sp 0xffffd5d2bfa8
WRITE of size 4 at 0xfc1faafe0014 thread T0
#0 0x000000400b68 in main /tmp/squares.c:24
0xfc1faafe0014 is located 0 bytes after 4-byte region [0xfc1faafe0010,0xfc1faafe0014)
allocated by thread T0 here:
#0 0xffffac474488 in malloc (/usr/local/lib64/libasan.so.8+0xd4488)
#1 0x000000400ab4 in main /tmp/squares.c:14
SUMMARY: AddressSanitizer: heap-buffer-overflow /tmp/squares.c:24 in main
Now run the sequence out loud. Which kind? heap-buffer-overflow, so a heap block was touched outside itself. Which line failed? First frame in my file is #0, squares.c:24, which is squares[i] = i * i;, a WRITE of size 4, one int. What is the memory's story? It is 0 bytes after a region, so the first byte past the end, and the region is 4 bytes. That number is the whole case. Four bytes is one int, and the loop is bounded by i < count with count of 4, so the indexing is not wrong at all. The block is. Hypothesis: line 24 is innocent and the mistake is on line 14, the allocation the report so helpfully pointed at, which says malloc(count) and asks for four bytes where it meant four elements. Fix: malloc(count * sizeof *squares), which is lesson 3's rule about sizing a request from the object rather than by hand. Re-run: the program prints last square is 9 and exits with nothing on standard error.
Two habits are worth extracting from that. The first is that the failing line is not always the wrong line. The sanitizer reports where the damage was done, and the allocation stack exists precisely because the cause is often there instead, so read both before you edit either. The second is that two reports with an identical first line can have unrelated causes, and here the single field separating them was the region size: 16 bytes said the block was right and the index was wrong, 4 bytes said the index was right and the block was wrong. Always fix and re-run until the run is clean, because the sanitizer stops at the first error it meets and has nothing to say about anything later in the program.
What AddressSanitizer Does Not See
Everything so far invites a conclusion that would be a bad habit to leave with, so state it plainly: a clean run does not prove your program is free of undefined behaviour. AddressSanitizer is a memory-error detector, and there is a great deal of undefined behaviour that has nothing to do with memory at all. Signed integer overflow is the clearest case. A program on this platform that computed 2147483600 + 100 in an int printed big is -2147483596 and ran to completion with an empty standard error, and that wrap is not a guarantee you may rely on: signed overflow is undefined behaviour, and the same expression can behave differently at another optimization level or in a different compiler. Shifting by at least the width of the type is the same story, and so is dividing an integer by zero.
Reading uninitialized memory is the near miss that catches people out. Lesson 3 showed a fresh malloc block printing -1094795586, which is the sanitizer's own fill byte repeated, along with a -Wmaybe-uninitialized warning from gcc and no sanitizer report whatsoever. And no tool at all will notice that your loop sums the wrong elements, or that you wrote - where you meant +. So hold the sanitizer in its proper place. A clean run is necessary, never sufficient. It is one of three checks, alongside compiler warnings, which caught mistakes here that the sanitizer could not, and the discipline of writing code whose meaning is defined in the first place. Tests tell you the answer is right; the sanitizer tells you the memory was handled legally; only care tells you the program means what you think it means.
Key Takeaways
- A sanitizer report is a form with fixed fields, not prose. Read the kind on the
ERRORline, the operation on theREAD/WRITE of size Nline, the location in the first stack trace, and the memory's story in theis locatedandallocated bylines. Ignore the raw addresses,pc,bp,spand the shadow bytes. - In any stack trace, your line is the first frame naming a file you wrote, which is often not
#0. In an allocation stack#0ismallocinside the sanitizer's own library and your line is#1. - The
is locatedline is the one that solves the case. Divide the region size by the element size for the block's element count, and divide the byte distance by the element size for how many elements past the end you went.0 bytes after a 16-byte regiononintmeans index 4 of a four-element block. - The workflow is fixed: kind, failing line, the memory's story, hypothesis, fix, re-run until clean. The failing line is not always the wrong line, which is why
allocated byandfreed byare printed. The sanitizer stops at the first error, so re-running is part of the method rather than a formality. - Memory access errors abort the run at the offending instruction and output the program had not yet printed never arrives; a leak is reported after a complete, successful run, which is why leaking programs print perfect output and still fail.
- A clean run never proves the absence of undefined behaviour. Signed overflow, shift and division UB, and reading uninitialized memory all pass through undetected, as do plain logic errors. The sanitizer is one check beside compiler warnings and the discipline of writing defined code, not a replacement for either.
Create an account to track your progress and access interactive exercises. Already have one? Sign in.
Reading AddressSanitizer Reports - Quiz
Test your understanding of the lesson.
Practice Exercises
Fix It From the Report
You are given a complete program and you write nothing from scratch. It reads a positive count, allocates a block of that many ints called values, reads the numbers into it, allocates a second block called doubled, sets each element of doubled to twice the matching element of values while adding it to a running total, and prints doubled total followed by that total. All of that logic is correct and none of it needs changing. What the program has instead are three memory mistakes, and your job is the one the sanitizer was built for: run it, read the report, work out which line is wrong, fix that one thing, and run it again. There are no TODO markers, because the reports are the instructions. The program compiles without a single warning, so nothing is waiting for you at compile time. Run the first test case and the first mistake stops the program before it prints anything, with a report reading ERROR: AddressSanitizer: heap-buffer-overflow, then READ of size 4 on the next line, then a stack frame reading #0 in main /app/code/main.c:44, then the line 0 bytes after 12-byte region, and then, under allocated by thread T0 here, a frame reading #1 in main /app/code/main.c:14. Work that through in the order the lesson gave you. The kind is a heap overflow, so a malloc block was touched outside itself. The failing line is 44, the first frame naming your own file. The block was allocated on line 14 and is 12 bytes, which for an int block is 3 elements with valid indices 0 through 2, and the address touched is 0 bytes after the end, which is element 3. Something reached one element past a three element block, and line 44 is inside a loop whose bound you can now check. Fix that and run again, because AddressSanitizer stops at the first error it meets and has nothing to say about anything later in the program. The second and third mistakes are of the other kind: the program runs to completion, prints exactly the right output, exits 0, and LeakSanitizer files its report afterwards, which is why this exercise sets fail_on_memory_leak and why byte-perfect output is not evidence of anything. One of those leaks is on the path where every number was read successfully and one is on the path where scanf gives up part way through, so a run that only exercises the happy path will not show you both. Read the allocation line at the bottom of each leak report to learn which of the two blocks was lost, since line 14 allocates values and line 31 allocates doubled. Every path through the finished program returns 0, exactly one free is written for each allocation on every route that reaches it, and a clean run prints nothing whatsoever on standard error.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!