Back to Blog
high SEVERITY5 min read

xcb_get_image_reply() NULL Deref on malloc() Failure

The XCB image-reply handler in the screen-streaming bridge allocated a buffer with `malloc()` and immediately wrote to it with `memset()` and `memcpy()` without checking for allocation failure. Under memory pressure, this produces a null-pointer dereference that crashes the process, turning a resource-exhaustion condition into an immediate denial of service. The fix adds a NULL check that frees the intermediate reply and returns early instead of writing to invalid memory.

O
By Orbis AppSec
•Technically reviewed by Anupam Mediratta•Published October 7, 2026•Reviewed October 7, 2026

Answer Summary

The affected code is first-party C code implementing `xcb_get_image_reply()` in a screen-streaming proxy, not a published package or version. An attacker — or simply sustained memory pressure during screen capture — can trigger `malloc()` failure, causing the process to dereference a NULL pointer in `memset()`/`memcpy()` and crash. The fix checks the `malloc()` return value, frees the intermediate `r2` reply, logs the condition, and returns `NULL` instead of writing to invalid memory. This is tracked as CWE-476 (NULL Pointer Dereference).

Vulnerability at a Glance

cweCWE-476
fixAdded NULL check on `malloc()` result; frees intermediate reply and returns NULL on failure
riskProcess crash (denial of service) when memory allocation fails during image streaming
languageC
root cause`malloc()` return value used unchecked by `memset()` and `memcpy()`
vulnerabilityNULL Pointer Dereference

A crash path hiding inside a routine image-copy routine

Screen-streaming code spends most of its time doing the same unglamorous thing: pull a frame from the X server, copy it into a buffer, hand it to the next stage of the pipeline. That repetition is exactly where defensive checks get skipped — the code "always works," until the one time memory is tight. That's what happened in xcb_get_image_reply(), the function responsible for turning a raw XCB protocol reply into a usable xcb_get_image_reply_t structure for the streaming bridge.

The function computes a buffer size from the reply length (len = (size_t)r2->length * 4), calls malloc() to allocate room for both the struct header and the pixel payload, and then immediately writes into that memory with memset() and memcpy(). There was no check between the allocation and the writes. If malloc() returned NULL — which it will do under memory exhaustion, a plausible condition for a long-running screen-capture process pushing frame after frame — the very next instruction dereferences a null pointer.

Affected Versions

Affected not applicable (first-party code)
Fixed in not applicable (first-party code)
Ecosystem C / XCB-based streaming bridge
CVE / GHSA not assigned
CWE CWE-476: NULL Pointer Dereference

The Vulnerability Explained

Here's the code path before the fix:

size_t len = (size_t)r2->length * 4;
xcb_get_image_reply_t *out =
    malloc(sizeof(xcb_get_image_reply_t) + len);
memset(out, 0, sizeof(xcb_get_image_reply_t));
out->response_type = 1;
out->depth = r2->depth;

The problem is simple to state and easy to miss in review: out is used by memset() on the very next line with no check that malloc() actually succeeded. malloc() returns NULL when the allocation can't be satisfied — a routine, expected outcome under memory pressure, not an exotic edge case. memset(out, 0, sizeof(xcb_get_image_reply_t)) then writes zero bytes starting at address 0, and the kernel delivers SIGSEGV. The subsequent memcpy() at line 307, which would have copied the actual pixel data into out, never even executes because the process is already dead.

Because len is derived from r2->length, the size of the allocation request tracks the size of the frame being captured. A streaming session handling larger frames, or a host already under memory pressure from other processes, is exactly the scenario where malloc() is most likely to fail — and that's precisely when this code had no safety net. The result for a service built on this bridge isn't information disclosure or privilege escalation; it's a hard crash of the entire streaming process at the worst possible moment: when the system is already resource-constrained. For a long-lived relay process, that converts transient memory pressure into a full denial of service that may need an external supervisor to restart.

The Fix

The fix inserts a single guard clause between the allocation and the first write, exactly where the missing check belonged:

xcb_get_image_reply_t *out =
    malloc(sizeof(xcb_get_image_reply_t) + len);
if (!out) {
    free(r2);
    plog("streamproxy: out-of-memory allocating xcb reply\n");
    return NULL;
}
memset(out, 0, sizeof(xcb_get_image_reply_t));

Three things happen in the new branch, each necessary:

  1. free(r2) — r2 is the intermediate XCB reply object that was already allocated earlier in the function. Without freeing it on the failure path, the function would leak that memory on every out-of-memory event, compounding the original problem.
  2. plog(...) — the failure is logged so operators can see that the process is under memory pressure instead of silently losing frames or, previously, crashing with no diagnostic trail.
  3. return NULL — callers of xcb_get_image_reply() are expected to handle a NULL reply (it's the function's documented failure signal), so propagating NULL here lets the existing caller-side error handling do its job instead of letting the process die inside memset().

