Skip to content

Memory-Safety Mechanisms — Middle Level

Topic: Memory-Safety Mechanisms Focus: How the detection and mitigation tools actually work — sanitizers (shadow memory, redzones, quarantine), Valgrind, hardened allocators, and the C-level defenses (_FORTIFY_SOURCE, annotated APIs) — and the real cost of spatial vs temporal protection.


Table of Contents

  1. Introduction
  2. Prerequisites
  3. Glossary
  4. Core Concepts
  5. Real-World Analogies
  6. Mental Models
  7. Code Examples
  8. Pros & Cons
  9. Use Cases
  10. Coding Patterns
  11. Best Practices
  12. Edge Cases & Pitfalls
  13. Common Mistakes
  14. Tricky Points
  15. Test Yourself
  16. Cheat Sheet
  17. Summary
  18. Further Reading
  19. Related Topics
  20. Diagrams & Visual Aids

Introduction

Focus: How do the tools that catch memory bugs actually catch them? Not "use ASan" but "what does ASan do to your program so that an out-of-bounds write becomes a precise report?"

At the junior level, memory safety is a property and a taxonomy of bugs. At this level, you open the toolbox and see the mechanisms. There are two distinct jobs:

  • Detection — finding bugs during development and testing. This is what AddressSanitizer (ASan), MemorySanitizer (MSan), UndefinedBehaviorSanitizer (UBSan), and Valgrind/Memcheck do. They instrument your program — adding checks and bookkeeping — so that an illegal access is caught the moment it happens, with a stack trace, instead of silently corrupting memory.

  • Mitigation — making bugs harder to exploit in production, where you can't pay sanitizer overhead. This is what hardened allocators (Scudo, GWP-ASan, hardened glibc malloc), guard pages, _FORTIFY_SOURCE, and stack canaries do. They don't fix the bug; they raise the cost of turning it into a working exploit, and often turn a silent corruption into a clean crash.

The unifying technique behind most spatial detection is the redzone: place a small forbidden region immediately around each allocation, and any access that lands there is an overflow. The unifying technique behind temporal detection is the quarantine: don't immediately reuse freed memory; hold it in a "poisoned" state for a while so a use-after-free hits poison instead of valid reused data. Both rely on shadow memory: a compact side table that records, for every byte of your program, whether that byte is currently accessible.

🎓 Why this matters at this level: Once you understand how the tools work, you understand their blind spots. ASan won't catch a use-after-free if the quarantine recycled the memory already. MSan needs the whole program instrumented or it gives false reports. _FORTIFY_SOURCE only protects calls where the compiler can compute the buffer size. Knowing the mechanism tells you when a clean tool run is real assurance and when it's a false sense of security.

This page covers shadow memory and redzones, the ASan/MSan/UBSan split, Valgrind's very different (binary-translation) approach, hardened allocator techniques, the C defenses (_FORTIFY_SOURCE, annotated/checked APIs), and why temporal safety is fundamentally more expensive to enforce than spatial safety. senior.md goes into the language designs (Rust's borrow checker, managed-runtime guarantees); professional.md into hardware (MTE, CHERI) and the industry migration.


Prerequisites

  • Required (junior level): the spatial/temporal split, the bug taxonomy (overflow, UAF, double-free, uninitialized read), and why C is unsafe by default.
  • Required: comfort with C pointers, malloc/free, stack vs heap, and reading a stack trace.
  • Helpful: a rough idea of virtual memory and pages (the OS hands memory in ~4 KiB pages; access permissions are per page).
  • Helpful: having compiled something with -fsanitize=address and seen a report.
  • Helpful: awareness that the heap is managed by an allocator (malloc) with its own metadata, separate from your data.

You do not yet need: Rust's borrow checker internals (senior.md), or hardware tagging like MTE/CHERI (professional.md).


Glossary

