Skip to content
pwnsy
exploitationintermediate#heap-overflow#stack-overflow#memory-safety#exploitation#cwe

Stack vs Heap Overflow: Two Memory Regions, Two Exploits

How stack and heap buffer overflows differ, why their exploitation models diverge, and what defends each region of memory.

A buffer overflow is a write that runs past the end of its buffer. What that write damages, and how an attacker turns the damage into control, depends entirely on where the buffer lives. A buffer on the stack has different neighbors than a buffer on the heap, and those neighbors decide the exploit. This guide compares the two regions and their exploitation models, building on the mechanics in buffer overflow explained.

Both classes share a single root cause, an unchecked out-of-bounds write, tracked as CWE-787. MITRE splits the location-specific forms into CWE-121 stack-based and CWE-122 heap-based overflows.

Two regions of memory

A running program divides its writable memory into regions with different jobs.

The stack stores function call frames. Each call pushes a frame with the function's local variables and bookkeeping including the saved return address, and each return pops it. The stack is highly ordered, it grows and shrinks in a strict last-in-first-out pattern, and the position of things within a frame is fixed by the compiler.

The heap stores memory the program requests explicitly at runtime, the allocations behind objects, buffers, and structures whose size or lifetime is not known at compile time. An allocator hands out and reclaims these blocks. The heap layout is dynamic: which block sits next to which depends on the exact sequence of allocations and frees the program has performed.

That difference in structure, rigidly ordered versus dynamically arranged, is the reason the two overflows are exploited so differently.

The stack overflow model

On the stack, the valuable neighbor is the saved return address, and it sits at a known offset from a local buffer. A linear overflow past the buffer walks toward that return address and, given enough length, overwrites it. When the function returns, execution jumps to whatever the attacker wrote. The path from bug to hijacked control is short and reliable because the target's location is predictable.

Local function pointers and saved frame pointers are secondary stack targets, but the return address is the classic prize. Stack exploitation is largely about reaching that fixed offset with the right value.

The heap overflow model

The heap has no return addresses. An overflow past a heap buffer corrupts one of two things: the allocator's own metadata (the headers the allocator uses to track block sizes and free lists), or the contents of an adjacent allocation that happens to sit right after the overflowed buffer.

Corrupting an adjacent object is often the cleaner path. If the buffer that overflows sits just before an object containing a function pointer or a length field, the overflow overwrites that pointer or field. Later, when the program uses the corrupted object, it follows the attacker's pointer or trusts the attacker's length. Corrupting allocator metadata is the other path, and historically it let attackers trick the allocator into returning a pointer of their choosing during a later operation.

The catch is that heap layout is not fixed. The attacker rarely knows for free which object sits after the vulnerable buffer. So heap exploitation includes a preparation phase called heap grooming: making allocations and frees in a deliberate order so that the object the attacker wants to corrupt lands immediately after the buffer that will overflow. Techniques like heap spraying are part of that layout control.

Grooming is the real work of heap exploitation

The overflow itself may be a single unchecked copy. The engineering effort goes into shaping the heap so the write lands on a useful target. An attacker allocates and frees objects to arrange predictable adjacency, then triggers the overflow. This is why heap bugs are considered harder to exploit than stack bugs, and why the same bug can be exploitable in one build and not another.

Side by side

PropertyStack overflowHeap overflow
Memory regionFunction call framesDynamically allocated blocks
Primary targetSaved return addressAllocator metadata or adjacent object
Target locationFixed offset, predictableDepends on allocation order
Preparation neededMinimal, reach the offsetHeap grooming to arrange adjacency
Typical hijack pointFunction returnLater use of corrupted pointer or length
Signature defenseStack canaryAllocator hardening, metadata checks
Shared root causeUnchecked out-of-bounds writeUnchecked out-of-bounds write

Why the defenses diverge

Because the regions differ, the region-specific mitigations differ too.

