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_SOURCEand hardened libraries in release builds. - Removing the whole class of bug → a memory-safe language, with
unsafecode kept small and reviewed.
What the bugs are
Spatial and temporal bugs, and why a stack overflow reaches the way back.
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:
- If slot = G, the function restores its saved registers and returns.
- If slot ≠ G, a failure handler ends the process without returning
(
__stack_chk_failon 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.
Term
guard page
Take away what an exploit needs
Section
Non-executable memory
W^X: a page may be written or run, not both.
Formula
ASLR entropy
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.
Section
Control-flow protection
Shadow stacks and PAC for returns; landing pads and CFG for indirect calls.
Find it before it ships, or remove it
AddressSanitizer in testing; a language that makes the bug impossible.