Term Definition
Instrumentation Extra code the compiler (or a binary translator) inserts to monitor your program's behavior at runtime.
Shadow memory A compact side table mapping each region of application memory to metadata (e.g. "addressable or poisoned"). ASan uses 1 shadow byte per 8 app bytes.
Redzone A forbidden buffer placed immediately before/after an allocation. Accessing it = overflow detected.
Poison / unpoison Marking a region in shadow memory as inaccessible (poison) or accessible (unpoison). Freed memory and redzones are poisoned.
Quarantine A pool of recently-freed blocks held unreusable (and poisoned) so use-after-free is detectable before the memory is recycled.
Guard page A page marked no-access by the OS, placed next to an allocation so an overflow into it triggers a hardware fault (segfault) immediately.
AddressSanitizer (ASan) Compiler-based detector for spatial bugs (OOB) and temporal bugs (UAF, double-free), via redzones + quarantine + shadow memory.
MemorySanitizer (MSan) Detector for uninitialized reads. Tracks "is this bit initialized?" in shadow memory.
UndefinedBehaviorSanitizer (UBSan) Detector for many UB forms: signed overflow, misaligned access, bad casts, null deref, OOB on known-size objects.
ThreadSanitizer (TSan) Detector for data races (covered more in concurrency topics; relevant because races can break memory safety).
Valgrind / Memcheck A binary instrumentation tool: runs your unmodified binary on a synthetic CPU, tracking definedness and addressability. No recompile needed; much slower.
_FORTIFY_SOURCE A glibc/compiler feature that replaces certain libc calls (memcpy, strcpy, sprintf...) with bounds-checked variants when the destination size is known at compile time.
Stack canary A random value placed between local buffers and the saved return address; checked on return. A mismatch means a stack overflow corrupted it → abort.
Hardened allocator A malloc implementation (Scudo, hardened glibc, GWP-ASan) that adds randomization, metadata protection, and free-list hardening to resist exploitation.
Free-list The allocator's internal linked structure of available blocks. Corrupting it (via double-free or overflow into metadata) is a classic exploitation primitive.
Metadata separation Storing allocator bookkeeping away from user data (out-of-line) so an overflow can't corrupt it.
Fat pointer A pointer that carries bounds (base + length) alongside the address, enabling per-access spatial checks.

Core Concepts

1. Shadow Memory: The Universal Bookkeeping Trick

The core problem for a detector is: for any address the program touches, is that address currently legal to access? You can't store a flag inside each byte (that byte holds the user's data). So tools keep a parallel shadow memory — a separate region that records metadata for application memory.

ASan's scheme is elegant: 1 shadow byte describes 8 application bytes. Because allocations are 8-byte aligned, each 8-byte slot is either fully addressable (shadow = 0), fully poisoned (shadow = negative), or partially addressable (shadow = k means "the first k of these 8 bytes are valid"). The shadow address is computed by a cheap formula: shadow = (addr >> 3) + offset. So checking an access is:

shadow_value = *((addr >> 3) + Offset);
if (shadow_value != 0 && (addr & 7) >= shadow_value)
    report_error();    // this byte is poisoned -> bug

A shift, an add, a load, a compare, a branch — a handful of instructions per memory access. That's the ~2× slowdown ASan costs. Cheap enough for testing, too much for production.

2. Redzones: Catching Spatial Bugs

Around every allocation, ASan inserts redzones — extra bytes that are poisoned in shadow memory. Heap allocations get redzones on both sides; stack arrays and globals get them too. The layout looks like:

[ left redzone (poison) ][ your 16 bytes (valid) ][ right redzone (poison) ]

Now an off-by-one write to arr[16] lands in the right redzone. The shadow byte for that address is non-zero, the check fires, and ASan prints heap-buffer-overflow with the exact line, the allocation site, and how far out of bounds you went. The redzone is why ASan can describe the overflow so precisely — it caught the first byte over the line, not a downstream symptom.

The blind spot: a wild access that jumps over the redzone (e.g. arr[1000000]) may land in genuinely-valid memory and not be caught. Redzones catch adjacent overflows, which is the overwhelming majority of real bugs.

