5.1.18 · D2C Programming

Visual walkthrough — Common memory errors — null dereference, buffer overflow, use-after-free, double free, memory leak

2,223 words10 min readBack to topic

Before we draw anything, three plain-word ideas we will keep reusing. If the parent note assumed them, we build them here.

Throughout, whenever we cross a line the standard forbids, we are in Undefined Behaviour — the program may crash, may work, may corrupt silently. That ambiguity is exactly why we draw so carefully.


Step 1 — Draw the memory strip and the pointer

WHAT. We lay memory out as a horizontal strip of boxes, each labelled with its address. We malloc a small block and draw the returned address landing inside p.

WHY. Every error below is "the arrow from p lands in the wrong place or at the wrong time." So we must first fix, on paper, what "the right place at the right time" looks like. This is our reference frame.

PICTURE.

Figure — Common memory errors — null dereference, buffer overflow, use-after-free, double free, memory leak

int *p = malloc(4);   // 4 bytes for one int

Reading the equation-of-code term by term:

  • malloc(4) — asks for a block of 4 boxes. Its value is the address of box #100 (say).
  • p — a pointer variable; after the line, the number stored inside p is .
  • The amber arrow in the figure is that stored number: it starts at p and lands on the first box of the block.

Every following step is just one of these three conditions failing, drawn.


Step 2 — Break condition 1: the arrow points at box #0 (null deref)

WHAT. Suppose malloc failed and returned NULL, which is address . Now the arrow from p points at box #0. We write through it anyway.

WHY. malloc returns NULL when the system can't spare the memory. Box #0 is deliberately left unmapped — no real box lives there — so the CPU traps the moment you touch it. This is the safest memory error precisely because it usually crashes loudly instead of corrupting quietly.

PICTURE.

Figure — Common memory errors — null dereference, buffer overflow, use-after-free, double free, memory leak

int *p = malloc(HUGE);  // fails → p == NULL  (address 0)
*p = 42;                // arrow lands on the wall → segfault

Term by term:

  • p == NULL — the number inside p is . In the figure the arrow bends left off the strip into the hatched "unmapped wall".
  • *p = 42 — "go to the box p names and store 42 there." But that box is the wall.

Breaking from Step 1 gives exactly this picture.


Step 3 — Break condition 3: the arrow overshoots the block (buffer overflow)

WHAT. The arrow starts at the right block, but the index we add pushes it past the last box, into a neighbouring variable.

WHY. A C array carries no length with it. p[i] is computed as "address of p + i boxes" with no check that i is still inside. So writing one box too far silently overwrites whatever lives next — an adjacent variable, or (on the stack) a saved return address, which is the classic exploit.

PICTURE.

Figure — Common memory errors — null dereference, buffer overflow, use-after-free, double free, memory leak

int a[5];              // valid boxes: a[0] .. a[4]
for (int i = 0; i <= 5; i++) a[i] = 0;   // i==5 overshoots

Term by term:

  • a[5] — declares 5 boxes, indices . There is no box a[5].
  • i <= 5 — the loop runs i = 0,1,2,3,4,5. The last pass, a[5] = 0, writes into the neighbour (amber "collision" box in the figure).
  • Fix: i < 5 keeps every access inside with .

The string version is the same overshoot, counting the hidden terminator (see Strings in C and the null terminator):


Step 4 — The block dies: what free(p) actually changes

WHAT. We call free(p). In the picture, the block flips from alive (cyan) to dead (grey hatch) — but the arrow inside p does not move.

WHY. This is the single most important frame of the whole page. free is an ordinary function receiving a copy of the address. It hands the boxes back to the allocator, but it cannot reach back and edit your variable p. So p keeps pointing at a block that is no longer yours — a dangling pointer.

PICTURE.

Figure — Common memory errors — null dereference, buffer overflow, use-after-free, double free, memory leak

free(p);   // block → dead;  p still holds the old address

