Memory-Safety Vulnerabilities

A single unchecked array index can let a stranger run code on your server. This sounds absurd until you trace the mechanism. A program writes past the end of a buffer. This silently corrupts adjacent memory. Later, that corrupted memory is trusted as data, a pointer, or even an instruction. This is the story of memory-safety vulnerabilities. They are the root cause behind decades of the most damaging security incidents in computing. Examples range from the 1988 Morris Worm to Heartbleed to modern zero-click exploits against phones. Memory-safety bugs matter because C and C++ still power the operating systems, browsers, hypervisors, and embedded firmware that everything else runs on. Both languages give programmers direct control over raw memory and pointers. They provide essentially no runtime enforcement of bounds or object lifetimes. That control is prized for performance. But it means the language itself will not stop a programmer from reading or writing memory it should not touch. Industry telemetry from Microsoft and Google has repeatedly found something troubling. Memory-safety issues account for roughly 65-70% of all high-severity security vulnerabilities in large C/C++ codebases. The big picture is that memory safety is a contract violation. Every valid pointer and object has an implicit contract. That contract governs which addresses it may legitimately reference and for how long. An out-of-bounds access or a stale pointer breaks that contract. The processor does not know or enforce the contract. It will happily execute whatever instruction is next. It will load or store whatever address it is given. Exploitation is the craft of turning a broken contract into attacker-controlled behavior. This might mean corrupting a return address, overwriting a function pointer, or leaking secret bytes that should never have been readable. This article builds a precise mental model of why these bugs are exploitable, not merely that they are dangerous. It defines the categories of memory-safety violations. It walks through the mechanics of how a bug becomes a working exploit. It gives a worked trace of a real overflow. It also catalogs the pitfalls that make these bugs so persistent. It sets up, but does not detail, two natural follow-on topics. These are the classic Stack Buffer Overflow attack and the mitigations. The mitigations — ASLR, DEP, and stack canaries — were built to blunt it.

Key takeaways
  • Memory-safety bugs occur when a program accesses memory outside the bounds or lifetime that a pointer or object legitimately owns.
  • The core categories are spatial (out-of-bounds read/write) and temporal (use-after-free, double-free) violations.
  • C/C++ enforce no bounds or lifetime checks at runtime, so a violation is undefined behavior, not a caught error.
  • Exploitability comes from memory layout: attacker-controlled data sits adjacent to security-critical data like return addresses or vtable pointers.
  • Corrupting control-flow data (return addresses, function pointers) can redirect execution to attacker-chosen code.
  • These bugs motivate both language-level fixes (Memory Safety by design) and runtime mitigations like ASLR, DEP, and stack canaries.
The core insight

Memory-safety vulnerabilities arise because unchecked pointer arithmetic lets a program's data accidentally overwrite the program's own control information, and the hardware will trust whatever it finds there.

The idea

A program is memory-safe if every memory access it performs stays within the spatial bounds of the object being accessed and within the temporal bounds of that object's lifetime. Spatial safety means an access to array `a[i]` only ever touches bytes that truly belong to `a`. Temporal safety means a pointer is only dereferenced while the object it points to is still alive — not before it is initialized and not after it has been freed or has gone out of scope. C and C++ pointers carry no bounds metadata and no liveness metadata at runtime. A `char p` is just an address; the language trusts the programmer to keep track of what `p` may legally reference. When that trust is violated — writing past the end of a buffer, dereferencing a dangling pointer, using an uninitialized pointer — the C standard says the behavior is <strong>undefined</strong>. In practice "undefined" does not mean "crash." It means the CPU executes whatever the corrupted state now describes: it may silently succeed, crash later far from the real bug, or — critically for security — produce a coherent but attacker-manipulable state. The taxonomy of memory-safety violations breaks into two families. <strong>Spatial violations</strong> include out-of-bounds reads and writes (buffer overflows/overreads on the stack, heap, or global data), off-by-one errors, and format-string bugs that expose or write to arbitrary stack addresses. <strong>Temporal violations</strong> include use-after-free (dereferencing a pointer to freed heap memory), double-free (freeing the same allocation twice, corrupting allocator metadata), and uninitialized-memory use (reading a variable before it holds a defined value). Both families share the same underlying cause: a mismatch between the memory the program believes it is manipulating and the memory it actually* manipulates, with the C Memory Model providing no automatic detection.

