5.3.12 · D4Build Systems & Toolchain

Exercises — Address Sanitizer (ASan) — detecting buffer overflows at runtime

2,561 words12 min readBack to topic

Before we begin, one figure fixes the whole mental model so every exercise below can point back to it.

Figure — Address Sanitizer (ASan) — detecting buffer overflows at runtime
Recall The three things every exercise uses (open if you forgot)

1. The map. One shadow byte describes 8 application bytes. Which shadow byte? . The symbol means "shift the bits right by 3", which is the same as "divide by and throw away the remainder" — that is how 8 app bytes collapse to 1 shadow byte. 2. The shadow value . = all 8 bytes green (legal). with = only the first bytes legal. negative = whole granule red (redzone / freed). 3. The check for an -byte access at address (with , aligned): it reports a bug when Here is the offset inside the granule (the last 3 bits of the address), and is the index of the last byte you touch.


Level 1 — Recognition

Goal: name the pieces. No arithmetic yet.

L1.1

Which single compiler flag turns AddressSanitizer on in both GCC and Clang?

Recall Solution

-fsanitize=address. You must use it when compiling and linking every object, because the runtime replaces malloc/free process-wide. See Compiler Instrumentation and -fsanitize flags.

L1.2

A shadow byte holds the value . Is the 8-byte granule it describes fully legal, partly legal, or fully poisoned?

Recall Solution

Fully legal (green). Zero literally means "all 8 bytes addressable". Look at the leftmost stack of tiles in the figure: the note on it reads 0.

L1.3

Match each ASan concept to its one-line role: (a) redzone, (b) shadow memory, (c) quarantine, (d) Offset.

Recall Solution
  • (a) redzone — poisoned padding placed around each buffer so an adjacent overflow lands in red.
  • (b) shadow memory — the compact map, 1 byte per 8 app bytes, storing legality.
  • (c) quarantine — after free, the block is held (not reused) so use-after-free stays detectable.
  • (d) Offset — the fixed constant added in so the shadow region sits somewhere that never collides with real data.

Level 2 — Application

Goal: plug numbers into the two formulas.

L2.1

An application byte lives at address . Using , compute its shadow byte address.

Recall Solution

divides by 8: . Add the offset: . So the one shadow byte guarding this granule sits at . In decimal: , , shadow .

L2.2

Two application addresses and . Do they share the same shadow byte? What about ?

Recall Solution

and (the low 3 bits are shifted away), so and share one shadow byte — they are the first and last byte of the same 8-byte granule. , a different shadow byte: starts the next granule. See the two adjacent tile-stacks in the figure — each stack of 8 tiles gets exactly one note.

L2.3

char *p = malloc(8); returns a granule with shadow byte . You write p[3]. Walk the check and decide: report or not?

Recall Solution

Since , the first condition is false, so the whole AND is false → no report. The check short-circuits the instant it sees a fully-green granule. That is the fast common case: one shadow load, one compare-to-zero, done.


Level 3 — Analysis

Goal: the subtle partial-granule and boundary cases — every sign, every offset.

L3.1

char *p = malloc(5); so the granule holding p[0..7] has shadow . For each of p[4], p[5], p[6] (each a 1-byte write, ), decide report or not. Assume p is 8-byte aligned, so p[i] has .

Recall Solution

The condition is , i.e. .

  • p[4]: ? No → legal. Byte 4 is inside the first green bytes (indices ).
  • p[5]: ? Yesreported. Index 5 is the first poisoned byte in the same granule.
  • p[6]: ? Yesreported. This is exactly the case one shadow byte per 8 was designed to express — a granule that is partly green.

L3.2

A 4-byte access (, e.g. writing an int) at offset into a granule with shadow . Report or not? Now redo it for .

Recall Solution

Last byte touched .

  • Offset 2: . Is ? Yesreported. The int spans bytes — and byte 5 is poisoned. This is the classic multi-byte access straddling the boundary: even though its first byte (offset 2) is green, ASan reports because its last byte crosses into red.
  • Offset 3: reported too (spans bytes ). Notice a single shadow byte cannot say "bytes 0–4 ok and bytes 6–7 ok but 5 bad" — the model is strictly prefix-green: the first bytes green, the rest red. That prefix rule is why the check compares against the last byte you touch.

L3.3

What shadow value would you expect for a byte deep inside a freed 16-byte block that ASan has poisoned? Is the check's first clause enough to fire, regardless of offset?

Recall Solution

A freed / redzone granule gets a negative shadow value (ASan uses specific negative codes like heap-freed). For the comparison, negatives behave as "the whole granule is red": since is true and is always true when is negative (any offset a negative number), every access into it reports — no matter which byte. That is why use-after-free is caught on the first touch. See Stack vs Heap Memory Layout for why the freed heap block stays mapped long enough to be poisoned rather than unmapped.


