Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOut of bounds write in GPU
DescriptionOut of bounds write in GPU
ComponentGPU
Bug ClassOOB
Tracker524460000
Fix commitfe6faaabfe75 (chromium/src) +10/-6
CISA KEVNot listed
CreditedGoogle
Disclosed2026-08-06

Changed Functions

FunctionChangeNotes
if
ui/base/x/x11_shm_image_pool.cc
modified
for
ui/base/x/x11_shm_image_pool.cc
modified

Files Changed

  • ui/base/x/x11_shm_image_pool.cc
From fe6faaabfe75a5ea9c4f2383ffc07ff80fdc3a74 Mon Sep 17 00:00:00 2001
From: kylechar <kylechar@chromium.org>
Date: Tue, 21 Jul 2026 13:05:01 -0700
Subject: [PATCH] Compute XShmImagePool size including row bytes

XShmImagePool::Resize() was checking if a shared memory region was big
enough using computeMinByteSize() which assumes the minimum possible
stride. The shared memory region is used with the scanline padded stride
which could be larger than the minimum. Compute the required size using
the scanline padded stride to avoid potential OOB access past end of the
shared memory region.

Bug: 524460000
Change-Id: Iabe795c6fac7487fd4addd1ecf2e07ca7026e763
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8128799
Reviewed-by: Kramer Ge <fangzhoug@chromium.org>
Commit-Queue: Kyle Charbonneau <kylechar@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1665713}
---

diff --git a/ui/base/x/x11_shm_image_pool.cc b/ui/base/x/x11_shm_image_pool.cc
index 8e418ac..dfab3aa1 100644
--- a/ui/base/x/x11_shm_image_pool.cc
+++ b/ui/base/x/x11_shm_image_pool.cc
@@ -154,9 +154,18 @@
   if (color_type == kUnknown_SkColorType)
     return false;
 
+  const auto* visual_info = connection_->GetVisualInfoFromId(visual_);
+  if (!visual_info) {
+    return false;
+  }
+  size_t row_bytes = RowBytesForVisualWidth(*visual_info, pixel_size.width());
+
   SkImageInfo image_info = SkImageInfo::Make(
       pixel_size.width(), pixel_size.height(), color_type, kPremul_SkAlphaType);
-  std::size_t needed_frame_bytes = image_info.computeMinByteSize();
+  std::size_t needed_frame_bytes = image_info.computeByteSize(row_bytes);
+  if (SkImageInfo::ByteSizeOverflowed(needed_frame_bytes)) {
+    return false;
+  }
 
   if (needed_frame_bytes > frame_bytes_ ||
       needed_frame_bytes < frame_bytes_ * kShmResizeShrinkThreshold) {
@@ -209,11 +218,6 @@
     }
   }
 
