High chrome Uninitialized Memory 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUninitialized Use in ANGLE
DescriptionUninitialized Use in ANGLE
ComponentANGLE
Bug ClassUninitialized Memory
Tracker501584689
Fix commit6f0191582467 (angle/angle) +60/-23
CISA KEVNot listed
CreditedGoogle
Disclosed2026-05-27

Files Changed

  • src/libANGLE/Framebuffer.cpp
  • src/libANGLE/Texture.cpp
  • src/libANGLE/renderer/gl/TextureGL.cpp
  • src/libANGLE/renderer/renderer_utils.cpp
  • src/tests/angle_end2end_tests_expectations.txt
  • src/tests/gl_tests/RobustResourceInitTest.cpp
From 6f0191582467d7163244badc45708a4560c510bf Mon Sep 17 00:00:00 2001
From: Shahbaz Youssefi <syoussefi@chromium.org>
Date: Thu, 23 Apr 2026 09:38:04 -0400
Subject: [PATCH] Initialize textures during syncState

Instead of marking the textures as initialized on syncState, actually
initialize them.

In most cases, this makes DIRTY_OBJECT_TEXTURES_INIT obsolete because
textures needing initialization also get synced.  However, this is not
true for some corner cases, such as glTexImage2D with nullptr resetting
a texture that is already synced.

Bug: chromium:501584689
Change-Id: I88ab23f91fce6f3077a25303a9f352317f07bbe9
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7791058
Commit-Queue: Shahbaz Youssefi <syoussefi@chromium.org>
Reviewed-by: Geoff Lang <geofflang@chromium.org>
---

diff --git a/src/libANGLE/Framebuffer.cpp b/src/libANGLE/Framebuffer.cpp
index 161f69f..cdfe0fc 100644
--- a/src/libANGLE/Framebuffer.cpp
+++ b/src/libANGLE/Framebuffer.cpp
@@ -2801,7 +2801,6 @@
     switch (bufferType)
     {
         case GL_COLOR:
-            ASSERT(drawBuffers.any());
             return partialClearNeedsInit(context, drawBuffers, false, false);
         case GL_DEPTH:
             return partialClearNeedsInit(context, {}, true, false);
diff --git a/src/libANGLE/Texture.cpp b/src/libANGLE/Texture.cpp
index 102cffc..ae705c7 100644
--- a/src/libANGLE/Texture.cpp
+++ b/src/libANGLE/Texture.cpp
@@ -2013,24 +2013,6 @@
         return angle::Result::Continue;
     }
 
-    // Clear the base image(s) immediately if needed
-    if (context->isRobustResourceInitEnabled())
-    {
-        ImageIndexIterator it =
-            ImageIndexIterator::MakeGeneric(mState.mType, baseLevel, baseLevel + 1,
-                                            ImageIndex::kEntireLevel, ImageIndex::kEntireLevel);
-        while (it.hasNext())
-        {
-            const ImageIndex index = it.next();
-            const ImageDesc &desc  = mState.getImageDesc(index.getTarget(), index.getLevelIndex());
-
-            if (desc.initState == InitState::MayNeedInit)
-            {
-                ANGLE_TRY(initializeContents(context, GL_NONE, index));
-            }
-        }
-    }
-
     ANGLE_TRY(syncState(context, Command::GenerateMipmap));
     ANGLE_TRY(mTexture->generateMipmap(context));
 
@@ -2488,9 +2470,9 @@
 angle::Result Texture::syncState(const Context *context, Command source)
 {
     ASSERT(hasAnyDirtyBit() || source == Command::GenerateMipmap);
+    ANGLE_TRY(ensureInitialized(context));
     ANGLE_TRY(mTexture->syncState(context, mDirtyBits, source));
     mDirtyBits.reset();
-    mState.mInitState = InitState::Initialized;
     return angle::Result::Continue;
 }
 