Level 4 — Synthesis

Goal: assemble the full mechanism and predict behaviour across cases.

L4.1

You have a 3-byte-aligned buffer malloc(13). That is 13 bytes = one full granule (8) + a partial granule (5). Give the two shadow bytes' values for the region actually returned to the user (ignore redzones), and then classify p[12] and p[13].

Recall Solution
  • Granule 0 (bytes 0–7): all used → shadow .
  • Granule 1 (bytes 8–15): only bytes 8,9,10,11,12 are user bytes (that is 5 of them, indices inside this granule) → shadow .
  • p[12]: inside granule 1 at intra-granule offset . Check: ? No → legal (it is the 13th and last user byte).
  • p[13]: intra-granule offset . Check: ? Yes → reported (first redzone byte). See the two-tile picture in the figure: a full green stack next to a "5-green-3-red" stack.

L4.2

Explain, using the check formula, why ASan reliably catches a[4] on int a[4]; but can silently miss a[1000000].

Recall Solution

a[4] lands in the redzone immediately after the array — a granule whose shadow is negative. The check's first clause is true and the second is true → reported. This is the adjacent overflow ASan is built for. a[1000000] computes an address far away that may fall inside another live object's green memory. There the shadow byte is , the check short-circuits, and ASan stays silent. Redzones are finite padding; they guard neighbours, not the whole address space. Pair ASan with Fuzzing (libFuzzer / AFL) to reach bugs and with Undefined Behaviour in C and C++ awareness to know its blind spots.

L4.3

Memory-cost synthesis. If shadow is 1 byte per 8 app bytes, what fraction of extra memory does the shadow map alone add? If ASan's total overhead is quoted as "~2–3× RAM", where does the rest come from?

Recall Solution

Shadow map: extra. That alone is small. The large "2–3×" figure comes from redzones (padding around every allocation), the quarantine (freed blocks held un-reused), and per-allocation bookkeeping. So the shadow is cheap; the safety padding and quarantine dominate — which is exactly why ASan is a debug tool, not a production one.


Level 5 — Mastery

Goal: predict ASan's exact verdict and reproduce the reasoning end-to-end.

L5.1

Given this program, list in order every access and state ASan's verdict for each, with the intra-granule arithmetic. Assume p is 8-byte aligned.

char *p = malloc(10);   // 10 bytes
p[9]  = 'a';
p[10] = 'b';
free(p);
char c = p[0];
Recall Solution

Sizes: malloc(10) → granule 0 = bytes 0–7 (shadow 0), granule 1 = bytes 8–15 but only 8,9 are user → shadow .

  • p[9] = 'a': granule 1, offset , : ? No → legal.
  • p[10] = 'b': granule 1, offset , : ? Yes → heap-buffer-overflow, WRITE of size 1.
  • free(p): poisons both granules (now negative) and quarantines the block.
  • char c = p[0]: granule 0 now negative, true and true → heap-use-after-free, READ of size 1. ASan also prints where it was freed. (In a real run the process typically aborts at the first error — p[10] — unless halt_on_error=0; conceptually every listed access is a violation.)

L5.2

Design question. You must sanitize a library libfoo but the final binary also links a third-party object compiled without -fsanitize=address. Predict what can go wrong and give the correct fix.

Recall Solution

ASan's runtime intercepts malloc/free globally. If some code allocates through the intercepted malloc (getting redzones) but other code was built assuming plain allocation — or if you fail to link the ASan runtime at all — you get link errors or false reports (e.g. mismatched allocator/deallocator). Fix: compile and link every translation unit and the final binary with -fsanitize=address, or if you truly cannot rebuild a dependency, isolate it or fall back to Valgrind Memcheck, which needs no recompilation because it instruments by emulation instead. Contrast with siblings MemorySanitizer (MSan) and UndefinedBehaviorSanitizer (UBSan), which have the same "instrument everything consistently" requirement.

L5.3

Prove the boundary identity you have been using: for a granule with shadow () and a 1-byte access, the largest legal intra-granule offset is , and the smallest reported offset is . Then confirm it agrees with L3.1.

Recall Solution

For the report condition is , i.e. offset . So an offset is legal exactly when offset , whose largest integer value is ; it is reported exactly when offset , whose smallest value is . With : largest legal offset (matches p[4] legal), smallest reported (matches p[5] reported). ∎


Recall Master self-test (open only after finishing)
  1. Shadow byte means what? ::: All 8 bytes green/legal.
  2. Shadow of malloc(13)'s tail granule? ::: .
  3. Report condition for an -byte access at ? ::: AND .
  4. Why is a[1000000] sometimes missed? ::: It can land in another live object's green memory.
  5. Shadow-map memory overhead alone? ::: (1 byte per 8).