CVE-2026-78914
Overview
Background
- Stencil buffer
- A per-pixel auxiliary buffer whose bits Skia’s Ganesh backend partitions into a clip bit and user bits, used to mask and accumulate coverage during rendering.
- `performStencilClearsAsDraws()`
- A capability flag that reports a driver bug on which a plain stencil clear is unreliable, so the clear must instead be issued as an explicit full-surface draw op.
- Stencil-then-cover path rendering
- A path-fill technique that first writes coverage into the stencil buffer and then “covers” it with a draw that reads those stencil contents, meaning it depends on the stencil starting from a known (zeroed) state.
- `GrUserStencilSettings`
- The struct describing a stencil test/op, including which bits an op writes via its
fWriteMaskand whichGrUserStencilOpit applies.
Root Cause Analysis
In SurfaceDrawContext.cpp, when performStencilClearsAsDraws() is true the code performed the mandatory initial stencil clear by calling internalStencilClear(nullptr, /* inside mask */ false). As the added comment states, that path lowers to gZeroStencilClipBit, whose fWriteMask == clipBit, so it zeroes only the clip bit and leaves the user bits untouched and therefore uninitialized. The invariant being violated is that a freshly targeted stencil buffer must be fully zeroed before a consumer reads it; here the user bits held undefined contents. A stencil-then-cover path renderer that reads those user bits on first use would then read uninitialized stencil memory, producing the “uninitialized resource” condition the advisory describes.
The fix records an explicit full-surface FillRectOp using kZeroAllStencilBits, which applies GrUserStencilOp::kZeroClipAndUserBits with a 0xffff write mask so that both the clip bit and all user bits are zeroed, restoring the known-zero invariant.
internalStencilClear, fWriteMask == clipBit) as a full initial stencil clear, leaving the user bits uninitialized; the fix issues a draw that zeroes clip and user bits (GrUserStencilOp::kZeroClipAndUserBits with a 0xffff mask) so nothing downstream reads undefined stencil state.Attack Path
- Reach the draws-as-clear path
Cause rendering on a device/driver where
performStencilClearsAsDraws()returns true, so the initial stencil clear goes through the draw-op branch rather than a native clear. - Leave user bits uninitialized
The old
internalStencilClear(nullptr, false)call zeroes only the clip bit, so the stencil buffer’s user bits retain undefined contents after the “clear.” - Trigger a stencil-then-cover read Submit content whose fill is handled by a stencil-then-cover path renderer, whose cover step reads the stencil user bits on their first use, consuming the uninitialized values (as the patch comment states).
- Observe uninitialized-resource behavior
The undefined stencil contents influence rendering; any escalation to a concrete read/write primitive, information disclosure, or code execution is not shown by the diff
(inferred).
Impact Assessment
performStencilClearsAsDraws() is set can cause a stencil-then-cover path renderer to read uninitialized stencil user bits, matching the advisory’s “uninitialized resource in Skia” classification. The vendor advisory rates this low severity and does not report in-the-wild exploitation; NVD scores it CVSS 6.5. The advisory states no exploited-in-the-wild status and no severity here, so none is asserted. Any progression beyond the uninitialized read to a disclosure or memory-corruption primitive is (inferred) and not demonstrated by the patch.Files Changed
src/gpu/ganesh/SurfaceDrawContext.cpp
Audit Directions
- Partial clears mistaken for full clearsAudit every use of
internalStencilClearandgZeroStencilClipBitfor cases that require a fully zeroed buffer but only zero the clip bit (fWriteMask == clipBit). - Capability-gated fallback pathsReview other
performStencilClearsAsDraws()(and similar driver-workaround) branches to confirm the draw-based fallback initializes exactly the same state as the native operation it replaces. - Read-before-init on GPU attachmentsLook for stencil/depth/render-target consumers such as stencil-then-cover path renderers that read buffer contents on first use and confirm every producer guarantees a fully initialized starting state.
Patch
From e0eb3d588ca59cbe70f1094b6f295d1275f929af Mon Sep 17 00:00:00 2001
From: Greg Daniel <egdaniel@google.com>
Date: Wed, 22 Jul 2026 10:51:38 -0400
Subject: [PATCH] [ganesh] Fully clear initial stencil bits when stencil clear is a draw
There is a driver bug where clearing stencil requires a manual draw
(performStencilClearsAsDraws). Previously, we called
internalStencilClear() which only zeroed the stencil clip bit
(fWriteMask == clipBit). However, the initial stencil clear must also
zero the user bits. Otherwise, stencil-then-cover path renderers might
read undefined stencil contents on their first use.
This change records a full-surface draw that zeroes both clip and user
bits when clearing as draw.
Bug: b/536166543
Change-Id: Ibedb70474a5aeabe8e8c9fc84e403faa256dadc1
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/1300916
Commit-Queue: Greg Daniel <egdaniel@google.com>
Commit-Queue: Robert Phillips <robertphillips@google.com>
Reviewed-by: Robert Phillips <robertphillips@google.com>
---
diff --git a/src/gpu/ganesh/SurfaceDrawContext.cpp b/src/gpu/ganesh/SurfaceDrawContext.cpp
index a8339d2..c6caae4 100644
--- a/src/gpu/ganesh/SurfaceDrawContext.cpp
+++ b/src/gpu/ganesh/SurfaceDrawContext.cpp
@@ -830,7 +830,25 @@
if (this->caps()->performStencilClearsAsDraws()) {
// There is a driver bug with clearing stencil. We must use an op to manually clear the
// stencil buffer before the op that required 'setNeedsStencil'.
- this->internalStencilClear(nullptr, /* inside mask */ false);
+ // NOTE: internalStencilClear() only zeroes the *clip* bit (gZeroStencilClipBit lowers
+ // to fWriteMask == clipBit). The initial clear must also zero the user bits, otherwise
+ // stencil-then-cover path renderers read undefined stencil contents
+ constexpr static GrUserStencilSettings kZeroAllStencilBits(
+ GrUserStencilSettings::StaticInit<
+ 0x0000,
+ GrUserStencilTest::kAlways,
+ 0xffff,
+ GrUserStencilOp::kZeroClipAndUserBits,
+ GrUserStencilOp::kZeroClipAndUserBits,
+ 0xffff>());
+ GrPaint paint;
+ paint.setXPFactory(GrDisableColorXPFactory::Get());
+ SkRect rtRect =
+ SkRect::Make(this->asSurfaceProxy()->backingStoreDimensions());
+ this->addDrawOp(nullptr,
+ FillRectOp::MakeNonAARect(fContext, std::move(paint),
+ SkMatrix::I(), rtRect,
+ &kZeroAllStencilBits));
} else {
this->getOpsTask()->setInitialStencilContent(
OpsTask::StencilContent::kUserBitsCleared);