Medium chrome Uninitialized Memory 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUninitialized Use in ANGLE
DescriptionUninitialized Use in ANGLE
ComponentANGLE
Bug ClassUninitialized Memory
Tracker513769898
Fix commit8e09325ebad4 (angle/angle) +74/-4
CISA KEVNot listed
CreditedGoogle
Disclosed2026-07-29

Files Changed

  • src/libANGLE/State.cpp
  • src/tests/gl_tests/RobustResourceInitTest.cpp
From 8e09325ebad45c7e11630a79754361e965e5fab0 Mon Sep 17 00:00:00 2001
From: Zhenyao Mo <zmo@chromium.org>
Date: Thu, 11 Jun 2026 11:19:38 -0700
Subject: [PATCH] Fix robust resource initialization bypass for read framebuffer

A state-tracking omission in State::setReadFramebufferBinding allowed
robust resource initialization to be bypassed when binding a read
framebuffer with uninitialized resources. Additionally,
State::setObjectDirty was manually setting DIRTY_OBJECT_READ_FRAMEBUFFER
instead of using the setReadFramebufferDirty() helper, which also
bypassed the robust initialization check when attachments were modified
on a bound read framebuffer.

This CL fixes both bypass points by ensuring
DIRTY_OBJECT_READ_ATTACHMENTS is correctly armed when robust resource
initialization is enabled.

Bug: chromium:513769898
Test: RobustResourceInitTest.BindReadFramebufferBypass
Change-Id: I25806d19f968b606461c78cc51aaee8fe1101c29
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7927356
Auto-Submit: Zhenyao Mo <zmo@chromium.org>
Commit-Queue: Zhenyao Mo <zmo@chromium.org>
Reviewed-by: Geoff Lang <geofflang@chromium.org>
---

diff --git a/src/libANGLE/State.cpp b/src/libANGLE/State.cpp
index 5478c12..70d56ba 100644
--- a/src/libANGLE/State.cpp
+++ b/src/libANGLE/State.cpp
@@ -2942,9 +2942,18 @@
     mReadFramebuffer = framebuffer;
     mDirtyBits.set(state::DIRTY_BIT_READ_FRAMEBUFFER_BINDING);
 
-    if (mReadFramebuffer && mReadFramebuffer->hasAnyDirtyBit())
+    if (mReadFramebuffer)
     {
-        mDirtyObjects.set(state::DIRTY_OBJECT_READ_FRAMEBUFFER);
+        if (mReadFramebuffer->hasAnyDirtyBit())
+        {
+            mDirtyObjects.set(state::DIRTY_OBJECT_READ_FRAMEBUFFER);
+        }
+
+        if (isRobustResourceInitEnabled() && mReadFramebuffer->hasResourceThatNeedsInit())
+        {
+            mDirtyObjects.set(state::DIRTY_OBJECT_READ_ATTACHMENTS);
+            mDirtyObjects.set(state::DIRTY_OBJECT_READ_FRAMEBUFFER);
+        }
     }
 }
 
@@ -4033,13 +4042,13 @@
     switch (target)
     {
         case GL_READ_FRAMEBUFFER:
-            mDirtyObjects.set(state::DIRTY_OBJECT_READ_FRAMEBUFFER);
+            setReadFramebufferDirty();
             break;
         case GL_DRAW_FRAMEBUFFER:
             setDrawFramebufferDirty();
             break;
         case GL_FRAMEBUFFER:
-            mDirtyObjects.set(state::DIRTY_OBJECT_READ_FRAMEBUFFER);
+            setReadFramebufferDirty();
             setDrawFramebufferDirty();
             break;
         case GL_VERTEX_ARRAY:
diff --git a/src/tests/gl_tests/RobustResourceInitTest.cpp b/src/tests/gl_tests/RobustResourceInitTest.cpp
index 6d0436a..5c54ef1 100644
--- a/src/tests/gl_tests/RobustResourceInitTest.cpp
+++ b/src/tests/gl_tests/RobustResourceInitTest.cpp
@@ -3545,9 +3545,70 @@
     EXPECT_PIXEL_COLOR_EQ(kSize - 1, kSize - 1, GLColor::green);
 }
 
