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:
- Crash the application silently when parsing systems with many displays or unusual XRandR configurations
- Corrupt heap metadata in ways that affect subsequent allocations, potentially leading to exploitable conditions
- 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.
Prevention & Best Practices
1. Never Use the ptr = realloc(ptr, size) Pattern Directly
The idiomatic safe pattern in pure C is:
/* Safe realloc pattern */
x_screen *tmp = realloc(xrr->screens, new_count * sizeof(x_screen));
if (tmp == NULL) {
/* handle error — do NOT overwrite xrr->screens */
return FALSE;
}
xrr->screens = tmp;
memcpy(&xrr->screens[new_count-1], &ts, sizeof(x_screen));
Using a temporary pointer preserves the original on failure and makes error handling explicit.
2. Prefer GLib Allocators in GLib-Based Code
If your codebase already uses GLib (as hardinfo2 does), prefer g_new(), g_renew(), and g_malloc() over their C standard library equivalents. These wrappers abort on failure, eliminating the NULL-return failure mode entirely for code where recovery isn't meaningful.
3. Always Check memcpy Destination Validity
Before any memcpy into a heap-allocated buffer, verify:
- The destination pointer is non-NULL
- The destination index is within bounds (index < allocated_count)
- The source size matches the destination element size
4. Use AddressSanitizer During Development
Compile with -fsanitize=address during development and testing. AddressSanitizer will catch heap buffer overflows at the exact instruction that causes them, making these bugs trivial to find during testing.
gcc -fsanitize=address -g -o hardinfo2 hardinfo2/x_util.c ...
5. Static Analysis
Tools that can detect this pattern:
- Semgrep: Rules for unchecked
reallocreturn values (semgrep.dev/r?q=realloc) - Clang Static Analyzer:
clang --analyzedetects NULL dereference afterrealloc - Coverity: Detects the
ptr = realloc(ptr, ...)memory leak pattern - Cppcheck: Flags missing NULL checks after dynamic allocation
Standards References
- CWE-122: Heap-based Buffer Overflow
- CWE-476: NULL Pointer Dereference
- OWASP Memory Safety: OWASP Secure Coding Practices
Key Takeaways
reallocreturn values must never be assigned directly back to the source pointer — the classicptr = realloc(ptr, n)pattern silently leaks memory on failure and sets the pointer to NULL, priming the next line for a NULL dereference.- Every
memcpyinto a heap buffer needs a NULL check on the destination — infill_xrr_info(), thememcpy(&xrr->screens[xrr->screen_count-1], ...)call had no such guard. - GLib's
g_renew()is the right tool here — sincehardinfo2already depends on GLib, usingg_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_countandoutput_countinteger counters infill_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))athardinfo2/x_util.c:282, called immediately after areallocwhose return value was not checked for NULL. - Missing control: No NULL check on the return value of
realloc/mallocbefore thememcpydestination pointer was dereferenced; no bounds validation ensuringscreen_count-1was within the allocated array size. - CWE: CWE-122 — Heap-based Buffer Overflow
- Fix: Replaced
malloc/reallocwithg_renew()(which aborts on failure) and addedif (xrr->screens)/if (xrr->outputs)guards before eachmemcpycall.
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.