Medium watch CVSS 8.3 chrome UAF 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
8.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in ANGLE
DescriptionUse after free in ANGLE
ComponentANGLE
Bug ClassUAF
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable
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.
Tracker513048243
Fix commitb085ab3a85c9 (angle/angle) +31/-26
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-05-14 (per vendor advisory)
Fix landed2026-07-31 (public git)
Disclosed2026-09-08
Patch gap39 days — fix public in source before the advisory

Background

`angle::Result`
ANGLE’s status enum (Continue on success, Stop on error) propagated by the ANGLE_TRY macro, which returns early when a result is not Continue.
`orphanSibling` / image orphaning
when an EGLImage or its source/target is destroyed, ANGLE walks the sibling images and lets each recover its data before the backing storage is freed.
`TextureStorage11::onDestroy` / `recoverFromAssociatedStorage`
D3D11 texture-storage teardown that first asks each associated Image to copy its data out of the storage, then frees the storage.
Manual result tracking
the pre-fix idiom of a local angle::Result result updated inside a loop with if (subResult != Continue) result = subResult; before a final ANGLE_TRY(result).

Root Cause Analysis

During image orphaning and D3D11 storage destruction, ANGLE loops over siblings/associated images, calls a recovery routine on each, and tracks whether any failed in a local angle::Result before a final ANGLE_TRY(result). The manual tracking was error-prone: a failing recovery could be recorded and then effectively lost or mis-sequenced across iterations, so ANGLE_TRY(result) saw Continue even though an image had failed to recover its data. Execution then proceeded to free the TextureStorage while an associated Image still referenced that storage, producing a use-after-free when the image was later accessed.

The fix introduces angle::ResultAccumulator, whose operator= only overwrites when the incoming result is non-Continue, so the first error is retained (“sticky”) and ANGLE_TRY(result) reliably bails before the unsafe free. orphanImages and the TextureStorage11_2D/Cube/3D/2DArray::onDestroy loops were converted to accumulate through this type, guaranteeing a recovery failure aborts teardown instead of continuing to release still-referenced memory.

Key insight
The mistake was hand-rolled result tracking that could drop an error mid-loop, letting destruction proceed past a failed data recovery and free storage an image still points at; the sticky ResultAccumulator preserves the first non-Continue result so ANGLE_TRY stops before the UAF.

Attack Path

  1. Set up shared images attacker JavaScript (via WebGL/EGLImage sharing on the D3D11 backend) creates textures and images that share backing TextureStorage.
  2. Force a failing recovery it drives teardown (deleting a texture/image) under conditions where recoverFromAssociatedStorage/orphanSibling fails for one sibling mid-loop.
  3. Error is dropped pre-fix, the manual tracking loses that failure, so ANGLE_TRY(result) does not abort onDestroy.
  4. Storage freed while referenced teardown continues and frees the TextureStorage even though an associated Image still holds a reference.
  5. Use-after-free a later access through the surviving image dereferences the freed storage, which heap grooming can turn into a controlled read/write in the GPU process.

Impact Assessment

An attacker gains a use-after-free in the GPU process on the D3D11 backend, arising when image/storage teardown proceeds despite a failed data-recovery step. Preconditions are use of the D3D11 renderer and an image-sharing/teardown sequence that makes a per-sibling recovery fail; reliable exploitation additionally needs heap grooming to reclaim the freed storage with attacker data.

Files Changed

  • src/libANGLE/renderer/d3d/d3d11/TextureStorage11.cpp
  • src/libANGLE/Image.cpp
  • src/libANGLE/Error.h

Audit Directions

  • Hand-rolled result tracking in loops
    audit loops that update a local Result/error variable conditionally before a single ANGLE_TRY/check; these can drop or mis-order failures.
  • Free-after-recovery ordering
    flag teardown paths that free backing storage only after a recovery step, and confirm a failed recovery actually aborts the free.
  • Sticky-error adoption
    search remaining destruction/orphaning code for the old if (r != Continue) result = r; idiom and migrate it to ResultAccumulator.
From b085ab3a85c9fdcfda4f5900d881f554e7109ba9 Mon Sep 17 00:00:00 2001
From: wangra <[email protected]>
Date: Thu, 30 Jul 2026 22:50:17 -0400
Subject: [PATCH] Introduce ResultAccumulator to aggregate results

Introduce ResultAccumulator to aggregate results to simplify manual
result tracking.

Test: angle_end2end_tests --gtest_filter="ImageTest.SiblingOrphaningOnBackendFailure"
Bug: b/513048243
Change-Id: I5a092a6c6cb0fa2b1269f1f619952b18a004ec76
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/8178168
Reviewed-by: Shahbaz Youssefi <[email protected]>
Reviewed-by: Geoff Lang <[email protected]>
Commit-Queue: Ran Wang <[email protected]>
---
diff --git a/src/libANGLE/Error.h b/src/libANGLE/Error.h
index 5b140e3..fc8c218 100644
--- a/src/libANGLE/Error.h
+++ b/src/libANGLE/Error.h
@@ -123,6 +123,27 @@
     Stop,
 };
 
