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
- Attacker crafts a malicious ROM file with a graphics layout entry containing oversized
width/heightvalues. - Victim loads the ROM into the emulator (or a ROM is distributed via a ROM-sharing site, bundled in a pack, etc.).
- The emulator processes the graphics layout, calls into
drawgfx.c, and triggers the overflow. - Heap corruption occurs, overwriting an adjacent heap object — potentially a C++ vtable pointer, a function pointer stored in a struct, or allocator freelist metadata.
- On the next virtual function call or callback, execution is redirected to attacker-controlled data.
- 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.