Back to Blog
high SEVERITY4 min read

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.

O
By Orbis AppSec
•Published September 30, 2026•Reviewed September 30, 2026

Answer Summary

The XShmGetImage function in first-party X11 streaming proxy code (zcall-bridge/streamproxy.c) copies image data without validating source image height against destination buffer height. An attacker or server returning a larger source image can cause memcpy to write beyond the destination buffer's allocated bounds, corrupting the heap. The fix adds a height comparison to use the smaller of the two heights before the copy loop, preventing out-of-bounds writes. CWE-119 (Improper Restriction of Operations within the Bounds of a Memory Buffer).

Vulnerability at a Glance

cweCWE-119
fixAdd height comparison; iterate only up to minimum of source and destination heights
riskHeap corruption, information disclosure, potential code execution
languageC
root causememcpy loop iteration controlled by untrusted source image height without validation
vulnerabilityBuffer Overflow (Heap)

The Vulnerability Explained

The XShmGetImage function retrieves pixel data from an X11 drawable (window or pixmap) using shared memory for efficiency. During image transfer, the function allocates a destination buffer and copies rows of pixel data using memcpy in a loop.

The vulnerable code iterates based on the source image height without checking whether the source is larger than the destination:

for (unsigned int r = 0; r < (unsigned int)im->height; r++)
    memcpy(image->data + (size_t)r * image->bytes_per_line,
           im->data + (size_t)r * im->bytes_per_line, copy);

Here, im is the source image returned by the X server, and image is the pre-allocated destination buffer. If im->height is larger than image->height, the loop writes past the end of the destination buffer on each row iteration, corrupting heap memory.

Attack Scenario

Consider a video conferencing application that fetches window thumbnails via XShmGetImage:

  1. The application allocates a destination buffer for a 480×360 image.
  2. A compromised or attacker-controlled X server returns an XImage struct with height = 1200.
  3. The loop iterates 1200 times, writing 1200 rows of pixel data into a buffer sized for 360 rows.
  4. Each of the extra 840 writes corrupts adjacent heap objects.

An attacker with X server control (or network interception on an unencrypted X display connection) can trigger this without privilege escalation.

Affected Versions

Affected N/A (first-party code)
Fixed in N/A (see fix commit in pull request)
Ecosystem N/A
CVE / GHSA Not assigned
CWE CWE-119: Improper Restriction of Operations within the Bounds of a Memory Buffer

The vulnerability exists in the XShmGetImage implementation in the zcall-bridge streaming proxy. No version number is assigned; this is a first-party fix applied via pull request review.

The Fix

The fix introduces a critical validation step: compare both heights before entering the loop, and iterate only up to the minimum.

Before:

size_t copy = im->bytes_per_line < image->bytes_per_line
                  ? im->bytes_per_line
                  : image->bytes_per_line;
for (unsigned int r = 0; r < (unsigned int)im->height; r++)
    memcpy(image->data + (size_t)r * image->bytes_per_line,
           im->data + (size_t)r * im->bytes_per_line, copy);

After:

size_t copy = im->bytes_per_line < image->bytes_per_line
                  ? (size_t)im->bytes_per_line
                  : (size_t)image->bytes_per_line;
unsigned int rows = (unsigned int)im->height < (unsigned int)image->height
                        ? (unsigned int)im->height
                        : (unsigned int)image->height;
for (unsigned int r = 0; r < rows; r++)
    memcpy(image->data + (size_t)r * image->bytes_per_line,
           im->data + (size_t)r * im->bytes_per_line, copy);

Why This Works

  1. Height comparison: A new variable rows holds the minimum of source and destination heights. The loop will never exceed the destination buffer's actual row count.
  2. Early type casting: Both im->height and image->height are cast to unsigned int at assignment time, preventing any type confusion or sign-extension bugs during the comparison.
  3. Byte-width safety: The copy variable (bytes per row) was already capped to the smaller of the two; now the iteration count is also capped, creating a complete bounds check.

The fix is minimal and focused: it does not alter the API, does not allocate additional memory, and does not change behavior for validly-sized images.

Key Takeaways

  • Validate both dimensions when copying 2D buffers: When memcpy is called in a loop (row-by-row, chunk-by-chunk), validate both the loop count AND the byte count against the destination size. A single dimension check is insufficient.
  • Never trust height or width from untrusted sources: X server responses, network image headers, or API responses can be crafted. Always compare against the destination allocation before use.
  • Type mismatches enable overflow: The original code mixed signed and unsigned comparisons without consistent casting. Always cast to the same type before boundary checks to avoid wraparound or sign-extension issues.
  • Shared memory APIs are high-risk: XShm and similar zero-copy mechanisms skip normal marshalling checks. Code using them must add extra validation where normal copy APIs would fail safely.

How Orbis AppSec Detected This

Source: The X server response, specifically the im->height field in the XImage struct returned by XGetImage.

Sink: The memcpy call inside the row-copy loop, invoked with a loop bound derived from im->height.

Missing control: No validation that im->height does not exceed image->height before the loop begins. The code validated bytes-per-line but not row count.

CWE: CWE-119 — Improper Restriction of Operations within the Bounds of a Memory Buffer.

Fix: Add a height comparison that computes the minimum of source and destination heights, and use that minimum as the loop bound instead of the untrusted source height.

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

Heap buffer overflows in image-processing and streaming code often stem from assumptions that all dimensions come from the same trusted source. The XShmGetImage fix demonstrates that when source and destination are decoupled (especially across network or IPC boundaries), every dimension must be validated independently.

The minimal height comparison added here—comparing two integer fields before a loop—prevents an attacker or compromised server from overwriting heap memory. For streaming and real-time imaging applications, this kind of bounds checking is as critical as input validation in web frameworks.

Prevention and further reading

Frequently Asked Questions

Why does the fix cast both heights to unsigned int before comparing them?

The original code cast only during the loop condition, allowing a negative or very large im->height to bypass validation. Casting to unsigned int early ensures the comparison and minimum selection work on the same type and prevent wraparound or sign-extension issues.

Could an attacker trigger this by sending a crafted X11 server response?

Yes—if the application calls XShmGetImage against a remote or compromised X server, that server controls the height field in the returned XImage struct. An oversized height value would cause the memcpy loop to overflow the destination buffer.

Does the fix change the public API of XShmGetImage?

No. The fix is internal to the copy loop and does not alter the function signature, parameters, or return value. Calling code requires no changes.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #89

Related Articles

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 happens in C++ and how to fix it

A critical buffer overflow in `create_hex_string()` within `hmlangw.cpp` let an unconditional 16-iteration loop write past the bounds of a 100-byte `hex` buffer using unchecked `sprintf` calls. The fix replaces `sprintf` with `snprintf` and caps the loop iterations based on the actual destination buffer size, closing off a memory corruption path reachable from serial or network input.

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.

high

image-size 1.2.1 DoS: Zero-Valued Dimensions in Image Buffer Parser

A high-severity denial-of-service vulnerability in image-size 1.2.1 allows attackers to crash Node.js services using malicious image buffers with zero-valued dimensions. The fix removes the vulnerable `queue` dependency and tightens dimension validation in version 2.0.3.