+class [[nodiscard]] ResultAccumulator
+{
+  public:
+    ResultAccumulator() : mResult(Result::Continue) {}
+    ResultAccumulator(Result result) : mResult(result) {}
+
+    ResultAccumulator &operator=(Result result)
+    {
+        if (result != Result::Continue)
+        {
+            mResult = result;
+        }
+        return *this;
+    }
+
+    operator Result() const { return mResult; }
+
+  private:
+    Result mResult;
+};
+
 // TODO(jmadill): Remove this when refactor is complete. http://anglebug.com/42261727
 egl::Error ResultToEGL(Result result);
 }  // namespace angle
diff --git a/src/libANGLE/Image.cpp b/src/libANGLE/Image.cpp
index 33db5aa..0bad2f0 100644
--- a/src/libANGLE/Image.cpp
+++ b/src/libANGLE/Image.cpp
@@ -129,7 +129,7 @@
 {
     ASSERT(outReleaseImage != nullptr);
 
-    angle::Result result = angle::Result::Continue;
+    angle::ResultAccumulator result = angle::Result::Continue;
 
     if (mTargetOf.get() != nullptr)
     {
@@ -143,11 +143,7 @@
     {
         for (Image *sourceImage : mSourcesOf)
         {
-            angle::Result orphanResult = sourceImage->orphanSibling(context, this);
-            if (orphanResult != angle::Result::Continue)
-            {
-                result = orphanResult;
-            }
+            result = sourceImage->orphanSibling(context, this);
         }
         mSourcesOf.clear();
     }
diff --git a/src/libANGLE/renderer/d3d/d3d11/TextureStorage11.cpp b/src/libANGLE/renderer/d3d/d3d11/TextureStorage11.cpp
index 4ad3d2c..8b86f1c 100644
--- a/src/libANGLE/renderer/d3d/d3d11/TextureStorage11.cpp
+++ b/src/libANGLE/renderer/d3d/d3d11/TextureStorage11.cpp
@@ -1000,7 +1000,7 @@
 
 angle::Result TextureStorage11_2D::onDestroy(const gl::Context *context)
 {
-    angle::Result result = angle::Result::Continue;
+    angle::ResultAccumulator result = angle::Result::Continue;
     for (unsigned i = 0; i < gl::IMPLEMENTATION_MAX_TEXTURE_LEVELS; i++)
     {
         if (mAssociatedImages[i] != nullptr)
@@ -1009,10 +1009,7 @@
 
             // We must let the Images recover their data before we delete it from the
             // TextureStorage.
-            if (IsError(mAssociatedImages[i]->recoverFromAssociatedStorage(context)))
-            {
-                result = angle::Result::Stop;
-            }
+            result = mAssociatedImages[i]->recoverFromAssociatedStorage(context);
         }
     }
     ANGLE_TRY(result);
@@ -1932,7 +1929,7 @@
 
 angle::Result TextureStorage11_Cube::onDestroy(const gl::Context *context)
 {
-    angle::Result result = angle::Result::Continue;
+    angle::ResultAccumulator result = angle::Result::Continue;
     for (unsigned int level = 0; level < gl::IMPLEMENTATION_MAX_TEXTURE_LEVELS; level++)
     {
         for (unsigned int face = 0; face < gl::kCubeFaceCount; face++)
@@ -1943,10 +1940,7 @@
 
                 // We must let the Images recover their data before we delete it from the
                 // TextureStorage.
-                if (IsError(mAssociatedImages[face][level]->recoverFromAssociatedStorage(context)))
-                {
-                    result = angle::Result::Stop;
-                }
+                result = mAssociatedImages[face][level]->recoverFromAssociatedStorage(context);
             }
         }
     }
@@ -2445,7 +2439,7 @@
 
 angle::Result TextureStorage11_3D::onDestroy(const gl::Context *context)
 {
-    angle::Result result = angle::Result::Continue;
+    angle::ResultAccumulator result = angle::Result::Continue;
     for (unsigned i = 0; i < gl::IMPLEMENTATION_MAX_TEXTURE_LEVELS; i++)
     {
         if (mAssociatedImages[i] != nullptr)
@@ -2454,10 +2448,7 @@
 
             // We must let the Images recover their data before we delete it from the
             // TextureStorage.
-            if (IsError(mAssociatedImages[i]->recoverFromAssociatedStorage(context)))
-            {
-                result = angle::Result::Stop;
-            }
+            result = mAssociatedImages[i]->recoverFromAssociatedStorage(context);
         }
     }
 
@@ -2825,7 +2816,7 @@
 
 angle::Result TextureStorage11_2DArray::onDestroy(const gl::Context *context)
 {
-    angle::Result result = angle::Result::Continue;
+    angle::ResultAccumulator result = angle::Result::Continue;
     for (auto iter : mAssociatedImages)
     {
         if (iter.second)
@@ -2834,10 +2825,7 @@
 
             // We must let the Images recover their data before we delete it from the
             // TextureStorage.
-            if (IsError(iter.second->recoverFromAssociatedStorage(context)))
-            {
-                result = angle::Result::Stop;
-            }
+            result = iter.second->recoverFromAssociatedStorage(context);
         }
     }
     mAssociatedImages.clear();
Loading diff…

Original Bug Report

reported by [email protected]

Potential Use-After-Free in ANGLE EGLImage Orphaning on Windows

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 Use-After-Free (UAF) vulnerability exists in ANGLE’s EGLImage implementation on Windows when a source texture is destroyed during Out-Of-Memory (OOM) conditions. If the backend fails to orphan the source sibling due to an allocation failure, a raw pointer to the freed object is retained in the image state. This dangling pointer can subsequently be dereferenced during rendering or upon the image’s own destruction.

Affected files:

  • third_party/angle/src/libANGLE/Image.cpp
  • third_party/angle/src/libANGLE/renderer/d3d/EGLImageD3D.cpp
  • third_party/angle/src/libANGLE/Texture.cpp
  • third_party/angle/src/libANGLE/Renderbuffer.cpp

Estimated timestamp from git blame: 2018-07-27

The vulnerability is located in ANGLE’s EGLImage management logic, primarily affecting the D3D11 backend on Windows. When an ImageSibling (such as a gl::Texture or gl::Renderbuffer) that serves as the source for an egl::Image is destroyed, ANGLE attempts to “orphan” the sibling. Orphaning is intended to detach the image from its source while preserving its content for other objects that may still be using the image.

In third_party/angle/src/libANGLE/Image.cpp, the function Image::orphanSibling calls the backend’s implementation of orphan:

angle::Result Image::orphanSibling(const gl::Context *context, ImageSibling *sibling) {
    ANGLE_TRY(mImplementation->orphan(context, sibling)); // [1] Returns early on error
    if (mState.source == sibling) {
        ...
        mState.source = nullptr; // [2] Never reached if ANGLE_TRY fails
    }
}

In the D3D11 backend (third_party/angle/src/libANGLE/renderer/d3d/EGLImageD3D.cpp), orphan calls copyToLocalRendertarget, which attempts to create a copy of the texture’s render target. If this allocation fails (e.g., due to VRAM exhaustion), it returns an error. Because of the ANGLE_TRY macro at [1], the error causes the function to return immediately, skipping the crucial line at [2] that nulls the mState.source pointer.

When the source object is a gl::Texture or gl::Renderbuffer, its destruction proceeds even if orphaning fails because the error is explicitly ignored in the onDestroy method (e.g., in third_party/angle/src/libANGLE/Texture.cpp):

void Texture::onDestroy(const Context *context) {
    ...
    egl::RefCountObjectReleaser<egl::Image> releaseImage;
    (void)orphanImages(context, &releaseImage); // [3] Error is swallowed
    ...
}

As a result, the egl::Image maintains a dangling raw pointer to the now-freed gl::Texture object.

Potential Sinks and Impact

The dangling pointer can be dereferenced in several ways:

  1. Virtual Function Calls: Rendering using a target texture that refers to the orphaned egl::Image will eventually call EGLImageD3D::getRenderTarget. This method performs a virtual call (getAttachmentRenderTarget) on the dangling mState.source pointer, potentially allowing for control flow hijacking.
  2. Heap Corruption: When the egl::Image itself is destroyed, Image::onDestroy calls mState.source->removeImageSource(this). This leads to a memory write to the freed memory at the offset of the mSourcesOf member, which can be leveraged for memory corruption exploits.

Suggested Potential Steps to Reproduce

An attacker operating from a compromised renderer could potentially follow these steps:

  1. Induce heavy VRAM pressure in the GPU process by allocating many large textures.
  2. Create a SharedImage with initial pixel data. On Windows, this frequently triggers the EGLImageBacking path, which creates an egl::Image from a source GL texture and then attempts to release the source texture immediately if no workarounds are active.
  3. Trigger the destruction of the source texture. If orphaning fails due to the VRAM pressure, the egl::Image retains a dangling pointer to the texture.
  4. Attempt to use or destroy the SharedImage to trigger a dereference of the dangling pointer.

The Image::orphanSibling logic should be modified to ensure that mState.source is nulled out even if the backend orphaning call returns an error. While the backend might fail to preserve the data under OOM conditions, the reference to the dying sibling must be cleared to maintain memory safety.

Note: These findings are based on manual code review; a working proof-of-concept has not yet been produced.

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