CVE-2026-8541
Overview
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) androwBytesto 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);pixelspoints 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).
computeByteSize(row_bytes) large; the fix adds a CHECK_GE(mapping.size(), image_info.computeByteSize(row_bytes)) size guard before SkBitmap::installPixels.Attack Path
- 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 actualmappingshared buffer (inferred: producer/renderer influence over frame metadata vs. buffer size). - Unchecked install
Pre-patch, the code computes
SkImageInfo/RowBytesand callsframe.installPixels(image_info, pixels, row_bytes, ...)without comparing againstmapping.size(). - Out-of-bounds read
A subsequent consumer of the resulting
SkBitmap(thumbnail encode/draw) reads the fullcomputeByteSize(row_bytes)extent frompixels, reading past the end of the undersizedmappingbuffer, as the advisory’s “out of bounds read in UI” indicates. - Post-patch
The added
CHECK_GEfails whenmapping.size()is insufficient, aborting before any out-of-bounds read.
Impact Assessment
Files Changed
chrome/browser/ui/thumbnails/background_thumbnail_video_capturer.cc
Audit Directions
- `installPixels` / adopt-external-buffer callsFlag every
SkBitmap::installPixels,SkImage::MakeRasterData, or similar API fed a raw pointer and caller-computedSkImageInfo/rowBytes; require a preceding size check against the real buffer length. - Geometry from one source, buffer from anotherAudit paths where dimensions (
coded_size,content_rect) and the backing allocation (mapping,SharedMemory) come from different fields of the same producer message; verifycomputeByteSize≤ buffer size. - IPC-supplied image/frame metadataReview 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.
Patch
From 791187a955bee94e1f65b53deb98a842a7a2e8c8 Mon Sep 17 00:00:00 2001
From: Stephen Nusko <nuskos@chromium.org>
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 <nuskos@chromium.org>
Reviewed-by: Dana Fried <dfried@chromium.org>
Commit-Queue: Dana Fried <dfried@chromium.org>
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);
},
Original Bug Report
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.ccchrome/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.
- Compromise the GPU process.
- Wait for the browser process to request a background tab capture (e.g., user hovers over a background tab) and intercept the
FrameSinkVideoConsumer::OnFrameCapturedcallback. - Send a message with
info.coded_sizeset to a small dimension (e.g.,100x100) and a backing shared memory region of40,000bytes. - Set
content_rectto maliciously large dimensions, e.g.,gfx::Rect(0, 0, 100, 10000). - The browser process maps the memory, passes the size check (as
coded_sizematches the buffer), and creates anSkBitmapof size100x10000. - The browser routes the frame to a background thread for JPEG compression, where the encoder reads
9,900rows 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.