The stack's signature defense is the stack canary, a random value placed between local buffers and the saved return address and checked before return. A linear overflow that reaches the return address must corrupt the canary on the way, and the check catches it. This works well precisely because the stack layout is ordered and the thing being protected has a known position.

The heap's defenses instead protect the allocator. Modern allocators add integrity checks to their metadata, keep bookkeeping separate from user data, randomize placement, detect obvious corruption of free lists, and use safe unlinking so a corrupted header cannot be turned into an arbitrary write as easily as it once could. These raise the cost of metadata corruption. They do less about one object overflowing into the next, which is why memory safety at the language level matters most for the heap.

Region-neutral mitigations apply to both. Non-executable memory, ASLR, and control-flow integrity constrain what an attacker can do after either overflow, covered in exploit mitigations explained.

How to defend against both

The prevention story is the same for both regions, the mitigation story is per region.

  1. Bound every write. Use length-limited copy and concatenation functions and pass the real destination size. An unchecked write is the bug in both regions.
  2. Validate sizes and lengths at the boundary. Reject input longer than any buffer meant to hold it, and check length fields before trusting them, especially before allocating.
  3. Enable stack protection and allocator hardening. Turn on stack canaries and use a hardened allocator with metadata integrity checks. These target the two regions' signature attacks.
  4. Turn on region-neutral mitigations. Non-executable memory, ASLR, and control-flow integrity apply after either overflow and should always be on.
  5. Prefer memory-safe languages. For code that does not require manual memory management, a bounds-checked language removes both classes at once. This is the strongest lever for heap safety in particular.
  6. Fuzz allocation-heavy code. Fuzzing with allocator instrumentation surfaces both the linear stack overflow and the object-adjacency heap overflow that review overlooks.
One bug class, two disciplines

Treat stack and heap overflows as the same root cause with two exploitation disciplines. Fix them the same way at the source with bounds checking and memory safety, then apply the region's specific mitigation on top: canaries for the stack, allocator hardening for the heap. Skipping the region-specific layer leaves the easy path open even after the shared fixes.

A concrete picture of each overflow

Imagine a function that copies attacker-controlled input into a fixed-size local buffer without checking the length. On the stack, that buffer sits inside the current function's frame. Below it, toward higher addresses that the copy walks into, lie the saved frame pointer and the saved return address that the function will use to get back to its caller. A copy of, say, a few hundred bytes into a sixty-four byte buffer runs straight through the buffer, over the saved frame pointer, and into the saved return address. When the function finishes and executes its return, the processor loads the address the attacker wrote and jumps there. The distance from the buffer to the return address is fixed by the compiler's frame layout, so the attacker can compute exactly how many bytes of padding to send before the value that overwrites the return address. That predictability is what makes stack overflows the textbook example of control-flow hijack.

Now put the same unchecked copy into a buffer that was allocated on the heap. There is no return address anywhere near it. Whatever sits immediately after the buffer is whatever the allocator placed there, which depends on the program's history of allocations and frees. It might be allocator bookkeeping that describes the next block, or it might be a completely separate object the program is still using. The overflow corrupts that neighbor. If the neighbor is an object holding a function pointer, the pointer now holds an attacker value, and the next time the program calls through it, control transfers. If the neighbor is a length or size field, the program later trusts an attacker-chosen size and reads or writes out of bounds a second time. The corruption is real in both cases, but the attacker only gets a clean result if they arranged for a useful neighbor to be there, which is the job that has no equivalent on the stack.

Walking through heap grooming

Because the useful target has to be positioned next to the vulnerable buffer, heap exploitation includes a preparation phase. Conceptually it runs like this.

  1. Understand the allocator's behavior. Allocators tend to reuse recently freed blocks of the same size and to place similar allocations together. The attacker uses this predictability rather than fighting it.
  2. Shape the layout. By making the program allocate and free objects in a chosen order, the attacker arranges a freed slot of the right size directly before the object they want to corrupt, or directly before where the vulnerable buffer will land.
  3. Place the vulnerable buffer. The attacker triggers the allocation of the buffer that will overflow so that it falls into the prepared slot, immediately ahead of the target object.
  4. Place the target. The object holding the function pointer or length field is allocated so it lands right after the buffer.
  5. Trigger the overflow. The unchecked copy runs, and the overflow spills out of the buffer and into the adjacent target, overwriting the pointer or field.
  6. Trigger the use. The program later uses the corrupted object, following the attacker's pointer or trusting the attacker's size, and control or a further corruption follows.

