Back to Blog
critical SEVERITY9 min read

How buffer overflow in memcpy happens in C x_util.c and how to fix it

A critical buffer overflow vulnerability was discovered in `hardinfo2/x_util.c` where `memcpy` operations copied data into dynamically allocated arrays without validating that the destination buffer was large enough. The vulnerable pattern used raw `malloc`/`realloc` without checking the return value before immediately using the pointer as a `memcpy` destination, meaning a failed allocation could lead to a NULL pointer dereference or out-of-bounds write. The fix replaces the unsafe manual alloca

O
By Orbis AppSec
•Technically reviewed by Anupam Mediratta•Published June 28, 2026•Reviewed June 28, 2026

Answer Summary

This vulnerability is a heap buffer overflow (CWE-122) in C, located in `hardinfo2/x_util.c` within the `fill_xrr_info()` and `fill_basic_xlib()` functions. The root cause is that `malloc`/`realloc` return values were used directly as `memcpy` destinations without checking for NULL or validating that the allocated size matched the intended write. The fix replaces the raw allocator calls with GLib's `g_renew()` macro and adds explicit NULL guards before each `memcpy`, ensuring writes only proceed when the destination buffer is valid and correctly sized.

Vulnerability at a Glance

cweCWE-122
fixReplace malloc/realloc with g_renew() and add NULL checks before each memcpy call
riskOut-of-bounds memory write leading to memory corruption, crash, or potential code execution
languageC
root causerealloc()/malloc() return values used as memcpy destinations without NULL or bounds validation
vulnerabilityHeap Buffer Overflow via unchecked memcpy destination

How Buffer Overflow in memcpy Happens in C x_util.c and How to Fix It

Summary

A critical heap buffer overflow was discovered in hardinfo2/x_util.c — specifically in the fill_xrr_info() and fill_basic_xlib() functions. The vulnerable code called malloc or realloc to grow arrays of X11 screen and output structures, then immediately used the returned pointer as a memcpy destination without checking whether the allocation succeeded. This means a failed allocation (returning NULL) would cause memcpy to write structured data to address zero, corrupting memory or crashing the process. The fix replaces the unsafe pattern with GLib's g_renew() macro and adds an explicit NULL guard before each memcpy.


Introduction

The hardinfo2/x_util.c file is responsible for collecting X11 display information — enumerating screens and outputs from the X Resize and Rotate (XRandR) extension. It's the kind of low-level infrastructure code that rarely gets scrutinized for security issues because it "just works" most of the time. But buried inside fill_xrr_info() at lines 275–282 and 320–327, a dangerous pattern had been quietly waiting: dynamic arrays grown with realloc, followed immediately by memcpy into the newly allocated region — with no check that the allocation actually succeeded.

This post walks through exactly what went wrong, how it could be exploited, and what the fix does differently.


The Vulnerability Explained

The Vulnerable Code Pattern

Here is the original code from x_util.c around line 275 (screen array growth):

/* VULNERABLE: before the fix */
xrr->screen_count++;
if (xrr->screens == NULL)
    xrr->screens = malloc(xrr->screen_count * sizeof(x_screen));
else
    xrr->screens = realloc(xrr->screens, xrr->screen_count * sizeof(x_screen));
memcpy(&xrr->screens[xrr->screen_count-1], &ts, sizeof(x_screen));

And the parallel pattern for outputs at line 320:

/* VULNERABLE: before the fix */
xrr->output_count++;
if (xrr->outputs == NULL)
    xrr->outputs = malloc(xrr->output_count * sizeof(x_output));
else
    xrr->outputs = realloc(xrr->outputs, xrr->output_count * sizeof(x_output));
memcpy(&xrr->outputs[xrr->output_count-1], &to, sizeof(x_output));

What Makes This Dangerous

There are two compounding problems here:

Problem 1: No NULL check after allocation.
Both malloc and realloc can return NULL when the system is out of memory. When realloc fails, it returns NULL and leaves the original pointer unchanged — but here, the return value overwrites xrr->screens directly. So a failed realloc call sets xrr->screens = NULL, and then the very next line executes:

memcpy(&xrr->screens[xrr->screen_count-1], &ts, sizeof(x_screen));
//      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
//      This is memcpy(NULL + offset, ...) — undefined behavior / crash

Problem 2: The realloc memory leak trap.
The classic C pattern ptr = realloc(ptr, new_size) is itself a memory leak bug: if realloc fails, the original ptr is lost (overwritten with NULL), making the previously allocated memory unreachable. In this code, the original xrr->screens pointer is overwritten unconditionally.

Attack Scenario

This code runs in a local CLI tool that parses X11 display configuration. An attacker who can:

  • Cause the process to run under memory pressure (e.g., via resource limits with ulimit -v)
  • Or manipulate the number of screens/outputs parsed (e.g., through a crafted XRandR configuration or by controlling the input file fed to fill_xrr_info)

...could trigger realloc to fail at exactly the moment screen_count or output_count is incremented. The subsequent memcpy into the NULL-derived pointer writes sizeof(x_screen) bytes of structured data starting at address zero (or a small offset from zero), which on Linux typically causes a segfault — but on systems where address zero is mapped, could corrupt critical memory regions.