3. Quarantine: Catching Temporal Bugs

When you free(p), a normal allocator immediately makes that block available for reuse — which is exactly why use-after-free is so dangerous (the memory becomes something else's). ASan instead poisons the freed block and puts it in a quarantine: a FIFO queue of freed blocks that are not reused until the queue fills up and the oldest are released.

While a block sits in quarantine, its shadow is poisoned. Any read or write through a dangling pointer hits poison → heap-use-after-free, with two stack traces: where it was freed and where it's now being used. That paired report is the gift of quarantine.

The blind spot, and it's important: quarantine is finite. If enough other frees happen, your freed block leaves quarantine, gets reused, unpoisoned, and a later use-after-free will not be caught (the memory is legal again — just holding the wrong object). So ASan finding no UAF doesn't prove there isn't one. This is a fundamental limit of detection-by-poisoning, and a key reason temporal safety is harder than spatial.

4. The Sanitizer Family — Different Tools for Different Bugs

They are not interchangeable. Each targets a class:

  • ASanaddressability: OOB (spatial) and UAF/double-free (temporal). The default first reach. ~2× slow, ~3× memory.
  • MSanuninitialized reads. It tracks, bit by bit, whether each value has been written before it's used in a way that affects program behavior. Catches "read garbage" bugs ASan can't. Requirement: ideally the entire program (incl. libraries) is instrumented, or MSan reports false positives from uninstrumented code that "wrote" the memory invisibly.
  • UBSan — a grab-bag of undefined behavior: signed integer overflow, shift-by-too-much, misaligned pointers, null dereference, invalid enum/bool values, and (with -fsanitize=bounds) OOB on objects whose size the compiler knows. Very cheap; turn on broadly.
  • TSandata races. Relevant here because in many languages a data race is itself undefined behavior and can corrupt memory. (Detailed in concurrency topics.)

You typically can't run ASan and MSan together (their shadow schemes conflict); you run them in separate CI jobs.

5. Valgrind/Memcheck — Detection Without Recompiling

Valgrind takes a completely different approach. Instead of the compiler inserting checks, Valgrind runs your unmodified binary on a synthetic CPU via dynamic binary translation. Memcheck (its main tool) tracks two things per byte: addressable? (A-bits) and defined? (V-bits). Every memory access and every use of a value is checked against this shadow state.

Trade-offs versus ASan:

  • ✅ No recompile, works on any binary, catches uninitialized reads and OOB and leaks in one tool.
  • ✅ Catches some bugs in third-party libraries you can't rebuild.
  • ❌ Much slower — often 10–50× — because every instruction is interpreted.
  • ❌ Doesn't catch stack or global OOB well (no redzones around stack arrays), where ASan shines.

Rule of thumb: ASan for speed and stack/global coverage; Valgrind when you can't recompile or need its leak detection on an arbitrary binary.

6. Hardened Allocators — Mitigation in Production

In production you can't afford ASan's overhead, but you can use a hardened allocator that costs little and removes common exploitation primitives:

  • Free-list hardening — encrypt/checksum the pointers in the free-list so an attacker who corrupts a freed chunk can't redirect a future allocation to an address of their choosing. Also detect double-free by checking whether a chunk is already free.
  • Metadata separation — keep allocator bookkeeping out-of-line, away from user data, so a heap overflow corrupts (poisoned/guarded) padding instead of the size/next fields the allocator trusts.
  • Randomization — randomize where allocations land so heap layout isn't predictable (defeats "groom the heap so object B sits right after A").
  • Delayed/randomized reuse — like a lightweight quarantine, reduces UAF reliability.

Two notable systems:

  • Scudo — the default hardened allocator on Android (and available in LLVM). Adds chunk headers with checksums, randomization, and quarantine, at modest cost.
  • GWP-ASan — "Guard-page-Without-Performance-cost ASan." A sampling detector for production: it places a small fraction of allocations on guard pages (real OS no-access pages on either side), so the rare overflow or UAF that hits one of those allocations triggers a hardware fault and a precise crash report from a real user. By sampling (e.g. 1 in 10,000 allocations), the average overhead is negligible, yet across a billion devices it catches bugs ASan never saw in the lab.

7. Guard Pages — Hardware-Enforced Spatial Safety

A guard page is a whole memory page the OS marks no-access. Place an allocation so its end abuts a guard page, and any overflow into it triggers an immediate hardware fault — no shadow memory, no instrumentation, the MMU does the work for free. The cost is granularity: pages are ~4 KiB and you "waste" a page per guarded allocation, so you can't guard every allocation this way (that's why GWP-ASan samples). Electric Fence (efence) was the classic all-allocations-on-guard-pages debug allocator; GWP-ASan is its production-viable, sampled descendant.