The same bug can be reliably exploitable in one build and stubborn in another because the allocator's layout differs, which is why heap work is considered more of an engineering effort than stack work. Techniques like heap spraying are part of shaping and populating the layout so the attacker's data is where it needs to be.

The bug is the same size, the exploit is not

Both overflows can be a single unchecked copy of a handful of lines. The difference in effort is entirely downstream of the bug. On the stack, the high-value target is at a known offset, so reaching it is arithmetic. On the heap, the high-value target has to be maneuvered into place first, so most of the attacker's work happens before the overflow even fires. Judging the severity of an overflow by the size of the flawed code misreads both classes.

Detection signals

Memory-corruption bugs are best caught in development with instrumentation, and their exploitation attempts leave characteristic traces in production.

  • Crashes with a corrupted return address. A stack overflow that fails to fully control execution often crashes at the return, with the instruction pointer set to a value that looks like input data, for example repeated bytes or ASCII. A crash where the faulting address mirrors user-supplied content is a strong stack-overflow signal.
  • Stack canary aborts. A process terminating with a stack-protection failure means a linear write reached the canary before the return address. That is a direct indicator that a stack buffer was overrun.
  • Allocator integrity aborts. Modern allocators terminate the process when they detect corrupted metadata, for example an invalid free-list pointer or a mismatched size. These aborts are the heap analog of a canary trip and point at a heap overflow or a related corruption.
  • Sanitizer reports in testing. Address-sanitizing instrumentation reports the exact out-of-bounds write with a distinction between stack and heap, the offending offset, and the allocation involved. This is the most precise signal and belongs in continuous testing.
  • Repeated allocation patterns before a crash. A burst of same-size allocations and frees followed by a crash can indicate grooming attempts preceding a heap overflow.

Where the unchecked write comes from

Both overflows start from the same small set of coding patterns, and recognizing them is the fastest way to prevent either region's version.

  • Copying without a destination bound. Using a copy or concatenation routine that keeps going until it hits the end of the source, with no reference to the size of the destination, is the classic origin. If the source is longer than the destination, the write runs past the buffer regardless of which region holds it.
  • Trusting an attacker-supplied length. Reading a length field from input and using it to size a copy, without checking it against the real buffer, lets the attacker specify how far the write goes. This is common in parsers of binary formats and network protocols.
  • Off-by-one errors. Writing one element past the end, often a terminating byte written at the buffer's length rather than length minus one, is enough to corrupt a single adjacent byte. On the stack that byte can be part of the saved frame pointer, and on the heap it can be the low byte of an adjacent pointer or size.
  • Integer problems feeding a size. An arithmetic overflow or a signed-to-unsigned confusion can compute a size that is smaller than intended for the allocation but larger for the copy, producing an out-of-bounds write. The allocation looks fine and the copy overruns it.
  • Miscounting element size. Allocating or bounding by element count while copying by byte count, or the reverse, when the element is larger than one byte, undersizes the buffer by a multiple.

Every one of these produces a write that lands outside the intended buffer. The region the buffer sits in decides the exploit, but the origin is the same handful of mistakes, which is why bounds checking and validated sizes at the trust boundary are the shared front-line fix for both.

