Skip to content
Free public beta until December 4, 2026: every course is open, no card needed. After that, the first chapter of each course stays free and full access is a one-time $39 ($19 for up to 100 founding members chosen for helpful feedback). Your progress is kept. How the beta works

Every protection against memory-safety bugs

Stack canaries, non-executable memory, ASLR and PIE, control-flow protection, guard pages, sanitizers and hardened libraries, and memory-safe languages: what each one stops, and what gets past it.

Ask what the attacker or the bug needs, then pick the protection that takes it away. They stack; none is enough alone.

  • A contiguous overflow of a stack buffer reaching the return address → a stack canary catches it at the return (not a non-contiguous write, and not a heap overflow).
  • Running bytes the attacker wrote → non-executable memory (W^X, NX/XN bits): data pages can't be run.
  • Knowing where code and data are → ASLR, with PIE so the program itself moves too. One leaked address undoes it for that region.
  • Reusing existing code by redirecting a return or an indirect call → control-flow protection: shadow stacks or pointer authentication for returns; endbr64 / BTI landing pads and Control Flow Guard for indirect calls.
  • Runaway recursion or an overflow off the end of the stack → the guard page turns it into a clean crash.
  • Finding the bug before it ships → sanitizers (AddressSanitizer, UBSan) and fuzzing in test builds; _FORTIFY_SOURCE and hardened libraries in release builds.
  • Removing the whole class of bug → a memory-safe language, with unsafe code kept small and reviewed.

What the bugs are

Spatial and temporal bugs, and why a stack overflow reaches the way back.

Writing past the end of an array or buffer, so the extra bytes land on whatever is stored next to it in memory. Not the same thing as integer overflow.
Using heap memory through a pointer after that memory has been freed; the program may read garbage, corrupt another object or crash later.

Detect it at run time

Formal. In a function compiled with a stack protector, the prologue copies a per-process random guard value G into a canary slot placed between the frame's local buffers (at lower addresses) and its saved registers and return address (at higher addresses). Before returning, the epilogue reloads the slot and compares it with G:

  1. If slot = G, the function restores its saved registers and returns.
  2. If slot ≠ G, a failure handler ends the process without returning (__stack_chk_fail on Linux and macOS; on Windows the comparison itself is a call to __security_check_cookie, which reports the failure).

Because the stack grows down and an overflow writes upward through consecutive addresses, any contiguous overflow of a local buffer that reaches the saved registers or the return address must overwrite the canary slot first, and is detected at step 2 unless it rewrote the slot with exactly G.

In plain English. The compiler puts a secret number between your arrays and the address the function will return to, and checks it on the way out. Writing far enough past an array to reach the return address means writing over the secret number, so the program notices and stops before the bad return happens.

Example. On Linux x86-64, clang -O1 -fstack-protector-strong compiles a function with a local char buf[16] into a prologue that loads the guard from fs:0x28 (thread-local storage) and stores it in a slot above buf, and an epilogue that compares that slot with fs:0x28 and calls __stack_chk_fail if they differ. A copy that runs far enough past the end of buf to reach the saved return address changes the slot on the way, so the process is ended at the check, with the C library's "stack smashing detected" message (SIGABRT), instead of returning to a damaged address.

Why this rule exists. It says exactly what the protection covers, and so what it doesn't: the check runs only at return, only catches contiguous overwrites that cross the slot, and says nothing about heap memory, out-of-bounds reads, or locals and pointers corrupted and used before the function returns.

Section

Stack canaries

A guard value checked before the return; catches contiguous overwrites only.

A strip of forbidden addresses just past the end of the stack, placed there so running off the end is caught instead of silently trampling other memory.

Take away what an exploit needs

W^X: a page may be written or run, not both.

The flag in a page table entry that forbids running the bytes in that page as instructions, so data such as the stack can't be executed.

Formal. If a region's base address is chosen uniformly from N possible positions (the randomization range divided by the required alignment), its entropy is

k = \log_2 N \text{ bits},

and a single guess of the base, made without other information, is right with probability

P = 2^{-k}.

Every address inside the region moves with its base, so learning any one of them determines the base and reduces the region's remaining entropy to 0.

In plain English. Randomization picks one of 2^k places. More bits means more places and a smaller chance that a guess is right. But a whole library or stack moves as one block, so a single leaked address gives away where all of it is.

Example. A region placed at a random page-aligned (4 KiB = 2^12) address within a 2^40-byte range has N = 2^40 / 2^12 = 2^28 positions. That is k = 28 bits, so one guess succeeds with probability 2^-28, about 1 in 268 million. A 32-bit system with only 8 bits of randomization gives 1 in 256. (Actual bit counts depend on the OS, its version and settings, and the region; these numbers are illustrative.)

Why this formula exists. It turns "the addresses are random" into a number that can be compared across systems and regions. It shows why 64-bit address spaces made ASLR much stronger. It also shows why keeping addresses secret matters as much as the bit count: once an address is known, all k bits of protection are gone.

Section

ASLR and PIE

Random placement, and why one leaked address removes it.

Shadow stacks and PAC for returns; landing pads and CFG for indirect calls.

Checking at every return and indirect call or jump that the destination is one the program's own code allows.

Find it before it ships, or remove it

AddressSanitizer in testing; a language that makes the bug impossible.

Extra memory a sanitizer sets aside to record, for every few bytes of the program's memory, whether those bytes may currently be used.
A programming language whose rules, checked by the compiler and at run time, stop a program from reading or writing memory it doesn't own.