Stack Frame Simulator: the call stack, and the exploit it enables.
A stack frame is the slice of the call stack one function call owns: its locals, the saved
RBP, and the return address. Watch frames grow down and fill, with that return
address sitting exactly where a 24-byte write into a 16-byte buffer will eventually corrupt it.
Then toggle the mitigations off and watch the exploit succeed.
The centre column draws the runtime stack growing downward, one box per function call. Inside
each frame you see the saved RBP, the return address, an optional canary, the locals,
and — in the attack scenarios — a fixed-size buffer rendered byte by byte. The execution log
narrates every push, write, and return. The scenario buttons pick which program runs; the
mitigation toggles turn the stack canary, NX bit, and ASLR on or off; and the state readout up
top flips from "live" to the crash kind when something goes wrong.
Run the basic scenario first to watch frames push and pop cleanly, each return address pointing
back into its caller. Then switch to overflow or RIP overwrite with all three mitigations off and
run it: the write spills past the buffer, corrupts the saved RBP, and eventually
overwrites the return address with 0xdeadbeef_c0de, which the next ret
jumps to. The surprising part is how each defence changes the ending — the canary aborts before
ret ever fires, NX turns the jump into a SIGSEGV, and only with everything switched
off does the exploit actually land.
The stack grows down, and that's the bug
A historical accident every exploit since 1988 has lived inside.
On x86_64 (and most architectures since), the stack grows from high addresses toward low.
A function's prologue subtracts from rsp to reserve space for locals;
push instructions subtract first, then write. Inside a frame, then, the local
variables sit at lower addresses than the saved frame pointer and the return address that
the call instruction stashed for us.
This is the layout that makes stack buffer overflows famous. A C function that does
char buf[16]; strcpy(buf, untrusted_input); writes upward through memory
from the buffer's start. The buffer is at the bottom of the frame. Past its top boundary
sit, in order: saved rbp, then the return address. A 24-byte input
overruns the buffer by 8 bytes and lands on saved rbp. A 32-byte input
(or more, with padding) lands on the return address. Replace the return address with an
attacker-chosen value and the next ret instruction is no longer returning to
the caller — it's jumping wherever the attacker pointed.
Aleph One's 1996 Phrack article Smashing the Stack for Fun and Profit made this
exploitation technique famous, but the Morris worm (1988) had used it in production
against a vulnerable fingerd already. Every CVE involving "stack-based
buffer overflow" since is a variation. The mitigation arms race below is the response.
Three mitigations, three layers
None of them perfect. Together they make the textbook exploit hard.
Stack canaries (StackGuard, ProPolice, /GS). The compiler inserts a
random magic value between the locals and the saved frame pointer at function entry; the
epilogue checks the canary before returning, and if it's been corrupted, calls
__stack_chk_fail which aborts the process. Cost: a few extra instructions
per function with arrays. Defeated by: information leaks (read the canary first, then
write it back unchanged); attacks that don't pass through the canary (heap, format
string, type confusion).
NX / DEP / W^X. Mark every page either writeable or executable, never both. The stack is writeable, so any code on the stack cannot run. An attacker who overwrites the return address can't simply point it at shellcode they wrote into the buffer — the CPU faults on the first fetch. Cost: roughly zero. Defeated by: return- oriented programming (ROP), which chains existing executable gadgets in libc to perform arbitrary work without injecting any code.
ASLR. Randomise the base addresses of the stack, heap, libc, and
(with PIE) the binary itself at every process start. Now the attacker who wants to point
the return address at system("/bin/sh") doesn't know where
system lives. Cost: a small startup overhead and slightly worse caching.
Defeated by: information leaks (read one libc pointer from anywhere → recompute the base);
the address is only 30-some bits of entropy on x86_64 in practice.
Modern exploit chains assume all three are on and work around them — usually by leaking an address first (defeats ASLR), constructing a ROP chain (defeats NX), and either leaking the canary or finding a write that bypasses it (defeats canary). Together they raised the cost of exploitation from "a weekend with gdb" to "a research project with a memory disclosure primitive," which is the meaningful security win even though none of the three is individually bulletproof.
Rust, Go, Java don't have this bug
Or rather: they can't, by construction.
The buffer overflow is a property of writing past an array's end without anything noticing.
C and C++ allow it because arrays decay to pointers and pointers have no length. Rust's
slices carry length, and indexing past the end panics. Go's slices have bounds checks that
don't elide. Java's arrays throw ArrayIndexOutOfBoundsException. None of
these are perfect — Go's unsafe package can do this, Rust's
unsafe blocks can do this, JNI in Java can do this — but the language's
default surface doesn't.
This is the argument the US government's 2024 ONCD report made for "memory-safe languages" in critical software: roughly 70% of serious CVEs in C/C++ codebases are memory-safety bugs, mostly buffer overflows and use-after-frees, and most of those simply can't exist in Rust or Go. The diagram above is the bug class the report is talking about. Adopt a memory-safe language for new code that touches untrusted input, and the cost of the attack surface drops by a large constant factor — not because the attacker is less creative, but because the easiest hole has been sealed at the source.