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¶
- Introduction
- Prerequisites
- Glossary
- Core Concepts
- Real-World Analogies
- Mental Models
- Code Examples
- Pros & Cons
- Use Cases
- Coding Patterns
- Best Practices
- Edge Cases & Pitfalls
- Common Mistakes
- Tricky Points
- Test Yourself
- Cheat Sheet
- Summary
- Further Reading
- Related Topics
- 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_SOURCEonly 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=addressand 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:
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:
- ASan — addressability: OOB (spatial) and UAF/double-free (temporal). The default first reach. ~2× slow, ~3× memory.
- MSan — uninitialized 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. - TSan — data 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 replacesmemcpy/strcpy/sprintf/snprintf/... with__*_chkvariants that verify the copy fits, aborting on overflow. Free at runtime when sizes are known; does nothing when they aren't (its blind spot).=3extends 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_SOURCEis 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_SOURCEalone and assuming it covers allmemcpycalls (only the compile-time-sized ones). - Forgetting that
mallocdoesn'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.mdstory.
Test Yourself¶
- Explain ASan's shadow-memory scheme and why the per-access check is so cheap.
- What do redzones catch, and what do they miss?
- How does quarantine detect use-after-free, and what is its fundamental limit?
- When would you reach for Valgrind over ASan, and vice versa?
- Name three techniques a hardened allocator uses and what each defeats.
- Why is GWP-ASan affordable in production when guard pages are coarse?
- When does
_FORTIFY_SOURCEdo nothing, and why? - 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_mbcatches 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_SOURCEand the__*_chkfunctions — glibc manual and the-D_FORTIFY_SOURCEdocumentation.
Related Topics¶
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
In this topic
- junior
- middle
- senior
- professional