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

Overview

High
Severity
4.3
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
CWECWE-457 (Use of uninitialized)
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-04-11 (per vendor advisory)
Fix landed2026-04-27 (public git)
Disclosed2026-05-27
Patch gap30 days — fix public in source before the advisory

Background

Robust resource initialization
An ANGLE/WebGL security feature that guarantees newly-allocated texture/buffer storage is zero-cleared before it can be sampled or read, so a program never observes residual GPU memory. For performance the clear is deferred: new storage is marked InitState::MayNeedInit and cleared just-in-time.
`mState.mInitState`
The texture’s global initialization state, used by Texture::ensureInitialized as a fast path: if it reads Initialized, the per-level clear loop is skipped entirely.
`ensureInitialized`
The routine that actually performs the deferred zero-clear of any MayNeedInit levels; this is exactly what the patch now calls from syncState.
`FramebufferGL::shouldSyncStateBeforeCheckStatus`
In the GL backend this returns true, so glCheckFramebufferStatus forces attachment syncState, which is how a merely-dirty (parameter-changed) texture reaches Texture::syncState without any draw/read.

Root Cause Analysis

The invariant robust-resource-init relies on is that mState.mInitState == Initialized must imply the underlying VRAM has actually been cleared. The vulnerable Texture::syncState broke this: after flushing dirty bits (mTexture->syncState(context, mDirtyBits, source); mDirtyBits.reset();) it executed mState.mInitState = InitState::Initialized; unconditionally. But a backend syncState only synchronizes state parameters (filters, swizzle, base level) — it does not perform the deferred zero-clear. So any code path that dirties a texture and then triggers syncState without a real initialization — most directly glCheckFramebufferStatus on a GL-backend FBO with that texture attached (via shouldSyncStateBeforeCheckStatus → syncAllDrawAttachmentState → Texture::syncState) — would flip the global init state to Initialized while the per-level ImageDesc states and the physical VRAM stayed uninitialized. A later draw sampling that texture hit ensureInitialized’s fast path, saw Initialized, and skipped the clear, sampling residual GPU memory.

The patch replaces the lie with the truth: syncState now calls ANGLE_TRY(ensureInitialized(context)); before flushing dirty bits and deletes the mState.mInitState = InitState::Initialized; assignment. Now syncing a dirty texture actually clears any MayNeedInit levels rather than merely asserting they are clean. Supporting changes: generateMipmap drops its now-redundant inline clear loop (since syncState initializes), renderer_utils.cpp marks multisample textures initialized after the backend clears them (an initialization invisible to the front-end), TextureGL.cpp broadens the blit-clear path, and Framebuffer.cpp drops a now-invalid ASSERT(drawBuffers.any()). New tests TextureAfterCheckStatus and SampleReinitSampleRGB verify a dirtied texture read back after glCheckFramebufferStatus, and a texture recreated with null data, both sample as cleared. The advisory labels this “Uninitialized Use” (CWE-457), which the diff substantiates; the realized security impact is cross-origin GPU-memory disclosure.

Key insight
syncState asserted a texture was initialized as a side effect of flushing unrelated parameter dirty bits, without ever performing the deferred zero-clear — so a status check turned an uninitialized texture into one the init fast-path would skip.

Attack Path

  1. Create a texture with lazy storage In a WebGL/GLES context on the ANGLE GL backend, allocate texImage2D(..., null) so storage is MayNeedInit (deferred clear). (inferred: reachable from WebGL; the diff proves the GL-backend sync path.)
  2. Dirty a parameter Call texParameteri(GL_TEXTURE_MIN_FILTER, GL_NEAREST) so the texture has a dirty bit but is still uncleared.
  3. Attach and check status Attach the texture to an FBO and call glCheckFramebufferStatus. In the GL backend this forces Texture::syncState, which (pre-patch) marks mState.mInitState = Initialized without clearing.
  4. Sample the poisoned texture Bind the default framebuffer and draw a quad sampling the texture; ensureInitialized sees Initialized, skips the clear, and the shader samples uninitialized VRAM.
  5. Exfiltrate glReadPixels the rendered result, reading back residual GPU memory (potentially cross-origin compositor tiles, video frames, or other contexts’ contents) — an information-disclosure primitive.

Impact Assessment

This runs in the GPU process but the leaked data crosses back into the attacker’s renderer via readPixels, defeating robust-resource-init and Site Isolation for GPU memory contents. No special privilege is needed beyond a WebGL context on an ANGLE GL-backend configuration; the precondition is that the texture be dirtied and routed through the GL-backend status-check sync path. The primitive is a read/disclosure of uninitialized (possibly cross-origin) VRAM — serious confidentiality loss but not, on its own, memory corruption or RCE (the low CVSS C:L is arguably understated for cross-origin leakage; classify from the diff as uninitialized-memory use yielding disclosure).

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

Audit Directions

  • Other `mInitState = Initialized` assignments
    Grep ANGLE for any place that sets a global/per-level init state to Initialized without a corresponding clear (buffers, renderbuffers, image/EGLImage siblings).
  • Backend `shouldSyncStateBeforeCheckStatus` / status-check sync
    Review all callers that route glCheckFramebufferStatus (and other non-draw operations) into Texture::syncState, and whether other backends (Metal, D3D) share the fast-path assumption.
  • `ensureInitialized` fast-path consumers
    Audit every reader of mState.mInitState that short-circuits per-level clears to confirm the global flag can no longer be set ahead of the physical clear.
  • `DIRTY_OBJECT_TEXTURES_INIT` corner cases
    The commit notes glTexImage2D(null) resetting an already-synced texture is still handled separately; audit that path and similar “already synced then invalidated” transitions.
From 6f0191582467d7163244badc45708a4560c510bf Mon Sep 17 00:00:00 2001
From: Shahbaz Youssefi <[email protected]>
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 <[email protected]>
Reviewed-by: Geoff Lang <[email protected]>
---

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 [email protected]

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