Common misconceptions

  • "Stack canaries stop all stack overflows." A canary catches a linear overflow that must cross it to reach the return address. It does less against an overflow that overwrites a local function pointer sitting between the buffer and the canary, or against a non-linear write that skips the canary. It is a strong control, not a complete one.
  • "The heap has no control-flow targets." The heap has no return addresses, but it holds function pointers, virtual table pointers on objects, and callback structures. Overwriting one of those transfers control just as a return-address overwrite does, only at the point of use rather than at a return.
  • "A non-executable stack makes overflows harmless." Non-executable memory stops injected code from running, so attackers reuse existing code instead through return-oriented and similar techniques. The overflow still hands over control, covered in exploit mitigations explained.
  • "Heap overflows are too unreliable to worry about." Grooming exists precisely to make them reliable. A determined attacker shapes the layout until the outcome is deterministic, so treating heap bugs as merely theoretical is a mistake.
  • "Bounds checking hurts performance too much to use." The expensive checks are the ones inside hot loops, and even those are often affordable. Length-limited copies and validated sizes at trust boundaries cost little and remove the root cause of both classes.

How exploitation of the two regions evolved

Early exploitation focused on the stack, because overwriting a saved return address to run injected shellcode was direct and reliable. Defenders responded region by region. Stack canaries were introduced to catch linear overwrites of the return address, and non-executable memory removed the ability to run code placed on the stack. Attackers adapted by reusing existing executable code through return-oriented programming, and address randomization was added to make the locations of that code unpredictable. As the stack hardened, attention shifted toward the heap, where the absence of a fixed target made grooming the central skill and allocator metadata a prime object of attack. Allocators then added integrity checks, safe unlinking, and separation of bookkeeping from user data to blunt metadata corruption, which pushed attackers toward overflowing directly into adjacent objects. Control-flow integrity arrived as a region-neutral constraint on where any hijacked pointer may lead. The arc across both regions is the same: a specific mitigation raises the cost of one path, attackers move to the next, and the durable answer turns out to be removing the unchecked write at the source through bounds checking and memory-safe languages.

Frequently asked questions

Are stack and heap overflows the same bug? They share one root cause, an out-of-bounds write tracked as CWE-787. They differ in which memory region is corrupted and therefore in how the corruption is turned into control, which is why they carry separate identifiers, CWE-121 for the stack and CWE-122 for the heap.

Why is a heap overflow considered harder to exploit than a stack overflow? On the stack the valuable target, the saved return address, is at a compiler-fixed offset, so reaching it is straightforward. On the heap the valuable target has to be positioned next to the vulnerable buffer through grooming, which adds an engineering step that the stack does not require.

Does a stack canary protect the heap? No. A canary is a value placed on the stack between local buffers and the saved return address. The heap has no return addresses and no canary in that sense. Heap integrity relies on allocator hardening instead.

Can address randomization alone prevent these overflows? It does not prevent the overflow. It makes the addresses an attacker needs less predictable, raising the cost of turning either overflow into reliable control. It is one region-neutral layer among several and is defeated by information leaks that reveal addresses.

What single change removes both classes? Writing the code in a memory-safe language, or bounds-checking every write in an unsafe one. Both classes are unchecked out-of-bounds writes, so removing the unchecked write at the source removes both at once. Region-specific mitigations only raise the cost of exploiting a bug that still exists.

Where does a use-after-free fit relative to these? A use-after-free is a distinct temporal bug on the heap, where a program uses memory after it has been freed. It often reaches similar outcomes, such as corrupting a function pointer, but its root cause is lifetime rather than an out-of-bounds write. See use-after-free explained.

Which should I fix first if I have limited time? Fix the unchecked writes wherever the input is attacker-controlled and the buffer is small and fixed, regardless of region, because those are the most directly exploitable. Then enable both the stack and heap mitigations so any bug you missed is harder to exploit.

The stack and the heap are two answers to the question of where a program keeps its data, and each answer shapes how an overflow of that data gets exploited. The stack's order makes return-address hijack direct, the heap's flexibility makes layout grooming the attacker's main effort. For a current picture of which memory-corruption bugs are under active exploitation across both regions, see our Exploit Intelligence dashboard.

Sources & further reading

Sharetwitterlinkedin

Related guides