8. C-Level Defenses: _FORTIFY_SOURCE, Canaries, Annotated APIs

You can harden C source without changing your code much:

  • _FORTIFY_SOURCE=2/=3 — when the compiler can determine the destination buffer's size at compile time, it replaces memcpy/strcpy/sprintf/snprintf/... with __*_chk variants that verify the copy fits, aborting on overflow. Free at runtime when sizes are known; does nothing when they aren't (its blind spot). =3 extends coverage to more cases via __builtin_dynamic_object_size.
  • Stack canaries (-fstack-protector-strong) — a random value between local buffers and the saved return address, verified on return. A linear stack overflow that overwrites the return address must pass through the canary, corrupting it, which the check detects → abort before the corrupted return is used. Defeats the classic "smash the stack" pattern.
  • Annotated / checked APIs — design functions that take a buffer and its size (void f(char *buf, size_t cap)), and use compiler attributes (__attribute__((access(write_only, 1, 2))), __counted_by__) to let the compiler reason about bounds. This is "make the size impossible to forget."

None of these makes C safe; together they turn many silent corruptions into clean aborts and shrink the exploitable surface.

9. Spatial Is Cheap, Temporal Is Expensive

A theme worth internalizing: spatial safety is much cheaper to enforce than temporal safety.

  • Spatial: you need bounds (base + length) at the access. That's local information — carry a length, check an index. A predictable branch the CPU mostly predicts correctly, often eliminated by the optimizer.
  • Temporal: you need to know whether the pointed-to object is still the same live object it was when the pointer was made. That's a global, time-dependent fact. Detecting it requires either never reusing memory (GC, quarantine — costly in memory) or per-object liveness tracking. There is no cheap local check for "is this still alive?" This is why GC and quarantine exist, why UAF is the hardest class to mitigate, and why hardware temporal-safety mechanisms (MTE — professional.md) are such a big deal.

Real-World Analogies

  • Shadow memory = a wet-paint chart. Your building has rooms (memory). A separate clipboard at the door says, for each room, "freshly painted — do not enter." You never write that on the wall inside the room (that's the tenant's space); you keep it on the clipboard (shadow). Before entering any room you glance at the clipboard.

  • Redzone = caution tape around the edges. Each work area has a meter of caution tape on every side. Step onto the tape and an alarm sounds immediately — you haven't yet reached the next work area, so the guard knows precisely which area you overran and by how much.

  • Quarantine = the returned-rental cooldown. When a tool is returned it doesn't go straight back on the shelf; it sits in a "returned" bin for a while. If you try to grab "your" returned tool from the bin, staff catch you — but only while it's still in the bin. Once the bin overflows and the tool goes back on the public shelf, your claim to it is invisible again.

  • Guard page = a locked steel door, not tape. Instead of an alarm you can ignore, walking into a guard page hits a locked door the building's own structure enforces (the MMU). You can't proceed — the OS faults the process. Cheaper to enforce (no clipboard checks), but you can only afford so many steel doors.

  • _FORTIFY_SOURCE = a tailor who knows the cloth size. When the tailor (compiler) knows the exact size of the garment (buffer), they refuse a cut that wouldn't fit. When the size is custom/unknown at fitting time, they can't help — that's the blind spot.