+// Tests that binding an FBO to GL_READ_FRAMEBUFFER correctly triggers robust resource
+// initialization on read-back (glReadPixels), even if it wasn't previously bound as
+// GL_DRAW_FRAMEBUFFER.
+TEST_P(RobustResourceInitTest, BindReadFramebufferBypass)
+{
+    ANGLE_SKIP_TEST_IF(!hasGLExtension());
+
+    // Setup an uninitialized texture, then bind it.
+    GLTexture tex;
+    setupTexture(&tex);
+    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, kWidth, kHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
+
+    // Attach to a custom FBO
+    GLFramebuffer fbo;
+    glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo);
+    glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 0);
+    EXPECT_GLENUM_EQ(GL_FRAMEBUFFER_COMPLETE, glCheckFramebufferStatus(GL_READ_FRAMEBUFFER));
+
+    // Bind default framebuffer to GL_READ_FRAMEBUFFER to clear any dirty bits
+    glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
+
+    // Read pixels from the default framebuffer to ensure no dirty bits remain in the context
+    std::vector<GLColor> defaultData(kWidth * kHeight);
+    glReadPixels(0, 0, kWidth, kHeight, GL_RGBA, GL_UNSIGNED_BYTE, defaultData.data());
+
+    // Bind our FBO back to GL_READ_FRAMEBUFFER
+    glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo);
+
+    // Read pixels. If robust resource init is bypassed, this will return the "bad data".
+    // If robust resource init is working, it will return transparent black (0).
+    checkFramebufferNonZeroPixels(0, 0, 0, 0, GLColor::transparentBlack);
+    EXPECT_GL_NO_ERROR();
+}
+
+// Tests that attaching a texture to a bound GL_READ_FRAMEBUFFER correctly triggers robust resource
+// initialization on read-back, even if it wasn't previously bound as GL_DRAW_FRAMEBUFFER.
+TEST_P(RobustResourceInitTest, AttachToBoundReadFramebufferBypass)
+{
+    ANGLE_SKIP_TEST_IF(!hasGLExtension());
+
+    // Setup an uninitialized texture, then bind it.
+    GLTexture tex;
+    setupTexture(&tex);
+    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, kWidth, kHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
+
+    // Create a custom FBO and bind to GL_READ_FRAMEBUFFER
+    GLFramebuffer fbo;
+    glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo);
+
+    // Attach texture to the bound GL_READ_FRAMEBUFFER
+    glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 0);
+    EXPECT_GLENUM_EQ(GL_FRAMEBUFFER_COMPLETE, glCheckFramebufferStatus(GL_READ_FRAMEBUFFER));
+
+    // Read pixels. If robust resource init is bypassed, this will return the "bad data".
+    // If robust resource init is working, it will return transparent black (0).
+    checkFramebufferNonZeroPixels(0, 0, 0, 0, GLColor::transparentBlack);
+    EXPECT_GL_NO_ERROR();
+}
+
 ANGLE_INSTANTIATE_TEST_ES2_AND_ES3_AND(
     RobustResourceInitTest,
     ES3_METAL().enable(Feature::EmulateDontCareLoadWithRandomClear),
+    ES3_METAL().enable(Feature::AllocateNonZeroTextures),
+    ES2_METAL().enable(Feature::AllocateNonZeroTextures),
     ES2_VULKAN().enable(Feature::AllocateNonZeroMemory));
 
 GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(RobustResourceInitTestES3);
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 6d0436a..5c54ef1 100644
--- a/src/tests/gl_tests/RobustResourceInitTest.cpp
+++ b/src/tests/gl_tests/RobustResourceInitTest.cpp
@@ -3545,9 +3545,70 @@
     EXPECT_PIXEL_COLOR_EQ(kSize - 1, kSize - 1, GLColor::green);
 }
 
