Back to Blog
medium SEVERITY8 min read

Heap Buffer Overflow in drawgfx.c: How ROM Dimensions Can Lead to Code Execution

A heap buffer overflow vulnerability in MAME's drawgfx.c allowed attackers to craft malicious ROM files with manipulated width/height values, causing memcpy to write beyond allocated buffer boundaries and potentially overwrite function pointers for arbitrary code execution. The fix introduces proper buffer-length validation before the copy operation, closing a dangerous attack vector that existed wherever untrusted ROM data controlled memory operations. Understanding this class of vulnerability

O
By Orbis AppSec
•Technically reviewed by Anupam Mediratta•Published May 22, 2026•Reviewed June 3, 2026

Answer Summary

This is a heap buffer overflow vulnerability (CWE-122) in MAME's C codebase, specifically in the drawgfx.c graphics rendering module. Attackers could craft malicious ROM files with oversized width/height dimensions that caused memcpy to write past heap buffer boundaries, potentially overwriting function pointers for code execution. The fix adds explicit buffer-length validation before memcpy operations, ensuring that calculated copy sizes never exceed allocated buffer capacity when processing untrusted ROM dimension data.

Vulnerability at a Glance

cweCWE-122 (Heap-based Buffer Overflow)
fixAdded explicit size validation to ensure copy operations stay within allocated buffer bounds
riskArbitrary code execution through crafted ROM files
languageC
root causeMissing buffer-length validation before memcpy with attacker-controlled dimensions
vulnerabilityHeap buffer overflow via malicious ROM dimension values

Introduction

When a program trusts external data to control how much memory it reads or writes, it hands attackers a powerful weapon. This is exactly what happened in src/emu/drawgfx.c, where graphics layout dimensions sourced directly from ROM files were used, without validation, to drive a memcpy operation. The result: a classic heap buffer overflow (CWE-120) that could allow an attacker to overwrite adjacent heap objects — including function pointers — and achieve arbitrary code execution.

This vulnerability serves as a textbook example of why every size or dimension value derived from untrusted input must be validated before it influences memory operations. Whether you're writing an emulator, a file parser, an image decoder, or a network protocol handler, the lesson here applies directly to your code.


The Vulnerability Explained

What Is a Heap Buffer Overflow?

A heap buffer overflow occurs when a program writes more data into a heap-allocated buffer than the buffer was sized to hold. Unlike stack overflows (which famously corrupt return addresses), heap overflows corrupt adjacent heap metadata or objects. Modern heaps contain allocator metadata, vtable pointers, and function pointers — all of which become attacker-controlled write targets when a heap overflow is triggered.

CWE-120 specifically describes the "classic" buffer copy without checking the size of the input — precisely what was happening here.

The Vulnerable Code Pattern

In drawgfx.c around line 117, graphics offset data was being copied using memcpy, with the size of the copy derived from glcopy.width and glcopy.height:

// VULNERABLE (conceptual representation)
// glcopy.width and glcopy.height come from ROM file data — untrusted input

size_t copy_size = glcopy.width * glcopy.height * sizeof(pixel_t);

// No validation that copy_size fits within the destination buffer!
memcpy(dest_buffer, src_data, copy_size);

The critical flaw: glcopy.width and glcopy.height originate from graphics layout definitions embedded in ROM files. ROM files are external, user-supplied data. An attacker can craft a ROM with arbitrarily large width and height values.

How the Math Becomes a Weapon

Consider a destination buffer allocated for a standard tile size, say 8×8 pixels:

pixel_t *dest_buffer = malloc(8 * 8 * sizeof(pixel_t));  // 64 bytes allocated

Now an attacker crafts a ROM where glcopy.width = 0xFFFF and glcopy.height = 0xFFFF. The computed copy size becomes:

0xFFFF × 0xFFFF × sizeof(pixel_t) = ~4 GB

Even without the integer overflow angle, if the attacker supplies width = 256 and height = 256 against an 8×8 destination, memcpy will happily write 65,536 bytes starting at dest_buffer — obliterating everything the heap allocator placed after it.

Integer Overflow Makes It Worse

There's a secondary hazard: the multiplication width * height * sizeof(pixel_t) can itself integer-overflow on 32-bit size types. An attacker can pick values that wrap the multiplication result to a small number, causing malloc to allocate a tiny buffer while memcpy later uses the un-wrapped large value — a classic "allocate small, write large" pattern.

// Example of integer overflow leading to under-allocation:
uint32_t w = 0x10000, h = 0x10000;
size_t sz = w * h;  // Overflows to 0 on 32-bit!
void *buf = malloc(sz);          // malloc(0) — returns a tiny valid pointer
memcpy(buf, src, w * h);         // Writes 4GB from that tiny pointer

Real-World Attack Scenario

  1. Attacker crafts a malicious ROM file with a graphics layout entry containing oversized width/height values.
  2. Victim loads the ROM into the emulator (or a ROM is distributed via a ROM-sharing site, bundled in a pack, etc.).
  3. The emulator processes the graphics layout, calls into drawgfx.c, and triggers the overflow.
  4. Heap corruption occurs, overwriting an adjacent heap object — potentially a C++ vtable pointer, a function pointer stored in a struct, or allocator freelist metadata.
  5. On the next virtual function call or callback, execution is redirected to attacker-controlled data.
  6. Arbitrary code executes in the context of the emulator process — with whatever privileges the user is running.

This is a remote code execution (RCE) primitive delivered through a data file, a particularly dangerous class of vulnerability because users routinely download and open ROM files from the internet.


The Fix

What Changed

