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
Tracker513043537
Fix commit5242f8c4150e (angle/angle) +374/-54
CISA KEVNot listed
CreditedGoogle
Disclosed2026-07-29

Files Changed

  • include/platform/autogen/FeaturesMtl_autogen.h
  • include/platform/autogen/FrontendFeatures_autogen.h
  • include/platform/frontend_features.json
  • include/platform/mtl_features.json
  • src/libANGLE/Framebuffer.cpp
From 5242f8c4150e57281ea7cfb12c2226a491194cad Mon Sep 17 00:00:00 2001
From: Le Hoang Quyen <[email protected]>
Date: Wed, 20 May 2026 00:35:19 +0800
Subject: [PATCH] Metal: Fix uninitialized textures on invalidate.

- Added a front-end feature `set_need_init_on_invalidation` to mark
  attachments as needing initialization on invalidation.
- Added `markAttachmentsUninitialized` in `Framebuffer.cpp` to set the
  init state to `MayNeedInit` and flag dirty bits.
- Updated `discard`, `invalidate`, and `invalidateSub` in
  `Framebuffer.cpp` to use this new method.
- Added `overrideInvalidateAttachments` helper in `Framebuffer.cpp` to
  filter packed depth/stencil attachments when the feature is enabled.
- Optimized `invalidateSub` to treat sub-area invalidations as no-ops
  in robust resource init mode if the feature is enabled.
- Enabled `set_need_init_on_invalidation` in Metal backend.
- Added a metal feature to force non-zero texture creations for
  testing purpose.
- Added a new test `ClearThenInvalidateThenReadBack` in
  `RobustResourceInitTest.cpp`.

We only apply the fix to the Metal backend for now. The D3D11 and GL
backends will be next.

Bug: chromium:513043537
Change-Id: I344bff69ec8c5ca48d385b867397ed384031feaf
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7859172
Reviewed-by: Geoff Lang <[email protected]>
Reviewed-by: Shahbaz Youssefi <[email protected]>
Commit-Queue: Quyen Le <[email protected]>
---

diff --git a/include/platform/autogen/FeaturesMtl_autogen.h b/include/platform/autogen/FeaturesMtl_autogen.h
index 3efcc45..202f71c 100644
--- a/include/platform/autogen/FeaturesMtl_autogen.h
+++ b/include/platform/autogen/FeaturesMtl_autogen.h
@@ -338,6 +338,12 @@
         &members,
     };
 
+    FeatureInfo allocateNonZeroTextures = {
+        "allocateNonZeroTextures",
+        FeatureCategory::MetalFeatures,
+        &members,
+    };
+
 };
 
 inline FeaturesMtl::FeaturesMtl()  = default;
diff --git a/include/platform/autogen/FrontendFeatures_autogen.h b/include/platform/autogen/FrontendFeatures_autogen.h
index 4431bb7..9faa0a7 100644
--- a/include/platform/autogen/FrontendFeatures_autogen.h
+++ b/include/platform/autogen/FrontendFeatures_autogen.h
@@ -225,6 +225,12 @@
         &members,
     };
 
+    FeatureInfo setNeedInitOnInvalidation = {
+        "setNeedInitOnInvalidation",
+        FeatureCategory::FrontendFeatures,
+        &members,
+    };
+
 };
 
 inline FrontendFeatures::FrontendFeatures()  = default;
diff --git a/include/platform/frontend_features.json b/include/platform/frontend_features.json
index 6f79c21..4b99462 100644
--- a/include/platform/frontend_features.json
+++ b/include/platform/frontend_features.json
@@ -270,6 +270,13 @@
             "description": [
                 "Allow #extension ... : disable after non preprocessor tokens in WebGL."
             ]
+        },
+        {
+            "name": "set_need_init_on_invalidation",
+            "category": "Features",
+            "description": [
+                "Set Texture's InitState to MayNeedInit inside invalidateFramebuffer or discardFramebuffer."
+            ]
         }
     ]
 }
diff --git a/include/platform/mtl_features.json b/include/platform/mtl_features.json
index 386e3a8..78f0cbc 100644
--- a/include/platform/mtl_features.json
+++ b/include/platform/mtl_features.json
@@ -411,6 +411,13 @@
                 "Inject statements into loop bodies when needed to ensure infinite loops are optimized out."
             ],
             "issue": "http://crbug.com/1513738"