+// Tests that binding an FBO to GL_READ_FRAMEBUFFER correctly triggers robust resource
+// initialization on read-back (glReadPixels), even if it wasn't previously bound as
+// GL_DRAW_FRAMEBUFFER.
+TEST_P(RobustResourceInitTest, BindReadFramebufferBypass)
+{
+    ANGLE_SKIP_TEST_IF(!hasGLExtension());
+
+    // Setup an uninitialized texture, then bind it.
+    GLTexture tex;
+    setupTexture(&tex);
+    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, kWidth, kHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
+
+    // Attach to a custom FBO
+    GLFramebuffer fbo;
+    glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo);
+    glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 0);
+    EXPECT_GLENUM_EQ(GL_FRAMEBUFFER_COMPLETE, glCheckFramebufferStatus(GL_READ_FRAMEBUFFER));
+
+    // Bind default framebuffer to GL_READ_FRAMEBUFFER to clear any dirty bits
+    glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
+
+    // Read pixels from the default framebuffer to ensure no dirty bits remain in the context
+    std::vector<GLColor> defaultData(kWidth * kHeight);
+    glReadPixels(0, 0, kWidth, kHeight, GL_RGBA, GL_UNSIGNED_BYTE, defaultData.data());
+
+    // Bind our FBO back to GL_READ_FRAMEBUFFER
+    glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo);
+
+    // Read pixels. If robust resource init is bypassed, this will return the "bad data".
+    // If robust resource init is working, it will return transparent black (0).
+    checkFramebufferNonZeroPixels(0, 0, 0, 0, GLColor::transparentBlack);
+    EXPECT_GL_NO_ERROR();
+}
+
+// Tests that attaching a texture to a bound GL_READ_FRAMEBUFFER correctly triggers robust resource
+// initialization on read-back, even if it wasn't previously bound as GL_DRAW_FRAMEBUFFER.
+TEST_P(RobustResourceInitTest, AttachToBoundReadFramebufferBypass)
+{
+    ANGLE_SKIP_TEST_IF(!hasGLExtension());
+
+    // Setup an uninitialized texture, then bind it.
+    GLTexture tex;
+    setupTexture(&tex);
+    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, kWidth, kHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
+
+    // Create a custom FBO and bind to GL_READ_FRAMEBUFFER
+    GLFramebuffer fbo;
+    glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo);
+
+    // Attach texture to the bound GL_READ_FRAMEBUFFER
+    glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 0);
+    EXPECT_GLENUM_EQ(GL_FRAMEBUFFER_COMPLETE, glCheckFramebufferStatus(GL_READ_FRAMEBUFFER));
+
+    // Read pixels. If robust resource init is bypassed, this will return the "bad data".
+    // If robust resource init is working, it will return transparent black (0).
+    checkFramebufferNonZeroPixels(0, 0, 0, 0, GLColor::transparentBlack);
+    EXPECT_GL_NO_ERROR();
+}
+
 ANGLE_INSTANTIATE_TEST_ES2_AND_ES3_AND(
     RobustResourceInitTest,
     ES3_METAL().enable(Feature::EmulateDontCareLoadWithRandomClear),
+    ES3_METAL().enable(Feature::AllocateNonZeroTextures),
+    ES2_METAL().enable(Feature::AllocateNonZeroTextures),
     ES2_VULKAN().enable(Feature::AllocateNonZeroMemory));
 
 GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(RobustResourceInitTestES3);
Loading diff…

Original Bug Report

reported by vm...@google.com

ANGLE: Potential robust resource initialization bypass in State::setReadFramebufferBinding

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 state-tracking omission in ANGLE allows WebGL applications to potentially bypass robust resource initialization when binding a read framebuffer. This could enable the disclosure of uninitialized GPU memory through operations like glReadPixels, leaking data from other origins or processes. The issue stems from an asymmetry in how read and draw framebuffer bindings are handled in the libANGLE State class.

Affected files:

  • third_party/angle/src/libANGLE/State.cpp
  • third_party/angle/src/libANGLE/State.h
  • third_party/angle/src/libANGLE/Context.cpp
  • third_party/angle/src/libANGLE/Framebuffer.cpp
  • third_party/angle/src/libANGLE/renderer/vulkan/vk_helpers.cpp
  • third_party/angle/src/libANGLE/renderer/vulkan/TextureVk.cpp

