High watch CVSS 8.3 chrome OOB 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
8.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOut of bounds write in GPU
DescriptionOut of bounds write in GPU
ComponentGPU
Bug ClassOOB
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker524460000
Fix commitfe6faaabfe75 (chromium/src) +10/-6
CWECWE-787 (Out-of-bounds write)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-06-16 (per vendor advisory)
Fix landed2026-07-21 (public git)
Disclosed2026-08-06
Patch gap16 days — fix public in source before the advisory

Background

`XShmImagePool`
a Chrome UI class that manages a pool of X11 MIT-SHM (shared memory) images used to blit rendered frames to an X server.
Stride / row bytes
the number of bytes actually spanning one row of a bitmap, which the X server may pad up to an alignment (the “scanline padded stride”) so it can exceed the theoretical minimum.
`SkImageInfo::computeMinByteSize()`
a Skia helper returning the smallest possible buffer size for an image, assuming the minimum stride (width × bytes-per-pixel) with no padding.
`SkImageInfo::computeByteSize(rowBytes)`
a Skia helper that computes the buffer size for an explicitly given stride, and can be checked for overflow with SkImageInfo::ByteSizeOverflowed().

Root Cause Analysis

The vulnerable path is XShmImagePool::Resize(), whose body appears in the edited hunk of ui/base/x/x11_shm_image_pool.cc. It sized (and validated the sufficiency of) the shared memory region using image_info.computeMinByteSize(), which assumes the minimum stride, while the pixels are actually installed into the bitmap with state.bitmap.installPixels(image_info, state.shmaddr, row_bytes) using row_bytes = RowBytesForVisualWidth(*visual_info, pixel_size.width()) — the larger, scanline-padded stride. When the padded stride exceeds the minimum, the last rows of the bitmap extend past the end of the allocated/validated shared memory region, so writes into the bitmap can go out of bounds.

The fix computes the required size with image_info.computeByteSize(row_bytes) — the same padded stride later used for installPixels — so the size check reflects the real memory footprint, and additionally rejects the case where the byte-size computation overflows via SkImageInfo::ByteSizeOverflowed().

The patch also hoists the visual_info/row_bytes lookup earlier so row_bytes is available when computing the needed size (returning false if GetVisualInfoFromId(visual_) fails).

Key insight
The size check used a minimum-stride estimate (computeMinByteSize()) while the bitmap was actually laid out with the larger padded stride; the fix makes the check use the identical padded row_bytes (computeByteSize(row_bytes)) so the validated size matches the real access pattern.

Attack Path

  1. Choose a visual with padded stride An attacker-influenced rendering path triggers XShmImagePool::Resize() for a size/visual whose RowBytesForVisualWidth() padded stride is strictly larger than the minimum width × bpp stride.
  2. Under-size the shared region Resize() computes needed_frame_bytes with computeMinByteSize(), so the allocated/accepted shared memory region is smaller than what the padded stride requires.
  3. Install oversized bitmap over the region state.bitmap.installPixels(image_info, state.shmaddr, row_bytes) maps a bitmap using the larger row_bytes on top of the too-small shmaddr region.
  4. Out-of-bounds write Subsequent writes to the final scanlines of the bitmap fall past the end of the shared memory region, producing the out-of-bounds write in the GPU/UI process (inferred as to the precise sink; the diff shows only the sizing mismatch).

Impact Assessment

An attacker able to influence the image size/visual reaching XShmImagePool::Resize() can cause an out-of-bounds write past a shared memory region in the process performing X11 SHM presentation (GPU/browser UI on X11/Linux). The advisory classifies this as an out-of-bounds write in GPU; no exploited-in-the-wild status is stated, and any escalation to memory corruption primitives or code execution is (inferred). The precondition is running on X11 with MIT-SHM and a visual whose scanline padding exceeds the minimum stride.

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

Audit Directions

  • Min-size vs. real-stride mismatch
    Search for computeMinByteSize() (or any minimum/packed size estimate) used to validate a buffer that is later accessed with a padded/aligned stride; the sizing helper must use the same stride as the eventual access.
  • Stride/alignment from external drivers
    Audit code that derives strides from visuals, formats, or GPU/driver-reported alignments (e.g. RowBytesForVisualWidth) and confirm allocation and bounds checks use that padded value, not width × bpp.
  • Unchecked size arithmetic
    Look for image/buffer size computations lacking overflow checks (missing ByteSizeOverflowed()-style guards), where large width/height/stride can wrap and defeat the subsequent bounds check.
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