CVE-2026-76041
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifsrc/gpu/ganesh/GrResourceProvider.cpp |
modified |
Files Changed
src/gpu/ganesh/GrResourceProvider.cpp
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.