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
Tracker523723064
Fix commit0ec59299eab9 (angle/angle) +43/-12
CISA KEVNot listed
CreditedGoogle
Disclosed2026-08-25

Files Changed

  • src/libANGLE/State.cpp
  • src/libANGLE/State.h
  • src/libANGLE/Texture.cpp
  • src/tests/gl_tests/RobustResourceInitTest.cpp
From 0ec59299eab94c4f01855d56d6ecfac75d6ed939 Mon Sep 17 00:00:00 2001
From: Shahbaz Youssefi <syoussefi@chromium.org>
Date: Mon, 06 Jul 2026 23:07:02 -0400
Subject: [PATCH] Always sync the texture after robust init sync

In the Vulkan backend, when handling the TEXTURES_INIT "dirty object"
bit, a clear is only staged in the backing image.  This operation
signals to the front-end that the texture is dirty.

If a state sync is happening for a draw call that doesn't initially
consider the textures dirty, it will not actually sync the texture to
flush the staged clear.

With this change, every time the TEXTURES_INIT dirty object is set, the
TEXTURES dirty object is also set.

At this point, the only reason TEXTURES_INIT even exists is because
TEXTURES is synced _after_ DRAW_FRAMEBUFFER.  And the only reason for
that is the deferred clear optimization in the Vulkan backend.  If the
Vulkan backend is made to pick up clears during texture sync (e.g.
stashed in the context) for framebuffer sync to be able to pick up as
deferred clears, then TEXTURE sync can be moved earlier than
DRAW_FRAMEBUFFER and TEXTURES_INIT can be completely removed.

Bug: chromium:523723064
Change-Id: I2b7e101f0ed019d5f81fe597099bf7ae3569ef32
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/8050824
Reviewed-by: Geoff Lang <geofflang@chromium.org>
Reviewed-by: Charlie Lao <cclao@google.com>
Commit-Queue: Shahbaz Youssefi <syoussefi@chromium.org>
---

diff --git a/src/libANGLE/State.cpp b/src/libANGLE/State.cpp
index ac2dc8c..0a965b3 100644
--- a/src/libANGLE/State.cpp
+++ b/src/libANGLE/State.cpp
@@ -2682,12 +2682,14 @@
         return;
     }
 
-    if (texture->hasAnyDirtyBit())
+    const bool needsRobustInit =
+        isRobustResourceInitEnabled() && texture->initState() == InitState::MayNeedInit;
+    if (texture->hasAnyDirtyBit() || needsRobustInit)
     {
         setTextureDirty(textureIndex);
     }
 
-    if (isRobustResourceInitEnabled() && texture->initState() == InitState::MayNeedInit)
+    if (needsRobustInit)
     {
         mDirtyObjects.set(state::DIRTY_OBJECT_TEXTURES_INIT);
     }
@@ -4130,15 +4132,12 @@
         if (!image)
             continue;
 
-        if (image->hasAnyDirtyBit())
+        const bool needsRobustInit =
+            isRobustResourceInitEnabled() && image->initState() == InitState::MayNeedInit;
+        if (image->hasAnyDirtyBit() || needsRobustInit)
         {
             ANGLE_TRY(image->syncState(context, Command::Other));
         }
-
-        if (isRobustResourceInitEnabled() && image->initState() == InitState::MayNeedInit)
-        {
-            mDirtyObjects.set(state::DIRTY_OBJECT_IMAGES_INIT);
-        }
     }
 
     // Set all active blocks dirty on executable change
@@ -4231,13 +4230,15 @@
         if (!image.texture.get())
             return;
 
-        if (image.texture->hasAnyDirtyBit())
+        const bool needsRobustInit =
+            isRobustResourceInitEnabled() && image.texture->initState() == InitState::MayNeedInit;
+        if (image.texture->hasAnyDirtyBit() || needsRobustInit)
         {
             mDirtyImages.set(unit);
             mDirtyObjects.set(state::DIRTY_OBJECT_IMAGES);
         }
 
-        if (isRobustResourceInitEnabled() && image.texture->initState() == InitState::MayNeedInit)
+        if (needsRobustInit)
         {
             mDirtyObjects.set(state::DIRTY_OBJECT_IMAGES_INIT);
         }
