High watch CVSS 4.3 chrome Info Disclosure 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
4.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInformation leak in Skia
DescriptionInformation leak in Skia
ComponentSkia
Sub-componentGanesh
Bug ClassInfo Disclosure
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.
Tracker540027341
Fix commit5e4f3217c17a (skia) +8/-6
CWECWE-200 (Information exposure)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-07-28 (per vendor advisory)
Fix landed2026-08-05 (public git)
Disclosed2026-08-18
Patch gap13 days — fix public in source before the advisory

Background

`GrResourceProvider`
the Ganesh (GPU) backend object that creates and hands out GPU resources such as GrTexture instances, including uploading initial pixel data.
`GrGpu::writePixels`
the backend method (called through fGpu) that uploads CPU-side texel data into a GPU texture, returning a bool that reports whether the upload actually succeeded.
`SkAssertResult`
a Skia macro that evaluates its argument but only checks the result in debug builds; in release builds the check is compiled out, so a failing call is silently ignored.
Information leak
exposure of memory whose contents were never legitimately initialized, here the backing store of a GPU texture that was returned despite a failed upload.

Root Cause Analysis

Inside a texture-creation path in src/gpu/ganesh/GrResourceProvider.cpp, the code allocated a texture, prepared tmpTexels, and called fGpu->writePixels(...) to populate the texture’s levels, wrapping that call in SkAssertResult. Because SkAssertResult discards the return value in release builds, a failed writePixels did not stop execution, and the function fell through to return texture;, handing back a texture whose contents were partially or wholly uninitialized. This violates the invariant that a texture returned with requested initial data has actually been populated with that data, so a caller could sample the texture and observe stale or uninitialized GPU/driver memory.

The fix replaces SkAssertResult with an explicit if (!fGpu->writePixels(...)) { return nullptr; }, so that any upload failure aborts creation and returns nullptr instead of leaking an unpopulated texture.

Key insight
The single core mistake is using SkAssertResult to guard fGpu->writePixels, which lets a failed upload pass unnoticed in release builds and returns an uninitialized texture; the fix converts the ignored result into a real control-flow check that returns nullptr on failure.

Attack Path

  1. Trigger texture creation An attacker drives content (e.g., graphics operations reaching Ganesh) that causes GrResourceProvider to create a texture with initial pixel data via the affected path.
  2. Force the upload to fail The attacker arranges conditions under which fGpu->writePixels returns false so the texel data is not written into the texture (inferred).
  3. Uninitialized texture is returned Because the failure was only guarded by SkAssertResult, execution continues to return texture;, yielding a texture backed by uninitialized memory.
  4. Read back leaked contents The attacker samples or reads the returned texture, causing previously uninitialized GPU/driver memory to be surfaced into rendered output or otherwise observable data (inferred).

Impact Assessment

An attacker gains an information leak: the contents of an uninitialized GPU texture that may reflect stale memory, reachable in the process that runs Skia/Ganesh rendering, with the precondition that a writePixels upload fails while the create path still returns the texture. The vendor advisory rates this high severity and does not report in-the-wild exploitation; NVD scores it CVSS 4.3. Any escalation beyond disclosure of uninitialized memory is not demonstrated by the patch (inferred).

Changed Functions

FunctionChangeNotes
if
src/gpu/ganesh/GrResourceProvider.cpp
modified

Files Changed

  • src/gpu/ganesh/GrResourceProvider.cpp

Audit Directions

  • `SkAssertResult` on fallible operations
    Search for SkAssertResult (and other assert-only macros) wrapping calls that can fail at runtime, especially uploads, allocations, and I/O, since the result is discarded in release builds.
  • Return-before-verify in resource creation
    Review Ganesh/GPU resource providers and factories where a resource is returned after an initialization/upload step, and confirm every fallible init step gates the success return.
  • Uninitialized-buffer surfaces
    Look for paths that produce textures, buffers, or images intended to hold caller-supplied or zeroed data but continue on partial/failed population, which can expose stale memory.
From 5e4f3217c17a8d88195d4c2924613420a256457b Mon Sep 17 00:00:00 2001
From: Michael Ludwig <michaelludwig@google.com>
Date: Tue, 04 Aug 2026 14:39:10 -0400
Subject: [PATCH] [ganesh] Check result inside GrResourceProvider::writePixels

Bug: 540027341
Fixed: 540027341
Change-Id: Icde9d077dfb3ec3cdb7a6e775c6472443dba8781
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/1315336
Auto-Submit: Michael Ludwig <michaelludwig@google.com>
Reviewed-by: Robert Phillips <robertphillips@google.com>
Commit-Queue: Robert Phillips <robertphillips@google.com>
---
diff --git a/src/gpu/ganesh/GrResourceProvider.cpp b/src/gpu/ganesh/GrResourceProvider.cpp
index 6ecf6bb..9149cbd 100644
--- a/src/gpu/ganesh/GrResourceProvider.cpp
+++ b/src/gpu/ganesh/GrResourceProvider.cpp
@@ -922,11 +922,13 @@
     if (tempColorType == GrColorType::kUnknown) {
         return nullptr;
     }