Even without a NULL-return scenario, if screen_count is manipulated to be larger than the actual allocated array size (e.g., by corrupting the count variable before the realloc call), the memcpy destination &xrr->screens[xrr->screen_count-1] would point beyond the end of the allocated buffer — a classic heap buffer overflow.

Real-World Impact

For hardinfo2, which is a system information tool often run to gather hardware details, this vulnerability could:

  1. Crash the application silently when parsing systems with many displays or unusual XRandR configurations
  2. Corrupt heap metadata in ways that affect subsequent allocations, potentially leading to exploitable conditions
  3. Be weaponized in environments where the tool is run as part of automated provisioning scripts that an attacker can influence

The Fix

What Changed

The fix, applied at lines 275–283 and 320–328, replaces the unsafe malloc/realloc pattern with GLib's g_renew() macro and adds a NULL guard before each memcpy:

/* FIXED: after the patch */
xrr->screen_count++;
xrr->screens = g_renew(x_screen, xrr->screens, xrr->screen_count);
if (xrr->screens)
    memcpy(&xrr->screens[xrr->screen_count-1], &ts, sizeof(x_screen));
memset(&ts, 0, sizeof(x_screen)); /* clear the temp */

And for outputs:

/* FIXED: after the patch */
xrr->output_count++;
xrr->outputs = g_renew(x_output, xrr->outputs, xrr->output_count);
if (xrr->outputs)
    memcpy(&xrr->outputs[xrr->output_count-1], &to, sizeof(x_output));
memset(&to, 0, sizeof(x_output)); /* clear the temp */

Why g_renew() Is Safer

g_renew(type, ptr, count) is a GLib macro that expands to a type-safe realloc equivalent. Critically, GLib's memory allocation functions abort the process on allocation failure rather than returning NULL silently. This is a deliberate design choice in GLib: for most application-level code, a failed allocation is an unrecoverable condition, and aborting with a clear error message is safer than continuing with a NULL pointer.

This means the if (xrr->screens) guard serves a different but complementary purpose: it protects against any edge case where the pointer might be NULL for other reasons (e.g., initial state, future refactoring), while g_renew() itself ensures that if allocation fails, the program terminates cleanly rather than proceeding to corrupt memory.

Before vs. After: Side-by-Side

Aspect Before After
Allocator Raw malloc/realloc g_renew() (GLib)
Failure behavior Returns NULL silently, overwrites pointer Aborts process with error message
NULL check before memcpy ❌ Missing ✅ Present
Memory leak on realloc failure ✅ Present (classic ptr = realloc(ptr,...) trap) ❌ Eliminated
Type safety Manual sizeof calculation Type-parameterized macro

The Note About Lines 280, 325, and 378

The PR description flags additional instances of the same pattern at lines 280, 325, and 378. This is important: the same malloc/realloc + immediate memcpy anti-pattern appears multiple times in the file, and all instances should be reviewed and patched with the same g_renew() + NULL check approach.


Key Takeaways

  • realloc return values must never be assigned directly back to the source pointer — the classic ptr = realloc(ptr, n) pattern silently leaks memory on failure and sets the pointer to NULL, priming the next line for a NULL dereference.
  • Every memcpy into a heap buffer needs a NULL check on the destination — in fill_xrr_info(), the memcpy(&xrr->screens[xrr->screen_count-1], ...) call had no such guard.
  • GLib's g_renew() is the right tool here — since hardinfo2 already depends on GLib, using g_renew() eliminates the silent-failure mode entirely and makes the code type-safe.
  • The same vulnerable pattern appeared at least four times in x_util.c — lines 280, 282, 325, 327, and 378 all used the same anti-pattern, underscoring the need for a systematic fix rather than a single-line patch.
  • Memory pressure is a realistic attack vector for local tools — even for CLI tools, an attacker who can control resource limits or input data volume can trigger allocation failures to exploit these patterns.

How Orbis AppSec Detected This

  • Source: The screen_count and output_count integer counters in fill_xrr_info(), which are incremented each time a new screen or output record is parsed from X11 data — values that can be influenced by the X server configuration or input files.
  • Sink: memcpy(&xrr->screens[xrr->screen_count-1], &ts, sizeof(x_screen)) at hardinfo2/x_util.c:282, called immediately after a realloc whose return value was not checked for NULL.
  • Missing control: No NULL check on the return value of realloc/malloc before the memcpy destination pointer was dereferenced; no bounds validation ensuring screen_count-1 was within the allocated array size.
  • CWE: CWE-122 — Heap-based Buffer Overflow
  • Fix: Replaced malloc/realloc with g_renew() (which aborts on failure) and added if (xrr->screens) / if (xrr->outputs) guards before each memcpy call.

Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.


Conclusion

The vulnerability in hardinfo2/x_util.c is a textbook example of how a small, easy-to-miss omission — skipping the NULL check after realloc — can create a critical memory safety issue. The pattern ptr = realloc(ptr, n); memcpy(ptr + offset, ...) is deceptively readable but fundamentally unsafe in C. The fix is equally concise: switch to g_renew() for GLib-based code and add a single if (ptr) guard before each memcpy. For developers writing C code that manages dynamic arrays, this case is a strong reminder that every allocation is a potential failure point, and every failure point needs explicit handling before the pointer is used.


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.