The fix doesn't change the allocation strategy, the sizing logic, or the function's signature — it closes the one gap between "allocation requested" and "allocation assumed to have succeeded."

Key Takeaways

  • malloc(sizeof(xcb_get_image_reply_t) + len) where len scales with frame size is more likely to fail under load than a fixed small allocation — size-dependent allocations deserve a NULL check even if "it never fails in testing."
  • A memset() or memcpy() immediately following malloc() is a strong signal to check for this exact bug pattern during review: the allocator's return value needs to be validated before it's dereferenced, not after.
  • When a failure path appears late in a function, check what else was already allocated — here, r2 had to be explicitly freed in the new error branch to avoid trading a crash for a leak.
  • Returning NULL on failure only helps if callers already check for it; confirm the calling convention before relying on an early return as the fix.

How Orbis AppSec Detected This

  • Source: the XCB protocol reply (r2, from xcb_get_image_reply's own intermediate call) whose length field drives the size of a subsequent heap allocation.
  • Sink: memset(out, 0, sizeof(xcb_get_image_reply_t)) and memcpy() writing through the pointer returned by malloc(sizeof(xcb_get_image_reply_t) + len).
  • Missing control: no NULL check on the malloc() return value before it was dereferenced.
  • CWE: CWE-476 (NULL Pointer Dereference).
  • Fix: added an if (!out) guard that frees the intermediate reply, logs the out-of-memory condition, and returns NULL instead of writing through an invalid pointer.

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

This is a textbook CWE-476 finding made concrete: a size-dependent malloc() in xcb_get_image_reply() feeding straight into memset() and memcpy() with no check in between. The fix is minimal — four lines — but it changes the failure mode from "process segfaults mid-frame under memory pressure" to "function returns NULL and logs why." For code sitting in a streaming hot path, that difference is the gap between a logged, recoverable error and an unexplained crash in production.

Prevention and further reading

Frequently Asked Questions

Why does `xcb_get_image_reply()` allocate `sizeof(xcb_get_image_reply_t) + len` instead of a fixed size?

The function builds a reply struct that embeds variable-length pixel data; `len` comes from `r2->length * 4`, so the allocation size scales with the size of the captured image frame, making larger or repeated allocations more likely to fail under memory pressure.

What happens to the original `r2` reply object in the fixed version?

On allocation failure, the fix calls `free(r2)` before returning `NULL`, preventing a memory leak of the intermediate XCB reply that would otherwise be abandoned when the function bails out early.

Does the fix change the function's return type or calling convention?

No. `xcb_get_image_reply()` still returns `xcb_get_image_reply_t *`; callers that already check for a `NULL` return (as any correct XCB client should) will now receive `NULL` cleanly on out-of-memory instead of a crashed process.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #121

Related Articles

high

CVE-2026-73089: browserslist 4.28.1 DoS via Query Result Caching

CVE-2026-73089 is a high-severity denial-of-service vulnerability in browserslist versions before 4.28.7. The issue stems from unbounded memory growth when caching results from distinct browser queries, eventually causing out-of-memory crashes in build processes and development servers.

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.

high

How Inherited libvips Vulnerabilities in sharp Impact Image Processing and How to Fix Them

A critical vulnerability (GHSA-f88m-g3jw-g9cj) was discovered where the sharp image processing library inherited four dangerous libvips vulnerabilities that could be exploited through maliciously crafted images. The fix involved upgrading sharp from version 0.34.5 to 0.35.0, which includes hardened input handling and updated libvips bindings to prevent exploitation of these inherited weaknesses.

critical

How NULL pointer dereference from unchecked malloc() happens in C and how to fix it

A critical memory safety vulnerability was discovered in `bench/tokenizer/tokenizer.c` where `malloc()` was called without checking its return value before passing the pointer to `memcpy()`. If allocation fails and `malloc()` returns NULL, the subsequent `memcpy()` writes to address zero, causing heap corruption or potential arbitrary code execution. The fix adds a single NULL check immediately after allocation, exiting cleanly on failure rather than proceeding with a dangerously invalid pointer

high

TweenMax `_applyCycle` Prototype Pollution via vars.cycle Keys

A bundled copy of the TweenMax animation library copied attacker-influenceable `vars.cycle` property names straight onto a tween configuration object using an unguarded `for...in` loop, so a key named `__proto__`, `constructor`, or `prototype` was written through to the object's prototype chain. The fix adds an explicit key denylist to both copies of the `_applyCycle` helper so those three names are skipped during the merge. No CVE or GHSA is assigned; the issue is tracked as CWE-1321 (Improperl