Skip to content

CWE-122: Heap-based Buffer Overflow

Overview

A heap-based buffer overflow happens when a program writes beyond the end of a buffer stored on the heap. The write can corrupt another live object or allocator metadata, or land in unused padding without an immediate symptom. This is common in C and C++, including code that uses owning containers but writes through unchecked indices or raw pointers. It can also occur in unsafe code and native components used by memory-safe languages.

Relationship to Other CWEs

MITRE files CWE-122 as a Variant and marks it Allowed for mapping, so a finding that identifies a write beyond a heap buffer belongs here rather than on a parent.

  • CWE-122 (this page) - a write past the end of a heap allocation
  • CWE-121 (Stack-based Buffer Overflow) - the same missing bounds check with the buffer on the stack, where the write can reach a saved return address. The fix is the same shape; what differs is what gets corrupted, and therefore which mitigations apply - stack canaries are a CWE-121 control and do nothing here
  • CWE-787 (Out-of-bounds Write) - the parent, and the right level to report at when the finding does not say where the destination was allocated
  • CWE-119 (Improper Restriction of Operations within the Bounds of a Memory Buffer) - the Class above that, which MITRE marks Discouraged for mapping
  • CWE-190 (Integer Overflow or Wraparound) - one way a heap buffer ends up smaller than the code believes: the size passed to the allocator is computed, and the computation wraps. Where a finding names both, CWE-190 is the cause and CWE-122 is the consequence
  • CWE-788 (Access of Memory Location After End of Buffer) - another MITRE parent for this weakness, covering reads as well as writes.

Risk

High to Critical: A heap overflow can corrupt application data, crash the process, or enable code execution through overwritten function pointers, object metadata or allocator bookkeeping. Severity depends on attacker control, reachability, the overwritten data and available mitigations. Some overflows have no immediate visible effect, which does not make the write valid.

Remediation Steps

Core Principle: Size every allocation from the data that will actually go into it, keep that size beside the pointer, and bound every write by it - never by the length of the source.

Trace the Data Path

  • Source: Any length, element count, or content value influenced by user input, file data, or network data that either determines the allocation size or drives a write into the allocation
  • Sink: The write into the heap buffer - an unbounded copy or format call, a loop indexing it, or a copy whose length argument comes from somewhere other than the allocation
  • Missing Controls: No check that the bytes written fit the size actually requested from the allocator; or a size calculation that can wrap or truncate, so the allocator returns less memory than the rest of the code assumes it got

Bound Every Write by the Allocated Size (Primary Defense)

For a raw heap pointer, sizeof reports the pointer's size, not the buffer's capacity. Carry the requested allocation size alongside the pointer, or use a container that manages its size. Some compilers and allocators expose size information, but that is not a portable replacement for tracking writable bounds. A container's reserved capacity also does not make indices beyond its logical size valid.

Make the Size Calculation Overflow-Proof (Primary Defense)

  • Constrain the operands before multiplying or adding, not the product afterwards - once the arithmetic has wrapped, the result looks perfectly reasonable
  • Prefer an allocation call that performs the element-count multiplication itself and fails on overflow, rather than computing the product and passing it in
  • Bound the count against what the format or protocol actually permits, so an absurd length is rejected as invalid input rather than merely surviving the arithmetic

Use Self-Managing Containers Where the Size Is Not Fixed

A container that owns its buffer tracks its length and manages growth. Use its append operations or checked access; an unchecked index or a raw-pointer write can still exceed its bounds. Check any size arithmetic performed before calling the container. Use raw allocations where the required lifetime, interface or layout calls for them.

Harden the Allocator as Defense in Depth

Allocator hardening can validate metadata, and address randomization can make allocation locations less predictable. These measures reduce exploitability without making an out-of-bounds write valid. Metadata checks detect corruption when the allocator inspects the affected records; they may miss writes that alter only application data.

Test with Overflow Payloads

  • Input exactly at the allocated capacity, one byte over, and far over
  • Length and count fields set to values that wrap the size calculation
  • Fuzz any function that allocates from an untrusted length or writes untrusted data into an allocation
  • Confirm with a sanitizer that no out-of-bounds write occurred; a heap overflow that lands in allocator padding produces no crash at all
  • Re-scan with the security scanner to confirm the finding is resolved

Common Vulnerable Patterns

// VULNERABLE - pseudo-code
function load_records(count, source):
    buffer = allocate_on_heap(count * record_size)   // the product can wrap to a small value
    for i in 0 .. count - 1:
        buffer[i] = read_record(source, i)           // the loop trusts count, not the allocation
// Attack: a count large enough that count * record_size wraps
// Result: a small allocation is written as though it were large, corrupting
// neighbouring heap objects and the allocator's bookkeeping

Why this is vulnerable: the allocation uses a product that can wrap, while the loop still writes the original number of records. Sharing the same count does not make those bounds consistent: the unchecked multiplication can request a smaller buffer than the loop needs.

The symptom arrives somewhere else entirely. Past the end of an allocation sit other live objects and the allocator's own records, in an order no source file states and which changes with the program's allocation history, so a heap overflow typically reproduces as a crash inside the allocator during an unrelated later operation, or as a field in some other object that changed for no reason its own code can explain. Allocators also round requests up, so a small overflow may land in that padding and corrupt nothing observable - an absence of symptoms is not evidence the write stayed in bounds.

Secure Patterns

// SECURE - pseudo-code
function load_records(count, source):
    if count < 0 or count > max_records_permitted:
        reject("record count out of range")
    buffer = allocate_array_on_heap(count, record_size)   // allocator does the multiply, fails on overflow
    if buffer is null:
        reject("allocation failed")
    for i in 0 .. count - 1:
        buffer[i] = read_record(source, i)

Why this works: the multiplication happens inside the allocation call, which fails rather than wrapping, so a count that cannot be allocated never reaches the loop. The protocol-level bound rejects an absurd count as invalid input before that, which is a better diagnostic than an allocation failure and keeps the request within a size the process can actually serve. Checking the returned pointer closes the remaining path, where a failed allocation is written through as though it had succeeded.

Common Pitfalls

  • Checking the size after computing it: if (count * item_size > limit) reject performs the multiplication first, so a wrapped product passes the check comfortably - the check has to constrain the operands (count > limit / item_size) before the arithmetic happens rather than inspect its result.
  • Using the pointer's size as the buffer's capacity: sizeof(pointer) measures the pointer. Store the requested capacity and pass it with the pointer wherever bounds are checked.
  • Resizing without updating the recorded capacity: stale capacity after growth can cause repeated, insufficient growth requests; stale capacity after shrinking can directly permit oversized writes. Update the record after a successful resize and preserve the old record on failure.
  • Treating allocator hardening or a clean run as the fix: heap metadata checks fire when the allocator is next used, report corruption that has already happened, and see nothing when the overflow lands in a neighbouring object - and because allocations are rounded up, a genuine overflow can run for years without a crash. Neither the hardening nor the absence of symptoms substitutes for bounding the write.

Language-Specific Guidance

  • C - sizing malloc from the data, overflow-safe size arithmetic (calloc, reallocarray, __builtin_mul_overflow), carrying the capacity beside the pointer, safe realloc growth
  • C++ - std::vector and std::string as heap buffers, reserve versus resize, checked .at() access, handing .data() to a C API safely

Additional Resources