High watch CVSS 4.3 chrome Uninitialized Memory 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
4.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUninitialized resource in ANGLE
DescriptionUninitialized resource in ANGLE
ComponentANGLE
Sub-componentGL
Bug ClassUninitialized Memory
Action tierwatch — information disclosure — security-relevant, below the RCE-class bar
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker535379043
Fix commitdd6e3b30e154 (angle/angle) +90/-2
CWECWE-908
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-07-16 (per vendor advisory)
Fix landed2026-07-23 (public git)
Disclosed2026-08-25
Patch gap33 days — fix public in source before the advisory

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_STENCIL8 resource that stores both the depth and stencil aspects in one backing image, so both aspects share one InitState.
`InitState`
ANGLE’s per-resource tracking of whether the backing memory has been initialized; a resource marked initialized is not zeroed again.
`ensureAttachmentsInitialized`
The FramebufferGL hook that lazily initializes framebuffer attachments before they are used; it has a native fast path and a default fallback in FramebufferImpl.

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.

Key insight
The single core mistake was letting a native clear of one depth-stencil aspect mark the whole shared-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

  1. Allocate a packed depth-stencil renderbuffer Create a GL_DEPTH24_STENCIL8 renderbuffer and attach it via GL_DEPTH_STENCIL_ATTACHMENT, leaving it (or invalidating it back to) an uninitialized state.
  2. Trigger a single-aspect clear Bind a framebuffer with only the depth-stencil attachment and clear just one aspect (e.g. glClearBufferfv(GL_DEPTH, ...)), so depth != stencil.
  3. False initialization marking The fast path clears depth natively but marks the shared InitState initialized, so the stencil aspect keeps its uninitialized VRAM while ANGLE skips zeroing it.
  4. 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

An attacker running WebGL/GL content can read uninitialized GPU (VRAM) memory of the other depth-stencil aspect, an information disclosure that may leak data from prior GPU allocations, within the GPU process rendering context. The precondition is robust resource initialization being enabled and a packed depth-stencil resource being partially cleared before read-back. The vendor advisory rates this high severity and does not report in-the-wild exploitation; NVD scores it CVSS 4.3.

Files Changed

  • src/libANGLE/renderer/gl/FramebufferGL.cpp
  • src/tests/gl_tests/RobustResourceInitTest.cpp

Audit Directions

  • Shared init-state across sub-resources
    Audit 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 default
    Review renderer backends’ fast paths in ensure*Initialized for cases that update init tracking more coarsely than the actual memory they touch.
  • Partial vs. full clears
    Look for depth-only or stencil-only clear/invalidate handling that assumes a packed resource is fully covered.
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)
 {
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 vm...@google.com

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