Skip to content

Memory errors

C does not check how memory is used. A leak, a use after free, an uninitialised read or a read past the end runs to the end unless a tool catches it.

Helps to know firstThe heap: malloc and free

Helps to know first
This topic

First, the picture

Each of these programs runs to the end without a complaint, and each is wrong. Watch the memory, then read what the tool says.

Four ways to break memory

Four short programs that run to the end, and what the tools say

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

Sanitiser report at the end

Direct leak of 16 byte(s) in 1 object(s) allocated from:

Output

main has started. p has no value yet.

0.00 / 11.70 s
Describe this picture

The 17 lines of leak.c with the line about to run marked, beside a picture of memory: “Stack” on top, with main’s card holding p, an int * of 8 bytes at 0x7fe8, and “Heap” below it. Each box shows its type and name at the top left, its address at the top right, and its value in the middle, ”?” until a line writes one. Under the picture are the switch, “Mistake”, with the positions “leak”, “use after free”, “uninitialised” and “past the end”; a box headed “Sanitiser report at the end” holding one line; and the program’s output. Under everything is one sentence on the current step. The clip plays the leak once, 10 steps in about 12 seconds, and rests on the end. Line 6 opens a block of 16 bytes at 0x1000, and p’s arrow points to it; line 9 writes 7 into its first box. Line 10 opens a second block at 0x1020 and p’s arrow swings to it, leaving the first block with no arrow at all. Line 13 writes 8 into the second block, 8 appears in the output, and line 15 hatches the second block, empties its boxes and marks its card “freed”. The first block, holding 7, stays to the end. The report box reads Direct leak of 16 byte(s) in 1 object(s) allocated from:. With “use after free”, the program is use-after-free.c: main holds p at 0x7fe8 and q at 0x7fe0; a block at 0x1000 gets 7; q takes the same address, so two arrows point at the block; line 11 hatches it and marks it “freed”, and both arrows turn dashed; line 12 sets p to NULL and its arrow goes, while q’s dashed arrow stays; line 13 prints ”?” because it reads the freed block. The report reads ERROR: AddressSanitizer: heap-use-after-free. With “uninitialised”, the program is uninitialised.c: main holds total, an int at 0x7fec, and i, an int at 0x7fe8, side by side in one row. total shows ”?” from start to end; i counts 1 to 4 while the loop adds each value to total, which stays ”?”; the output is ”?”. The report reads “No report”. With “past the end”, the program is pointer-past-end.c, and there is no heap: main’s card holds its array a, five int boxes from 0x7fd0 in rows of 8 bytes, a[0] and a[1] in the bottom row, a[2] and a[3] in the middle row and a[4] at 0x7fe0 in the top row; sum, an int at 0x7fcc; and p, an int * at 0x7fc0, while the loop runs. Line 5 fills a with 1 to 5. p’s arrow starts at a[0] and moves on one box each pass while sum becomes 1, 3, 6, 10 and 15. Then p takes 0x7fe4 and a dashed box labelled “a[5]” and “past the end” appears beside a[4], with p’s arrow on it; the next line reads it, and sum turns to ”?”. p goes when the loop ends, the dashed box with it, and the output is ”?”. The report reads ERROR: AddressSanitizer: stack-buffer-overflow. This position runs 18 steps in about 14 seconds. A press on the switch swaps the program, keeping a paused step where the new program has one. The transport plays and pauses, steps one line back or forward, and scrubs; a paused step is kept in the page address as mem.step, and the switch as mem.mistake.

Four mistakes that run

Every program in the switch ran to its last line. Nothing stopped and nothing crashed, and each plain build printed a number as if it were right. That is the trouble with memory mistakes in C: the language does not check how memory is used, so a wrong program usually runs as if it were right.

A leak. In leak.c, p holds the address of the first block. Line 10 stores the address of a second block in p, and the first address is gone: no box holds it any more. The block is still taken, and with its address lost, nothing can ever free it.

A use after free. In use-after-free.c, q is a copy of p, so both point at the block. free(p) gives the block back, and p = NULL clears p. But q still holds 0x1000. Reading q[0] reads memory the program has given away, and what it prints is whatever is there now. Setting p to NULL protected p, and only p.

An uninitialised read. In uninitialised.c, total is declared without a value. Its box holds whatever bytes were left there, and the loop adds 1, 2, 3 and 4 to that. Reading it is undefined behaviour, so C promises nothing about the output. On this machine it printed a number that depended on what was left in the box.

A read past the end. In pointer-past-end.c, p walks along a, as in Arrays and pointer arithmetic (2.3). But the test is p <= a + 5, so p takes one step too many, to a + 5, 0x7fe4 in the picture: the dashed box past a[4]. A pointer may hold that address; it may not be followed. sum += *p follows it, and that is undefined behaviour. On this machine, with this build, it read the 4 bytes after a[4], so the sum came out as 15 plus whatever those bytes held. Written with an index, for (int i = 0; i <= 5; i++) sum += a[i];, the mistake is the same: a[5] reads those 4 bytes too.

Key idea

C runs a memory mistake as if it were a correct line. A leak, a use after free, an uninitialised read and a read past the end all run to the end; only a tool tells you.

The code and its cost

Fixing each one

MistakeFixFixed output
leakfree(p); before the second malloc8, no report
use after freeread q[0] before free(p)7, no report
uninitialisedint total = 0;10
past the endp < a + 5 (with an index, i < 5)15

In the leak’s fix, the first block is freed while p still points at it; a later malloc may then hand the same bytes out again.

Dangling pointers and NULL