Term by term:

  • The block boxes turn grey: condition 2 ("block is alive") is now false.
  • The amber arrow is unchanged — this is the trap. p still names those grey boxes.

Step 5 — Touch the dead block: use-after-free

WHAT. After Step 4, we dereference p anyway.

WHY. Condition 2 is broken. Worse than Step 2's honest crash: the allocator may re-lend those exact boxes to the next malloc. Then p and some other pointer name the same boxes — writing through p silently scribbles on the other variable's data. Often no crash, just corruption.

PICTURE.

Figure — Common memory errors — null dereference, buffer overflow, use-after-free, double free, memory leak

free(p);
printf("%d\n", *p);   // reads a dead (maybe reused) box
  • *p — follows the stale arrow into the grey/re-lent region.
  • The figure shows a second arrow q (from a later malloc) landing on the same boxes: that overlap is the corruption.

Step 6 — Free the dead block again: double free

WHAT. We call free(p) a second time on the still-dangling p.

WHY. The allocator keeps its bookkeeping (block size, "next free block") inline, right next to your data. The first free filed this block into its records. Filing it twice tangles that internal list — corrupting metadata an attacker can steer, or triggering an abort.

PICTURE.

Figure — Common memory errors — null dereference, buffer overflow, use-after-free, double free, memory leak

free(p);
free(p);   // second free tangles the allocator's list
  • Both free calls carry the same address (arrow unchanged from Step 4).
  • The figure shows the allocator's "free-list" chain looping back on itself — the corruption.

Step 7 — Never free at all: the leak (and the conservation law)

WHAT. We overwrite the only arrow to a live block before freeing it. The block stays alive but unreachable — no pointer names it, so it can never be freed.

WHY. Nothing crashes. The block just sits there occupying memory forever. In a loop or a long-running server, these lost blocks pile up until the machine thrashes or the OOM-killer strikes. This is condition 1's opposite: the arrow left, but the block stayed.

PICTURE.

Figure — Common memory errors — null dereference, buffer overflow, use-after-free, double free, memory leak

char *p = malloc(100);   // block A, 100 bytes
p = malloc(200);         // arrow jumps to block B; A now unreachable
free(p);                 // frees only B
  • p = malloc(200) — the arrow leaves block A and lands on block B.
  • Block A in the figure has no arrow — it is orphaned (amber "lost" tag). It is leaked.
  • free(p) reclaims only B.

Tools that watch this ledger for you: Valgrind and AddressSanitizer.


The one-picture summary

WHAT. One strip, one lifetime, all five errors pinned to the exact moment they occur.

Figure — Common memory errors — null dereference, buffer overflow, use-after-free, double free, memory leak

The block is born (malloc), lives, dies (free). Along the time axis each error is a wrong touch:

  • before birth / on a failed birth → arrow at box #0 → null deref
  • during life, past the edgebuffer overflow
  • after death, touchinguse-after-free
  • after death, freeing againdouble free
  • arrow lost before deathmemory leak
Recall Feynman: the whole walkthrough in plain words

Picture a rented locker (a heap block) and a sticky note with its number (the pointer). malloc rents the locker and writes its number on your note. Null deref: the rental failed, your note says "locker 0" — a wall — and you reach in and smack the wall (crash). Overflow: your stuff is too big for the locker, so it spills into the neighbour's and wrecks their things. Now you hand the locker back (free) — but nobody erases your note, it still shows the old number. Use-after-free: you reach into a locker you already returned; the gym may have re-rented it, so you're now rummaging in a stranger's locker. Double free: you try to return the same locker twice and jam the front desk's list. Leak: you scribble a new number over your note before returning the old locker — now no one knows where it is, and it's rented forever. The cure fits on a matchbook: check the rental worked, keep one note per locker, erase the note the instant you return the locker (that makes both a re-return and a wrong-reach harmless), and return every locker exactly once.