Mental Models

Model 1: Detection poisons, hardware faults. Software detectors (ASan, Memcheck) work by poisoning shadow metadata and checking it in software on every access — flexible but ~2–50× slow. Hardware mechanisms (guard pages, later MTE/CHERI) make the MMU/CPU enforce the rule — cheap but coarse. Production tools blend them (GWP-ASan samples onto guard pages).

Model 2: A clean tool run is evidence, not proof. ASan with no findings means "no detectable bug on these inputs, within quarantine limits, in instrumented code." Combine it with fuzzing (to drive new inputs) and remember the blind spots (quarantine eviction, uninstrumented libs, wild jumps over redzones). Coverage = tool × inputs.

Model 3: Mitigation raises the attacker's cost; it doesn't remove the bug. Canaries, free-list hardening, randomization, metadata separation — none fixes the overflow or UAF. They make a given bug harder or less reliable to weaponize, and often convert silent corruption into a clean abort. Defense in depth, not a cure.

Model 4: Spatial is local, temporal is global. Whenever a mechanism seems expensive (quarantine memory, GC pauses, MTE), it's almost always paying for temporal safety — the hard one. Bounds checks (spatial) are nearly free by comparison.


Code Examples

Defensive/educational only. These show how the tools report, and how to write checkable code — no exploits.

Seeing ASan's redzone catch an off-by-one

// fortify.c
#include <stdlib.h>
int main(void) {
    int *a = malloc(4 * sizeof(int));  // valid: a[0..3]
    a[4] = 42;                         // right-redzone write -> ASan fires
    free(a);
    return 0;
}
clang -fsanitize=address -g fortify.c -o fortify && ./fortify
# ==..==ERROR: AddressSanitizer: heap-buffer-overflow ... WRITE of size 4
#   #0 ... in main fortify.c:5
#   0x... is located 0 bytes to the right of 16-byte region [0x..,0x..)
#   allocated by thread T0 here: ... malloc ... fortify.c:4

Note the precision: "0 bytes to the right of a 16-byte region," plus the allocation site. That's the redzone + shadow memory working.

Quarantine catching use-after-free (and its limit)

// uaf.c
#include <stdlib.h>
int main(void) {
    int *p = malloc(sizeof(int));
    *p = 7;
    free(p);          // ASan poisons + quarantines
    return *p;        // heap-use-after-free: reports BOTH free site and use site
}

If you inserted thousands of unrelated malloc/free between the free(p) and the return *p, the quarantine might evict p's block and ASan would no longer catch it — the mechanism's honest limit.

MSan catching an uninitialized read

// msan.c (compile every TU with -fsanitize=memory for accurate results)
#include <stdio.h>
int main(void) {
    int x;                 // never written
    if (x == 0)            // use of uninitialized value
        printf("zero\n");
    return 0;
}
clang -fsanitize=memory -fsanitize-memory-track-origins -g msan.c -o msan && ./msan
# ==..==WARNING: MemorySanitizer: use-of-uninitialized-value
#   #0 ... in main msan.c:5
#   Uninitialized value was created by an allocation of 'x' ...

_FORTIFY_SOURCE aborting a provable overflow

// fs.c  -- compile: gcc -O2 -D_FORTIFY_SOURCE=2 fs.c
#include <string.h>
int main(void) {
    char buf[8];
    // Compiler KNOWS buf is 8 bytes; this copies 16 -> __memcpy_chk aborts.
    memcpy(buf, "0123456789ABCDEF", 16);
    return buf[0];
}
// Runtime: *** buffer overflow detected ***: terminated (abort, not corruption)

When the destination size is not known at compile time, _FORTIFY_SOURCE cannot help — that's the case to be aware of.

Annotating an API so size can't be forgotten

