High chrome Uninitialized Memory 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUninitialized resource in ANGLE
DescriptionUninitialized resource in ANGLE
ComponentANGLE
Bug ClassUninitialized Memory
Tracker535379043
Fix commitdd6e3b30e154 (angle/angle) +90/-2
CISA KEVNot listed
CreditedGoogle
Disclosed2026-08-25

Files Changed

  • src/libANGLE/renderer/gl/FramebufferGL.cpp
  • src/tests/gl_tests/RobustResourceInitTest.cpp
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)
 {
Loading diff…

Regression Test / PoC

shipped with the fix
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)
 {
Loading diff…

Original Bug Report

reported by [email protected]

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.cpp
  • third_party/angle/src/libANGLE/Framebuffer.cpp
  • third_party/angle/src/libANGLE/renderer/gl/BlitGL.cpp
  • third_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.

  1. Create a packed depth-stencil Renderbuffer:
    const rb = gl.createRenderbuffer();
    gl.bindRenderbuffer(gl.RENDERBUFFER, rb);
    gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH24_STENCIL8, 512, 512);
    
  2. 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);
    
  3. Perform a partial clear of only the depth component:
    gl.clearBufferfv(gl.DEPTH, 0, new Float32Array([1.0]));
    
    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.
  4. Bind rb to a second FBO with an active color attachment.
  5. Perform stencil-tested draws for each reference value 0..255 to write corresponding mapped colors to the color buffer.
  6. 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.

View on issue tracker