Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in ANGLE
DescriptionUse after free in ANGLE
ComponentANGLE
Bug ClassUAF
Tracker516954622
Fix commit1cadf7e26cb6 (angle/angle) +24/-7
CISA KEVNot listed
CreditedGoogle
Disclosed2026-07-29

Files Changed

  • src/libANGLE/renderer/d3d/d3d11/TextureStorage11.cpp
From 1cadf7e26cb6f7817cc1606c932bc1e178f9b10a Mon Sep 17 00:00:00 2001
From: wangra <[email protected]>
Date: Tue, 09 Jun 2026 10:03:35 -0400
Subject: [PATCH] D3D11: Fix Use-After-Free in TextureStorage11 onDestroy

Ensures all associated images are disassociated during TextureStorage11
destruction, even if recovery fails. This prevents early loop
termination from leaving sibling images with dangling pointers to the
deleted storage.

Bug: b/516954622
Change-Id: I2c87c9c0bc764245aefc20773d85f9c78eaa7a53
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7910783
Reviewed-by: Geoff Lang <[email protected]>
Reviewed-by: Shahbaz Youssefi <[email protected]>
Commit-Queue: Ran Wang <[email protected]>
---

diff --git a/src/libANGLE/renderer/d3d/d3d11/TextureStorage11.cpp b/src/libANGLE/renderer/d3d/d3d11/TextureStorage11.cpp
index d1eed0b..828454a 100644
--- a/src/libANGLE/renderer/d3d/d3d11/TextureStorage11.cpp
+++ b/src/libANGLE/renderer/d3d/d3d11/TextureStorage11.cpp
@@ -1088,6 +1088,7 @@
 
 angle::Result TextureStorage11_2D::onDestroy(const gl::Context *context)
 {
+    angle::Result result = angle::Result::Continue;
     for (unsigned i = 0; i < gl::IMPLEMENTATION_MAX_TEXTURE_LEVELS; i++)
     {
         if (mAssociatedImages[i] != nullptr)
@@ -1096,9 +1097,13 @@
 
             // We must let the Images recover their data before we delete it from the
             // TextureStorage.
-            ANGLE_TRY(mAssociatedImages[i]->recoverFromAssociatedStorage(context));
+            if (IsError(mAssociatedImages[i]->recoverFromAssociatedStorage(context)))
+            {
+                result = angle::Result::Stop;
+            }
         }
     }
+    ANGLE_TRY(result);
 
     if (mHasKeyedMutex)
     {
@@ -2213,6 +2218,7 @@
 
 angle::Result TextureStorage11_Cube::onDestroy(const gl::Context *context)
 {
+    angle::Result 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++)
@@ -2223,12 +2229,15 @@
 
                 // We must let the Images recover their data before we delete it from the
                 // TextureStorage.
-                ANGLE_TRY(mAssociatedImages[face][level]->recoverFromAssociatedStorage(context));
+                if (IsError(mAssociatedImages[face][level]->recoverFromAssociatedStorage(context)))
+                {
+                    result = angle::Result::Stop;
+                }
             }
         }
     }
 
-    return angle::Result::Continue;
+    return result;
 }
 
 TextureStorage11_Cube::~TextureStorage11_Cube() {}
@@ -2900,6 +2909,7 @@
 
 angle::Result TextureStorage11_3D::onDestroy(const gl::Context *context)
 {
+    angle::Result result = angle::Result::Continue;
     for (unsigned i = 0; i < gl::IMPLEMENTATION_MAX_TEXTURE_LEVELS; i++)
     {
         if (mAssociatedImages[i] != nullptr)
@@ -2908,11 +2918,14 @@
 
             // We must let the Images recover their data before we delete it from the
             // TextureStorage.
-            ANGLE_TRY(mAssociatedImages[i]->recoverFromAssociatedStorage(context));
+            if (IsError(mAssociatedImages[i]->recoverFromAssociatedStorage(context)))
+            {
+                result = angle::Result::Stop;
+            }
         }
     }
 
-    return angle::Result::Continue;
+    return result;
 }
 
 TextureStorage11_3D::~TextureStorage11_3D() {}
@@ -3276,6 +3289,7 @@
 
 angle::Result TextureStorage11_2DArray::onDestroy(const gl::Context *context)
 {
+    angle::Result result = angle::Result::Continue;
     for (auto iter : mAssociatedImages)
     {
         if (iter.second)
@@ -3284,12 +3298,15 @@
 
             // We must let the Images recover their data before we delete it from the
             // TextureStorage.
-            ANGLE_TRY(iter.second->recoverFromAssociatedStorage(context));
+            if (IsError(iter.second->recoverFromAssociatedStorage(context)))
+            {
+                result = angle::Result::Stop;
+            }
         }
     }
     mAssociatedImages.clear();
 
-    return angle::Result::Continue;
+    return result;
 }
 
 TextureStorage11_2DArray::~TextureStorage11_2DArray() {}
Loading diff…

Original Bug Report

reported by [email protected]

Potential UAF in ANGLE D3D11 due to early exit in TextureStorage11::onDestroy on recovery failure

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. 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 D3D11 backend on Windows due to an early loop termination in the onDestroy method of TextureStorage11 subclasses. If recovering an associated image fails (e.g., during memory pressure), the loop terminates immediately, leaving subsequent sibling images with dangling raw pointers to the deleted storage. These dangling pointers are later dereferenced via a virtual function call during image cleanup, leading to potential arbitrary code execution inside the sandboxed GPU process.

