CVE-2026-76041
Overview
Background
- `GrResourceProvider`
- the Ganesh (GPU) backend object that creates and hands out GPU resources such as
GrTextureinstances, including uploading initial pixel data. - `GrGpu::writePixels`
- the backend method (called through
fGpu) that uploads CPU-side texel data into a GPUtexture, returning aboolthat 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
texturethat 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.
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
- Trigger texture creation
An attacker drives content (e.g., graphics operations reaching Ganesh) that causes
GrResourceProviderto create atexturewith initial pixel data via the affected path. - Force the upload to fail
The attacker arranges conditions under which
fGpu->writePixelsreturnsfalseso the texel data is not written into thetexture(inferred). - Uninitialized texture is returned
Because the failure was only guarded by
SkAssertResult, execution continues toreturn texture;, yielding atexturebacked by uninitialized memory. - 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
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
| Function | Change | Notes |
|---|---|---|
ifsrc/gpu/ganesh/GrResourceProvider.cpp |
modified |
Files Changed
src/gpu/ganesh/GrResourceProvider.cpp
Audit Directions
- `SkAssertResult` on fallible operationsSearch 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 creationReview 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 surfacesLook 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.
Patch
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; }
Original Bug Report
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.cppthird_party/skia/include/private/SkAssert.hthird_party/skia/src/gpu/ganesh/GrGpu.cppcc/paint/image_transfer_cache_entry.ccgpu/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:
- Origin A (Victim): Renders content (e.g., text, images) to a GPU-backed surface of size
WxHwith formatF. Upon release, the backing texture is placed inGrResourceCache’sfScratchMap. By design, its pixel data is not cleared (third_party/skia/src/gpu/ganesh/GrResourceCache.cpp:209-219). - 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. - Origin B (Attacker) Memory Pressure: The attacker executes JavaScript to allocate significant GPU memory (e.g., multiple large WebGL textures or
OffscreenCanvasobjects). This places the GPU process under memory pressure, targeting staging-buffer exhaustion without crashing the process. - Origin B Request: The attacker executes
ctx.drawImage(img, 0, 0)on a 2D canvas, whereimgis a same-origin image of matching sizeWxHand formatF. Becauseimgis same-origin, Blink does not mark the canvas as tainted (third_party/skia/renderer/modules/canvas/canvas2d/canvas_2d_recorder_context.cc:2326). - Scratch Lookup: The GPU process deserializes the image transfer via
ServiceImageTransferCacheEntry::Deserialize. To upload the pixels, Skia callsGrResourceProvider::createTexture, which retrieves Origin A’s uncleared texture viagetExactScratchand callswritePixels(std::move(scratch), ...)(third_party/skia/src/gpu/ganesh/GrResourceProvider.cpp:105). - 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.fBufferis null), and the function safely returnsfalse(third_party/skia/src/gpu/ganesh/vk/GrVkGpu.cpp:977-981). - The Sink: The
falsereturn value reachesGrResourceProvider::writePixelsand is silently swallowed by theSkAssertResultmacro atGrResourceProvider.cpp:925. The function returns the uncleared scratch texture. - Information Leak:
ServiceImageTransferCacheEntry::Deserializesuccessfully checksif (!image)because the returnedSkImagewrapping the stale texture is non-null. The OOP-R rasterizer samples from this stale texture. The attacker then callsctx.getImageData()orcanvas.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:
SkAssertResultis confirmed to discard the return value in non-debug builds (!SK_DEBUG) atthird_party/skia/include/private/SkAssert.h:124(#define SkAssertResult(cond) if (cond) {} do {} while(false)). - Vulnerable Sink:
GrResourceProvider.cpp:925callsSkAssertResult(fGpu->writePixels(...))on a recycled scratch texture (passed fromgetExactScratchvia line 105). - Failure Condition: Vulkan backend staging-buffer allocation in
GrVkGpu::uploadTexDataOptimalexplicitly returnsfalseon 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 fromfScratchMapdirectly without clearing existing pixel data.fShouldInitializeTexturesdefaults tofalseand is not applied to scratch reuse. - Cross-Origin Shared Context:
SharedContextStatemaintains a singleraw_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
drawImageviaWouldTaintCanvasOrigin(canvas_2d_recorder_context.cc:2326). A same-origin image avoids tainting the canvas (origin_clean_remains true), allowinggetImageDataInternalto 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) validatesif (!image) { return nullptr; }. BecauseSkAssertResultswallows the failure and returns the stale texture wrapped in a validSkImage, this check passes. - A-RENDERER Variant Note: The prior validation refuted the deterministic
rowBytes % bpptrigger becauseSkImageInfo::validRowBytescorrectly rejects unalignedrow_bytesupstream 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.