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

Overview

High
Severity
5.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOut of bounds read in UI
DescriptionOut of bounds read in UI
ComponentUI
Bug ClassOOB
Action tierwatch — information disclosure — security-relevant, below the RCE-class bar
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.
Tracker496645393
Fix commit791187a955be (chromium/src) +8/-6
CWECWE-125 (Out-of-bounds read)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-03-26 (per vendor advisory)
Fix landed2026-04-01 (public git)
Disclosed2026-05-12
Patch gap41 days — fix public in source before the advisory

Background

`BackgroundThumbnailVideoCapturer`
a browser-UI component that receives captured video frames of a tab/page and turns them into thumbnail bitmaps.
`SkBitmap::installPixels`
a Skia call that adopts an externally supplied pixel buffer as a bitmap’s backing store, using the caller-provided SkImageInfo (dimensions/format) and rowBytes to interpret it, without itself verifying the buffer is large enough.
`mapping`
the shared-memory region holding the frame’s pixel data (mapping.size() is its byte length); pixels points into it.
`SkImageInfo::computeByteSize(rowBytes)`
Skia’s computation of how many bytes a bitmap of the given dimensions and row stride actually requires.

Root Cause Analysis

The handler built an SkImageInfo from bitmap_size (derived from content_rect) and a row_bytes from media::VideoFrame::RowBytes(...) using info->coded_size.width(), then handed pixels straight to frame.installPixels(...) with no check that the backing mapping was large enough for that geometry. Because installPixels trusts the supplied SkImageInfo/rowBytes, any later consumer of the SkBitmap reads width*height/rowBytes worth of bytes starting at pixels; if the frame’s declared dimensions imply more bytes than mapping.size(), those reads run off the end of the shared buffer. The frame geometry and the mapping size are supplied by the capture producer, so a mismatch between the claimed size and the actual buffer is attacker-influenceable and was never validated.

The fix pre-computes image_info and row_bytes and adds CHECK_GE(mapping.size(), image_info.computeByteSize(row_bytes)) before installPixels, aborting if the buffer cannot hold the described bitmap and thereby preventing the out-of-bounds read. The enclosing function name is not shown in the hunk header (the @@ context does not name it).

Key insight
The core mistake was installing a producer-described bitmap over a shared-memory buffer without confirming the buffer was at least computeByteSize(row_bytes) large; the fix adds a CHECK_GE(mapping.size(), image_info.computeByteSize(row_bytes)) size guard before SkBitmap::installPixels.

Attack Path

  1. Supply a mismatched frame The capture producer provides a video frame whose declared geometry (coded_size.width(), content_rect) implies a larger byte size than the actual mapping shared buffer (inferred: producer/renderer influence over frame metadata vs. buffer size).
  2. Unchecked install Pre-patch, the code computes SkImageInfo/RowBytes and calls frame.installPixels(image_info, pixels, row_bytes, ...) without comparing against mapping.size().
  3. Out-of-bounds read A subsequent consumer of the resulting SkBitmap (thumbnail encode/draw) reads the full computeByteSize(row_bytes) extent from pixels, reading past the end of the undersized mapping buffer, as the advisory’s “out of bounds read in UI” indicates.
  4. Post-patch The added CHECK_GE fails when mapping.size() is insufficient, aborting before any out-of-bounds read.

Impact Assessment

The advisory classifies this as an out of bounds read in UI, rated high. An attacker able to cause a thumbnail-capture frame whose declared dimensions exceed its backing shared-memory buffer could induce an out-of-bounds read in the browser-process UI thumbnail path, potentially disclosing adjacent memory to downstream consumers of the bitmap. The preconditions are reaching the background thumbnail capture path with a size/buffer mismatch; any escalation beyond an OOB read (e.g. info disclosure to a web principal) is not demonstrated by the diff and is (inferred).

Files Changed

  • chrome/browser/ui/thumbnails/background_thumbnail_video_capturer.cc