Affected files:

  • third_party/angle/src/libANGLE/renderer/d3d/d3d11/TextureStorage11.cpp
  • third_party/angle/src/libANGLE/renderer/d3d/d3d11/Image11.cpp
  • third_party/angle/src/libANGLE/renderer/d3d/TextureD3D.cpp

Estimated timestamp from git blame: 2014-08-12

Description

A potential Use-After-Free (UAF) vulnerability has been identified in ANGLE’s D3D11 backend. When a TextureStorage11 subclass is destroyed via onDestroy (e.g., TextureStorage11_2D::onDestroy), it loops through and attempts to recover the data for all associated Image11 instances across different mipmap levels or faces:

// In third_party/angle/src/libANGLE/renderer/d3d/d3d11/TextureStorage11.cpp
angle::Result TextureStorage11_2D::onDestroy(const gl::Context *context) {
    for (unsigned i = 0; i < gl::IMPLEMENTATION_MAX_TEXTURE_LEVELS; i++) {
        if (mAssociatedImages[i] != nullptr) {
            mAssociatedImages[i]->verifyAssociatedStorageValid(this);
            ANGLE_TRY(mAssociatedImages[i]->recoverFromAssociatedStorage(context));
        }
    }
    ...
}

If recoverFromAssociatedStorage fails (for example, returning E_OUTOFMEMORY during staging texture creation in createStagingTexture), the ANGLE_TRY macro triggers an early exit from onDestroy and propagates the error code. Because the loop terminates immediately, any sibling Image11 instances at subsequent iterations are never visited.

However, the caller in TextureD3D::releaseTexStorage unconditionally deletes the storage object regardless of whether onDestroy succeeded:

// In third_party/angle/src/libANGLE/renderer/d3d/TextureD3D.cpp
auto err = mTexStorage->onDestroy(context);
SafeDelete(mTexStorage);
return err;

Consequently, the unvisited sibling Image11 objects still retain their raw mAssociatedStorage pointers (defined as TextureStorage11 *mAssociatedStorage; in Image11.h) pointing to the now-freed TextureStorage11 instance. When these sibling images are eventually cleaned up during context teardown, Image11::~Image11 calls disassociateStorage(), which attempts a pure-virtual call dispatch on the deleted storage instance:

// In third_party/angle/src/libANGLE/renderer/d3d/d3d11/Image11.cpp
void Image11::disassociateStorage() {
    if (mRecoverFromStorage) {
        mAssociatedStorage->disassociateImage(mAssociatedImageIndex, this); // Use-After-Free
        ...

Because ANGLE does not use base::raw_ptr (MiraclePtr) for backend D3D11 structures, this Use-After-Free is not mitigated by BackupRefPtr.

Potential Step-by-Step Trigger Sequence

Note: These are suggested/potential steps derived from static analysis, as our tooling does not currently have the capability to execute code.

  1. Establish a WebGL context utilizing the ANGLE D3D11 backend on Windows.
  2. Create a texture and define multiple mip levels (e.g., Level 0 and Level 1), which associates both corresponding Image11 objects with a single TextureStorage11_2D instance.
  3. Induce high GPU memory pressure (e.g., via a parallel WebGL context) to prepare the system for allocation failures.
  4. Redefine Level 0 of the texture with mismatched dimensions. This triggers TextureD3D::releaseTexStorage, calling onDestroy on the underlying TextureStorage11_2D.
  5. During the loop in onDestroy, processing Level 0 fails to allocate a staging texture inside recoverFromAssociatedStorage due to E_OUTOFMEMORY and returns an error. This causes onDestroy to exit early via the ANGLE_TRY macro.
  6. The loop is aborted before processing Level 1. TextureD3D::releaseTexStorage unconditionally deletes the storage instance, leaving the Level 1 Image11 holding a dangling raw pointer mAssociatedStorage to the deleted storage.
  7. Reclaim the freed storage memory via heap grooming/spraying with controlled data.
  8. Clean up the WebGL context. The destructor of the Level 1 Image11 is called, invoking disassociateStorage(), which triggers an indirect virtual method call via the controlled vtable pointer.

Suggested Fix

Modify onDestroy in all affected subclasses (such as TextureStorage11_2D, TextureStorage11_Cube, TextureStorage11_3D, and TextureStorage11_2DArray in TextureStorage11.cpp) to avoid terminating the loop early upon error. Instead, run the loop to completion to ensure all associated images are processed/disassociated, and return the accumulated error at the end. For example:

angle::Result TextureStorage11_2D::onDestroy(const gl::Context *context)
{
    angle::Result result = angle::Result::Continue;
    for (unsigned i = 0; i < gl::IMPLEMENTATION_MAX_TEXTURE_LEVELS; i++)
    { 
        if (mAssociatedImages[i] != nullptr)
        { 
            mAssociatedImages[i]->verifyAssociatedStorageValid(this);
            angle::Result err = mAssociatedImages[i]->recoverFromAssociatedStorage(context);
            if (err != angle::Result::Continue)
            { 
                result = err;
            }
        }
    }
    
    if (mHasKeyedMutex)
    {
        mRenderer->getStateManager()->invalidateBoundViews();
    }

    return result;
}

Evaluated with Chrome root at commit: b1520ef4a76878853a31f0943b565e42060edec8


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