-    SkAssertResult(fGpu->writePixels(texture.get(),
-                                     SkIRect::MakeSize(baseSize),
-                                     colorType,
-                                     tempColorType,
-                                     tmpTexels.get(),
-                                     mipLevelCount));
+    if (!fGpu->writePixels(texture.get(),
+                           SkIRect::MakeSize(baseSize),
+                           colorType,
+                           tempColorType,
+                           tmpTexels.get(),
+                           mipLevelCount)) {
+        return nullptr;
+    }
     return texture;
 }
Loading diff…

Original Bug Report

reported by aw...@chromium.org

GrResourceProvider::writePixels swallows upload failures, leaking cross-origin GPU textures

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: In Release builds, GrResourceProvider::writePixels uses the SkAssertResult macro which evaluates but discards the return value of fGpu->writePixels(). If a pixel upload fails on a recycled scratch texture under memory pressure, the stale, uncleared texture from a prior origin is returned as if successful. This can lead to a cross-origin GPU memory disclosure (Same-Origin Policy bypass) within the shared direct context.

Affected files:

  • third_party/skia/src/gpu/ganesh/GrResourceProvider.cpp
  • third_party/skia/include/private/SkAssert.h
  • third_party/skia/src/gpu/ganesh/GrGpu.cpp
  • cc/paint/image_transfer_cache_entry.cc
  • gpu/command_buffer/service/shared_context_state.h

Estimated timestamp from git blame: 2019-09-20

1. Summary of the Issue (Meant for Human Triage)

In Chromium Release builds, Skia’s GrResourceProvider::writePixels() uses the debug-only macro SkAssertResult to wrap the return value of the underlying fGpu->writePixels() call. In Release builds (where SK_DEBUG is not defined), this macro evaluates its condition but silently discards the resulting boolean. Consequently, any failure occurring during a pixel or texture upload—such as Vulkan or Metal staging-buffer out-of-memory (OOM) errors—is silently swallowed, and the texture is returned to the caller as if the write had succeeded.

Because Chromium’s Out-of-Process Rasterization (OOP-R) utilizes a single shared GrDirectContext (and a shared GrResourceCache) across all rendering origins, scratch textures are recycled across origins without being cleared. If an attacker induces a staging-buffer allocation failure precisely when uploading a same-origin image onto a canvas, Skia will silently fail the upload and return an uncleared scratch texture containing pixel data from a different origin. Because the attacker drew a same-origin image, the canvas remains un-tainted, allowing the attacker to read back the leaked cross-origin pixels (such as canvas tiles, decoded images, or glyph masks) using getImageData() or toBlob(), violating the Same-Origin Policy (SOP).


2. Proof-of-Concept & Detailed Execution Flow

The vulnerability is rooted in GrResourceProvider::writePixels inside third_party/skia/src/gpu/ganesh/GrResourceProvider.cpp:925-931:

    SkAssertResult(fGpu->writePixels(texture.get(),
                                     SkIRect::MakeSize(baseSize),
                                     colorType,
                                     tempColorType,
                                     tmpTexels.get(),
                                     mipLevelCount));
    return texture; // <-- Returned unconditionally

In Release builds, SkAssertResult is defined in third_party/skia/include/private/SkAssert.h:124 as:

#define SkAssertResult(cond)         if (cond) {} do {} while(false)

This discards the return value of fGpu->writePixels(). In contrast, the fresh-texture creation path in third_party/skia/src/gpu/ganesh/GrGpu.cpp:238-245 correctly checks and propagates the failure (if (!this->writePixels(...)) { return nullptr; }).

