Lesson 5 of 9 · 7 min read
Memory Corruption and the Mitigations Against It
In this lesson
TL;DR: Memory bugs break either bounds (out-of-bounds write or read) or lifetime (use-after-free, double free). They become exploits when the corrupted memory holds control data (return addresses, function pointers) or decisions (lengths, permission flags). Each mitigation blocks one step: canaries detect stack smashing, NX stops running injected bytes so attackers reuse existing code, ASLR hides where that code is until a leak reveals it, and CFI and shadow stacks restrict where indirect jumps and returns may land. None fixes the bug. Memory-safe languages remove the classes.
Before you read
Pick an answer before you read further. The lesson below explains it, and guessing first, right or wrong, helps the explanation stay with you.
1A bug lets an attacker read 64 KB of memory past the end of a buffer, but never write. Can it be critical?
The two families
| Family | Bug | What happens |
|---|---|---|
| Spatial | Out-of-bounds write (buffer overflow) | Overwrites neighbouring data or control information |
| Spatial | Out-of-bounds read (over-read) | Discloses neighbouring memory |
| Spatial | Integer overflow in a size calculation | Allocates too little, then writes too much |
| Temporal | Use-after-free | A stale pointer reads or writes memory now owned by another object |
| Temporal | Double free | Corrupts the allocator's bookkeeping |
| Temporal | Uninitialised read | Leaks old data or uses garbage as a pointer |
| Type | Type confusion | Interprets memory as the wrong type, so an integer becomes a pointer |
Precision matters in interviews: a buffer overflow is a write; an over-read is a different bug with different consequences.
From a bug to control
An exploit needs a primitive (what the bug lets you do: write these bytes there, read from here) and a target (what to corrupt):
- Return addresses on the stack: the classic target.
- Function pointers and C++ vtable pointers: redirect an indirect call.
- Allocator metadata: turn a heap overflow into a write somewhere else.
- Lengths and bounds: turn a small write into a large read or write.
- Security decisions: an
is_adminflag, a credentials structure. Changing them redirects nothing and still wins (a data-only attack).
What each mitigation blocks
Protecting: Code execution in the process
- 1
1 · Stack canary
detects the smash before return
- Stops: Linear overflow over the return address
- 2
2 · NX / W^X
data pages cannot execute
- Stops: Jump to bytes the attacker wrote
- 3
3 · ASLR + PIE
addresses unknown without a leak
- Stops: Chain existing code at known addresses
- 4
4 · Shadow stack / CFI
returns must match calls
- Stops: Return into the chain
- Stack canary. A random value between local buffers and saved return data, checked before return. Defeated by leaking it, or by a write that skips over it.
- NX (DEP, W^X). Memory is writable or executable, not both. Attackers answer with return-oriented programming: chaining short sequences of existing instructions that end in a return.
- ASLR and PIE. Libraries, heap, stack and (with PIE) the executable load at random addresses. One leaked pointer reveals a whole module's layout because modules move as units.
- RELRO. Makes the dynamic linker's global offset table read-only, removing an easy write target.
- CFI and shadow stacks. Forward-edge checks (Windows CFG, Intel IBT, ARM BTI) restrict indirect calls to valid targets; backward-edge protection (Intel CET shadow stacks, ARM pointer authentication) makes overwritten return addresses fail.
The mitigations in numbers and versions
| Mitigation | What it does | Numbers and availability |
|---|---|---|
| ASLR | Randomises where code, stack, heap and libraries load | Linux x86-64 randomises the mmap base with 28 bits by default (tunable up to 32); randomize_va_space=2 also randomises the heap |
| Non-executable memory (DEP, NX) | Data pages cannot run as code | Built into Windows since XP SP2 and Server 2003; Microsoft says it is not meant to stop every exploit on its own |
| Stack canaries | A secret value before the return address, checked on return | GCC's -fstack-protector-strong protects functions with local arrays or address-taken locals |
| Forward-edge CFI | Indirect calls may only go to valid targets | Windows Control Flow Guard (/guard:cf at compile and link), Clang CFI (needs link-time optimisation), Intel IBT, Arm BTI (ARMv8.5) |
| Backward-edge protection | Returns go back where they came from | Intel CET shadow stacks (Linux user space since 6.6, Windows per binary with /CETCOMPAT), GCC -fcf-protection=full |
| Arm pointer authentication | A cryptographic code in a pointer's unused high bits | ARMv8.3; with 48-bit virtual addresses the code is 7 bits; per-process random keys on Linux |
| Arm memory tagging (MTE) | A 4-bit tag on each 16-byte granule must match the pointer's tag | ARMv8.5; synchronous mode stops the faulting access, asynchronous mode reports later; Android apps opt in |
Worked example: probabilistic mitigations and the restart budget
Several mitigations are probabilistic, and their strength depends on how many tries the attacker gets.
- ASLR, 28 bits: about 268 million possible bases, so a blind guess almost never works. That is why real exploits use an information leak instead: one leaked pointer reveals the whole module's position, because modules move as units, and the entropy for that module drops to zero.
- Pointer authentication, 7 bits: a forged return pointer passes with probability 1 in 128, and each wrong guess crashes the process. Against a process that is not restarted, that is strong; against a server that silently restarts crashed workers, the attacker expects success in about 128 tries.
- Memory tagging, 4 bits: a stale or overflowing pointer matches its target's tag by chance 1 time in 16. Synchronous mode, where the bad access never happens, is the one to use where accuracy matters.
The design lesson: pair probabilistic mitigations with crash monitoring and restart limits, so repeated crashes are treated as an attack signal rather than a reliability blip.
Memory bug
overflow, use-after-free
Primitive
read or write memory
Information leak
learn addresses
Control flow
redirect a call or return
Gates, with their strength
MTE, canaries
- 4-bit tags catch most bad accesses
ASLR
- 28 bits, gone after one leak
CFI, shadow stacks, PAC
- Valid targets only; 7-bit PAC
The way around the gates
Data-only attack
- Flip a flag or credential pointer
- No control-flow change at all
Catch the bug itself
Memory tagging and canaries detect the corrupting access, probabilistically.
What mitigations do not do
They do not remove the bug, prevent over-reads, stop data-only attacks, or protect code built without them (third-party libraries, JIT output). So a triage note saying "mitigations are enabled, so low severity" is wrong: severity depends on the primitive, who can reach the bug and what the process can access. Mitigations change likelihood and cost, not impact.
The fix for the class
Memory-safe languages enforce bounds and lifetimes by construction (Rust's ownership and borrow checking, bounds-checked indexing; garbage-collected languages). Google has reported that shifting new Android development to memory-safe languages sharply reduced the share of memory-safety vulnerabilities, even though most existing C and C++ remained, because bugs cluster in new and recently changed code. The realistic programme: memory-safe languages for new parsers and network-facing code, sandboxing for the legacy parsers that remain, hardened builds, and continuous fuzzing with sanitizers.
The evidence for memory-safe languages
The numbers are consistent across large codebases. Microsoft reported in 2019 that about 70% of the CVEs it assigns each year were memory-safety issues; the Chromium project reported about 70% of its high-severity security bugs were memory-safety bugs. Android reported in 2024 that the share of memory-safety vulnerabilities fell from 76% in 2019 to 24%, as new code moved to memory-safe languages, while most existing C and C++ stayed in place; Google described the relationship as a correlation driven by new code. CISA, the NSA, the FBI and allied agencies published "The Case for Memory Safe Roadmaps" in December 2023, urging vendors to publish plans, and the NSA and CISA followed with guidance on memory-safe languages in June 2025. The practical shape of a migration: new code and especially parsers in memory-safe languages, legacy parsers isolated in sandboxed processes, hardened builds for what remains, continuous fuzzing with sanitizers, and tracking the share of memory-safety vulnerabilities over time.
Worked example: triaging a fuzzer crash
AddressSanitizer reports a heap-buffer-overflow WRITE of 16 bytes in an image decoder used by a messaging service. Reachability: remote, no interaction (images arrive in messages). Primitive: attacker-controlled bytes past a heap buffer whose size the attacker influences. Process: the media service, which also holds message keys. Mitigations: ASLR, NX, CFI on. Verdict: treat as exploitable remote code execution in a sensitive process; fix immediately, look for the same size calculation elsewhere, add the input as a regression test, and move decoding into a sandboxed process with no keys.
Misconceptions interviewers listen for
- "Return-oriented programming gives root." It gives the privileges of the vulnerable process.
- "Mitigations are enabled, so the bug is low severity." Mitigations change exploit cost, not the presence of a bug; one leak defeats ASLR.
- "Canaries stop all stack bugs." Not writes that skip the canary, leaked canaries or overwrites of locals before it.
- "CFI covers returns." Forward-edge and backward-edge protections are separate mechanisms.
- "Rewrite everything in Rust." The evidence is strongest for new code; legacy code is isolated and hardened.
What if · interview follow-ups
Hold your answer as the constraints change
You are the security engineer for a company that ships a C++ media-processing service that parses uploaded files, running on Linux x86-64 and Arm servers. The interviewer asks how far each mitigation goes.
Start here
What if an attacker has found a heap overflow but has no information leak?
What does ASLR force them to guess?
Addresses: with about 28 bits of randomisation, blind guessing is hopeless, and non-executable memory stops injected code. The attacker's next step is to find or create a leak, so treat any bug that can disclose memory as part of the same exploit chain and fix both.
Carry it forwardASLR is strong against blind guessing and weak against any leak; leaks are exploit enablers.
Weak answerRating the overflow low because ASLR is on.
Constraint changes
What if they also have an arbitrary read?
Which mitigations depend on a secret the attacker can now read?
ASLR and stack canaries both depend on secrets in readable memory, so both fall. What remains is non-executable memory, control-flow integrity, shadow stacks and pointer authentication, whose keys are not readable from user memory. Make sure the binary was built with them and that every module is covered.
Carry it forwardDistinguish mitigations that rely on secrecy from those that do not.
Weak answerAssuming the full mitigation stack still applies after a read primitive.
Constraint changes
What if they have an arbitrary write but every control-flow protection holds?
Do they need to change control flow at all?
No: a data-only attack changes values the program trusts, such as a permission flag, a length or a pointer to credentials, without redirecting any call or return. Only removing the bug, or running the parser in a process with almost no privilege, helps.
Carry it forwardControl-flow defences do not stop attacks on data; contain the process's privilege.
Weak answerDeclaring the bug unexploitable because CFI and shadow stacks are on.
Constraint changes
What if the crashing worker is restarted automatically and runs on Arm with pointer authentication?
How many guesses does a 7-bit code allow?
A forged pointer passes about 1 time in 128, and each failure just restarts the worker, so the attacker expects success after roughly 128 attempts. Rate-limit restarts per client, alert on crash bursts, and treat repeated crashes in the parser as an attack.
Carry it forwardProbabilistic mitigations need a limited attempt budget; restarts give it away.
Weak answerCounting pointer authentication as a hard stop on a self-healing service.
Constraint changes
What if leadership asks how to reduce this class of bug over five years?
What did the large codebases' data show?
Write new code, starting with file parsers, in a memory-safe language; isolate legacy parsers in sandboxed processes with no credentials; build what remains with strong stack protectors, CFI and shadow stacks or pointer authentication; fuzz continuously with sanitizers; and track the share of memory-safety vulnerabilities, as Android did when it fell from 76% to 24%.
Carry it forwardMigrate new and highest-risk code first, contain the rest, and measure the class over time.
Weak answerProposing a full rewrite as the only plan.
Now defend it
As the defender, what would you monitor for memory-corruption exploitation attempts?
Crash rates and signatures per service and per client, especially repeated crashes in parsers; mitigation-triggered terminations such as CFI violations or stack-protector failures; restart counts; build flags for every shipped binary, so modules missing protections are visible; and fuzzing findings by component over time.
How this shows up in interviews
"Explain ASLR, DEP and stack canaries", "what is ROP?", "is this crash exploitable?" and "how would you reduce memory-safety risk in a large C++ codebase?" all draw on this page. Interviewers listen for the step each mitigation blocks, the precondition that defeats it, and the refusal to treat mitigations as a fix.
Say it in 30 seconds: "Memory bugs break bounds or lifetimes, and become exploits when they corrupt control data or decisions. Canaries catch stack smashing, NX forces code reuse, ASLR hides addresses until something leaks, CFI and shadow stacks restrict where control can go. They raise the cost; they don't remove the bug, and data-only attacks walk around the control-flow ones. The real fix is memory-safe code for anything parsing untrusted input, with sandboxing for what's left."
Check yourself
Say or write your answer first, then reveal. Explaining it from memory, in your own words, is the skill an interviewer is checking.
1Why does one leaked library address defeat ASLR for that library?
2Which mitigation stops an attacker from overwriting a 'role' field in a user object from 'user' to 'admin'?
3Why does one information leak usually defeat ASLR for a module?
4What is a data-only attack, and which mitigations does it bypass?