How it works

Exploiting a memory-safety bug is a pipeline: find a bug that lets you write (or read) outside intended bounds, understand what lives adjacent to that bounds in the real memory layout, and then choose what to overwrite so that the corrupted value is later trusted by the program in a way that gives the attacker control. The stack-smashing case is the canonical illustration, and it generalizes directly to heap corruption and use-after-free. Consider a stack frame containing a local buffer and, at a fixed offset above it, the saved return address the CPU will jump to when the current function returns. If a write into that buffer is not bounds-checked, and the attacker controls the data written, the attacker can write past the end of the buffer and keep going — overwriting saved registers and eventually the return address itself. When the function executes `return`, the CPU loads whatever bytes now occupy that memory slot as the next instruction pointer. If the attacker placed a valid code address there (their own shellcode, or a useful existing function), execution jumps there instead of back to the real caller. ``` FUNCTION vulnerable_copy(input: byte array, input_len: integer) buffer: byte[64] // fixed-size local buffer on the stack // BUG: no check that input_len <= 64 FOR i FROM 0 TO input_len - 1 buffer[i] := input[i] // unchecked write; can exceed 64 END FOR RETURN // CPU reads the (possibly corrupted) return address here END FUNCTION FUNCTION safe_copy(input: byte array, input_len: integer) buffer: byte[64] n := MIN(input_len, 64) // FIX: clamp to the true bound of buffer FOR i FROM 0 TO n - 1 buffer[i] := input[i] END FOR RETURN END FUNCTION ``` The same principle applies to temporal bugs. A use-after-free arises when memory is freed, the allocator hands that same memory to a new allocation (perhaps one the attacker controls the contents of), and the program then dereferences its stale pointer to the old object — but is really now touching attacker-supplied data of a different type. If that stale pointer is, say, a C++ object with a virtual function table pointer, and the attacker's replacement data places a chosen address where the vtable pointer is expected, calling a virtual method on the stale pointer jumps to an attacker-chosen address. ``` FUNCTION use_after_free_pattern() obj := allocate(SomeClass) // heap allocation, contains a vtable pointer free(obj) // memory returned to allocator; obj is now dangling // ... some intervening code triggers a new, attacker-influenced allocation // that the allocator reuses this exact freed slot for ... obj.virtual_method() // BUG: obj is dangling; loads a vtable ptr from // attacker-controlled memory and calls through it END FUNCTION ``` The generic exploitation recipe, spanning both bug classes, is: ``` ALGORITHM exploit_memory_bug INPUT: a program P with a spatial or temporal memory-safety bug OUTPUT: attacker-controlled execution or data leak 1. Locate a bug: an access whose bounds/lifetime the attacker can violate. 2. Determine adjacency: what security-relevant data sits next to (or now aliases) the corrupted memory — return address, saved frame pointer, function pointer, vtable pointer, heap metadata, or a length field. 3. Craft a payload: bytes that, when written to the adjacent location, encode a useful address (attacker code, existing library function, or a chain of instruction fragments). 4. Trigger the vulnerable path with the crafted payload as input. 5. Trigger use of the corrupted value: a function return, an indirect call, or a trusted read of corrupted length/pointer data. 6. Execution or data flow now follows the attacker's chosen value. ```

Figure
Stack layout: how an unchecked write reaches the return address
Stack frame of vulnerable_copy(), growing upward in address high addr low addr Saved return address (target) Saved frame pointer buffer[64] — intended write region (64 bytes of legitimate space) attacker writes 96 bytes into a 64-byte buffer excess 32 bytes overwrite frame ptr, then return address

Complexity & correctness

Memory-safety bugs are not an algorithmic complexity story in the usual sense — the vulnerable code itself runs in the same asymptotic time as its safe counterpart, since a bounds check is `O(1)` per access and adds no asymptotic overhead. The complexity worth analyzing is detection and exploitation cost, and the correctness argument is really an invariant-violation argument.

Detection complexity. Static analysis for memory-safety bugs is undecidable in general (it reduces to reasoning about arbitrary pointer aliasing and program termination), so practical tools trade soundness or completeness for tractability. Dynamic tools such as AddressSanitizer instrument every memory access with an `O(1)`-amortized shadow-memory check, giving roughly 2x-3x runtime and memory overhead in exchange for near-complete detection of observed out-of-bounds and use-after-free accesses along the executed path. Fuzzing-based discovery is essentially a randomized search over an exponential input space; its expected time to find a bug is not bounded by a clean formula but empirically improves with coverage-guided feedback, which is why compilers and OS vendors invest heavily in it.

Correctness argument (why the bug is exploitable, not just "wrong"). The reasoning is an invariant-violation proof. Define the invariant a safe program must maintain: for every write through pointer `p` at offset `k`, `k` lies within `[0, size(p))` and `p`'s target object is currently live. The vulnerable `vulnerable_copy` function above violates this invariant whenever `input_len > 64`, because the loop bound is `input_len`, not `min(input_len, 64)` — there is no clamp enforcing the buffer's true capacity. Once the invariant is broken, the only thing standing between "corrupted bytes" and "hijacked control flow" is the memory layout chosen by the compiler and platform ABI. Because that layout is deterministic and, absent mitigations, predictable, an attacker can reason about exactly which offset the return address occupies relative to the buffer and construct a payload accordingly — this is why the return-address overwrite in the worked example below is not a coincidence but a direct, exploitable consequence of the broken bound-invariant plus a fixed, knowable stack layout.

Tradeoffs. Eliminating memory-safety bugs entirely usually means giving up manual memory control: garbage-collected or borrow-checked languages (Rust, Java, Go) enforce the safety invariant at compile time or via runtime checks, at some cost to raw performance or expressiveness. Retrofitting safety onto existing C/C++ code (sanitizers, hardened allocators, CFI) buys partial protection at runtime cost, and mitigations like ASLR/DEP/stack canaries — foreshadowed here and developed elsewhere — raise the cost of exploitation without removing the underlying bug.

Worked example

Trace a concrete, non-trivial case: a login function with an oversized password copied into a fixed 16-byte stack buffer on a hypothetical 32-bit little-endian machine, where the return address sits 20 bytes above the start of the buffer (16 bytes of buffer + 4 bytes of saved frame pointer, a common but platform-specific layout). ``` FUNCTION check_login(password: byte array, len: integer) local_buf: byte[16] FOR i FROM 0 TO len - 1 // BUG: no bound check against 16 local_buf[i] := password[i] END FOR RETURN END FUNCTION ``` Suppose the attacker calls this with a 24-byte password, and the real address of a small piece of attacker-supplied shellcode already placed elsewhere in memory (e.g., in an environment variable) is `0x0804A050`.

