5.3.12 · D5Build Systems & Toolchain

Question bank — Address Sanitizer (ASan) — detecting buffer overflows at runtime

1,550 words7 min readBack to topic

Parent: Address Sanitizer (ASan) — detecting buffer overflows at runtime.


True or false — justify

True or false: ASan reasons about your code at compile time to prove it has no overflows.
False. ASan is a runtime tool; the compiler only injects check code, and a check only fires on a line you actually execute — an overflow in an unrun branch is invisible. That is static analysis's job, not ASan's.
True or false: if your test suite passes under ASan, your program is memory-safe.
False. ASan only sees the paths your inputs reach. "Passes under ASan" means "no bug on these executions", which is why you pair it with Fuzzing (libFuzzer / AFL) to reach more paths.
True or false: ASan doubles memory use because it stores one metadata byte for every application byte.
False. It stores one shadow byte per 8 application bytes (an 8-byte granule), so the shadow costs roughly 1/8 of RAM; the ~2–3× total comes from redzones and the free quarantine, not a 1:1 map.
True or false: a shadow byte value of 0 means the granule is poisoned.
False, it is the opposite. 0 = all 8 bytes are valid (green, go); a negative value means the whole granule is poisoned (red, dead). "Green is GO, red is DEAD."
True or false: ASan can catch a read from p + 1000000 where p is a small buffer.
Usually false. A far jump often lands inside another valid object's green memory, so the shadow reads 0 and ASan stays silent. Redzones are finite; ASan reliably catches adjacent overflows, not arbitrary wild pointers.
True or false: enabling -fsanitize=address on one source file is enough.
False. The runtime intercepts malloc/free globally, so you must compile and link the whole program consistently with the flag (see Compiler Instrumentation and -fsanitize flags); mixing sanitized and non-sanitized objects gives link errors or false reports.
True or false: ASan is a good production hardening layer because it's just one flag.
False. It costs ~2× CPU and ~2–3× RAM and is a debug/test tool. Production hardening uses other mechanisms; ASan is meant for your test and fuzzing pipeline.
True or false: reading a poisoned byte is fine; only writing to it triggers ASan.
False. The injected check runs on both loads and stores. A read of poisoned memory (e.g. reading freed data) is reported just like a write — the report just says READ instead of WRITE.

Spot the error

What's wrong with this claim: "ASan uses an emulated virtual machine like Valgrind to watch memory."?
Wrong mechanism. ASan works by compiler instrumentation — the compiler bakes check code into your binary. Valgrind Memcheck is the one that emulates; that's why Valgrind needs no recompile but is much slower.
Spot the error: "Because malloc(8) fills one granule, p[8] overflows into the same granule's poisoned tail."
The tail of the granule isn't the issue: 8 bytes exactly fill the granule, so its shadow is 0. p[8] reaches the next granule, which is a redzone with a poisoned (negative) shadow — that's what fires.
Spot the error: "ASan poisons freed memory, so a double-free is caught the same way as a plain access."
A double-free is caught by the runtime's free bookkeeping (the block is already in quarantine/marked freed), not by a shadow-check on a load/store. Different code path, same tool — ASan reports double-free explicitly.
Spot the error: "Since the shadow value 5 means bytes 0–4 are valid, index 5 is still inside a valid granule so it's allowed."
Being in the same granule does not mean allowed. Shadow = 5 says only bytes 0–4 are addressable; index 5 is the first poisoned byte of that partial granule, so it is reported. Granularity lives inside one shadow byte.
Spot the error: "ASan replaces undefined behaviour with defined behaviour so my UB is now safe."
ASan exposes the Undefined Behaviour in C and C++, it doesn't legalise it. It aborts with a report at the offending access; the bug is still a bug — you must fix it.
Spot the error: "MSan and ASan do the same job, so I only need one."
Different bugs. MemorySanitizer (MSan) finds reads of uninitialised memory; ASan finds out-of-bounds and use-after-free. UndefinedBehaviorSanitizer (UBSan) catches signed overflow, bad shifts, etc. They are complementary siblings.

Why questions

Why does ASan right-shift the address by 3 (A >> 3) rather than by some other amount?
Shifting right by 3 divides by 8, mapping each 8-byte granule to exactly one shadow byte. It's the shift that matches the granule size — the whole "one byte per 8 bytes" scheme depends on it.
Why add an Offset in Shadow(A) = (A >> 3) + Offset?
So the computed shadow region sits at a fixed, reserved place in the address space (e.g. 0x7fff8000) that never overlaps real program data. Without it, shadow bytes could collide with actual objects.
Why does ASan quarantine freed blocks instead of returning them to malloc immediately?
If freed memory were reused right away, a later use-after-free would land on a reallocated valid object (green) and go unnoticed. Quarantine keeps the block poisoned long enough to catch stale accesses.
Why are stack and global redzones inserted by the compiler while heap redzones come from the runtime?
Heap sizes are decided at runtime, so the malloc replacement adds the padding then. Stack arrays and globals have layouts fixed at compile time, so the compiler places their redzones (this is why redzones differ per memory region).
Why can ASan tell you the exact line and where a freed block was allocated and freed?
The runtime records allocation and free stack traces as metadata for each block. On a violation it dovetails the current access stack with those saved traces to print all three locations.

Edge cases

Edge case: you write to p[7] after malloc(8). Reported or not, and why?
Not reported. Bytes 0–7 fill the granule whose shadow is 0 (all green); index 7 is the last legal byte. The overflow only begins at p[8].
Edge case: malloc(0) — what does the granule look like?
A zero-size allocation still returns a pointer surrounded by redzones; the object's own granule has essentially no valid bytes, so any dereference immediately reaches poisoned shadow and is reported.
Edge case: an unaligned 4-byte access that straddles two granules (offset 6, so it touches bytes 6,7 of one granule and 0,1 of the next). Can the simple single-byte check miss it?
A straddling access can poke a poisoned byte in the neighbouring granule that the single shadow-byte check for the first granule doesn't cover. ASan handles this by generating checks for the granules the access actually spans, so the poisoned second granule still fires.
Edge case: overflow that lands in memory owned by a different, still-valid object (buffers packed with no redzone between). Reported?
Not reliably. If the stray write hits another live object's green bytes, the shadow reads 0 and ASan is silent. Redzones guard the edges; overflows large enough to skip the redzone into another green region can slip through.
Edge case: reading one byte before the start of a heap buffer (p[-1]). Does ASan catch it?
Yes. malloc adds a redzone on both sides, so p[-1] lands in the poisoned left redzone; the shadow is negative and ASan reports a heap-buffer-overflow (a "left" overflow/underflow).
Edge case: your program never frees a leaked buffer but never over-reads it. Does ASan's overflow check flag it?
No — no out-of-bounds access happened, so no shadow check fires. Leaks are found by ASan's separate leak detector (LeakSanitizer) at exit, not by the redzone/shadow mechanism.

Recall Fast self-check
  1. Does a shadow byte of 0 mean poisoned or valid? ::: Valid (all 8 green).
  2. Can ASan catch p + 1000000? ::: Usually no — it may land in another green object.
  3. Why quarantine freed blocks? ::: So use-after-free lands on still-poisoned memory instead of reused green memory.
  4. Which sanitizer finds uninitialised reads instead? ::: MemorySanitizer (MSan).