+        },
+        {
+            "name": "allocate_non_zero_textures",
+            "category": "Features",
+            "description": [
+                "Clear allocated textures to non-zero values to help detect uninitialized reads."
+            ]
         }
     ]
 }
diff --git a/src/libANGLE/Framebuffer.cpp b/src/libANGLE/Framebuffer.cpp
index a8d5c85..f1d13ab 100644
--- a/src/libANGLE/Framebuffer.cpp
+++ b/src/libANGLE/Framebuffer.cpp
@@ -337,6 +337,22 @@
     return !stencilAttachment || stencilAttachment->getStencilSize() == 8;
 }
 
+angle::Result CheckAttachmentEnclosed(const Context *context,
+                                      const FramebufferAttachment &attachment,
+                                      const Rectangle &area,
+                                      bool *enclosedOut)
+{
+    *enclosedOut = true;
+    if (attachment.isAttached())
+    {
+        ANGLE_TRY(attachment.ensureSizeResolved(context));
+
+        Extents size = attachment.getSize();
+        *enclosedOut = area.encloses(Rectangle(0, 0, size.width, size.height));
+    }
+    return angle::Result::Continue;
+}
+
 }  // anonymous namespace
 
 FramebufferStatus FramebufferStatus::Complete()
@@ -1606,6 +1622,15 @@
     // can be no-ops, so we should probably do that to ensure consistency.
     // TODO(jmadill): WebGL behaviour, and robust resource init behaviour without WebGL.
 
+    if (context->getFrontendFeatures().setNeedInitOnInvalidation.enabled &&
+        context->isRobustResourceInitEnabled())
+    {
+        // We don't need to override the attachments list like in invalidate() because, unlike
+        // invalidate(), discard() allows a packed depth/stencil attachment's data to become
+        // undefined even if the attachments list only contains the depth or stencil aspect.
+        markAttachmentsUninitialized(context, count, attachments);
+    }
+
     return mImpl->discard(context, count, attachments);
 }
 
@@ -1617,6 +1642,15 @@
     // can be no-ops, so we should probably do that to ensure consistency.
     // TODO(jmadill): WebGL behaviour, and robust resource init behaviour without WebGL.
 
+    if (context->getFrontendFeatures().setNeedInitOnInvalidation.enabled &&
+        context->isRobustResourceInitEnabled())
+    {
+        auto overrideAttachments = overrideInvalidateAttachments(count, attachments);
+        markAttachmentsUninitialized(context, overrideAttachments.size(),
+                                     overrideAttachments.data());
+        return mImpl->invalidate(context, overrideAttachments.size(), overrideAttachments.data());
+    }
+
     return mImpl->invalidate(context, count, attachments);
 }
 