The fix, applied to src/emu/drawgfx.c, introduces explicit buffer-length validation before the memcpy call. The core principle: compute the required copy size first, compare it against the allocated destination buffer size, and abort (or clamp) if the source dimensions would exceed the destination.

// FIXED (conceptual representation)

size_t required_size = (size_t)glcopy.width * (size_t)glcopy.height * sizeof(pixel_t);

// Validate before copying
if (glcopy.width == 0 || glcopy.height == 0) {
    return;  // Nothing to copy
}

if (required_size > dest_buffer_size) {
    // Log the anomaly and refuse to process this graphics entry
    logerror("drawgfx: ROM-supplied dimensions (%u x %u) exceed buffer size %zu, skipping\n",
             glcopy.width, glcopy.height, dest_buffer_size);
    return;
}

// Safe to copy — dimensions are validated
memcpy(dest_buffer, src_data, required_size);

Key Security Improvements

Before After
width × height used directly without bounds check required_size computed with widened types to prevent overflow
No comparison against destination buffer capacity Explicit required_size > dest_buffer_size guard
Attacker-controlled dimensions drive memcpy length Dimensions validated against known-safe allocation size
Silent heap corruption on overflow Error logged, operation aborted safely

Why Casting to size_t Matters

Notice the cast (size_t)glcopy.width * (size_t)glcopy.height in the fix. If width and height are stored as uint16_t or uint32_t, performing the multiplication in their native type risks integer overflow before the result is ever compared. Casting to size_t (which is 64-bit on modern platforms) ensures the multiplication is performed in a wider type, producing the true mathematical result and allowing the bounds check to catch even extreme values.


Conclusion

The heap buffer overflow in drawgfx.c is a sharp reminder that data files are attack surfaces. When an emulator, image viewer, document renderer, or any file-processing application lets external data dictate the size of a memory operation, it must validate those values with the same suspicion it would apply to network input or user form data.

The fix is conceptually simple — check that the computed size fits within the allocated buffer before copying — but the consequences of omitting that check are severe: heap corruption, function pointer overwrite, and arbitrary code execution.

Key Takeaways

  • ✅ Validate all externally-supplied dimensions before using them in memory operations
  • ✅ Use widened integer types when computing sizes to prevent overflow before bounds checks
  • ✅ Compile with AddressSanitizer during development and testing to catch these bugs early
  • ✅ Fuzz your file parsers — automated fuzzing finds these bugs faster than manual review
  • ✅ Apply least privilege so that even successful exploits have limited blast radius

Security is not a feature you add at the end — it's a discipline woven into every line of code that touches untrusted data. When in doubt, validate first, copy second.


This post is part of our ongoing series on real-world security fixes. Vulnerability remediation powered by OrbisAI Security.

Prevention and further reading

Related Articles

critical

Aardvark.Cef.Process.Core Shared Memory Handler: Unvalidated memcpy

The shared memory handler in Aardvark.Cef.Process.Core failed to validate that the `length` parameter passed from JavaScript did not exceed the destination buffer size before calling `memcpy`. This allowed a compromised Chromium renderer process to corrupt heap memory by supplying an oversized `handle->length` value to the `openMapping` function. The fix adds explicit bounds checking using `GetArrayBufferByteLength()` before the copy operation.

high

XShmGetImage Heap Corruption: Unvalidated Image Height in streamproxy

The `XShmGetImage` function in the X11 shared memory image path copies pixel data row-by-row using `memcpy` without validating that the source image height matches the destination buffer height. An attacker or compromised server could provide an oversized source image, causing writes beyond the allocated heap buffer and triggering heap corruption or code execution.

critical

SGX Enclave ecall_store_data memcpy Buffer Overflow in 256-Byte

The Intel SGX enclave's trusted bridge functions `ecall_store_data` and `ecall_retrieve_data` used `memcpy()` to move data into and out of a fixed 256-byte `secure_storage` buffer without validating that `data_len` fit within destination boundaries. An attacker providing oversized `data_len` values could corrupt enclave memory, breaking SGX's confidentiality guarantees.

critical

How Buffer Overflow via strcpy() Happens in C++ XML Parsers and How to Fix It

A critical buffer overflow vulnerability was discovered in `buildroot-external/package/libxmlparser/xmlParser.cpp`, where the `toXMLString` function used `_tcscpy()` to write XML escape sequences into a destination buffer without any bounds checking. An attacker supplying a crafted XML document could overflow the buffer and potentially execute arbitrary code. The fix replaces all five unsafe `_tcscpy()` calls with `memcpy()` calls that copy only the exact number of bytes required for each escape

critical

How Heap Buffer Overflows Happen in C++ ZIP Extraction and How to Fix Them

A critical heap buffer overflow vulnerability was discovered in `TKLiveSync/unzip.cpp`, where ZIP archive entry names were copied into a `PATH_MAX`-sized heap buffer using `strcpy()` without any length validation. Since the ZIP specification allows entry names up to 65,535 bytes — far exceeding typical `PATH_MAX` values of 1,024 to 4,096 bytes — a crafted archive could overflow the buffer and corrupt heap memory. The fix replaces the unsafe `strcpy`/`dirname` pattern with `std::string` operation

medium

How Integer Overflow happens in C++ image processing and how to fix it

A signed integer overflow in OpenCV's `bilateralFilter.cpp` allowed the buffer size calculation `cal_width * cal_height * cn` to wrap around to a small or negative value, causing `padding.resize()` to allocate far less memory than needed. Subsequent `memcpy` operations would then write beyond the allocated buffer, creating a heap corruption primitive. The fix is a single targeted cast to `size_t` that promotes the multiplication to unsigned 64-bit arithmetic before any overflow can occur.