// Carry capacity; tell the compiler about it so it can warn/check.
#include <stddef.h>

__attribute__((access(write_only, 1, 2)))     // ptr=arg1, size=arg2
void fill(char *dst, size_t cap, char c) {
    for (size_t i = 0; i < cap; i++) dst[i] = c;   // bounded by cap
}

Pros & Cons

Sanitizers (ASan/MSan/UBSan):

  • ✅ Precise, instant, named-line reports with allocation/free stacks.
  • ✅ Catch the dominant bug classes; integrate cleanly into CI + fuzzing.
  • ❌ Slow (ASan ~2×, MSan more, Valgrind 10–50×) and memory-hungry → testing only.
  • ❌ Real blind spots: quarantine eviction (temporal), uninstrumented libs (MSan), non-adjacent wild accesses.

Hardened allocators / guard pages / fortify:

  • ✅ Cheap enough for production; remove common exploit primitives; convert corruption to clean abort.
  • ✅ GWP-ASan finds field bugs the lab missed via sampling.
  • ❌ Mitigation, not cure — the underlying bug remains; determined attackers adapt.
  • ❌ Guard pages are coarse (page granularity); can't protect every allocation.

Use Cases

  • CI for any C/C++ project → ASan + UBSan jobs on the test suite; MSan if you can instrument dependencies; a fuzzing job feeding ASan.
  • Hard-to-rebuild binaries / leak hunts → Valgrind/Memcheck.
  • Production native code at scale (Android, browsers) → hardened allocator (Scudo) + sampled GWP-ASan for field telemetry.
  • Legacy C you can't rewrite → compile with _FORTIFY_SOURCE=2/3, -fstack-protector-strong, and annotate hot APIs with size parameters.

Coding Patterns

CI matrix for C/C++:
  job: build+test with -fsanitize=address,undefined
  job: build+test with -fsanitize=memory          (if deps instrumented)
  job: fuzz target  with -fsanitize=address,fuzzer (libFuzzer)
  job: valgrind --leak-check=full ./tests          (slow; nightly)

Production build flags (defense in depth):
  -O2 -D_FORTIFY_SOURCE=3 -fstack-protector-strong -fstack-clash-protection
  link a hardened allocator (Scudo) + enable GWP-ASan sampling

Best Practices

  • Run ASan + UBSan on every CI build of the test suite. Treat any finding as a build failure, not a warning.
  • Pair sanitizers with fuzzing. Sanitizers find the bug; fuzzers find the input that triggers it. Together they're far stronger than either alone.
  • Don't trust a clean ASan run as proof of absence — remember quarantine eviction and the input dependence. Raise quarantine size (ASAN_OPTIONS=quarantine_size_mb=...) when hunting hard UAF.
  • Build production with _FORTIFY_SOURCE, stack protector, and a hardened allocator. These are nearly free and routinely turn an exploitable bug into a clean crash.
  • Carry sizes in your APIs and annotate them. Make "forgot the length" a compile-time-detectable mistake.
  • Use the right tool for the location: ASan for stack/global/heap with recompile; Valgrind when you can't recompile or want leak detection on an arbitrary binary.

Edge Cases & Pitfalls

  • ASan and MSan can't share a process — separate CI jobs.
  • MSan needs full instrumentation. An uninstrumented library that "initializes" your buffer looks uninitialized to MSan → false positives. Instrument deps or use interceptors.
  • _FORTIFY_SOURCE is a no-op when the size isn't compile-time known. Don't assume it covers dynamic-size copies.
  • Quarantine memory cost. A large quarantine catches more UAF but uses more RAM; it's a knob, not free.
  • Guard pages waste a page per guarded allocation — that's why only a sample can use them in production.
  • Stack canaries don't stop targeted writes that skip over the canary (e.g. an indexed write to exactly the return address); they stop linear overflows.
  • Sanitizer builds change timing and layout — a race that reproduces without ASan may hide under it (use TSan for races).

