CVE-2026-79118
Overview
Background
- Robust resource initialization
- An ANGLE feature that guarantees GPU resources are zero-initialized before an application can read them, preventing leaks of stale VRAM.
- Packed depth-stencil
- A single
GL_DEPTH24_STENCIL8resource that stores both the depth and stencil aspects in one backing image, so both aspects share oneInitState. - `InitState`
- ANGLE’s per-resource tracking of whether the backing memory has been initialized; a resource marked initialized is not zeroed again.
- `ensureAttachmentsInitialized`
- The
FramebufferGLhook that lazily initializes framebuffer attachments before they are used; it has a native fast path and a default fallback inFramebufferImpl.
Root Cause Analysis
FramebufferGL’s ensureAttachmentsInitialized took a native fast path whenever the requested color attachments matched the enabled draw buffers. On a partial clear of a packed depth-stencil resource — where depth != stencil, i.e. only one aspect is being cleared — this fast path cleared only the requested aspect natively but, because depth and stencil share a single InitState, marked the entire resource initialized. The other aspect’s uninitialized VRAM was therefore left in place while ANGLE believed it was clean, and a subsequent read of that aspect returned stale device memory.
The fix computes isPartialDepthStencilInit (true when both attachments exist, share the same resource, and only one aspect is being initialized) and routes such cases to FramebufferImpl::ensureAttachmentsInitialized, the default implementation that correctly initializes both aspects. This tightens the fallback condition so the shared-InitState shortcut can no longer falsely mark an uncleared aspect as initialized.
InitState resource as initialized; the fix detects the depth != stencil partial case on a shared packed resource and falls back to the implementation that initializes both aspects.Attack Path
- Allocate a packed depth-stencil renderbuffer
Create a
GL_DEPTH24_STENCIL8renderbuffer and attach it viaGL_DEPTH_STENCIL_ATTACHMENT, leaving it (or invalidating it back to) an uninitialized state. - Trigger a single-aspect clear
Bind a framebuffer with only the depth-stencil attachment and clear just one aspect (e.g.
glClearBufferfv(GL_DEPTH, ...)), sodepth != stencil. - False initialization marking
The fast path clears depth natively but marks the shared
InitStateinitialized, so the stencil aspect keeps its uninitialized VRAM while ANGLE skips zeroing it. - Read back the stale aspect Read the other aspect (the PoC uses a stencil test against a poison value) to observe leaked, uninitialized device memory contents.
Impact Assessment
Files Changed
src/libANGLE/renderer/gl/FramebufferGL.cppsrc/tests/gl_tests/RobustResourceInitTest.cpp
Audit Directions
- Shared init-state across sub-resourcesAudit resources whose aspects, layers, or faces share one initialization flag but can be initialized independently, where clearing one marks all as done.
- Native fast paths that skip the tracked defaultReview renderer backends’ fast paths in
ensure*Initializedfor cases that update init tracking more coarsely than the actual memory they touch. - Partial vs. full clearsLook for depth-only or stencil-only clear/invalidate handling that assumes a packed resource is fully covered.
Patch
From dd6e3b30e15447d4ccc736d5e11d5b962afe65c0 Mon Sep 17 00:00:00 2001 From: Tzarial <[email protected]> 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 <[email protected]> Reviewed-by: Geoff Lang <[email protected]> --- 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.