CVE-2026-19148
Overview
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
Patch
From fe6faaabfe75a5ea9c4f2383ffc07ff80fdc3a74 Mon Sep 17 00:00:00 2001 From: kylechar <[email protected]> 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 <[email protected]> Commit-Queue: Kyle Charbonneau <[email protected]> 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.