A pointer that still holds the address of a freed block is called dangling. Following it, as q[0] does, is a use after free. Calling free on it again is a double free. The C standard leaves both undefined: anything may happen, including nothing visible.

Setting a pointer to NULL right after free helps in two ways. A later free(p) becomes free(NULL), which does nothing. A later *p follows NULL and stops the program at once, at the line that made the mistake. But it helps only for that one pointer. Every other copy of the address, like q, still dangles, and you have to know where those copies are.

Indexing past either end

a[5] goes past the end of int a[5], and a[-1] goes before its start. Both are undefined behaviour. On this machine, with this build, they touch a neighbour: the 4 bytes after a[4] or below a[0], whatever lives there. A write, such as a[5] = 0;, changes it. On the stack, that neighbour may be another variable, or the bytes that say where to return.

The tools

Three kinds of tool catch these mistakes, each some of them.

The compiler’s warnings. Build with -Wall -Wextra. gcc warns about use-after-free.c at once:

use-after-free.c:13:5: warning: pointer 'q' used after 'free' [-Wuse-after-free]

For uninitialised.c it says nothing in a plain build. With optimisation on, -O2, it warns that 'total' is used uninitialized: the warning needs the analysis the optimiser does.

The sanitisers. Build with -fsanitize=address,undefined and run the program. The compiler adds checks around every memory access, and the program reports the first broken rule. AddressSanitizer then stops the program; UndefinedBehaviorSanitizer reports and carries on unless you ask it to halt. AddressSanitizer catches reads and writes outside a block or a stack array, use after free, double free, and, when the program ends, leaks. UndefinedBehaviorSanitizer catches, among other things, an index out of the bounds of an array whose size it knows. For pointer-past-end.c, AddressSanitizer reports the read:

ERROR: AddressSanitizer: stack-buffer-overflow
READ of size 4 thread T0

For the index form, past-the-end.c, UndefinedBehaviorSanitizer reports first:

past-the-end.c:8:17: runtime error: index 5 out of bounds for type 'int [5]'

Neither sanitiser checks reads of uninitialised memory: the run of uninitialised.c reports nothing.

Valgrind is another common tool. It runs the unchanged program on a simulated processor and checks every access as it goes, more slowly.

Reading a report

A report’s first line names the error. For use-after-free.c:

ERROR: AddressSanitizer: heap-use-after-free
READ of size 4 thread T0

The next lines say what was done, a read of 4 bytes, one int, and where: a list of functions, innermost first, each with its file and line. Further down, the report says where the block was freed and where it was allocated. The last line sums it up, here SUMMARY: AddressSanitizer: heap-use-after-free use-after-free.c:13 in main, the line of the printf.

What the checks cost

A sanitiser build runs more slowly and uses more memory than a plain one, because of the checks it adds. So programmers build with sanitisers while they develop and test, and ship the plain build.

Worked example

A double free. This program frees the same block twice:

int *p = malloc(4 * sizeof *p);
if (p == NULL)
    return 1;
free(p);
free(p);

gcc warns at the second free that p is used after free. The sanitiser build stops with ERROR: AddressSanitizer: attempting double-free. Even the plain build stops here: the C library notices and aborts with free(): double free detected in tcache 2. Put p = NULL; between the two calls and the second becomes free(NULL): the program runs cleanly, under every tool.

A leak, by the hour. A server that leaks the 16 bytes of leak.c on each request, at 1,000 requests a second, loses 16 × 1,000 = 16,000 bytes a second, and 16,000 × 3,600 = 57,600,000 bytes an hour, about 55 MiB. In a day, it is over a gigabyte.

Which tool catches which. From the four pictures:

MistakeCompiler warningSanitisers
leaknoyes, when the program ends
use after freeyes, hereyes
uninitialisedonly with -O2no
past the endno (the index form: only with -O2)yes

Neither column says “yes” for every mistake, and the uninitialised read gets nothing from the sanitisers. Use both, and keep the warnings on.

Try it yourself. In use-after-free.c, what does q hold after p = NULL;? 0x1000, unchanged: clearing p did not touch q.

Where it’s used

Crashes far from their cause. A write past the end of a buffer changes a neighbour, and the program fails much later, wherever that neighbour is next used. A sanitiser build stops at the write itself.

Security flaws. In software written in C, a read past an end or through a freed block can be steered by an attacker, who chooses what lies there. Many serious security flaws have been mistakes of exactly these four kinds.

Testing on every change. Projects in C build their tests with sanitisers and run them on every change, so a new memory mistake is caught the day it is written.

Reference card

MistakeWhat happensFix
leakthe last address of a block is lost; it is never freedfree before the address is lost
use after freereads or writes a block already given backuse it before free; clear every copy after
double freefrees a block twicep = NULL; after free(p)
uninitialised readreads whatever was left in the boxgive every variable a value first
past the endreads or writes a neighbourindices from 0 to n − 1; a pointer stops before a + n
ToolHowCatches
compiler warnings-Wall -Wextra, and -O2 for moresome uses after free and uninitialised reads
AddressSanitizer-fsanitize=addressout-of-bounds access, use after free, double free, leaks
UndefinedBehaviorSanitizer-fsanitize=undefinedout-of-bounds indices into arrays of known size, overflow
Valgrindruns the unchanged programmany of the same, more slowly

Platform: C11, gcc 13 on x86-64 Linux, with its sanitisers. The addresses in the pictures are short stand-ins, by one rule for the whole course.

End of the lesson

Where to go next.

New lessons by email

Are you 13 or over?

Accounts and email updates are for people aged 13 or over. We keep only your answer, never your date of birth.

Phasorium

Account

Sign inWith Google, GitHub or your email

Subjects

Site

Look