diff --git a/src/libANGLE/renderer/gl/TextureGL.cpp b/src/libANGLE/renderer/gl/TextureGL.cpp
index c91c6ac..dfaae06 100644
--- a/src/libANGLE/renderer/gl/TextureGL.cpp
+++ b/src/libANGLE/renderer/gl/TextureGL.cpp
@@ -2483,7 +2483,8 @@
 
     GLenum nativeInternalFormat =
         getLevelInfo(imageIndex.getTarget(), imageIndex.getLevelIndex()).nativeInternalFormat;
-    if (features.allowClearForRobustResourceInit.enabled &&
+    if ((features.allowClearForRobustResourceInit.enabled ||
+         !nativegl::SupportsTexImage(getType())) &&
         nativegl::SupportsNativeRendering(functions, mState.getType(), nativeInternalFormat))
     {
         BlitGL *blitter = GetBlitGL(context);
diff --git a/src/libANGLE/renderer/renderer_utils.cpp b/src/libANGLE/renderer/renderer_utils.cpp
index facd04c..096732d 100644
--- a/src/libANGLE/renderer/renderer_utils.cpp
+++ b/src/libANGLE/renderer/renderer_utils.cpp
@@ -914,6 +914,8 @@
     {
         // Call a specialized clear function to init a multisample texture.
         ANGLE_TRY(multisampleInitializer->initializeMultisampleTextureToBlack(context, t.get()));
+        // The above initialization is invisible to the front-end
+        t->setInitState(gl::InitState::Initialized);
     }
     else if (type == gl::TextureType::Buffer)
     {
diff --git a/src/tests/angle_end2end_tests_expectations.txt b/src/tests/angle_end2end_tests_expectations.txt
index 237a395..cc0f892 100644
--- a/src/tests/angle_end2end_tests_expectations.txt
+++ b/src/tests/angle_end2end_tests_expectations.txt
@@ -94,6 +94,7 @@
 42265226 VULKAN : GLSLTest_ES3.GLVertexIDIntegerTextureDrawElementsU8LineIds/* = SKIP
 235877059 VULKAN : MultisampleTestES3.ResolveToFBO/ES3_Vulkan_EmulatedPrerotation180 = SKIP
 235877059 VULKAN : MultisampleTestES3.ResolveToFBO/ES3_Vulkan_EmulatedPrerotation270 = SKIP
+506165684 VULKAN : RobustResourceInitTest.SampleReinitSampleRGB/* = SKIP
 // ANGLE is broken if a context is made current before a surface is created in the presence of multiple queue families
 42266900 VULKAN : EGLContextSharingTestNoFixture.*/*ForceDelayedDeviceCreationForTesting* = SKIP
 42266900 VULKAN : EGLSurfacelessContextTest.*/*ForceDelayedDeviceCreationForTesting* = SKIP
diff --git a/src/tests/gl_tests/RobustResourceInitTest.cpp b/src/tests/gl_tests/RobustResourceInitTest.cpp
index 1e05ed0..6935603 100644
--- a/src/tests/gl_tests/RobustResourceInitTest.cpp
+++ b/src/tests/gl_tests/RobustResourceInitTest.cpp
@@ -1633,8 +1633,8 @@
     EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::red);
 }
 
-// Basic test that textures are initialized correctly.
-TEST_P(RobustResourceInitTest, Texture)
+// Basic test that textures are initialized correctly.  Verification is done via glReadPixels.
+TEST_P(RobustResourceInitTest, TextureViaReadBack)
 {
     ANGLE_SKIP_TEST_IF(!hasGLExtension());
 
@@ -1653,6 +1653,58 @@
     EXPECT_GL_TRUE(initState);
 }
 
+// Basic test that textures are initialized correctly.  Verification is done by sampling, after
+// the texture is made dirty and glCheckFramebufferStatus on a framebuffer it is attached to.
+TEST_P(RobustResourceInitTest, TextureAfterCheckStatus)
+{
+    ANGLE_SKIP_TEST_IF(!hasGLExtension());
+
+    GLTexture texture;
+    glBindTexture(GL_TEXTURE_2D, texture);
+    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, kWidth, kHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
+    // Dirty the texture
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+
+    GLFramebuffer framebuffer;
+    glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
+    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
+    // Check framebuffer status.  In the GL backend, this syncs the texture because it's dirty.
+    EXPECT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+
+    // Unbind the framebuffer and sample from the texture.
+    glBindFramebuffer(GL_FRAMEBUFFER, 0);
+    ANGLE_GL_PROGRAM(testProgram, essl1_shaders::vs::Texture2D(), essl1_shaders::fs::Texture2D());
+    drawQuad(testProgram, essl1_shaders::PositionAttrib(), 0.0f);
+    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::transparentBlack);
+}
+
+// Test that after a texture with data is sampled, recreating it with no data makes it cleared.
+// Uses an RGB texture which may be emulated on some backends.
+TEST_P(RobustResourceInitTest, SampleReinitSampleRGB)
+{
+    ANGLE_SKIP_TEST_IF(!hasGLExtension());
+
+    const std::vector<GLColorRGB> kInitData(kWidth * kHeight, GLColorRGB(255, 0, 0));
+
+    GLTexture texture;
+    glBindTexture(GL_TEXTURE_2D, texture);
+    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, kWidth, kHeight, 0, GL_RGB, GL_UNSIGNED_BYTE,
+                 kInitData.data());
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+
+    // Draw once, the texture has data and should sample with data.  The texture is also sync'ed at
+    // this step.
+    ANGLE_GL_PROGRAM(testProgram, essl1_shaders::vs::Texture2D(), essl1_shaders::fs::Texture2D());
+    drawQuad(testProgram, essl1_shaders::PositionAttrib(), 0.0f);
+    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::red);
+
+    // Recreate the texture with no data.  It should be cleared to black.
+    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, kWidth, kHeight, 0, GL_RGB, GL_UNSIGNED_BYTE, nullptr);
+    drawQuad(testProgram, essl1_shaders::PositionAttrib(), 0.0f);
+    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::black);
+}
+
 // Test that uploading texture data with an unpack state set correctly initializes the texture and
 // the data is uploaded correctly.
 TEST_P(RobustResourceInitTest, TextureWithUnpackState)
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/src/tests/angle_end2end_tests_expectations.txt b/src/tests/angle_end2end_tests_expectations.txt
index 237a395..cc0f892 100644
--- a/src/tests/angle_end2end_tests_expectations.txt
+++ b/src/tests/angle_end2end_tests_expectations.txt
@@ -94,6 +94,7 @@
 42265226 VULKAN : GLSLTest_ES3.GLVertexIDIntegerTextureDrawElementsU8LineIds/* = SKIP
 235877059 VULKAN : MultisampleTestES3.ResolveToFBO/ES3_Vulkan_EmulatedPrerotation180 = SKIP
 235877059 VULKAN : MultisampleTestES3.ResolveToFBO/ES3_Vulkan_EmulatedPrerotation270 = SKIP
+506165684 VULKAN : RobustResourceInitTest.SampleReinitSampleRGB/* = SKIP
 // ANGLE is broken if a context is made current before a surface is created in the presence of multiple queue families
 42266900 VULKAN : EGLContextSharingTestNoFixture.*/*ForceDelayedDeviceCreationForTesting* = SKIP
 42266900 VULKAN : EGLSurfacelessContextTest.*/*ForceDelayedDeviceCreationForTesting* = SKIP
diff --git a/src/tests/gl_tests/RobustResourceInitTest.cpp b/src/tests/gl_tests/RobustResourceInitTest.cpp
index 1e05ed0..6935603 100644
--- a/src/tests/gl_tests/RobustResourceInitTest.cpp
+++ b/src/tests/gl_tests/RobustResourceInitTest.cpp
@@ -1633,8 +1633,8 @@
     EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::red);
 }
 
-// Basic test that textures are initialized correctly.
-TEST_P(RobustResourceInitTest, Texture)
+// Basic test that textures are initialized correctly.  Verification is done via glReadPixels.
+TEST_P(RobustResourceInitTest, TextureViaReadBack)
 {
     ANGLE_SKIP_TEST_IF(!hasGLExtension());
 
@@ -1653,6 +1653,58 @@
     EXPECT_GL_TRUE(initState);
 }
 
+// Basic test that textures are initialized correctly.  Verification is done by sampling, after
+// the texture is made dirty and glCheckFramebufferStatus on a framebuffer it is attached to.
+TEST_P(RobustResourceInitTest, TextureAfterCheckStatus)
+{
+    ANGLE_SKIP_TEST_IF(!hasGLExtension());
+
+    GLTexture texture;
+    glBindTexture(GL_TEXTURE_2D, texture);
+    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, kWidth, kHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
+    // Dirty the texture
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+
+    GLFramebuffer framebuffer;
+    glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
+    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
+    // Check framebuffer status.  In the GL backend, this syncs the texture because it's dirty.
+    EXPECT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+
+    // Unbind the framebuffer and sample from the texture.
+    glBindFramebuffer(GL_FRAMEBUFFER, 0);
+    ANGLE_GL_PROGRAM(testProgram, essl1_shaders::vs::Texture2D(), essl1_shaders::fs::Texture2D());
+    drawQuad(testProgram, essl1_shaders::PositionAttrib(), 0.0f);
+    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::transparentBlack);
+}
+
+// Test that after a texture with data is sampled, recreating it with no data makes it cleared.
+// Uses an RGB texture which may be emulated on some backends.
+TEST_P(RobustResourceInitTest, SampleReinitSampleRGB)
+{
+    ANGLE_SKIP_TEST_IF(!hasGLExtension());
+
+    const std::vector<GLColorRGB> kInitData(kWidth * kHeight, GLColorRGB(255, 0, 0));
+
+    GLTexture texture;
+    glBindTexture(GL_TEXTURE_2D, texture);
+    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, kWidth, kHeight, 0, GL_RGB, GL_UNSIGNED_BYTE,
+                 kInitData.data());
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+
+    // Draw once, the texture has data and should sample with data.  The texture is also sync'ed at
+    // this step.
+    ANGLE_GL_PROGRAM(testProgram, essl1_shaders::vs::Texture2D(), essl1_shaders::fs::Texture2D());
+    drawQuad(testProgram, essl1_shaders::PositionAttrib(), 0.0f);
+    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::red);
+
+    // Recreate the texture with no data.  It should be cleared to black.
+    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, kWidth, kHeight, 0, GL_RGB, GL_UNSIGNED_BYTE, nullptr);
+    drawQuad(testProgram, essl1_shaders::PositionAttrib(), 0.0f);
+    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::black);
+}
+
 // Test that uploading texture data with an unpack state set correctly initializes the texture and
 // the data is uploaded correctly.
 TEST_P(RobustResourceInitTest, TextureWithUnpackState)
Loading diff…

Original Bug Report

reported by vm...@google.com

Robust Resource Initialization Bypass in ANGLE GL backend via Framebuffer sync

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 without the Chrome Security team.

Overview: A logic error in ANGLE’s OpenGL backend potentially allows WebGL applications to bypass robust resource initialization and read uninitialized GPU memory. Calling glCheckFramebufferStatus on an FBO with a dirty texture attachment improperly sets the texture’s global state to initialized. This causes subsequent draw calls to skip clearing the texture, potentially disclosing cross-origin VRAM contents.

Affected files:

  • third_party/angle/src/libANGLE/Texture.cpp
  • third_party/angle/src/libANGLE/Framebuffer.cpp
  • third_party/angle/src/libANGLE/renderer/gl/FramebufferGL.cpp
  • third_party/angle/src/libANGLE/renderer/gl/TextureGL.cpp
  • third_party/angle/src/libANGLE/State.cpp
  • third_party/angle/src/libANGLE/FramebufferAttachment.cpp

Estimated timestamp from git blame: 2021-08-16

Summary

A potential logic error in ANGLE’s OpenGL backend allows WebGL renderers to bypass robust resource initialization. This vulnerability can lead to the disclosure of uninitialized GPU memory (VRAM), which may contain sensitive cross-origin data such as compositor tiles, video frames, or canvas contents from other processes.

Technical Details

When robust_resource_initialization is enabled, ANGLE defers the zero-clearing of new textures for performance reasons. The texture is allocated but its state is marked as InitState::MayNeedInit. The actual clearing is performed just-in-time before a draw or read operation.

However, a bypass exists in how Framebuffer Object (FBO) status checks are handled in the GL backend. When glCheckFramebufferStatus is called, gl::Framebuffer::checkStatusImpl checks if backend synchronization is required. For the GL backend, rx::FramebufferGL::shouldSyncStateBeforeCheckStatus() unconditionally returns true.

This forces a call to syncAllDrawAttachmentState, which iterates through attachments. If an attached texture has any dirty bits (e.g., from a modified texture parameter), it calls gl::Texture::syncState (third_party/angle/src/libANGLE/Texture.cpp).

The vulnerability is located at the end of gl::Texture::syncState:

angle::Result Texture::syncState(const Context *context, Command source)
{
    ASSERT(hasAnyDirtyBit() || source == Command::GenerateMipmap);
    ANGLE_TRY(mTexture->syncState(context, mDirtyBits, source));
    mDirtyBits.reset();
    mState.mInitState = InitState::Initialized; // <-- Flaw
    return angle::Result::Continue;
}

Here, mState.mInitState is unconditionally marked as Initialized. The GL backend’s syncState routine only synchronizes state parameters (like filters) and does not perform the robust resource clearing.

This creates a dangerous state mismatch: the texture’s global mState.mInitState claims the texture is clean, while the underlying per-level ImageDesc states and actual VRAM remain uninitialized. When the texture is subsequently bound as a sampler for a draw call, Texture::ensureInitialized uses the global mState.mInitState as a fast-path optimization, returns early, and completely skips the initialization step.

Potential Reproduction Steps

Note: These are suggested steps based on static analysis. Our tooling agent cannot dynamically run code to verify them.

  1. In a browser using the ANGLE GL backend, create a WebGL context.
  2. Create a WebGL texture and allocate empty storage: gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1024, 1024, 0, gl.RGBA, gl.UNSIGNED_BYTE, null).
  3. Modify a texture parameter to set a dirty bit: gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST).
  4. Create a Framebuffer Object (FBO) and attach the texture to it.
  5. Call gl.checkFramebufferStatus(gl.FRAMEBUFFER). This triggers the sync logic and improperly marks the texture as initialized.
  6. Bind the default framebuffer (the canvas).
  7. Draw a full-screen quad using the poisoned texture as the source sampler. The shader will sample directly from uninitialized VRAM.
  8. Call gl.readPixels() on the default canvas to capture the leaked VRAM contents.

Suggested Fix

Remove the unconditional mState.mInitState = InitState::Initialized; assignment from gl::Texture::syncState in third_party/angle/src/libANGLE/Texture.cpp.

Synchronizing dirty state bits (like texture parameters) should not implicitly fulfill the robust resource initialization requirements. If syncState needs to update the global initialization state, it should strictly re-derive it by checking the initialization status of all its underlying ImageDesc components.

Evaluated with Chrome root at commit: 096fc8fdbfacf2546485756d03f160a3d04fcc9b


Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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