Offset from buffer startBytes writtenDestinationEffect
0–1516 filler bytes (e.g. `0x41` "A")`local_buf[0..15]`Fills buffer exactly, no corruption yet
16–194 filler bytes (`0x41 0x41 0x41 0x41`)Saved frame pointerFrame pointer corrupted to `0x41414141`
20–23`0x50 0xA0 0x04 0x08` (little-endian encoding of `0x0804A050`)Saved return addressReturn address overwritten with attacker's chosen address
StepMachine stateResult
1`check_login` executes `RETURN`CPU pops the value at the return-address slot into the instruction pointer
2That slot now holds `0x0804A050`, not the real caller addressCPU jumps to `0x0804A050`
3`0x0804A050` contains attacker shellcodeArbitrary attacker code executes with the privileges of the vulnerable process

Two details make this trace realistic rather than trivial. First, the frame-pointer corruption (bytes 16-19) is collateral damage the attacker must account for — many real exploits must keep the corrupted frame pointer at least plausible or the crash happens before the return executes, depending on compiler flags. Second, the attacker needed to already know or guess `0x0804A050`; this is exactly the fact that motivates address-space layout randomization as a later-studied defense — without knowing the address, the same overflow produces only a crash, not code execution, which is why exploit reliability and mitigation strength are two sides of the same coin.

