Overview

High
Severity
6.5
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUninitialized Use in ANGLE
DescriptionUninitialized Use in ANGLE
ComponentANGLE
Sub-componentMetal
Bug ClassUninitialized Memory
Action tierwatch — information disclosure — security-relevant, below the RCE-class bar
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker513043537
Fix commit5242f8c4150e (angle/angle) +374/-54
CWECWE-457 (Use of uninitialized)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-05-14 (per vendor advisory)
Fix landed2026-05-22 (public git)
Disclosed2026-07-29
Patch gap68 days — fix public in source before the advisory

Background

`invalidateFramebuffer`/`discardFramebuffer`
GL entry points that tell the driver an attachment’s contents are no longer needed, permitting the backend to skip storing or preserving them.
Robust resource initialization
an ANGLE security mode (isRobustResourceInitEnabled) that guarantees every texture and renderbuffer is zero-filled before a client can observe its contents.
`InitState::MayNeedInit`
the per-attachment flag telling ANGLE the resource’s contents are undefined and must be lazily cleared before the next read.
Packed depth/stencil
a single texture holding both depth and stencil aspects, where invalidating one aspect can leave the combined attachment in an undefined state.

Root Cause Analysis

When an application called invalidate, invalidateSub, or discard on a framebuffer, the Metal backend allowed the attachment’s underlying texture contents to become undefined without ever marking that texture as needing re-initialization. Even though robust resource init was enabled, the InitState for the affected attachments was never transitioned back to MayNeedInit, so the invariant that “any client-observable read is preceded by a clear” was silently broken for invalidated attachments. A subsequent readPixels, sampling, or blit could therefore return stale GPU memory that the zero-fill guarantee was supposed to prevent.

The fix adds markAttachmentsUninitialized, which sets the init state to MayNeedInit and flags the framebuffer dirty bits, and calls it from discard, invalidate, and invalidateSub when the new setNeedInitOnInvalidation feature and robust init are both enabled, so ANGLE re-clears the resource before the next access. The overrideInvalidateAttachments helper additionally filters packed depth/stencil aspects so that partial-aspect invalidation does not incorrectly discard the sibling aspect’s contents.

Key insight
The core mistake is that framebuffer invalidation dropped attachment contents but forgot to reset each attachment’s InitState to MayNeedInit, defeating robust resource init; the fix reinstates that flag via markAttachmentsUninitialized on every invalidate/discard path.

Attack Path

  1. Allocate and populate a framebuffer A WebGL/GPU-process client creates a framebuffer whose color or packed depth/stencil attachment is backed by a Metal texture, relying on robust resource init to keep contents defined.
  2. Invalidate the attachment The client calls invalidateFramebuffer/discardFramebuffer, which lets the backend leave the texture’s contents undefined but, pre-patch, never marks the attachment MayNeedInit.
  3. Read back stale memory The client reads the attachment (via readPixels, sampling, or a blit); because the init state was not reset, ANGLE skips the clear and returns uninitialized GPU texture memory.
  4. Disclose adjacent contents (inferred) The recovered bytes may contain leftover data from prior GPU allocations, giving a cross-origin or cross-context information leak, though the patch itself only demonstrates the uninitialized read, not a specific victim buffer.

Impact Assessment

An attacker gains a read of uninitialized Metal texture memory in the GPU process, allowing disclosure of stale GPU contents to a page that can drive WebGL; the precondition is that robust resource init is active and the attachment was invalidated or discarded before being read. Escalation to leaking specific cross-origin pixels or other sensitive GPU allocations is (inferred) and not established by the diff. The vendor advisory rates this high severity and does not report in-the-wild exploitation; NVD scores it CVSS 6.5.

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

Audit Directions

  • Init-state resets on content-dropping ops
    Audit every path that lets attachment or texture contents become undefined (invalidate, discard, orphaning, aliasing) to confirm each transitions InitState back to MayNeedInit under robust init.
  • Backend parity for the fix
    The change is Metal-only; review the D3D11 and GL backends where the same invalidate/discard paths still lack markAttachmentsUninitialized.
  • Packed aspect handling
    Look for other places that treat packed depth/stencil attachments per-aspect, where invalidating one aspect can leave the combined resource observable and undefined.
From 5242f8c4150e57281ea7cfb12c2226a491194cad Mon Sep 17 00:00:00 2001
From: Le Hoang Quyen <lehoangquyen@chromium.org>
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 <geofflang@chromium.org>
Reviewed-by: Shahbaz Youssefi <syoussefi@chromium.org>
Commit-Queue: Quyen Le <lehoangquyen@google.com>
---

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 vm...@google.com

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