Audit Directions

  • `installPixels` / adopt-external-buffer calls
    Flag every SkBitmap::installPixels, SkImage::MakeRasterData, or similar API fed a raw pointer and caller-computed SkImageInfo/rowBytes; require a preceding size check against the real buffer length.
  • Geometry from one source, buffer from another
    Audit paths where dimensions (coded_size, content_rect) and the backing allocation (mapping, SharedMemory) come from different fields of the same producer message; verify computeByteSize ≤ buffer size.
  • IPC-supplied image/frame metadata
    Review video-frame and bitmap handlers that trust producer-declared width/height/stride without validating against the mapped region, especially “phase 1 quick fix” sites that may have deeper unvalidated callers.
From 791187a955bee94e1f65b53deb98a842a7a2e8c8 Mon Sep 17 00:00:00 2001
From: Stephen Nusko <[email protected]>
Date: Wed, 01 Apr 2026 06:56:31 -0700
Subject: [PATCH] Add size check before installing pixels in BackgroundThumbnailVideoCapturer.

This change pre-calculates the SkImageInfo and row bytes, and adds a
CHECK to verify that the provided memory mapping is large enough to hold
the bitmap data before calling SkBitmap::installPixels.

See (internal-only): go/code-terracotta-review-explainer

This is a phase 1 quick fix.

Bug: 496645393
Change-Id: I4bae557699e67ee1656b6d8c079542ff4dd8afe1
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7711554
Auto-Submit: Stephen Nusko <[email protected]>
Reviewed-by: Dana Fried <[email protected]>
Commit-Queue: Dana Fried <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1608491}
---

diff --git a/chrome/browser/ui/thumbnails/background_thumbnail_video_capturer.cc b/chrome/browser/ui/thumbnails/background_thumbnail_video_capturer.cc
index 7dfe004..2bc9af6 100644
--- a/chrome/browser/ui/thumbnails/background_thumbnail_video_capturer.cc
+++ b/chrome/browser/ui/thumbnails/background_thumbnail_video_capturer.cc
@@ -168,14 +168,16 @@
   effective_content_rect.Inset(scroll_insets);
 
   const gfx::Size bitmap_size(content_rect.right(), content_rect.bottom());
