CVE-2026-79118
Overview
Files Changed
src/libANGLE/renderer/gl/FramebufferGL.cppsrc/tests/gl_tests/RobustResourceInitTest.cpp
Patch
From dd6e3b30e15447d4ccc736d5e11d5b962afe65c0 Mon Sep 17 00:00:00 2001
From: Tzarial <zork@google.com>
Date: Thu, 16 Jul 2026 20:22:09 +0000
Subject: [PATCH] GL: Fix robust init bypass for packed depth-stencil
When robust resource initialization is enabled, FramebufferGL took a
fast path that cleared only one aspect of a packed depth-stencil
resource. This falsely marked the entire resource as initialized
due to shared InitState, potentially allowing the uncleared aspect
containing uninitialized VRAM to be read back.
This CL fixes the issue by falling back to the default implementation
for partial clears of packed depth-stencil attachments, which correctly
initializes both aspects.
Bug: b/535379043
Test: RobustResourceInitTestES3.PackedDepthStencilPartialClearLeaking
Change-Id: I63659d071a8bbf787cdc2bd961b8c5453f2546da
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/8131158
Commit-Queue: Tzarial <zork@chromium.org>
Reviewed-by: Geoff Lang <geofflang@chromium.org>
---
diff --git a/src/libANGLE/renderer/gl/FramebufferGL.cpp b/src/libANGLE/renderer/gl/FramebufferGL.cpp
index 38e521f..e24641c 100644
--- a/src/libANGLE/renderer/gl/FramebufferGL.cpp
+++ b/src/libANGLE/renderer/gl/FramebufferGL.cpp
@@ -1356,10 +1356,19 @@
bool depth,
bool stencil)
{
- if (colorAttachments != getState().getEnabledDrawBuffers())
+ const gl::FramebufferState &state = getState();
+ const gl::FramebufferAttachment *depthAttachment = state.getDepthAttachment();
+ const gl::FramebufferAttachment *stencilAttachment = state.getStencilAttachment();
+
+ const bool isPartialDepthStencilInit =
+ depthAttachment && stencilAttachment &&
+ depthAttachment->getResource() == stencilAttachment->getResource() && depth != stencil;
+
+ if (colorAttachments != state.getEnabledDrawBuffers() || isPartialDepthStencilInit)
{
// Fall back to the default implementation when there are gaps in the enabled draw buffers
- // to avoid modifying the draw buffer state.
+ // to avoid modifying the draw buffer state, or when we are performing a partial clear of a
+ // packed depth-stencil attachment.
return FramebufferImpl::ensureAttachmentsInitialized(context, colorAttachments, depth,
stencil);
}
diff --git a/src/tests/gl_tests/RobustResourceInitTest.cpp b/src/tests/gl_tests/RobustResourceInitTest.cpp
index 058d729..b1c4e26 100644
--- a/src/tests/gl_tests/RobustResourceInitTest.cpp
+++ b/src/tests/gl_tests/RobustResourceInitTest.cpp
@@ -2765,6 +2765,85 @@
EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::red) << "depth should be initialized to 1.0f";
}
+// Test that performing a partial clear on a packed depth-stencil resource does not bypass robust
+// init for the other aspect.
+TEST_P(RobustResourceInitTestES3, PackedDepthStencilPartialClearLeaking)
+{
+ ANGLE_SKIP_TEST_IF(!hasGLExtension());
+
+ constexpr int kSize = 16;
+
+ GLRenderbuffer rb;
+ glBindRenderbuffer(GL_RENDERBUFFER, rb);
+ glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, kSize, kSize);
+
+ GLFramebuffer setupFBO;
+ glBindFramebuffer(GL_FRAMEBUFFER, setupFBO);
+ glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rb);
+ ASSERT_GL_NO_ERROR();
+ ASSERT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+
+ // Poison stencil with 0x5A
+ glClearStencil(0x5A);
+ glClear(GL_STENCIL_BUFFER_BIT);
+ ASSERT_GL_NO_ERROR();
+
+ // Invalidate to force ANGLE to re-initialize it on next use.
+ const GLenum attachments[] = {GL_DEPTH_ATTACHMENT, GL_STENCIL_ATTACHMENT};
+ glInvalidateFramebuffer(GL_FRAMEBUFFER, 2, attachments);
+ ASSERT_GL_NO_ERROR();
+
+ // Create a FBO with no color attachments, only depth-stencil.
+ GLFramebuffer dsOnlyFBO;
+ glBindFramebuffer(GL_FRAMEBUFFER, dsOnlyFBO);
+ glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rb);
+ ASSERT_GL_NO_ERROR();
+ ASSERT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+
+ // Clear depth only.
+ // If bug is present, this clears depth natively, but marks both depth and stencil as
+ // initialized. Stencil is NOT cleared natively.
+ float depthClearValue = 1.0f;
+ glClearBufferfv(GL_DEPTH, 0, &depthClearValue);
+ ASSERT_GL_NO_ERROR();
+
+ // Bind to FBO with color attachment to verify stencil.
+ GLTexture colorBuffer;
+ glBindTexture(GL_TEXTURE_2D, colorBuffer);
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, kSize, kSize, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
+
+ GLFramebuffer readFBO;
+ glBindFramebuffer(GL_FRAMEBUFFER, readFBO);
+ glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, colorBuffer, 0);
+ glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rb);
+ ASSERT_GL_NO_ERROR();
+ ASSERT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+
+ // Clear color to blue.
+ glClearColor(0.0f, 0.0f, 1.0f, 1.0f);
+ glClear(GL_COLOR_BUFFER_BIT);
+ ASSERT_GL_NO_ERROR();
+
+ glEnable(GL_STENCIL_TEST);
+ glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);
+
+ // Draw green if stencil is 0 (expected robust init value).
+ glStencilFunc(GL_EQUAL, 0, 0xFF);
+ ANGLE_GL_PROGRAM(drawGreen, essl1_shaders::vs::Simple(), essl1_shaders::fs::Green());
+ drawQuad(drawGreen, essl1_shaders::PositionAttrib(), 0.0f);
+ ASSERT_GL_NO_ERROR();
+
+ // Draw red if stencil is 0x5A (poison value, indicates leak).
+ glStencilFunc(GL_EQUAL, 0x5A, 0xFF);
+ ANGLE_GL_PROGRAM(drawRed, essl1_shaders::vs::Simple(), essl1_shaders::fs::Red());
+ drawQuad(drawRed, essl1_shaders::PositionAttrib(), 0.0f);
+ ASSERT_GL_NO_ERROR();
+
+ // We expect the stencil to be 0, so the final color should be green.
+ // If the bug is present, it might be red.
+ EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::green);
+}
+
template <int Size, typename InitializedTest>
void VerifyRGBA8PixelRect(InitializedTest inInitialized)
{
Regression Test / PoC
diff --git a/src/tests/gl_tests/RobustResourceInitTest.cpp b/src/tests/gl_tests/RobustResourceInitTest.cpp
index 058d729..b1c4e26 100644
--- a/src/tests/gl_tests/RobustResourceInitTest.cpp
+++ b/src/tests/gl_tests/RobustResourceInitTest.cpp
@@ -2765,6 +2765,85 @@
EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::red) << "depth should be initialized to 1.0f";
}
+// Test that performing a partial clear on a packed depth-stencil resource does not bypass robust
+// init for the other aspect.
+TEST_P(RobustResourceInitTestES3, PackedDepthStencilPartialClearLeaking)
+{
+ ANGLE_SKIP_TEST_IF(!hasGLExtension());
+
+ constexpr int kSize = 16;
+
+ GLRenderbuffer rb;
+ glBindRenderbuffer(GL_RENDERBUFFER, rb);
+ glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, kSize, kSize);
+
+ GLFramebuffer setupFBO;
+ glBindFramebuffer(GL_FRAMEBUFFER, setupFBO);
+ glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rb);
+ ASSERT_GL_NO_ERROR();
+ ASSERT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+
+ // Poison stencil with 0x5A
+ glClearStencil(0x5A);
+ glClear(GL_STENCIL_BUFFER_BIT);
+ ASSERT_GL_NO_ERROR();
+
+ // Invalidate to force ANGLE to re-initialize it on next use.
+ const GLenum attachments[] = {GL_DEPTH_ATTACHMENT, GL_STENCIL_ATTACHMENT};
+ glInvalidateFramebuffer(GL_FRAMEBUFFER, 2, attachments);
+ ASSERT_GL_NO_ERROR();
+
+ // Create a FBO with no color attachments, only depth-stencil.
+ GLFramebuffer dsOnlyFBO;
+ glBindFramebuffer(GL_FRAMEBUFFER, dsOnlyFBO);
+ glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rb);
+ ASSERT_GL_NO_ERROR();
+ ASSERT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+
+ // Clear depth only.
+ // If bug is present, this clears depth natively, but marks both depth and stencil as
+ // initialized. Stencil is NOT cleared natively.
+ float depthClearValue = 1.0f;
+ glClearBufferfv(GL_DEPTH, 0, &depthClearValue);
+ ASSERT_GL_NO_ERROR();
+
+ // Bind to FBO with color attachment to verify stencil.
+ GLTexture colorBuffer;
+ glBindTexture(GL_TEXTURE_2D, colorBuffer);
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, kSize, kSize, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
+
+ GLFramebuffer readFBO;
+ glBindFramebuffer(GL_FRAMEBUFFER, readFBO);
+ glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, colorBuffer, 0);
+ glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rb);
+ ASSERT_GL_NO_ERROR();
+ ASSERT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+
+ // Clear color to blue.
+ glClearColor(0.0f, 0.0f, 1.0f, 1.0f);
+ glClear(GL_COLOR_BUFFER_BIT);
+ ASSERT_GL_NO_ERROR();
+
+ glEnable(GL_STENCIL_TEST);
+ glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);
+
+ // Draw green if stencil is 0 (expected robust init value).
+ glStencilFunc(GL_EQUAL, 0, 0xFF);
+ ANGLE_GL_PROGRAM(drawGreen, essl1_shaders::vs::Simple(), essl1_shaders::fs::Green());
+ drawQuad(drawGreen, essl1_shaders::PositionAttrib(), 0.0f);
+ ASSERT_GL_NO_ERROR();
+
+ // Draw red if stencil is 0x5A (poison value, indicates leak).
+ glStencilFunc(GL_EQUAL, 0x5A, 0xFF);
+ ANGLE_GL_PROGRAM(drawRed, essl1_shaders::vs::Simple(), essl1_shaders::fs::Red());
+ drawQuad(drawRed, essl1_shaders::PositionAttrib(), 0.0f);
+ ASSERT_GL_NO_ERROR();
+
+ // We expect the stencil to be 0, so the final color should be green.
+ // If the bug is present, it might be red.
+ EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::green);
+}
+
template <int Size, typename InitializedTest>
void VerifyRGBA8PixelRect(InitializedTest inInitialized)
{
Original Bug Report
Robust resource init bypass via FramebufferGL per-aspect clear on packed depth-stencil
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 potential issue in ANGLE’s OpenGL backend may allow uninitialized GPU memory to be disclosed to web content. When robust resource initialization is enabled, FramebufferGL can take a fast path that clears only one aspect of a packed depth-stencil resource. This falsely marks the entire resource as initialized, potentially allowing the uncleared aspect containing uninitialized VRAM to be read back.
Affected files:
third_party/angle/src/libANGLE/renderer/gl/FramebufferGL.cppthird_party/angle/src/libANGLE/Framebuffer.cppthird_party/angle/src/libANGLE/renderer/gl/BlitGL.cppthird_party/angle/src/libANGLE/Renderbuffer.cpp
Estimated timestamp from git blame: 2024-04-29
Potential robust resource init bypass via FramebufferGL per-aspect clear on packed depth-stencil
Root Cause Analysis
When robust resource initialization is enabled, the front-end tracks depth and stencil initialization needs separately in mState.mResourceNeedsInit. However, the underlying resource (such as a Renderbuffer) uses a single shared mInitState across all aspects.
During a partial clear of a packed depth-stencil attachment (such as clearing only the depth aspect via glClearBufferfv(GL_DEPTH)), the frontend calls Framebuffer::ensureClearBufferAttachmentsInitialized. Since only the depth aspect is requested, it forwards clearDepth = true and clearStencil = false to the backend:
// third_party/angle/src/libANGLE/Framebuffer.cpp
ANGLE_TRY(mImpl->ensureAttachmentsInitialized(context, clearColorAttachments, clearDepth,
clearStencil)); // clearDepth=true, clearStencil=false
On most backends, this delegates to FramebufferImpl::ensureAttachmentsInitialized, which initializes the entire packed resource. However, FramebufferGL overrides this method. When there are no color attachments on the FBO (such as on a depth-stencil-only FBO), the fast-path condition is met:
// third_party/angle/src/libANGLE/renderer/gl/FramebufferGL.cpp
if (colorAttachments != getState().getEnabledDrawBuffers()) { ... }
This routes the call directly to BlitGL::clearFramebuffer, which performs a per-aspect native clear. Consequently, only GL_DEPTH_BUFFER_BIT is cleared in the native GL driver, leaving the stencil aspect containing uninitialized native GL VRAM:
// third_party/angle/src/libANGLE/renderer/gl/BlitGL.cpp
if (clearMask != 0)
{
ANGLE_GL_TRY(context, mFunctions->clear(clearMask)); // GL_DEPTH_BUFFER_BIT only
}
Immediately following this clear, the frontend calls markAttachmentsInitialized(..., true, false), which marks the depth attachment as initialized:
// third_party/angle/src/libANGLE/Framebuffer.cpp
mState.mDepthAttachment.setInitState(InitState::Initialized);
Because the depth and stencil attachments point to the same packed Renderbuffer, this sets the shared Renderbuffer::mState.mInitState to InitState::Initialized.
When the subsequent safety-net sync (syncStateForClear) attempts to initialize the still-dirty stencil aspect, it calls ensureDrawAttachmentsInitialized, which invokes BlitGL::clearFramebuffer with depth = false and stencil = true.
Inside BlitGL::clearFramebuffer, CheckIfAttachmentNeedsClearing is called on the stencil attachment:
// third_party/angle/src/libANGLE/renderer/gl/BlitGL.cpp
if (attachment->initState() == gl::InitState::Initialized)
{
*needsClearInit = false;
return angle::Result::Continue;
}
Because the shared mInitState was already poisoned to Initialized during the depth clear, this check returns false. The stencil aspect is never cleared natively, but the frontend resets its tracking bit anyway.
As a result, the stencil aspect of the packed depth-stencil resource is left uninitialized, while ANGLE’s tracking state incorrectly reports that the resource is fully initialized.
Potential Reproduction Steps
Note: These are potential steps. Our tooling does not currently have the capability to execute code or run a live proof of concept.
- Create a packed depth-stencil Renderbuffer:
const rb = gl.createRenderbuffer(); gl.bindRenderbuffer(gl.RENDERBUFFER, rb); gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH24_STENCIL8, 512, 512); - Attach it to a fresh FBO with no color attachments:
const fb = gl.createFramebuffer(); gl.bindFramebuffer(gl.FRAMEBUFFER, fb); gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_STENCIL_ATTACHMENT, gl.RENDERBUFFER, rb); - Perform a partial clear of only the depth component:
At this point, the native depth aspect is cleared, but the native stencil aspect remains uninitialized VRAM, and ANGLE’s tracking incorrectly marks both aspects as initialized.
gl.clearBufferfv(gl.DEPTH, 0, new Float32Array([1.0])); - Bind
rbto a second FBO with an active color attachment. - Perform stencil-tested draws for each reference value
0..255to write corresponding mapped colors to the color buffer. - Use
gl.readPixels()to extract the uninitialized native stencil values, which may leak cross-origin GPU memory (such as texture data or pixels belonging to other browser tabs).
Suggested Fix
To address this issue, FramebufferGL::ensureAttachmentsInitialized should fall back to the safe base implementation (or BlitGL::clearFramebuffer must promote the clear mask to clear both aspects) if a partial clear of a packed depth-stencil resource is requested. Alternatively, ANGLE could track the initialization state of packed depth-stencil resources per-aspect, rather than sharing a single InitState on the underlying Renderbuffer object.
Evaluated with Chrome root at commit: b5b015ea5f690560237d1f0cff1405844cd12b8d
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.