diff --git a/src/libANGLE/State.h b/src/libANGLE/State.h
index 2676141..4da37a4 100644
--- a/src/libANGLE/State.h
+++ b/src/libANGLE/State.h
@@ -1626,7 +1626,10 @@
         return (this->*handlers[dirtyObject])(context, command);
     }
 
-    // Robust init must happen before Framebuffer init for the Vulkan back-end.
+    // Robust init must happen before Framebuffer init for the Vulkan back-end.  If deferred clears
+    // can be made to work in the Vulkan back-end such that textures could be sync'ed before the
+    // framebuffer, then TEXTURES_INIT and IMAGES_INIT can be removed since robust init happens
+    // during texture sync as well.
     static_assert(state::DIRTY_OBJECT_ACTIVE_TEXTURES < state::DIRTY_OBJECT_TEXTURES_INIT,
                   "init order");
     static_assert(state::DIRTY_OBJECT_TEXTURES_INIT < state::DIRTY_OBJECT_DRAW_FRAMEBUFFER,
diff --git a/src/libANGLE/Texture.cpp b/src/libANGLE/Texture.cpp
index 9fe7a74..b19c820 100644
--- a/src/libANGLE/Texture.cpp
+++ b/src/libANGLE/Texture.cpp
@@ -2450,7 +2450,8 @@
 
 angle::Result Texture::syncState(const Context *context, Command source)
 {
-    ASSERT(hasAnyDirtyBit() || source == Command::GenerateMipmap);
+    ASSERT(hasAnyDirtyBit() || source == Command::GenerateMipmap ||
+           (context->isRobustResourceInitEnabled() && mState.mInitState == InitState::MayNeedInit));
     ANGLE_TRY(ensureInitialized(context));
     ANGLE_TRY(mTexture->syncState(context, mDirtyBits, source));
     mDirtyBits.reset();
diff --git a/src/tests/gl_tests/RobustResourceInitTest.cpp b/src/tests/gl_tests/RobustResourceInitTest.cpp
index 39fd638..eb7fd89 100644
--- a/src/tests/gl_tests/RobustResourceInitTest.cpp
+++ b/src/tests/gl_tests/RobustResourceInitTest.cpp
@@ -2082,6 +2082,32 @@
 }
 
 // Test that after a texture with data is sampled, recreating it with no data makes it cleared.
+TEST_P(RobustResourceInitTest, SampleReinitSample)
+{
+    ANGLE_SKIP_TEST_IF(!hasGLExtension());
+
+    const std::vector<GLColor> kInitData(kWidth * kHeight, GLColor::red);
+
+    GLTexture texture;
+    glBindTexture(GL_TEXTURE_2D, texture);
+    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, kWidth, kHeight, 0, GL_RGBA, 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_RGBA, kWidth, kHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
+    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)
 {
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 39fd638..eb7fd89 100644
--- a/src/tests/gl_tests/RobustResourceInitTest.cpp
+++ b/src/tests/gl_tests/RobustResourceInitTest.cpp
@@ -2082,6 +2082,32 @@
 }
 
 // Test that after a texture with data is sampled, recreating it with no data makes it cleared.
+TEST_P(RobustResourceInitTest, SampleReinitSample)
+{
+    ANGLE_SKIP_TEST_IF(!hasGLExtension());
+
+    const std::vector<GLColor> kInitData(kWidth * kHeight, GLColor::red);
+
+    GLTexture texture;
+    glBindTexture(GL_TEXTURE_2D, texture);
+    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, kWidth, kHeight, 0, GL_RGBA, 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_RGBA, kWidth, kHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
+    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)
 {
Loading diff…

Original Bug Report

reported by rj...@google.com

Potential robust resource initialization bypass in ANGLE Vulkan via syncDirtyObjects snapshot

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 logic error in ANGLE’s Vulkan state synchronization potentially allows robust resource initialization (zero-clearing) to be bypassed. A new dirty state triggered during synchronization is not processed before the draw call, allowing a shader to sample uninitialized GPU memory. This could lead to a cross-origin information leak.

Affected files:

  • third_party/angle/src/libANGLE/State.h
  • third_party/angle/src/libANGLE/State.cpp
  • third_party/angle/src/libANGLE/renderer/vulkan/TextureVk.cpp
  • third_party/angle/src/libANGLE/Texture.cpp

Estimated timestamp from git blame: Unknown (Google3 checkout)

Summary

A potential vulnerability exists in ANGLE’s Vulkan backend that allows a WebGL application to bypass robust resource initialization (zero-clearing) for textures. By exploiting a snapshotting behavior in how dirty objects are synchronized in gl::State, an attacker can sample uninitialized GPU memory that may contain data from previous allocations. This results in a potential cross-origin information leak if the memory was previously used by other processes or origins.

Root Cause Analysis

The core issue is a state synchronization logic error in State::syncDirtyObjects (located in third_party/angle/src/libANGLE/State.h). This function synchronizes dirty objects before operations like draw calls:

const state::DirtyObjects &dirtyObjects = mDirtyObjects & bitset;  // SNAPSHOT
for (size_t dirtyObject : dirtyObjects) {
    ANGLE_TRY(dirtyObjectHandler(dirtyObject, context, command));
}
mDirtyObjects &= ~dirtyObjects;  // clear processed objects

The function takes a snapshot of the dirty objects bitset (dirtyObjects) and iterates over it. However, processing one dirty object can trigger another dirty object state with a higher index. Specifically, processing DIRTY_OBJECT_TEXTURES_INIT (index 1) calls State::syncTexturesInit, which eventually triggers the flagging of DIRTY_OBJECT_TEXTURES (index 8) via an observer chain. Since DIRTY_OBJECT_TEXTURES was not in the snapshot, the loop skips it, and its synchronization handler (State::syncTextures) is not called before the draw operation proceeds.

In the Vulkan backend, TextureVk::initializeContents stages a clear operation but does not immediately flush it. The flush is intended to occur during syncTextures -> TextureVk::syncState. Because syncTextures is skipped, the staged clear is never applied to the underlying VkImage before the shader uses it.

Potential Attack Sequence

Note: These are suggested steps to trigger the issue; our tooling has not yet executed a working proof of concept.

  1. Texture Setup: Create a texture and initialize it with data. Perform a draw call to ensure all states are clean.
  2. Compatible Redefinition: Call glTexImage2D with nullptr using the same dimensions and format. In the Vulkan backend, this triggers a compatible redefinition which allocates new uninitialized memory from the Vulkan Memory Allocator (VMA) but skips setting dirty bits on the frontend texture. The texture is only marked as needing initialization (InitState::MayNeedInit).
  3. The Draw Call: The attacker issues a draw call, triggering Context::prepareForDraw -> State::syncDirtyObjects.
  4. Snapshot and Init: State::syncDirtyObjects takes a snapshot containing DIRTY_OBJECT_TEXTURES_INIT (index 1). Processing this calls syncTexturesInit -> Texture::ensureInitialized -> TextureVk::initializeContents.
  5. Staging the Clear: TextureVk::initializeContents stages a zero-clear in ImageHelper and calls appendSubresourceUpdate. This triggers an observer chain notification (SubjectChanged).
  6. Observer Chain: The notification bubbles up from TextureVk to the frontend Texture, which flags DIRTY_BIT_IMPLEMENTATION. This sends a DirtyBitsFlagged notification to the Context, which updates State::mDirtyObjects to include DIRTY_OBJECT_TEXTURES (index 8).
  7. The Bypass: The syncDirtyObjects loop finishes iterating over the original snapshot. Because DIRTY_OBJECT_TEXTURES (index 8) was not in the snapshot, it is skipped. Thus, State::syncTextures is not called.
  8. Execution: ContextVk::handleDirtyTexturesImpl transitions the image layout for reading, incorrectly assuming initialization is complete. The draw call executes, and the shader samples from the uninitialized VMA memory since the zero-clear was never flushed.
  9. Data Leak: The attacker reads the framebuffer to capture the leaked GPU memory contents.

Impact

This is a potential high-severity security issue allowing cross-origin information disclosure. An attacker can read GPU memory that might contain sensitive data from other browser tabs, the OS compositor (e.g., UI fragments or desktop screenshots), or other processes. The GPU process is unsandboxed on Android and shared globally on other platforms.

Suggested Fix

Modify syncDirtyObjects to ensure that any new dirty bits set during the synchronization of earlier bits are also processed within the same pass (e.g., by re-evaluating mDirtyObjects or using a while loop instead of iterating over a single snapshot). Alternatively, TextureVk::initializeContents could be modified to immediately flush staged clears when robust resource initialization is enabled.

Evaluated with Chrome root at commit: 65b3256311f3ab6fb9870eaa522de7e6dd2663bb


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