Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInformation leak in Skia
DescriptionInformation leak in Skia
ComponentSkia
Bug ClassLogic Error
Tracker540027341
Fix commit5e4f3217c17a (skia) +8/-6
CISA KEVNot listed
CreditedGoogle
Disclosed2026-08-18

Changed Functions

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

Files Changed

  • src/gpu/ganesh/GrResourceProvider.cpp
From 5e4f3217c17a8d88195d4c2924613420a256457b Mon Sep 17 00:00:00 2001
From: Michael Ludwig <[email protected]>
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 <[email protected]>
Reviewed-by: Robert Phillips <[email protected]>
Commit-Queue: Robert Phillips <[email protected]>
---

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 [email protected]

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