@@ -1655,7 +1689,7 @@
     {
         ASSERT(HasSupportedStencilBitCount(glState.getDrawFramebuffer()));
 
-        const auto &depthStencil       = glState.getDepthStencilState();
+        const auto &depthStencil = glState.getDepthStencilState();
         // The least significant |stencilBits| of stencil mask state specify a
         // mask. Check only those bits, ignoring any masked high bits.
         // Only the stencil write mask can affect which stencil bits are cleared. Clears are always
@@ -1697,6 +1731,43 @@
     // can be no-ops, so we should probably do that to ensure consistency.
     // TODO(jmadill): Make a invalidate no-op in WebGL 2.0.
 
+    if (context->getFrontendFeatures().setNeedInitOnInvalidation.enabled &&
+        context->isRobustResourceInitEnabled())
+    {
+        DrawBufferMask colorMask;
+        bool invalidateDepth   = false;
+        bool invalidateStencil = false;
+
+        for (size_t i = 0; i < count; ++i)
+        {
+            GLenum attachment = attachments[i];
+            if (attachment >= GL_COLOR_ATTACHMENT0 &&
+                attachment < GL_COLOR_ATTACHMENT0 + IMPLEMENTATION_MAX_DRAW_BUFFERS)
+            {
+                colorMask.set(attachment - GL_COLOR_ATTACHMENT0);
+            }
+            if (attachment == GL_DEPTH_ATTACHMENT || attachment == GL_DEPTH_STENCIL_ATTACHMENT)
+            {
+                invalidateDepth = true;
+            }
+            if (attachment == GL_STENCIL_ATTACHMENT || attachment == GL_DEPTH_STENCIL_ATTACHMENT)
+            {
+                invalidateStencil = true;
+            }
+        }
+
+        bool allEnclosed = false;
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 120b907..6f13d2b 100644
--- a/src/tests/angle_end2end_tests_expectations.txt
+++ b/src/tests/angle_end2end_tests_expectations.txt
@@ -572,6 +572,7 @@
 496259841 WIN D3D11 : RobustResourceInitTestES3.InvalidateThenReadBack/* = SKIP
 496259841 WIN D3D11 : RobustResourceInitTestES3.DrawThenInvalidateThenReadBack/* = SKIP
 496259841 WIN D3D11 : RobustResourceInitTestES3.DrawThenInvalidateThenVerifyDepthStencil/* = SKIP
+514927166 WIN D3D11 : RobustResourceInitTestES3.ClearThenInvalidateThenReadBack/* = SKIP
 512896813 WIN D3D11 : LineLoopTestES3.LineLoopDrawArraysInstancedBaseInstance/* = SKIP
 512896813 WIN D3D11 : LineLoopTestES3.LineLoopDrawElementsInstancedBaseVertexBaseInstance/* = SKIP
 514615434 D3D11 : DrawBaseVertexBaseInstanceTest.NegativeBaseVertex/* = SKIP
diff --git a/src/tests/gl_tests/RobustResourceInitTest.cpp b/src/tests/gl_tests/RobustResourceInitTest.cpp
index 6935603..0c5d42c 100644
--- a/src/tests/gl_tests/RobustResourceInitTest.cpp
+++ b/src/tests/gl_tests/RobustResourceInitTest.cpp
@@ -809,6 +809,55 @@
     EXPECT_GL_NO_ERROR();
 }
 
+// Calling invalidate after clear should either read back the result of the clear call (invalidate
+// didn't happen) or transparent black (the init color).
+TEST_P(RobustResourceInitTestES3, ClearThenInvalidateThenReadBack)
+{
+    ANGLE_SKIP_TEST_IF(!hasGLExtension());
+
+    for (int i = 0; i < 10; ++i)
+    {
+        GLTexture tex;
+        GLRenderbuffer rb;
+        GLFramebuffer fbo;
+        glBindFramebuffer(GL_FRAMEBUFFER, fbo);
+
+        if (i % 2 == 0)
+        {
+            glBindTexture(GL_TEXTURE_2D, tex);
+            glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kWidth, kHeight);
+            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+            glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 0);
+        }
+        else
+        {
+            glBindRenderbuffer(GL_RENDERBUFFER, rb);
+            glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, kWidth, kHeight);
+            glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, rb);
+        }
+        ASSERT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+
+        // Clear to red
+        glClearColor(1.0f, 0.0f, 0.0f, 1.0f);
+        glClear(GL_COLOR_BUFFER_BIT);
+
+        // Invalidate
+        std::array<GLenum, 1> attachments = {GL_COLOR_ATTACHMENT0};
+        glInvalidateFramebuffer(GL_FRAMEBUFFER, 1, attachments.data());
+
+        const int w = getWindowWidth();
+        const int h = getWindowHeight();
+
+        // Read back, ensure the content is either red or transparent black
+        GLColor invalidated(100, 100, 100, 100);
+        glReadPixels(w / 2, h / 2, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, &invalidated);
+        EXPECT_TRUE(invalidated == GLColor::red || invalidated == GLColor::transparentBlack)
+            << "At iteration " << i << ", color is " << invalidated;
+        EXPECT_GL_NO_ERROR();
+    }
+}
+
 // Calling invalidate after draw should either read back the result of the draw call (invalidate
 // didn't happen) or the init depth/stencil values.
 TEST_P(RobustResourceInitTestES3, DrawThenInvalidateThenReadBackDepthStencil)
@@ -3372,6 +3421,7 @@
 GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(RobustResourceInitTestES3);
 ANGLE_INSTANTIATE_TEST_ES3_AND(RobustResourceInitTestES3,
                                ES3_METAL().enable(Feature::EmulateDontCareLoadWithRandomClear),
+                               ES3_METAL().enable(Feature::AllocateNonZeroTextures),
                                ES3_VULKAN().enable(Feature::AllocateNonZeroMemory));
 
 GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(RobustResourceInitTestES31);
Loading diff…

Original Bug Report

reported by [email protected]

ANGLE: glInvalidateFramebuffer bypasses robust resource initialization leading to information leak

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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: A potential vulnerability in ANGLE’s Metal and GL backends allows framebuffer invalidation to bypass robust resource initialization. This can lead to a cross-origin information leak by allowing a WebGL context to read uninitialized GPU memory. The issue affects platforms using tile-based rendering architectures, including macOS (Metal) and Android (GL).

Affected files:

  • third_party/angle/src/libANGLE/renderer/metal/FrameBufferMtl.mm
  • third_party/angle/src/libANGLE/renderer/gl/FramebufferGL.cpp
  • third_party/angle/src/libANGLE/Framebuffer.cpp

Estimated timestamp from git blame: 2017-10-03

Description

A potential vulnerability has been identified in ANGLE’s Metal and GL backends where the implementation of glInvalidateFramebuffer (and glDiscardFramebufferEXT) fails to correctly maintain the robust resource initialization state. Robust resource initialization is a security feature designed to ensure that GPU memory is zero-initialized before it can be read by a WebGL context, preventing the leakage of stale data from other origins or the system.

On tile-based architectures (such as Apple Silicon and many mobile GPUs), this logic gap can be exploited to read uninitialized GPU memory because the backend may discard a pending clear operation while the frontend still considers the resource to be initialized.

Technical Analysis

The issue stems from a logic gap in the ANGLE frontend and its interaction with specific backends:

  1. Frontend State Tracking: ANGLE uses a bitset mState.mResourceNeedsInit in third_party/angle/src/libANGLE/Framebuffer.cpp to track which attachments require initialization before being read. When a resource is first created, it is marked as needing initialization.
  2. Clear Operation: When gl.clear() is called, ANGLE’s frontend marks the affected attachments as initialized by resetting the corresponding bits in mResourceNeedsInit (via Framebuffer::markAttachmentsInitialized).
  3. Invalidate Logic Gap: When gl.invalidateFramebuffer() is called, the frontend passes the command to the backend but fails to update the mResourceNeedsInit state. A comment in third_party/angle/src/libANGLE/Framebuffer.cpp at line 1618 (and 1607 for discard) explicitly acknowledges this: // TODO(jmadill): WebGL behaviour, and robust resource init behaviour without WebGL.
  4. Backend Optimization: In backends like Metal (third_party/angle/src/libANGLE/renderer/metal/FrameBufferMtl.mm), an invalidation sets the store action to MTLStoreActionDontCare. On Tile-Based Deferred Rendering (TBDR) hardware, if a clear and an invalidate occur in the same render pass, the GPU may discard the results of the clear in its local tile memory instead of committing the cleared values to the texture’s backing memory in system RAM.
  5. Information Leak: Because the frontend metadata incorrectly indicates the resource is still “initialized,” a subsequent gl.readPixels() or similar operation bypasses the robust initialization check in Framebuffer::ensureReadAttachmentsInitialized (third_party/angle/src/libANGLE/Framebuffer.cpp:2612). The backend then reads the uninitialized backing memory, which contains stale GPU data.

Potential Steps to Reproduce (Suggested)

  1. Initialize a WebGL2 context (robust resource initialization is enabled by default in Chrome).
  2. Create a texture and attach it to a Framebuffer Object (FBO).
  3. Perform a full gl.clear(gl.COLOR_BUFFER_BIT). This causes ANGLE to mark the resource as initialized in its internal metadata.
  4. Call gl.invalidateFramebuffer() for the color attachment. In the Metal or GL backend, this causes the previously requested clear to be discarded from the GPU’s tile buffer.
  5. Call gl.readPixels(). Because the frontend believes the resource is already initialized, it skips the safety clear. The backend reads and returns the uninitialized stale memory from the texture’s backing store.

Suggested Fix

The Framebuffer::invalidate, Framebuffer::invalidateSub, and Framebuffer::discard methods in third_party/angle/src/libANGLE/Framebuffer.cpp should be updated to mark the invalidated attachments as needing initialization (e.g., by resetting their InitState to MayNeedInit and updating the mResourceNeedsInit bitset) when robust resource initialization is enabled. This ensures that any subsequent read operation will correctly trigger a robust clear if the content has been invalidated.

Evaluated with Chrome root at commit: b3153093eb3c78c3e88ccf562bcbc20437a04b0e


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