CVE-2026-11110
Overview
Files Changed
src/libANGLE/Framebuffer.cppsrc/tests/gl_tests/RobustResourceInitTest.cpp
Patch
From 006ccddab5023ecf7bfff174e47492f68ab1d6b5 Mon Sep 17 00:00:00 2001
From: Geoff Lang <geofflang@chromium.org>
Date: Tue, 14 Apr 2026 14:28:26 -0400
Subject: [PATCH] Only check stencil front write mask for partial init
The only stencil state that can cause a partial clear is when the
stencil front writemask is not 0xFF. `stencilMask` and `stencilBackMask`
are part of the stencil test and not checked for clears. Clears are also
always considered to use front-facing geometry for the write mask so
`stencilBackWritemask` does not need to be checked either.
Fixed: chromium:500528864
Change-Id: Ieda096221e5d301b7e7c16f1bde3eba88405f7f6
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7762167
Reviewed-by: Shahbaz Youssefi <syoussefi@chromium.org>
Commit-Queue: Geoff Lang <geofflang@chromium.org>
---
diff --git a/src/libANGLE/Framebuffer.cpp b/src/libANGLE/Framebuffer.cpp
index 0672c12..399863a 100644
--- a/src/libANGLE/Framebuffer.cpp
+++ b/src/libANGLE/Framebuffer.cpp
@@ -1690,15 +1690,13 @@
{
ASSERT(HasSupportedStencilBitCount(glState.getDrawFramebuffer()));
- // The least significant |stencilBits| of stencil mask state specify a
- // mask. Compare the masks for differences only in those bits, ignoring any
- // difference in the high bits.
const auto &depthStencil = glState.getDepthStencilState();
- const GLuint differentFwdMasks = depthStencil.stencilMask ^ depthStencil.stencilWritemask;
- const GLuint differentBackMasks =
- depthStencil.stencilBackMask ^ depthStencil.stencilBackWritemask;
-
- if (((differentFwdMasks | differentBackMasks) & 0xFF) != 0)
+ // The least significant |stencilBits| of stencil mask state specify a
+ // mask. Check only those bits, ignoring any masked high bits.
+ // Only the stencil write mask can affect which stencil bits are cleared. Clears are always
+ // considered to be front-facing geometry so the stencil back write mask does not need to be
+ // considered.
+ if ((depthStencil.stencilWritemask & 0xFF) != 0xFF)
{
return true;
}
diff --git a/src/tests/gl_tests/RobustResourceInitTest.cpp b/src/tests/gl_tests/RobustResourceInitTest.cpp
index 56ca97b..9faedd6 100644
--- a/src/tests/gl_tests/RobustResourceInitTest.cpp
+++ b/src/tests/gl_tests/RobustResourceInitTest.cpp
@@ -1992,6 +1992,8 @@
// Disable stencil writes and trigger a clear. Use a tricky mask that does not overlap the
// clear.
glStencilMask(0xF0);
+ // Set GL stencil func, it should have no effect on the clear.
+ glStencilFunc(GL_EQUAL, 0xBB, 0xAA);
clearFunc(0x0F);
EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::black);
Regression Test / PoC
diff --git a/src/tests/gl_tests/RobustResourceInitTest.cpp b/src/tests/gl_tests/RobustResourceInitTest.cpp
index 56ca97b..9faedd6 100644
--- a/src/tests/gl_tests/RobustResourceInitTest.cpp
+++ b/src/tests/gl_tests/RobustResourceInitTest.cpp
@@ -1992,6 +1992,8 @@
// Disable stencil writes and trigger a clear. Use a tricky mask that does not overlap the
// clear.
glStencilMask(0xF0);
+ // Set GL stencil func, it should have no effect on the clear.
+ glStencilFunc(GL_EQUAL, 0xBB, 0xAA);
clearFunc(0x0F);
EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::black);
Original Bug Report
Stale GPU Memory Disclosure via ANGLE Stencil Clear Bypass
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 security team.
Overview: A potential logic flaw in ANGLE’s resource initialization tracking allows an attacker to bypass mandatory zero-initialization of WebGL stencil buffers. By setting matching partial stencil test and write masks, an attacker can trick ANGLE into treating a partial stencil clear as a full clear, leaving uninitialized GPU memory intact to be extracted.
Affected files:
third_party/angle/src/libANGLE/Framebuffer.cpp
Estimated timestamp from git blame: 2024-09-10
Description
Chromium enforces the WEBGL_robustness extension for all WebGL contexts, which requires robust-resource-init to ensure newly allocated GPU memory is zero-initialized. This prevents malicious web pages from reading stale VRAM from other cross-origin tabs, system UI, or processes.
A potential vulnerability exists in ANGLE’s initialization tracking within third_party/angle/src/libANGLE/Framebuffer.cpp. When a web page clears a buffer, ANGLE determines if the clear covers the entire buffer (a “full clear”). If it does, ANGLE skips explicit zero-initialization, as the user’s clear will safely overwrite all stale data. This is evaluated in Framebuffer::partialClearNeedsInit.
However, the logic used to determine if a stencil clear is partial is flawed:
// Framebuffer::partialClearNeedsInit
const auto &depthStencil = glState.getDepthStencilState();
const GLuint differentFwdMasks = depthStencil.stencilMask ^ depthStencil.stencilWritemask;
const GLuint differentBackMasks =
depthStencil.stencilBackMask ^ depthStencil.stencilBackWritemask;
if (((differentFwdMasks | differentBackMasks) & 0xFF) != 0)
{
return true; // Is partial clear
}
To detect a partial clear, ANGLE incorrectly XORs the stencil test mask (stencilMask, set by glStencilFunc) with the stencil write mask (stencilWritemask, set by glStencilMask). According to the OpenGL ES specification, clear operations are only affected by the write mask; the test mask is irrelevant.
If an attacker sets both the test mask and the write mask to the same partial value (e.g., 0x0F), the XOR evaluates to 0. partialClearNeedsInit falsely returns false, signaling a full clear. ANGLE marks the buffer as safely initialized and skips the zeroing pass. The clear command is sent to the GPU backend, which respects the write mask (0x0F) and only clears the lower 4 bits of the stencil buffer. The upper 4 bits remain uninitialized, retaining stale GPU memory.
Potential Exploitation Steps
Note: These are suggested steps to trigger the vulnerability based on code analysis; our tooling cannot execute code to verify a live proof-of-concept.
An attacker can recover uninitialized bits from GPU memory using a stencil-test oracle by following these potential steps:
- Initialize a WebGL 2.0 context and create an FBO with a new
STENCIL_INDEX8renderbuffer. - Set identical partial masks for both the stencil test and the write mask:
gl.stencilFunc(gl.ALWAYS, 0, 0x0F); gl.stencilMask(0x0F); - Clear the stencil buffer:
(At this point, ANGLE marks the buffer as initialized, but the GPU only cleared the bottom 4 bits.)
gl.clearStencil(0); gl.clear(gl.STENCIL_BUFFER_BIT); - Restore the write mask to
0xFFand enable stencil testing to read back the stale upper 4 bits:gl.stencilMask(0xFF); gl.enable(gl.STENCIL_TEST); for (let ref = 0; ref < 16; ref++) { // Oracle: Only pass if the top 4 uninitialized bits match 'ref' gl.stencilFunc(gl.EQUAL, ref << 4, 0xF0); // Draw a fullscreen quad with a color unique to 'ref'. // Use readPixels() to determine the stale memory contents of each pixel. }
Suggested Fix
Modify Framebuffer::partialClearNeedsInit to only evaluate the stencilWritemask. A stencil clear should be considered partial if the write mask does not cover all bits of the currently bound stencil buffer format.
For an 8-bit stencil buffer, the check should simply verify that the mask is 0xFF:
if ((depthStencil.stencilWritemask & 0xFF) != 0xFF ||
(depthStencil.stencilBackWritemask & 0xFF) != 0xFF)
{
return true;
}
Evaluated with Chrome root at commit: 137d451a126685dd5010e6609db9f6d4a78d8234
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.