-  const auto* visual_info = connection_->GetVisualInfoFromId(visual_);
-  if (!visual_info)
-    return false;
-  size_t row_bytes = RowBytesForVisualWidth(*visual_info, pixel_size.width());
-
   for (FrameState& state : frame_states_) {
     state.bitmap = SkBitmap();
     if (!state.bitmap.installPixels(image_info, state.shmaddr, row_bytes))
Loading diff…

Original Bug Report

reported by aw...@chromium.org

OOB write in GPU process via XShmImagePool stride/allocation divergence

Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: XShmImagePool uses Skia’s tightly-packed computeMinByteSize() for shared memory allocation but an X11-derived padding for stride. A narrow window on a 16bpp X server may cause the stride to overcome the 1.5x allocation margin, leading to an OOB write. This can also be triggered by a malicious X server sending an inflated scanline_pad.

Affected files:

  • ui/base/x/x11_shm_image_pool.cc
  • ui/base/x/x11_util.cc

Estimated timestamp from git blame: Unknown (Google3 checkout)

1. Summary of the Issue (Meant for Human Triage)

An out-of-bounds (OOB) memory write exists in the X11 graphics pipeline within Chromium’s GPU process. The vulnerability stems from an architectural discrepancy in XShmImagePool::Resize, which uses two inconsistent calculations for buffer size and row stride. Specifically, the shared memory (SHM) segment is tightly allocated using Skia’s computeMinByteSize(), but the row stride passed to SkBitmap::installPixels is calculated via RowBytesForVisualWidth(), which relies on X11 scanline padding.

This divergence can be exploited in two ways:

  1. Benign X Server (Potential S1): On a standard 16-bit color X server (bpp=16, scanline_pad=32), a 1-pixel wide window causes a padded stride of 4 bytes against a minimum row size of 2 bytes. This divergence overcomes the 1.5x allocation margin used by XShmImagePool. If a web adversary can coerce the browser into creating a 1-pixel wide, highly tall (e.g., 1x4096) surface (such as an override-redirect popup), they can reliably trigger an OOB write of web-controlled pixels in the sandboxed GPU process.
  2. Malicious X Server (Hardening/S4): A compromised X server can supply an inflated, unvalidated scanline_pad (e.g., 255), forcing an arbitrary stride discrepancy and leading to a massive OOB write during rendering. While X11 is generally within the TCB, hardening this boundary provides defense-in-depth and prevents lateral escalation into the GPU process.

2. Proof-of-Concept & Detailed Execution Flow

The divergence occurs during window resizing/reallocation in software compositing mode. The execution flow for a potential web-triggered OOB write is as follows:

  1. Window Creation & Setup: A 1-pixel wide, 4096-pixel tall window or popup is requested. Chromium’s X11 platform window code (SanitizeBounds and AdjustSizeForDisplay in ui/ozone/platform/x11/x11_window.cc) explicitly permits 1x1 minimum bounds. The area is 4096, bypassing the kMinImageAreaForShmem = 256 exclusion in XShmImagePool::Resize.

  2. Tight Allocation: XShmImagePool::Resize(gfx::Size(1, 4096)) calculates the needed memory footprint tightly using Skia:

    // ui/base/x/x11_shm_image_pool.cc:157
    SkImageInfo image_info = SkImageInfo::Make(
        1, 4096, kRGB_565_SkColorType, kPremul_SkAlphaType);
    std::size_t needed_frame_bytes = image_info.computeMinByteSize(); 
    

    For 1x4096 at 16bpp (minRowBytes=2), computeMinByteSize() returns (4096 - 1) * 2 + 2 = 8192 bytes.

  3. Allocation with Threshold: The code multiplies this by kShmResizeThreshold (1.5x) and maps the SHM segment:

    frame_bytes_ = needed_frame_bytes * kShmResizeThreshold; // 8192 * 1.5 = 12288 bytes
    state.shmid = shmget(IPC_PRIVATE, frame_bytes_, IPC_CREAT | SHM_R | SHM_W | ...);
    state.shmaddr = reinterpret_cast<char*>(shmat(state.shmid, nullptr, 0));
    

    The kernel allocates and maps exactly 12288 bytes (perfectly aligning to three 4096-byte pages).

  4. Inflated Stride Calculation: The stride is calculated using the X server’s standard padding requirements:

    // ui/base/x/x11_util.cc:85
    size_t RowBytesForVisualWidth(const x11::Connection::VisualInfo& visual_info, int width) {
      auto bpp = visual_info.format->bits_per_pixel; // 16
      auto align = visual_info.format->scanline_pad; // 32
      size_t row_bits = bpp * width; // 16
      row_bits += (align - (row_bits % align)) % align; // 16 + 16 = 32 bits
      return (row_bits + 7) / 8; // 4 bytes
    }
    
  5. Pixel Binding: installPixels binds the 12288-byte buffer with a 4-byte stride:

    // ui/base/x/x11_shm_image_pool.cc:219
    if (!state.bitmap.installPixels(image_info, state.shmaddr, row_bytes))
    

    Skia’s validRowBytes() allows this because 4 >= minRowBytes(2).

  6. OOB Write: The compositor rasterizes into the canvas. For the final row (y = 4095), Skia computes the memory address: shmaddr + (4095 * 4) = shmaddr + 16380. The pixel write extends to 16381. Because the mapped segment ends at 12287, this results in an out-of-bounds write of >4,000 bytes into adjacent GPU process memory.

Suggested Fix: Ensure that the allocation size in XShmImagePool::Resize() explicitly accommodates the padded stride rather than relying on Skia’s unpadded computeMinByteSize(). Additionally, add validation for scanline_pad to ensure it is within reasonable bounds.

3. Technical Verification Details (Automated Audit Logs - Reviewers may skip this section)

Prior Critic Verdict: “Severity: Bug (S4 / Hardening). The technical analysis of the vulnerability is 100% accurate: there is an architectural discrepancy in XShmImagePool::Resize() between the shared memory allocation size (which uses Skia’s tightly bound computeMinByteSize()) and the row stride used by installPixels()… However, under Chromium’s threat model, this is not a security vulnerability [because it assumes a malicious X server].”

Double-Check / Synthesizer Notes: The prior Critic’s technical validation of the code divergence remains 100% accurate. However, the assumption that this ONLY triggers via a compromised X server (TCB) was re-evaluated.

  • Under standard benign X11 configurations (16bpp color, scanline_pad=32), the math proves that a 1-pixel wide window yields a stride of 4 bytes versus a minRowBytes of 2 bytes.
  • At a height of 4096 pixels, the computeMinByteSize is 8192 bytes. The 1.5x allocation threshold (kShmResizeThreshold) sets frame_bytes_ to 12288 bytes.
  • Skia’s write offset for the last row is 4095 * 4 = 16380.
  • Because 16380 > 12288 (and effectively bypasses page-rounding since 12288 is exactly 3 pages), an OOB write is mathematically proven.
  • ui/ozone/platform/x11/x11_window.cc SanitizeBounds bounds checking allows 1x1 windows (std::max(bounds.width(), 1)).
  • Web capabilities to spawn 1px-wide surfaces (e.g. override-redirect popups) elevate this from an S4 TCB-hardening issue to a potential S1 GPU sandbox escape. The technical mechanics of the divergence are unequivocally confirmed.

Evaluated with Chrome root at commit: 8c517fbcbb533e59ec9cedac868c8a9bdc30beb2


Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:

  • If you are familiar with the severity guidelines, you may adjust the severity.
  • If this is a false positive, and there’s no work to be done, please close as WAI.
  • If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.

Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.

View on issue tracker