+  const SkImageInfo image_info = SkImageInfo::MakeN32(
+      bitmap_size.width(), bitmap_size.height(), kPremul_SkAlphaType,
+      info->color_space.ToSkColorSpace());
+  const size_t row_bytes =
+      media::VideoFrame::RowBytes(media::VideoFrame::Plane::kARGB,
+                                  info->pixel_format, info->coded_size.width());
+  CHECK_GE(mapping.size(), image_info.computeByteSize(row_bytes));
   SkBitmap frame;
   frame.installPixels(
-      SkImageInfo::MakeN32(bitmap_size.width(), bitmap_size.height(),
-                           kPremul_SkAlphaType,
-                           info->color_space.ToSkColorSpace()),
-      pixels,
-      media::VideoFrame::RowBytes(media::VideoFrame::Plane::kARGB,
-                                  info->pixel_format, info->coded_size.width()),
+      image_info, pixels, row_bytes,
       [](void* addr, void* context) {
         delete static_cast<FramePinner*>(context);
       },
Loading diff…

Original Bug Report

reported by [email protected]

Potential OOB read in browser process via unvalidated content_rect in VideoConsumer

Project Fortify, an experimental security project, has identified the following potential security issue.

Overview: A compromised GPU process can trigger an out-of-bounds read in the browser process by supplying an unvalidated content_rect during frame capture. The browser uses these dimensions to construct an SkBitmap without checking if it exceeds the shared memory buffer size, leading to an OOB read during JPEG encoding or painting. This could result in a browser crash or potential information disclosure.

Affected files:

  • chrome/browser/ui/thumbnails/background_thumbnail_video_capturer.cc
  • chrome/browser/ui/thumbnails/thumbnail_image.cc

Estimated timestamp from git blame: 2026-02-27

Summary

There is a potential out-of-bounds (OOB) read vulnerability in the browser process when handling video frames captured from a compromised GPU process. Implementations of viz::mojom::FrameSinkVideoConsumer in the browser process, specifically BackgroundThumbnailVideoCapturer and BrowserLiveBackgroundController::VideoConsumer, fail to adequately validate the content_rect parameter before using it to size an SkBitmap backed by shared memory.

Vulnerability Details

When the GPU process sends a captured frame via the FrameSinkVideoConsumer::OnFrameCaptured Mojo IPC, it provides a shared memory handle (data), frame metadata (info), and a content_rect.

In BackgroundThumbnailVideoCapturer::OnFrameCaptured (and similarly in BrowserLiveBackgroundController), the code validates that the shared memory mapping size is large enough to hold a frame of info->coded_size. However, it completely ignores info->coded_size when constructing the resulting image. Instead, it derives the SkBitmap’s width and height directly from the unvalidated content_rect boundaries (content_rect.right() and content_rect.bottom()).

// chrome/browser/ui/thumbnails/background_thumbnail_video_capturer.cc
const gfx::Size bitmap_size(content_rect.right(), content_rect.bottom());
SkBitmap frame;
frame.installPixels(
    SkImageInfo::MakeN32(bitmap_size.width(), bitmap_size.height(), ...),
    pixels,
    media::VideoFrame::RowBytes(..., info->coded_size.width()),
    ...);

SkBitmap::installPixels accepts a raw void* for the memory buffer but does not take a buffer_size parameter. It inherently assumes the provided buffer is large enough for the requested height and row bytes. Because the content_rect height can arbitrarily exceed the coded_size height, this creates an SkBitmap that extends far beyond the bounds of the mapped shared memory.

When this SkBitmap is later passed to ThumbnailImage::CompressBitmap and encoded via gfx::JPEGCodec::Encode, the JPEG encoder iterates over the massive height, reading out of bounds into adjacent browser process memory.

Impact

An attacker compromising the GPU process could exploit this to read browser process memory. The OOB data is compressed into a JPEG and displayed in the browser UI (e.g., Tab Hover Cards). Because the compromised GPU process handles compositing the final screen, it can read the rendered pixels and extract the leaked browser memory. Additionally, if the OOB read hits an unmapped guard page, it will cause an immediate browser process crash (Denial of Service).

Suggested Reproduction Steps

Note: These are potential steps to trigger the issue, as the Fortify LLM agent cannot actively run code to provide a working PoC.

  1. Compromise the GPU process.
  2. Wait for the browser process to request a background tab capture (e.g., user hovers over a background tab) and intercept the FrameSinkVideoConsumer::OnFrameCaptured callback.
  3. Send a message with info.coded_size set to a small dimension (e.g., 100x100) and a backing shared memory region of 40,000 bytes.
  4. Set content_rect to maliciously large dimensions, e.g., gfx::Rect(0, 0, 100, 10000).
  5. The browser process maps the memory, passes the size check (as coded_size matches the buffer), and creates an SkBitmap of size 100x10000.
  6. The browser routes the frame to a background thread for JPEG compression, where the encoder reads 9,900 rows out of bounds.

Suggested Fix

Ensure that the dimensions derived from content_rect do not result in a byte size requirement that exceeds the mapped shared memory.

A safe pattern for this already exists in DevToolsEyeDropper::OnFrameCaptured (chrome/browser/devtools/devtools_eye_dropper.cc), which uses computeByteSize to explicitly validate the buffer size against the SkImageInfo before calling installPixels:

auto image_info = SkImageInfo::MakeN32(...);
const size_t row_bytes = media::VideoFrame::RowBytes(...);

// Ensure the unbounded pointer points to enough memory.
CHECK_GE(mapping.size(), image_info.computeByteSize(row_bytes));

frame.installPixels(image_info, ...);

This check should be applied to BackgroundThumbnailVideoCapturer, BrowserLiveBackgroundController::VideoConsumer, and any other implementers of FrameSinkVideoConsumer that manually construct SkBitmaps from content_rect.

Evaluated with Chrome root at commit: bb48272cafb7e24c93f55ef40da398cd206ee651


Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve accuracy over time. Please feel free to reach out to me if you have concerns or feedback.

View on issue tracker