Common pitfalls & traps

PitfallWhy it happensConsequence
Off-by-one bound check (`<=` instead of `<`)Confusing "size" with "last valid index"Writes one byte past the buffer, often enough to corrupt an adjacent length field or null-terminator slot
Trusting attacker-supplied length fieldsLength arrives in the same message as the data`memcpy(dst, src, len)` with unchecked `len` becomes an arbitrary-size overflow primitive
Freeing a pointer and continuing to use it later in the same functionManual memory management with many exit pathsClassic use-after-free; the freed slot may be reused with attacker data before the stale access
Double-free from duplicate cleanup paths (error handling + normal path both call `free`)Multiple exception/early-return paths each free the same resourceCorrupts heap allocator metadata, often exploitable into arbitrary write
Assuming "it didn't crash" means "it's safe"Undefined behavior can silently succeed on a given compiler/platformBug ships to production and is only found by an attacker, not by testing
Integer overflow feeding a size computation (`malloc(n * size)`)Multiplication wraps around before the bounds check runsAllocates a too-small buffer, turning a later "checked" copy into an overflow

Pioneers & history

Origins · 1 minRobert Tappan Morris and how this was discovered
RT
Robert Tappan Morris
born 1965 · Cornell University graduate student whose 1988 program exploited a buffer overflow in the Unix `fingerd` service, among other bugs, to self-propagate across the early internet

Morris did not set out to demonstrate memory-safety theory; he wrote a self-replicating program later called the Morris Worm. One of its infection vectors exploited a stack buffer overflow in the `fingerd` daemon on VAX systems, overwriting the return address to redirect execution into attacker-supplied code — a mechanism identical in kind to the one traced in this article's worked example, deployed at internet scale for the first time.

History
1972
the C language, designed by Dennis Ritchie, formalizes the pointer-and-array model that grants speed at the cost of no runtime bounds checking.
1988
the Morris Worm exploits a stack buffer overflow in `fingerd`, causing widespread outages and prompting the founding of the first CERT.
1996
Aleph One's "Smashing the Stack for Fun and Profit" publishes a widely read, systematic explanation of stack-based buffer overflow exploitation.
2001
Microsoft's "Trustworthy Computing" push follows a wave of buffer-overflow worms (Code Red, later Blaster in 2003), elevating memory safety to a company-wide engineering priority.
2014
the Heartbleed bug, an out-of-bounds read in OpenSSL, shows that memory-safety violations can leak private keys and credentials at internet scale without any crash at all.

Where this matters

Operating system kernels, web browsers, and hypervisors are written largely in C or C++ for performance and hardware-level control, which makes memory-safety bugs the dominant category of critical vulnerabilities in these systems; browser vendors run large-scale fuzzing infrastructure (e.g., OSS-Fuzz, ClusterFuzz) specifically to find such bugs before attackers do. Embedded and IoT firmware, often written in C with minimal tooling, is a frequent target precisely because bounds checks and modern mitigations are absent or disabled for resource reasons. Network-facing services — DNS resolvers, TLS libraries, image and video codecs parsing untrusted files — are classic hotspots because they process attacker-controlled input directly into fixed-size buffers. Because these bugs are so persistent, they are the direct motivation for the mitigations covered next: Stack Buffer Overflows as the canonical attack pattern, and the runtime defenses ASLR, DEP, and stack canaries designed specifically to make the exploitation pipeline described here unreliable or impossible even when the underlying bug remains.

Ask the tutor: How exactly does a stack canary detect a return-address overwrite before the vulnerable function returns?

Why doesn't the CPU just refuse an out-of-bounds memory access?