Common Mistakes

  • Treating Valgrind and ASan as the same tool. They have different coverage (Valgrind weak on stack/global OOB; ASan strong there).
  • Shipping a sanitizer build to production "for safety." Sanitizers are detectors, not mitigations, and the slowdown is unacceptable in prod.
  • Concluding "ASan is clean, so we're memory-safe." It's clean on the inputs you ran, within quarantine limits, in instrumented code.
  • Relying on _FORTIFY_SOURCE alone and assuming it covers all memcpy calls (only the compile-time-sized ones).
  • Forgetting that malloc doesn't zero memory and then having MSan (correctly) flag the uninitialized read you didn't notice.

Tricky Points

  • Why 1 shadow byte per 8 app bytes? Allocations are 8-byte aligned, so a single byte can encode the three relevant states (fully valid / fully poisoned / first-k-valid) for an 8-byte chunk, and the address math is a single shift. It's the sweet spot of memory cost vs. precision.
  • Why does ASan need both redzones and quarantine? Redzones handle spatial (adjacent overflow); quarantine handles temporal (UAF). They're orthogonal mechanisms for the two halves of safety, and ASan does both at once.
  • Why is GWP-ASan "without performance cost" if guard pages are expensive? Because it samples — only a tiny fraction of allocations get guard pages, so average overhead is negligible, while the huge install base still surfaces real bugs statistically.
  • Why can't sanitizers give temporal safety in production? The quarantine/shadow overhead is too high to keep memory poisoned forever; you'd need hardware help (MTE) to make per-access temporal checks cheap — that's the professional.md story.

Test Yourself

  1. Explain ASan's shadow-memory scheme and why the per-access check is so cheap.
  2. What do redzones catch, and what do they miss?
  3. How does quarantine detect use-after-free, and what is its fundamental limit?
  4. When would you reach for Valgrind over ASan, and vice versa?
  5. Name three techniques a hardened allocator uses and what each defeats.
  6. Why is GWP-ASan affordable in production when guard pages are coarse?
  7. When does _FORTIFY_SOURCE do nothing, and why?
  8. Argue why temporal safety is fundamentally more expensive to enforce than spatial safety.

Cheat Sheet

DETECTION (testing)
  ASan : OOB + UAF + double-free | redzones + quarantine + shadow(1:8) | ~2x
  MSan : uninitialized reads      | bit-level definedness shadow        | needs full instrumentation
  UBSan: signed overflow, misalign, bad cast, null, bounds | cheap, turn on broad
  TSan : data races (can break memory safety)
  Valgrind/Memcheck: A-bits + V-bits, no recompile, 10-50x, weak on stack/global

MITIGATION (production)
  _FORTIFY_SOURCE=2/3 : compile-time-sized libc calls -> *_chk, abort on overflow
  -fstack-protector-strong : canary catches LINEAR stack overflow
  hardened alloc (Scudo): free-list hardening, metadata separation, randomization
  GWP-ASan : SAMPLED guard pages -> field UAF/OOB reports, ~free on average
  guard page: OS no-access page -> hardware fault on adjacent overflow

KEY TRUTHS
  shadow poisons; hardware faults
  redzone = spatial(adjacent) ; quarantine = temporal(finite)
  clean ASan = evidence, not proof (inputs + quarantine + instrumentation)
  spatial(local, cheap) << temporal(global, costly)

Summary

The tools that defend C/C++ split into detection (find bugs in testing) and mitigation (raise exploit cost in production). Detection is built on shadow memory — a 1-byte-per-8-bytes side table recording addressability — plus redzones (poisoned padding that catches adjacent overflows, giving spatial safety) and a quarantine (poisoned, not-yet-reused freed blocks that catch use-after-free, giving temporal safety until the quarantine evicts the block). ASan does both; MSan adds uninitialized-read detection; UBSan covers other UB; Valgrind offers recompile-free coverage at much higher cost.

