Medium chrome Uninitialized Memory 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUninitialized Use in Skia
DescriptionUninitialized Use in Skia
ComponentSkia
Bug ClassUninitialized Memory
Tracker502351526
Fix commite6496d127941 (skia) +156/-3
CISA KEVNot listed
CreditedGoogle
Disclosed2026-07-29

Changed Functions

FunctionChangeNotes
switch
src/gpu/ganesh/ops/OpsTask.cpp
modified
if
src/gpu/ganesh/ops/OpsTask.cpp
modified
if
tests/StencilClearTest.cpp
modified

Files Changed

  • gn/tests.gni
  • src/gpu/ganesh/ops/OpsTask.cpp
  • tests/StencilClearTest.cpp
From e6496d127941b51f348f4c880032547d4c440c37 Mon Sep 17 00:00:00 2001
From: Robert Phillips <robertphillips@google.com>
Date: Thu, 25 Jun 2026 13:21:59 +0000
Subject: [PATCH] Fix Ganesh stencil UMR

When, for a given OpsTask, the stencil ops are discard/store there was a possibility of uninitialized values to creep into the stencil buffer.

This CL reduces the cases in which discard will be used, mapping the problematic cases to clear/store.

Bug: b/502351526
Change-Id: Ic5d7e352d4c59426f103f2cfc39d5cf3fd879213
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/1269136
Reviewed-by: Greg Daniel <egdaniel@google.com>
Commit-Queue: Robert Phillips <robertphillips@google.com>
---

diff --git a/gn/tests.gni b/gn/tests.gni
index ea33c23..2bb7714 100644
--- a/gn/tests.gni
+++ b/gn/tests.gni
@@ -513,6 +513,7 @@
   "$_tests/SlugTest.cpp",
   "$_tests/SmallPathRendererTest.cpp",
   "$_tests/SrcSrcOverBatchTest.cpp",
+  "$_tests/StencilClearTest.cpp",
   "$_tests/SurfaceDrawContextTest.cpp",
   "$_tests/SurfaceSemaphoreTest.cpp",
   "$_tests/TextBlobCacheTest.cpp",
