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
Published June 28, 2026Reviewed 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.


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 realloc return values (semgrep.dev/r?q=realloc)
  • Clang Static Analyzer: clang --analyze detects NULL dereference after realloc
  • Coverity: Detects the ptr = realloc(ptr, ...) memory leak pattern
  • Cppcheck: Flags missing NULL checks after dynamic allocation

Standards References


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.


References

Frequently Asked Questions

What is a heap buffer overflow via unchecked memcpy?

It occurs when a program copies data into a heap-allocated buffer without first verifying that the allocation succeeded and the buffer is large enough, potentially writing memory beyond the intended destination.

How do you prevent unchecked memcpy buffer overflows in C?

Always check the return value of malloc/realloc before using the pointer, use safe allocator wrappers like GLib's g_renew() that abort on failure, and add explicit bounds checks before every memcpy call.

What CWE is a heap buffer overflow?

CWE-122 (Heap-based Buffer Overflow) covers cases where data is written past the end of a heap-allocated buffer.

Is checking array indices enough to prevent this type of buffer overflow?

No — you must also verify that the allocation itself succeeded (non-NULL return) and that the allocated size matches the number of elements you intend to write.

Can static analysis detect this type of vulnerability?

Yes. Static analysis tools including Semgrep, Coverity, and clang-analyzer can flag patterns where realloc/malloc return values are used without NULL checks before pointer dereference or memcpy.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #280

Related Articles

high

How buffer overflow happens in C string operations with strcpy/strncpy and how to fix it

A critical buffer overflow vulnerability in `src/pomoc.c` was discovered where `strncpy()` was used unsafely to copy a socket path into a fixed-size buffer. The fix replaces the dangerous string copy with `snprintf()`, which provides automatic bounds checking and null-termination. This prevents attackers from exploiting the CLI tool through oversized input arguments.

high

How integer overflow in malloc happens in C libregexp and how to fix it

A high-severity integer overflow vulnerability was discovered in QuickJS's libregexp.c where multiplication to compute allocation size could wrap around, causing a heap overflow. The fix replaces the unsafe `malloc(sizeof(capture[0]) * lre_get_alloc_count(bc))` pattern with `calloc(lre_get_alloc_count(bc), sizeof(capture[0]))`, which safely handles the multiplication internally and prevents exploitation.

critical

How buffer overflow via sprintf() happens in C++ settings parsing and how to fix it

A critical buffer overflow vulnerability was discovered in `app/src/main/cpp/samp/settings.cpp` where `sprintf()` writes to a fixed 127-byte buffer (`char buff[0x7F]`) without bounds checking. If the `g_pszStorage` global variable contains a string longer than ~107 bytes, the formatted output exceeds the buffer, enabling stack corruption. The fix replaces `sprintf()` with `snprintf()` using `sizeof(buff)` to guarantee writes never exceed the declared buffer length.

high

How buffer overflow happens in C string copy functions and how to fix it

A high-severity buffer overflow vulnerability was discovered in `bin/nad/ftw.c` where unsafe `strncpy()` calls lacked proper NULL-termination guarantees. The fix replaces the vulnerable pattern with `strlcpy()`, a safer bounded string copy function that automatically handles NULL-termination and prevents buffer overflows in file tree walking operations.

high

How buffer overflow via strcpy() happens in C ubus.c and how to fix it

A high-severity buffer overflow vulnerability was discovered and fixed in `ubus.c` at line 577, where `strcpy()` was used to copy user-provided strings into dynamically allocated buffers without explicit size bounds checking. While current allocation logic correctly sizes the buffer, the use of `strcpy()` creates a dangerous coding pattern that could lead to exploitable memory corruption if the allocation logic ever changes or a TOCTOU race condition is introduced. The fix replaces the unbounded

critical

How buffer overflow in strcat() happens in C and how to fix it

A critical buffer overflow vulnerability was discovered in the `daemonize()` function of `tpl.c`, where command-line arguments are concatenated into a fixed-size 8192-byte buffer using `strcat()` without any bounds checking. An attacker who controls command-line arguments can overflow this buffer to corrupt adjacent memory and potentially achieve arbitrary code execution. The fix adds a buffer-length check before each concatenation to ensure writes never exceed the declared buffer size.