Potential Execution Sequence to Trigger the Leak:

  1. Origin A (Victim): Renders content (e.g., text, images) to a GPU-backed surface of size WxH with format F. Upon release, the backing texture is placed in GrResourceCache’s fScratchMap. By design, its pixel data is not cleared (third_party/skia/src/gpu/ganesh/GrResourceCache.cpp:209-219).
  2. Context Sharing: Chromium uses a single shared GrDirectContext (gpu/command_buffer/service/shared_context_state.h:368) across all origins for OOP-R, making Origin A’s scratch texture available to Origin B.
  3. Origin B (Attacker) Memory Pressure: The attacker executes JavaScript to allocate significant GPU memory (e.g., multiple large WebGL textures or OffscreenCanvas objects). This places the GPU process under memory pressure, targeting staging-buffer exhaustion without crashing the process.
  4. Origin B Request: The attacker executes ctx.drawImage(img, 0, 0) on a 2D canvas, where img is a same-origin image of matching size WxH and format F. Because img is same-origin, Blink does not mark the canvas as tainted (third_party/skia/renderer/modules/canvas/canvas2d/canvas_2d_recorder_context.cc:2326).
  5. Scratch Lookup: The GPU process deserializes the image transfer via ServiceImageTransferCacheEntry::Deserialize. To upload the pixels, Skia calls GrResourceProvider::createTexture, which retrieves Origin A’s uncleared texture via getExactScratch and calls writePixels(std::move(scratch), ...) (third_party/skia/src/gpu/ganesh/GrResourceProvider.cpp:105).
  6. Failed Staging-Buffer Allocation: fGpu->writePixels() routes to a backend-specific upload function (e.g., GrVkGpu::uploadTexDataOptimal). Due to the induced memory pressure, staging buffer allocation fails (slice.fBuffer is null), and the function safely returns false (third_party/skia/src/gpu/ganesh/vk/GrVkGpu.cpp:977-981).
  7. The Sink: The false return value reaches GrResourceProvider::writePixels and is silently swallowed by the SkAssertResult macro at GrResourceProvider.cpp:925. The function returns the uncleared scratch texture.
  8. Information Leak: ServiceImageTransferCacheEntry::Deserialize successfully checks if (!image) because the returned SkImage wrapping the stale texture is non-null. The OOP-R rasterizer samples from this stale texture. The attacker then calls ctx.getImageData() or canvas.toBlob(). Since the canvas was never tainted, the readback succeeds, granting the attacker cross-origin pixel data.

Suggested Fix

Replace the SkAssertResult macro in GrResourceProvider::writePixels with proper error handling, matching the fresh-texture path:

    if (!fGpu->writePixels(texture.get(),
                           SkIRect::MakeSize(baseSize),
                           colorType,
                           tempColorType,
                           tmpTexels.get(),
                           mipLevelCount)) {
        return nullptr;
    }

3. Technical Verification Details (Automated Audit Logs)

> “The report accurately identifies a flaw in GrResourceProvider::writePixels where SkAssertResult(fGpu->writePixels(...)) is used to check the return value. In release builds, SkAssertResult evaluates its argument but discards the boolean result, silently ignoring failures. When a scratch texture is reused across origins (since GrDirectContext is shared and scratch textures are only keyed on dimensions/format), it is not cleared. If an attacker can induce a failure during pixel upload (e.g., by exhausting GPU memory to fail the Vulkan/Metal staging buffer allocation), the uncleared scratch texture containing stale cross-origin data is returned as if the upload succeeded. This leads to a cross-origin GPU memory disclosure (SOP-tier), which is typically a High (S1) severity issue. However, exploiting this requires an attacker to precisely exhaust memory to win a tight GPU-OOM race on the staging buffer allocation while keeping the target scratch texture from being evicted, which justifies a downgrade to Medium (S2) severity per the generic ’tight race’ mitigator.”

Codebase Verification Logic:

  • Macro Definition: SkAssertResult is confirmed to discard the return value in non-debug builds (!SK_DEBUG) at third_party/skia/include/private/SkAssert.h:124 (#define SkAssertResult(cond) if (cond) {} do {} while(false)).
  • Vulnerable Sink: GrResourceProvider.cpp:925 calls SkAssertResult(fGpu->writePixels(...)) on a recycled scratch texture (passed from getExactScratch via line 105).
  • Failure Condition: Vulkan backend staging-buffer allocation in GrVkGpu::uploadTexDataOptimal explicitly returns false on allocation failure (GrVkGpu.cpp:977-981). Similar behavior applies to Metal (GrMtlGpu.mm).
  • Scratch Texture Reuse without Clear: GrResourceCache::findAndRefScratchResource (GrResourceCache.cpp:245-255) returns scratch resources from fScratchMap directly without clearing existing pixel data. fShouldInitializeTextures defaults to false and is not applied to scratch reuse.
  • Cross-Origin Shared Context: SharedContextState maintains a single raw_ptr<GrDirectContext> gr_context_ used by all renderer origins (gpu/command_buffer/service/shared_context_state.h:368).
  • Tainting Logic Bypass: The renderer process checks origin tainting against the image source requested in drawImage via WouldTaintCanvasOrigin (canvas_2d_recorder_context.cc:2326). A same-origin image avoids tainting the canvas (origin_clean_ remains true), allowing getImageDataInternal to bypass the security exception (base_rendering_context_2d.cc:375-378).
  • Upstream Null Check: ServiceImageTransferCacheEntry::Deserialize (cc/paint/image_transfer_cache_entry.cc:358-361) validates if (!image) { return nullptr; }. Because SkAssertResult swallows the failure and returns the stale texture wrapped in a valid SkImage, this check passes.
  • A-RENDERER Variant Note: The prior validation refuted the deterministic rowBytes % bpp trigger because SkImageInfo::validRowBytes correctly rejects unaligned row_bytes upstream before reaching Skia’s GPU upload path.

Evaluated with Chrome root at commit: 1e8af8af71d04992e29233209225cfdbeeb42672


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