diff --git a/src/gpu/ganesh/ops/OpsTask.cpp b/src/gpu/ganesh/ops/OpsTask.cpp
index 24ce928..6dbc866 100644
--- a/src/gpu/ganesh/ops/OpsTask.cpp
+++ b/src/gpu/ganesh/ops/OpsTask.cpp
@@ -589,7 +589,17 @@
     GrLoadOp stencilLoadOp;
     switch (fInitialStencilContent) {
         case StencilContent::kDontCare:
-            stencilLoadOp = GrLoadOp::kDiscard;
+            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 {
+                // This should only intentionally happen for the AtlasRenderTask which
+                // immediately inserts a clear.
+                stencilLoadOp = GrLoadOp::kDiscard;
+            }
             break;
         case StencilContent::kUserBitsCleared:
             SkASSERT(!caps.performStencilClearsAsDraws());
@@ -624,6 +634,7 @@
     // their store op might be "discard", and we currently make the assumption that a discard will
     // not invalidate what's already in main memory. This is probably ok for now, but certainly
     // something we want to address soon.
+    // b/160958008 forces discardStencilValuesAfterRenderPass to always return false.
     GrStoreOp stencilStoreOp = (caps.discardStencilValuesAfterRenderPass() && !fMustPreserveStencil)
             ? GrStoreOp::kDiscard
             : GrStoreOp::kStore;
@@ -647,6 +658,16 @@
     if (markStencilCleared) {
         stencil->markHasPerformedInitialClear();
     }
+
+#if defined(SK_DEBUG)
+    if (stencilLoadOp == GrLoadOp::kDiscard) {
+        // The only time we should have a stencil discard load-op is when either:
+        //    there is no stencil buffer
+        //    or stencil clears are being performed by draws
+        SkASSERT(!stencil || caps.performStencilClearsAsDraws());
+    }
+#endif
+
     flushState->setOpsRenderPass(renderPass);
     renderPass->begin();
 
@@ -736,10 +757,10 @@
         fTotalBounds.join(toMerge->fTotalBounds);
         fRenderPassXferBarriers |= toMerge->fRenderPassXferBarriers;
         if (fInitialStencilContent == StencilContent::kDontCare) {
-            // Propogate the first stencil content that isn't kDontCare.
+            // Propagate the first stencil content that isn't kDontCare.
             //
             // Once the stencil has any kind of initial content that isn't kDontCare, then the
-            // inital contents of subsequent opsTasks that get merged in don't matter.
+            // initial contents of subsequent opsTasks that get merged in don't matter.
             //
             // (This works because the opsTask all target the same render target and are in
             // painter's order. kPreserved obviously happens automatically with a merge, and kClear
diff --git a/tests/StencilClearTest.cpp b/tests/StencilClearTest.cpp
new file mode 100644
index 0000000..6bb1155
--- /dev/null
+++ b/tests/StencilClearTest.cpp
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2026 Google LLC
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "include/core/SkCanvas.h"
+#include "include/core/SkPaint.h"
+#include "include/core/SkPathBuilder.h"
+#include "include/core/SkSurface.h"
+#include "include/gpu/ganesh/GrDirectContext.h"
+#include "include/gpu/ganesh/SkSurfaceGanesh.h"
+#include "tests/CtsEnforcement.h"
+#include "tests/Test.h"
+
+namespace {
+
+SkPath make_star() {
+    SkPathBuilder starPath;
+    starPath.moveTo(0.0f, -33.3333f);
+    starPath.lineTo(9.62f, -16.6667f);
+    starPath.lineTo(28.867f, -16.6667f);
+    starPath.lineTo(19.24f, 0.0f);
+    starPath.lineTo(28.867f, 16.6667f);
+    starPath.lineTo(9.62f, 16.6667f);
+    starPath.lineTo(0.0f, 33.3333f);
+    starPath.lineTo(-9.62f, 16.6667f);
+    starPath.lineTo(-28.867f, 16.6667f);
+    starPath.lineTo(-19.24f, 0.0f);
+    starPath.lineTo(-28.867f, -16.6667f);
+    starPath.lineTo(-9.62f, -16.6667f);
+    starPath.close();
+
+    return starPath.detach();
+}
+
+sk_sp<SkSurface> gpu_surface(GrDirectContext* dContext) {
+    SkSurfaceProps props(SkSurfaceProps::kDynamicMSAA_Flag, kUnknown_SkPixelGeometry);
+
+    SkImageInfo ii = SkImageInfo::Make(256, 256, kRGBA_8888_SkColorType, kPremul_SkAlphaType);
+    return SkSurfaces::RenderTarget(dContext,
+                                    skgpu::Budgeted::kYes,
+                                    ii,
+                                    /* sampleCount= */ 4,
+                                    kTopLeft_GrSurfaceOrigin,
+                                    &props,
+                                    /*shouldCreateWithMips=*/true);
+}
+
+static void disable_split_reduction(GrContextOptions* options) {
+    options->fReduceOpsTaskSplitting = GrContextOptions::Enable::kNo;
+}
+
+} // anonymous namespace
+
+// This test exercises b/502351526.
+// The ultimate goal is to:
+//    create an OpsTask (O1) that uses a stencil buffer (S) and clears it
+//    create a new OpsTask (O2) that doesn't use a stencil
+//    later, get O2 to add a stencil (and, specifically, reuse S)
+// In the bug this will allow uninitialized values to creep into the
+// stencil buffer and, potentially, impact later stencil-based rendering.
+// When executed this test will trigger an assert if the bug reoccurs.
+DEF_GANESH_TEST_FOR_CONTEXTS(StencilClearTest,
+                             skgpu::IsRenderingContext,
+                             reporter,
+                             ctxInfo,
+                             disable_split_reduction,
+                             CtsEnforcement::kNextRelease) {
+    GrDirectContext* dContext = ctxInfo.directContext();
+
+    SkPath star = make_star();
+
+    // This first draw clears the stencil buffer and fills it with the first star.
+    // The OpsTask (O1) has clear/store stencilOps and marks the stencil buffer as cleared.
+    sk_sp<SkSurface> s1 = gpu_surface(dContext);
+    if (!s1) {
+        return;  // Dynamic MSAA isn't supported everywhere
+    }
+
+    {
+        SkCanvas* canvas = s1->getCanvas();
+
+        SkPaint paint;
+        paint.setColor(SK_ColorBLUE);
+        paint.setAntiAlias(true);
+
+        canvas->concat(SkMatrix::ScaleTranslate(4, 4, 128, 128));
+        canvas->drawPath(star, paint);
+    }
+
+    // This starts a new Task that doesn't (yet) need stencil.
+    // Previously, this OpsTask (O2) would have discard/store stencilOps. With
+    // the fix this will now have clear/store stencilOps.
+    sk_sp<SkSurface> s2 = gpu_surface(dContext);
+    {
+        SkCanvas* canvas = s2->getCanvas();
+
+        SkPaint paint;
+        paint.setColor(SK_ColorRED);
+        canvas->drawRect(SkRect::MakeWH(256, 256), paint);
+    }
+
+    // This block just serves to close the active Task from s2
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential uninitialized GPU stencil memory leak in Skia Ganesh

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 without the Chrome Security team.

Overview: A logic flaw in Skia’s Ganesh backend allows an OpsTask to attach and store uninitialized stencil buffer contents if a subsequent task enables stencil usage on a shared proxy. On tile-based Vulkan GPUs, this causes the task to write undefined tile memory residue back into the stencil buffer. A subsequent task can then read this uninitialized data, leading to a potential cross-origin GPU memory leak observable via canvas readback.

Affected files:

  • third_party/skia/src/gpu/ganesh/ops/OpsTask.cpp
  • third_party/skia/src/gpu/ganesh/SurfaceDrawContext.cpp
  • third_party/skia/src/gpu/ganesh/ops/OpsTask.h
  • gpu/command_buffer/service/shared_context_state.cc

Estimated timestamp from git blame: 2021-05-03

Summary

There is a potential information disclosure vulnerability in Skia’s Ganesh rendering backend. The issue stems from a synchronization failure between the GrRenderTargetProxy’s stencil flag and the individual OpsTask initialization states. When a stencil-requiring operation is recorded, it retroactively enables the stencil flag for the entire proxy, causing previously recorded tasks to attach the stencil buffer without properly initializing it. On tile-based Vulkan GPUs (primarily Android), this results in uninitialized tile memory residue being written to main memory and subsequently read by the attacker.

Technical Details

By reviewing the Skia source code, the vulnerability unfolds through the following mechanism:

  1. Task 0 (T0) Creation: A basic draw operation (no stencil required) is recorded into OpsTask T0. Its fInitialStencilContent defaults to StencilContent::kDontCare. The canvas’s SurfaceDrawContext currently has fNeedsStencil = false.
  2. Task Split: The task is split (e.g., due to an operation requiring a destination read). SurfaceDrawContext::willReplaceOpsTask is called. Because fNeedsStencil is false, it does not flag T0 to preserve the stencil (fMustPreserveStencil remains false).
  3. Task 1 (T1) Creation: A stencil-requiring operation is recorded into the new task T1. SurfaceDrawContext::setNeedsStencil() sets fNeedsStencil = true, updates T1’s state to kUserBitsCleared, and crucially, sets needsStencil = true on the shared GrRenderTargetProxy.
  4. T0 Execution (The Leak): During flush, OpsTask::onExecute runs for T0. It sees proxy->needsStencil() == true and attaches the stencil buffer. Because its state is kDontCare, it sets the load op to GrLoadOp::kDiscard (mapping to VK_ATTACHMENT_LOAD_OP_DONT_CARE). However, caps.discardStencilValuesAfterRenderPass() is hardcoded to false in Skia (due to bug b/160958008), and fMustPreserveStencil is false, so the store op becomes GrStoreOp::kStore. On tile-based Vulkan GPUs, this combination writes uninitialized on-chip tile memory (containing residue from prior cross-origin GPU passes) back to the stencil attachment in main memory.
  5. T1 Execution (The Read): T1 executes with kUserBitsCleared. If the attachment was cleared in a previous flush, stencil->hasPerformedInitialClear() is true. The code falls through to GrLoadOp::kLoad, causing T1 to load the uninitialized data stored by T0. The leaked bits dictate clipping, which can be read out as pixel colors.

Note: Chrome’s gpu/command_buffer/service/shared_context_state.cc disables fReduceOpsTaskSplitting, preserving task boundaries and preventing merge-time corrections of this state.

Potential Attacker Steps

Please note: These are suggested steps based on static code analysis. Our tooling agent does not yet have the ability to run code to produce a working Proof of Concept.

  1. Execute a WebGL or Canvas2D draw that requires a stencil, then force a flush (e.g., via getImageData()). This creates the stencil attachment and sets its sticky “cleared” bit to true.
  2. Draw a non-stencil primitive (creating T0).
  3. Execute an operation that forces a task split, such as drawing the canvas onto itself with a complex blend mode (triggering a destination read barrier).
  4. Draw a stencil-requiring primitive (creating T1).
  5. Force a flush by calling getImageData() and read the pixel values. The drawn pixels will reveal the leaked stencil residue bits from the uninitialized GPU tile memory.

Suggested Fix

There are two primary ways to address this state mismatch:

  • Fix in OpsTask Execution: In OpsTask::onExecute(), if fInitialStencilContent is kDontCare, the rendering pass should explicitly avoid storing the stencil buffer unless it was the task that actively requested it. Forcing stencilStoreOp = GrStoreOp::kDiscard for kDontCare tasks would prevent the uninitialized tile memory from being written back to the attachment.
  • Fix in Proxy State Propagation: Alternatively, when SurfaceDrawContext::setNeedsStencil() is called, it could iterate through all existing closed tasks that share the current GrRenderTargetProxy and update their fMustPreserveStencil or fInitialStencilContent states, rather than just updating the active OpsTask.

Evaluated with Chrome root at commit: 096fc8fdbfacf2546485756d03f160a3d04fcc9b


Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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