Estimated timestamp from git blame: 2019-05-13

Summary

A potential vulnerability has been identified in ANGLE’s state management that may allow a bypass of the robust resource initialization security feature. By manipulating framebuffer bindings, a malicious WebGL application could read uninitialized GPU VRAM through operations such as glReadPixels, glCopyTexImage2D, or glBlitFramebuffer. This uninitialized memory may contain residual data from other Chrome processes, other tabs, or other system applications.

Technical Details

The root cause appears to be an asymmetry in how ANGLE’s State class handles read and draw framebuffer bindings. While the draw framebuffer binding logic correctly arms the robust initialization state, the read framebuffer logic does not.

In third_party/angle/src/libANGLE/State.cpp, State::setReadFramebufferBinding (lines 2949-2961) updates the read framebuffer but fails to check if the new framebuffer contains resources that require initialization:

void State::setReadFramebufferBinding(Framebuffer *framebuffer)
{
    if (mReadFramebuffer == framebuffer)
        return;

    mReadFramebuffer = framebuffer;
    mDirtyBits.set(state::DIRTY_BIT_READ_FRAMEBUFFER_BINDING);

    if (mReadFramebuffer && mReadFramebuffer->hasAnyDirtyBit())
    {
        mDirtyObjects.set(state::DIRTY_OBJECT_READ_FRAMEBUFFER);
    }
}

By contrast, setDrawFramebufferBinding (lines 2981-2985) correctly arms DIRTY_OBJECT_DRAW_ATTACHMENTS if robust initialization is enabled and the framebuffer has uninitialized resources:

if (isRobustResourceInitEnabled() && mDrawFramebuffer->hasResourceThatNeedsInit())
{
    mDirtyObjects.set(state::DIRTY_OBJECT_DRAW_ATTACHMENTS);
    mDirtyObjects.set(state::DIRTY_OBJECT_DRAW_FRAMEBUFFER);
}

DIRTY_OBJECT_READ_ATTACHMENTS is the trigger for syncReadAttachments, which calls Framebuffer::ensureReadAttachmentsInitialized. If this bit is not armed during the binding change, subsequent read-back operations like glReadPixels skip the initialization (zeroing) step.

Furthermore, State::setObjectDirty (State.cpp:4043) fails to use the setReadFramebufferDirty() helper defined in State.h, manually setting only DIRTY_OBJECT_READ_FRAMEBUFFER for the GL_READ_FRAMEBUFFER target, which also misses the attachment initialization check.

Because many backends (like Vulkan and Metal) rely on the front-end to perform robust initialization and skip their own internal clears when this feature is enabled, this front-end bypass leads directly to reading uninitialized VRAM.

Potential Reproduction Steps

An attacker might attempt to trigger this via the following sequence:

  1. Create a texture with null data (marking it as InitState::MayNeedInit).
  2. Attach the texture to a Framebuffer Object (FBO).
  3. Ensure the FBO is not currently bound as the GL_READ_FRAMEBUFFER.
  4. Perform a gl.readPixels operation on the default framebuffer to clear any existing DIRTY_OBJECT_READ_ATTACHMENTS bits in the context.
  5. Bind the target FBO to GL_READ_FRAMEBUFFER. Due to the missing logic in setReadFramebufferBinding, the attachments dirty bit is not armed.
  6. Call gl.readPixels from the target FBO. The command may execute against uninitialized VRAM, returning residual data to the application.

Suggested Fix

  1. Update State::setReadFramebufferBinding in third_party/angle/src/libANGLE/State.cpp to include a check similar to the draw path:
if (isRobustResourceInitEnabled() && mReadFramebuffer && mReadFramebuffer->hasResourceThatNeedsInit())
{
    mDirtyObjects.set(state::DIRTY_OBJECT_READ_ATTACHMENTS);
}
  1. Modify State::setObjectDirty in third_party/angle/src/libANGLE/State.cpp to use the setReadFramebufferDirty() helper for the GL_READ_FRAMEBUFFER case.

Evaluated with Chrome root at commit: 1a8d40fc44df2088d5945c0bf53584038aa1614a


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