CVE-2026-15766
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
switchsrc/gpu/ganesh/ops/OpsTask.cpp |
modified |
Files Changed
src/gpu/ganesh/GrAttachment.hsrc/gpu/ganesh/GrCaps.cppsrc/gpu/ganesh/GrCaps.hsrc/gpu/ganesh/gl/GrGLCaps.cppsrc/gpu/ganesh/ops/OpsTask.cpp
Patch
From ca9aecfbf4e27de60619a4db811575e3fd1e1af6 Mon Sep 17 00:00:00 2001
From: Greg Daniel <egdaniel@google.com>
Date: Mon, 29 Jun 2026 11:45:26 -0400
Subject: [PATCH] Check clipped renderpass bounds against cleared stencil area
* If only a portion of the stencil attachment is cleared, we should not mark the entire attachment as cleared. This CL makes it such that we check which area has most recently been cleared to make an informed decision.
Bug: b/514010477
Change-Id: I3800ca3125f25341dffd818f6c557692020027be
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/1255256
Reviewed-by: Robert Phillips <robertphillips@google.com>
Commit-Queue: Greg Daniel <egdaniel@google.com>
---
diff --git a/src/gpu/ganesh/GrAttachment.h b/src/gpu/ganesh/GrAttachment.h
index a7b9ad5..a7d00d2 100644
--- a/src/gpu/ganesh/GrAttachment.h
+++ b/src/gpu/ganesh/GrAttachment.h
@@ -53,8 +53,11 @@
skgpu::Mipmapped mipmapped() const { return fMipmapped; }
- bool hasPerformedInitialClear() const { return fHasPerformedInitialClear; }
- void markHasPerformedInitialClear() { fHasPerformedInitialClear = true; }
+ SkIRect clearedArea() const { return fClearedArea; }
+ bool hasAreaBeenCleared(SkIRect attachmentArea) const {
+ return fClearedArea.contains(attachmentArea);
+ }
+ void markAreaCleared(SkIRect area) { fClearedArea = area; }
// This unique key is used for attachments of the same dimensions, usage, and sample cnt which
// are shared between multiple render targets at the same time. Only one usage flag may be
@@ -117,7 +120,9 @@
UsageFlags fSupportedUsages;
int fSampleCnt;
skgpu::Mipmapped fMipmapped;
- bool fHasPerformedInitialClear = false;
+ // Track which area of the attachment has already been cleared to cut down on unnecessary clear
+ // operations, which can be more expensive on desktop GPUs than loads.
+ SkIRect fClearedArea = SkIRect::MakeEmpty();
GrMemoryless fMemoryless;
using INHERITED = GrSurface;
diff --git a/src/gpu/ganesh/GrCaps.cpp b/src/gpu/ganesh/GrCaps.cpp
index 38610a4..ae76495 100644
--- a/src/gpu/ganesh/GrCaps.cpp
+++ b/src/gpu/ganesh/GrCaps.cpp
@@ -43,6 +43,8 @@
fUsePrimitiveRestart = false;
fPreferClientSideDynamicBuffers = false;
fPreferFullscreenClears = false;
+ fDiscardStencilValuesAfterRenderPass = false;
+ fClearsAreFasterThanLoads = false;
fTwoSidedStencilRefsAndMasksMustMatch = false;
fMustClearUploadedBufferData = false;
fShouldInitializeTextures = false;
@@ -227,7 +229,10 @@
writer->appendBool("MSAA Resolves Automatically", fMSAAResolvesAutomatically);
writer->appendBool("Use primitive restart", fUsePrimitiveRestart);
writer->appendBool("Prefer client-side dynamic buffers", fPreferClientSideDynamicBuffers);
- writer->appendBool("Prefer fullscreen clears (and stencil discard)", fPreferFullscreenClears);
+ writer->appendBool("Prefer fullscreen clears", fPreferFullscreenClears);
+ writer->appendBool("Discard stencil values after renderpass",
+ fDiscardStencilValuesAfterRenderPass);
+ writer->appendBool("Clears are faster than loads", fClearsAreFasterThanLoads);
writer->appendBool("Two-sided Stencil Refs And Masks Must Match",
fTwoSidedStencilRefsAndMasksMustMatch);
writer->appendBool("Must clear buffer memory", fMustClearUploadedBufferData);
diff --git a/src/gpu/ganesh/GrCaps.h b/src/gpu/ganesh/GrCaps.h
index cf074e9..e86c2f8 100644
--- a/src/gpu/ganesh/GrCaps.h
+++ b/src/gpu/ganesh/GrCaps.h
@@ -114,10 +114,11 @@
bool preferClientSideDynamicBuffers() const { return fPreferClientSideDynamicBuffers; }
- // On tilers, an initial fullscreen clear is an OPTIMIZATION. It allows the hardware to
- // initialize each tile with a constant value rather than loading each pixel from memory.
+ // The following 3 methods provide information that enables performance optimizations for tiler
+ // GPUs. Full clear operations (followed by discarding the content when finished) are more
+ // performant than load operations on these GPUs as they enable the hardware to initialize each
+ // tile with a constant value as opposed to loading each pixel from memory.
bool preferFullscreenClears() const { return fPreferFullscreenClears; }
-
// Should we discard stencil values after a render pass? (Tilers get better performance if we
// always load stencil buffers with a "clear" op, and then discard the content when finished.)
bool discardStencilValuesAfterRenderPass() const {
@@ -126,10 +127,15 @@
#if 0
// This method is actually just a duplicate of preferFullscreenClears(), with a descriptive
// name for the sake of readability.
- return this->preferFullscreenClears();
+ return fDiscardStencilValuesAfterRenderPass;
#endif
}
+ // Returns whether clearing an attachment is faster than loading the attachment. This is useful
+ // when you know the values are already cleared so it doesn't matter if we load or clear at the
+ // start of a render pass.
+ bool clearsAreFasterThanLoads() const { return fClearsAreFasterThanLoads; }
+
// D3D does not allow the refs or masks to differ on a two-sided stencil draw.
bool twoSidedStencilRefsAndMasksMustMatch() const {
return fTwoSidedStencilRefsAndMasksMustMatch;
@@ -605,6 +611,8 @@
bool fUsePrimitiveRestart : 1;
bool fPreferClientSideDynamicBuffers : 1;
bool fPreferFullscreenClears : 1;
+ bool fDiscardStencilValuesAfterRenderPass : 1;
+ bool fClearsAreFasterThanLoads : 1;
bool fTwoSidedStencilRefsAndMasksMustMatch : 1;
bool fMustClearUploadedBufferData : 1;
bool fBuffersAreInitiallyZero : 1;
diff --git a/src/gpu/ganesh/gl/GrGLCaps.cpp b/src/gpu/ganesh/gl/GrGLCaps.cpp
index fe2a182..9dc3137 100644
--- a/src/gpu/ganesh/gl/GrGLCaps.cpp
+++ b/src/gpu/ganesh/gl/GrGLCaps.cpp
@@ -118,6 +118,13 @@
return backend == GrGLANGLEBackend::kMetal;
}
+namespace {
+bool is_tiler_gpu(GrGLVendor vendor) {
+ return vendor == GrGLVendor::kARM ||
+ vendor == GrGLVendor::kImagination ||
+ vendor == GrGLVendor::kQualcomm;
+}
+} // anonymous namespace
void GrGLCaps::init(const GrContextOptions& contextOptions,
const GrGLContextInfo& ctxInfo,
const GrGLInterface* gli) {
@@ -221,10 +228,10 @@
}
}
- if (ctxInfo.vendor() == GrGLVendor::kARM ||
- ctxInfo.vendor() == GrGLVendor::kImagination ||
- ctxInfo.vendor() == GrGLVendor::kQualcomm ) {
+ if (is_tiler_gpu(ctxInfo.vendor())) {
fPreferFullscreenClears = true;
+ fDiscardStencilValuesAfterRenderPass = true;
+ fClearsAreFasterThanLoads = true;
}
if (GR_IS_GR_GL(standard)) {
diff --git a/src/gpu/ganesh/ops/OpsTask.cpp b/src/gpu/ganesh/ops/OpsTask.cpp
index 6dbc866..ddaa705 100644
--- a/src/gpu/ganesh/ops/OpsTask.cpp
+++ b/src/gpu/ganesh/ops/OpsTask.cpp
@@ -585,37 +585,67 @@
stencil = renderTarget->getStencilAttachment(fUsesMSAASurface);
}
- bool markStencilCleared = false;
GrLoadOp stencilLoadOp;
+ // Determine whether the stencil attachment's cleared area should be updated.
+ bool updateClearedStencilArea = false;
+ SkIRect boundsRequiredByStencil = fClippedContentBounds;
switch (fInitialStencilContent) {
case StencilContent::kDontCare:
- if (stencil && !caps.performStencilClearsAsDraws()) {
- // This OpTask has a stencil, doesn't care about its contents,
- // isn't clearing it with draws, and is going to store the result.
- // In that case, we proactively clear it so that uninitialized data won't
- // creep into the stencil buffer.
- stencilLoadOp = GrLoadOp::kClear;
- } else {
+ if (!stencil || caps.performStencilClearsAsDraws()) {
// This should only intentionally happen for the AtlasRenderTask which
// immediately inserts a clear.
stencilLoadOp = GrLoadOp::kDiscard;
+ break;
}
- break;
+ // This OpTask has a stencil, doesn't care about its contents, isn't clearing it with
+ // draws, and is going to store the result. In that case, we fallthrough to clear it so
+ // that uninitialized data won't creep into the stencil buffer.
+ [[fallthrough]];
case StencilContent::kUserBitsCleared:
SkASSERT(!caps.performStencilClearsAsDraws());
SkASSERT(stencil);
+ // Based upon Caps, determine which stencil load operation to use and perform any
+ // necessary updates to the renderpass's bounds and the stencil's cleared area.
+
+ // If the user bits are meant to be cleared we either do that via an initial clear the
+ // first time the area of the stencil is used, or we assume previous draws left the
+ // stencil cleared so we just load the current values. However, on some devices it is
+ // faster to just clear the stencil each time instead of doing a load.
+ //
+ // Since we'll never end up loading the stencil in this case there is no reason for us
+ // to do the clear rect tracking below, so we just break.
+ if (caps.clearsAreFasterThanLoads()) {
+ stencilLoadOp = GrLoadOp::kClear;
+ break;
+ }
+ // If stencil values are discarded after every renderpass, then we do not need to track
+ // which portion of the stencil attachment has already been cleared.
if (caps.discardStencilValuesAfterRenderPass()) {
- // Always clear the stencil if it is being discarded after render passes. This is
- // also an optimization because we are on a tiler and it avoids loading the values
Original Bug Report
Cross-origin GPU memory disclosure in Skia Ganesh-Vulkan via partial stencil clears
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: A logic error in Skia’s Ganesh Vulkan backend allows uninitialized GPU memory to be loaded into the stencil buffer. By exploiting a granularity mismatch between Skia’s initialization tracking and Vulkan’s render area clears, an attacker can leak stale VRAM contents from the GPU process.
Affected files:
third_party/skia/src/gpu/ganesh/ops/OpsTask.cppthird_party/skia/src/gpu/ganesh/GrAttachment.hthird_party/skia/src/gpu/ganesh/vk/GrVkOpsRenderPass.cppthird_party/skia/src/gpu/ganesh/vk/GrVkCommandBuffer.cppthird_party/skia/src/gpu/ganesh/vk/GrVkRenderPass.cppthird_party/skia/src/gpu/ganesh/vk/GrVkImage.cppthird_party/skia/src/gpu/ganesh/GrResourceProvider.cppthird_party/skia/src/gpu/ganesh/SurfaceDrawContext.cpp
Estimated timestamp from git blame: 2019-10-01
Summary
A potential vulnerability exists in Skia’s Ganesh Vulkan backend where the mechanism for tracking the initialization of stencil attachments does not account for the specific render area of the clear operation. This can result in a shared stencil attachment being marked as ‘cleared’ even if only a small sub-rectangle was actually initialized. Subsequent tasks reusing the attachment may skip the necessary clear and instead load uninitialized GPU memory (stale VRAM), which can then be exfiltrated using standard rendering operations.
Technical Analysis
In third_party/skia/src/gpu/ganesh/ops/OpsTask.cpp, Skia tracks whether a stencil attachment has been initialized via a global flag: GrAttachment::fHasPerformedInitialClear. When a task requires stencil bits to be cleared, it checks this flag. If it is false, Skia sets the load operation to kClear and marks the flag as true after execution.
However, in the Vulkan backend, VK_ATTACHMENT_LOAD_OP_CLEAR is strictly limited to the renderArea specified in the VkRenderPassBeginInfo. Skia uses the task’s fClippedContentBounds as this render area. If the task’s bounds are smaller than the full attachment (e.g., a path confined to a corner), only that sub-rectangle is cleared to zero. The rest of the attachment remains uninitialized.
Because GrDirectContext and its GrResourceCache are shared across different renderer processes in the GPU process for OOP-R (Out-of-Process Rasterization), a stencil attachment created by one origin can be recycled and reused by another. If a second task (potentially from a different origin) reuses this attachment, it will observe fHasPerformedInitialClear == true and skip the initial clear, instead performing a VK_ATTACHMENT_LOAD_OP_LOAD. This loads the uninitialized/stale VRAM into the active stencil buffer.
An attacker can visualize this stale data by using the ‘stencil-then-cover’ rendering path. The ‘cover’ pass can be configured to write a specific color wherever the stencil value is non-zero (GrUserStencilTest::kNotEqual 0). By drawing a full-screen path, the resulting color buffer will reflect the state of the uninitialized VRAM, which can then be read back via getImageData().
Potential Reproduction Steps
- Use a configuration with Ganesh-Vulkan enabled (e.g., Chrome on Android or Linux with
--use-vulkan). - Canvas A: Create a 1024x1024 accelerated canvas. Draw a complex path confined to the top-left 10x10 pixels. This triggers an initial clear of the stencil attachment but only for the 10x10
renderArea, while setting the globalfHasPerformedInitialClearlatch to true. - Canvas B: Create a second 1024x1024 accelerated canvas (possibly in a different origin). This context may receive the same cached
GrAttachmentfrom the GPU process. - The Leak: Draw a complex path covering the full canvas. Skia will skip the stencil clear because the latch is set, loading the stale VRAM from the previous attachment state.
- Exfiltration: Use
getImageData()on Canvas B to capture the pixels. Areas where the stale stencil data was non-zero will contain the draw color, revealing the VRAM contents.
Suggested Fix
Ensure that the fHasPerformedInitialClear flag is only set if the clear operation covers the full dimensions of the GrAttachment. If a partial clear is performed, the flag should remain false, or Skia should track the initialized region more granularly (e.g., using a dirty rect approach).
Evaluated with Chrome root at commit: 1a8d40fc44df2088d5945c0bf53584038aa1614a
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.