The CPU enforces page-level protection (via the MMU) but has no notion of "arrays," "buffers," or "objects" — those are language-level abstractions that exist only in source code and are erased by compilation. As long as an out-of-bounds access still lands on a page the process is permitted to read or write, the hardware sees a perfectly ordinary, permitted memory operation. Only when an access crosses into an unmapped or protected page does the CPU raise a fault (a segmentation fault). This is exactly why small overflows within the same stack or heap page often succeed silently: nothing at the hardware level distinguishes "buffer" from "the bytes right after it."

Is memory safety only a C/C++ problem?

It is overwhelmingly a C/C++ problem in practice because those languages expose raw pointers and manual memory management with no runtime enforcement. Languages like Java, Python, Go, and C# perform automatic bounds checking on array access and manage object lifetimes with garbage collection, which eliminates the vast majority of these bug classes by construction. Rust achieves memory safety without garbage collection through a compile-time ownership and borrowing system that statically enforces the same lifetime and aliasing invariants. Memory-safety bugs can still occur in these languages at the boundary where they call into unsafe C code or use explicitly unsafe blocks, but the attack surface shrinks dramatically.

What's the difference between a buffer overflow and a use-after-free in terms of exploitation difficulty?

Buffer overflows are generally more predictable: the attacker directly controls the overflowing data and often the length, giving fine-grained control over what gets overwritten and with what value. Use-after-free exploitation is typically harder because the attacker must first win a race or manipulate the allocator to ensure a specific new allocation reuses the freed memory (called "heap grooming" or "feng shui"), and only then does dereferencing the stale pointer touch attacker-controlled data. Both, however, reduce to the same underlying primitive once achieved: the attacker gets to make the program read or execute data it should never have trusted.

If a program never crashes during testing, does that mean it is memory-safe?

No, and this is one of the most dangerous misconceptions in software engineering. Undefined behavior can produce output that looks entirely correct while still corrupting memory that simply isn't read again, or isn't read in a way that changes visible behavior on that particular run. A bug can lie dormant for years until a different compiler, a different optimization level, a different input distribution, or an attacker deliberately crafting an input triggers the corrupted path. Absence of observed crashes is evidence of absence of symptoms, not evidence of memory safety; that gap is exactly why fuzzing, sanitizers, and formal analysis exist.

Why do exploit writers care so much about the exact stack or heap layout?

Because exploitation is fundamentally about predicting which adjacent memory a given overflow will reach and what value placed there produces useful behavior. The layout — where the return address sits relative to a buffer, where a vtable pointer sits relative to an object's fields, how the allocator packs chunks — is determined by the compiler, the ABI, and the allocator implementation, all of which are knowable and, without randomization, constant across runs. This determinism is precisely what mitigations like ASLR attack: by randomizing base addresses per run, the same overflow still corrupts the return address, but the attacker no longer reliably knows what address to write there.

Can memory-safety bugs leak data without any corruption at all, like Heartbleed?

Yes — out-of-bounds reads are just as much a memory-safety violation as out-of-bounds writes, and they need not crash or corrupt anything to be dangerous. Heartbleed exploited a missing length check in OpenSSL's heartbeat extension: the server was tricked into echoing back far more memory than the legitimate request justified, leaking whatever adjacent heap memory happened to contain, including private keys and session data. This illustrates that memory safety is symmetric — both reads and writes must respect an object's true bounds and lifetime — and that read-only violations can be just as catastrophic as write violations.

How does this topic connect to what comes next in stack overflows and mitigations?

This article establishes the mechanism: unchecked bounds plus a predictable memory layout equals a broken invariant an attacker can exploit. The Stack Buffer Overflow topic specializes this mechanism into the single most historically important and pedagogically clean instance — corrupting a saved return address on the stack. The defenses topic then asks a natural follow-up question: given that eliminating every such bug is impractical in legacy C/C++ codebases, how can systems make exploitation unreliable even when a bug exists? ASLR attacks the attacker's knowledge of addresses, DEP attacks their ability to execute injected code, and stack canaries attack their ability to overwrite a return address undetected.

Where this leads