CVE-2026-19148
Overview
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).
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
- Choose a visual with padded stride
An attacker-influenced rendering path triggers
XShmImagePool::Resize()for a size/visual whoseRowBytesForVisualWidth()padded stride is strictly larger than the minimumwidth × bppstride. - Under-size the shared region
Resize()computesneeded_frame_byteswithcomputeMinByteSize(), so the allocated/accepted shared memory region is smaller than what the padded stride requires. - Install oversized bitmap over the region
state.bitmap.installPixels(image_info, state.shmaddr, row_bytes)maps a bitmap using the largerrow_byteson top of the too-smallshmaddrregion. - 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
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
| Function | Change | Notes |
|---|---|---|
ifui/base/x/x11_shm_image_pool.cc |
modified | |
forui/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 mismatchSearch 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 driversAudit 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, notwidth × bpp. - Unchecked size arithmeticLook 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.
Patch
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))
Original Bug Report
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.ccui/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:
- 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 byXShmImagePool. 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. - 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:
-
Window Creation & Setup: A 1-pixel wide, 4096-pixel tall window or popup is requested. Chromium’s X11 platform window code (
SanitizeBoundsandAdjustSizeForDisplayinui/ozone/platform/x11/x11_window.cc) explicitly permits 1x1 minimum bounds. The area is 4096, bypassing thekMinImageAreaForShmem = 256exclusion inXShmImagePool::Resize. -
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
1x4096at 16bpp (minRowBytes=2),computeMinByteSize()returns(4096 - 1) * 2 + 2 = 8192bytes. -
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).
-
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 } -
Pixel Binding:
installPixelsbinds the12288-byte buffer with a4-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 because4 >= minRowBytes(2). -
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 to16381. Because the mapped segment ends at12287, 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 aminRowBytesof 2 bytes. - At a height of 4096 pixels, the
computeMinByteSizeis 8192 bytes. The 1.5x allocation threshold (kShmResizeThreshold) setsframe_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.ccSanitizeBoundsbounds 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.