Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in ANGLE
DescriptionUse after free in ANGLE
ComponentANGLE
Bug ClassUAF
Tracker513048243
Fix commitb085ab3a85c9 (angle/angle) +31/-26
CISA KEVNot listed
CreditedGoogle
Disclosed2026-09-08

Files Changed

  • src/libANGLE/Error.h
  • src/libANGLE/Image.cpp
  • src/libANGLE/renderer/d3d/d3d11/TextureStorage11.cpp
From b085ab3a85c9fdcfda4f5900d881f554e7109ba9 Mon Sep 17 00:00:00 2001
From: wangra <wangra@google.com>
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 <syoussefi@chromium.org>
Reviewed-by: Geoff Lang <geofflang@chromium.org>
Commit-Queue: Ran Wang <wangra@google.com>
---

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

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