Production mitigation can't pay detector overhead, so it uses hardened allocators (free-list hardening, metadata separation, randomization — Scudo), guard pages (hardware-enforced no-access), sampled GWP-ASan (field bug telemetry for ~free), and C-level defenses (_FORTIFY_SOURCE, stack canaries, annotated size-carrying APIs). The recurring lesson: a clean tool run is evidence, not proof; mitigation hardens but doesn't cure; and temporal safety is fundamentally more expensive than spatial safety because "is it still alive?" is a global, time-dependent question with no cheap local check — which is exactly why the next level looks to language design and hardware.


What You Can Build

  • A CI matrix for a small C library with separate ASan+UBSan, MSan, and nightly Valgrind jobs, plus a libFuzzer target feeding ASan.
  • A demonstration that increasing ASAN_OPTIONS=quarantine_size_mb catches a UAF that a small quarantine misses — proving the eviction limit empirically.
  • A "before/after" hardening of a legacy C program: add _FORTIFY_SOURCE=3, stack protector, and a hardened allocator, then show a previously-silent overflow now aborting cleanly.

Further Reading

  • AddressSanitizer: A Fast Address Sanity Checker — Serebryany et al., USENIX ATC 2012 (the shadow-memory + redzone design). https://www.usenix.org/conference/atc12/technical-sessions/presentation/serebryany
  • MemorySanitizer — https://clang.llvm.org/docs/MemorySanitizer.html
  • UndefinedBehaviorSanitizer — https://clang.llvm.org/docs/UndefinedBehaviorSanitizer.html
  • Valgrind / Memcheck manual — https://valgrind.org/docs/manual/mc-manual.html
  • GWP-ASan documentation — https://llvm.org/docs/GwpAsan.html
  • Scudo Hardened Allocator — https://llvm.org/docs/ScudoHardenedAllocator.html
  • glibc _FORTIFY_SOURCE and the __*_chk functions — glibc manual and the -D_FORTIFY_SOURCE documentation.

This is the middle tier of Memory-Safety Mechanisms. From here, senior.md moves from detecting bugs to language designs that prevent them (Rust's borrow checker, managed-runtime guarantees and their escape hatches), and professional.md covers hardware-enforced safety (ARM MTE memory tagging, CHERI capabilities, fat pointers) and the industry-wide migration to memory-safe languages. The tasks.md file gives hands-on sanitizer/Valgrind reasoning exercises, and interview.md drills the mechanisms. Adjacent roadmap areas — undefined behavior, the compilation pipeline, allocator/heap internals, and fuzzing — live in their own folders.


Diagrams & Visual Aids

Shadow Memory (1 byte per 8)

  application memory:  [ b0 b1 b2 b3 b4 b5 b6 b7 ][ b8 ... ]
                              │ maps to                │
  shadow memory:           [ 0x00 ]                 [ 0xFA ]
                          all 8 valid            poisoned (freed/redzone)
  check: s=*((addr>>3)+off); if s && (addr&7)>=s -> BUG

Heap Allocation With Redzones + Quarantine Lifecycle

  ALLOCATED:  [ RZ poison ][  user data (valid) ][ RZ poison ]
                   ^OOB-left      ^in bounds          ^OOB-right -> heap-buffer-overflow

  free(p):    user data -> POISONED, block enters quarantine FIFO
              use *p now  -> heap-use-after-free (free site + use site)

  quarantine full -> oldest block UNPOISONED + reused -> later UAF now UNDETECTABLE

Detection vs Mitigation

            DETECTION (testing)              MITIGATION (production)
            ───────────────────              ───────────────────────
  spatial   ASan redzones / UBSan bounds     _FORTIFY_SOURCE, canaries,
            Valgrind A-bits                   guard pages, hardened alloc
  temporal  ASan quarantine                   randomized reuse, free-list
            Valgrind                          hardening, sampled GWP-ASan
            ↑ slow, precise, named line       ↑ cheap, "harder to exploit",
                                                clean abort not cure