Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in ANGLE
DescriptionUse after free in ANGLE
ComponentANGLE
Bug ClassUAF
Tracker497724490
Fix commit3bb4e2424dc4 (angle/angle) +15/-14
CISA KEVNot listed
CreditedSyn4pse (@ret2happy)
Disclosed2026-05-05

Files Changed

  • src/libANGLE/renderer/metal/mtl_library_cache.h
  • src/libANGLE/renderer/metal/mtl_library_cache.mm
From 3bb4e2424dc4424edc30cddb384817bcab078438 Mon Sep 17 00:00:00 2001
From: Geoff Lang <geofflang@chromium.org>
Date: Wed, 01 Apr 2026 15:54:06 -0400
Subject: [PATCH] Metal: Store LibraryCacheEntry in a shared_ptr

This ensures that if an entry is removed from the cache while it is
compiling, it is not deleted until it is fully unreferenced.

If there were > kMaxCachedLibraries simultaneous compile requests, it
was possible that the cache gets trimmed while threads are compiling and
we can end up writing to removed cache entries.

Bug: chromium:497724490
Change-Id: Iba4281d5d12c6882de51fc1bf88d60157ffc7fb6
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7722080
Commit-Queue: Geoff Lang <geofflang@chromium.org>
Reviewed-by: dan sinclair <dsinclair@chromium.org>
---

diff --git a/src/libANGLE/renderer/metal/mtl_library_cache.h b/src/libANGLE/renderer/metal/mtl_library_cache.h
index d936e6a..93dfc15 100644
--- a/src/libANGLE/renderer/metal/mtl_library_cache.h
+++ b/src/libANGLE/renderer/metal/mtl_library_cache.h
@@ -77,7 +77,7 @@
         std::mutex lock;
     };
 
-    LibraryCacheEntry &getCacheEntry(LibraryKey &&key);
+    std::shared_ptr<LibraryCacheEntry> getCacheEntry(LibraryKey &&key);
 
     static constexpr unsigned int kMaxCachedLibraries = 128;
 
@@ -87,7 +87,8 @@
     // Lock for searching and adding new entries to the cache
     std::mutex mCacheLock;
 
-    using CacheMap = angle::base::HashingMRUCache<LibraryKey, LibraryCacheEntry, LibraryKeyHasher>;
+    using CacheMap = angle::base::
+        HashingMRUCache<LibraryKey, std::shared_ptr<LibraryCacheEntry>, LibraryKeyHasher>;
     CacheMap mCache;
 };
 
diff --git a/src/libANGLE/renderer/metal/mtl_library_cache.mm b/src/libANGLE/renderer/metal/mtl_library_cache.mm
index 5fb63ea..27512a1 100644
--- a/src/libANGLE/renderer/metal/mtl_library_cache.mm
+++ b/src/libANGLE/renderer/metal/mtl_library_cache.mm
@@ -36,15 +36,15 @@
                                                  bool usesInvariance)
 {
     ASSERT(source != nullptr);
-    LibraryCache::LibraryCacheEntry &entry =
+    std::shared_ptr<LibraryCache::LibraryCacheEntry> entry =
         getCacheEntry(LibraryKey(source, macros, disableFastMath, usesInvariance));
 
     // Try to lock the entry and return the library if it exists. If we can't lock then it means
     // another thread is currently compiling.
-    std::unique_lock<std::mutex> entryLockGuard(entry.lock, std::try_to_lock);
+    std::unique_lock<std::mutex> entryLockGuard(entry->lock, std::try_to_lock);
     if (entryLockGuard)
     {
-        return entry.library;
+        return entry->library;
     }
     else
     {
@@ -69,23 +69,23 @@
     }
 
     ASSERT(source != nullptr);
-    LibraryCache::LibraryCacheEntry &entry =
+    std::shared_ptr<LibraryCache::LibraryCacheEntry> entry =
         getCacheEntry(LibraryKey(source, macros, disableFastMath, usesInvariance));
 
     // Lock this cache entry while compiling the shader. This causes other threads calling this
     // function to wait and not duplicate the compilation.
-    std::lock_guard<std::mutex> entryLockGuard(entry.lock);
-    if (entry.library)
+    std::lock_guard<std::mutex> entryLockGuard(entry->lock);
+    if (entry->library)
     {
-        return entry.library;
+        return entry->library;
     }
 
-    entry.library = CreateShaderLibrary(metalDevice, *source, macros, disableFastMath,
-                                        usesInvariance, errorOut);
-    return entry.library;
+    entry->library = CreateShaderLibrary(metalDevice, *source, macros, disableFastMath,
+                                         usesInvariance, errorOut);
+    return entry->library;
 }
 
-LibraryCache::LibraryCacheEntry &LibraryCache::getCacheEntry(LibraryKey &&key)
+std::shared_ptr<LibraryCache::LibraryCacheEntry> LibraryCache::getCacheEntry(LibraryKey &&key)
 {
     // Lock while searching or adding new items to the cache.
     std::lock_guard<std::mutex> cacheLockGuard(mCacheLock);
@@ -98,7 +98,7 @@
 
     angle::TrimCache(kMaxCachedLibraries, kGCLimit, "metal library", &mCache);
 
-    iter = mCache.Put(std::move(key), LibraryCacheEntry());
+    iter = mCache.Put(std::move(key), std::make_shared<LibraryCacheEntry>());
     return iter->second;
 }
 
Loading diff…

Original Bug Report

reported by he...@gmail.com

UAF in Metal LibraryCache

Summary

ANGLE’s Metal shader-library cache returns a LibraryCacheEntry & from LibraryCache::getCacheEntry while holding mCacheLock, but LibraryCache::getOrCompileShaderLibrary drops that cache lock before taking entry.lock. Once the cache was converted to an evicting HashingMRUCache, concurrent shader-link tasks could call TrimCache and destroy the referenced entry before the caller locks it, leading to the UAF in GPU process during Metal library compilation.

Details

CreateMslShaderLib compiles translated MSL through the display-global Metal library cache. LibraryCache::getOrCompileShaderLibrary obtains a reference to an entry and only then locks that entry’s mutex:

ASSERT(source != nullptr);
LibraryCache::LibraryCacheEntry &entry =
    getCacheEntry(LibraryKey(source, macros, disableFastMath, usesInvariance));

// Lock this cache entry while compiling the shader. This causes other threads calling this
// function to wait and not duplicate the compilation.
std::lock_guard<std::mutex> entryLockGuard(entry.lock);
if (entry.library)
{
    return entry.library;
}

entry.library = CreateShaderLibrary(metalDevice, *source, macros, disableFastMath,
                                    usesInvariance, errorOut);
return entry.library;

LibraryCache::getCacheEntry protects lookup and insertion with mCacheLock, but it returns a reference to an object stored inside the MRU container and releases the cache lock before the caller touches that reference again. The same function also trims the MRU before inserting:

    // Lock while searching or adding new items to the cache.
    std::lock_guard<std::mutex> cacheLockGuard(mCacheLock);

    auto iter = mCache.Get(key);
    if (iter != mCache.end())
    {
        return iter->second;
    }

    angle::TrimCache(kMaxCachedLibraries, kGCLimit, "metal library", &mCache);

    iter = mCache.Put(std::move(key), LibraryCacheEntry());
    return iter->second;

That means the lifetime guarantee on entry ends when mCacheLock is released. A second worker can immediately re-enter getCacheEntry, hit angle::TrimCache, and erase the least-recently-used node that the first thread still references. The destructor for LibraryCacheEntry does lock entry.lock, but that only serializes destruction with in-flight compilation if the caller already has a valid pointer to the same object. Therefore it does not prevent a caller from dereferencing after the container node itself has been freed, and thus cause the UAF.

Bisection

This issue is introduced by the commit https://chromium-review.googlesource.com/c/angle/angle/+/4614362, which introduced the MRUCache, making the attacker available to free TrimCache and cause UAF.

Reproduction

Host the poc.html using python3 -m http.server 8080

Run chromium (e.g., https://storage.googleapis.com/chromium-browser-asan/mac-release-arm64/asan-mac-release-1606755.zip) with:

./Chromium.app/Contents/MacOS/Chromium http://127.0.0.1:8080/poc.html http://127.0.0.1:8080/poc.html http://127.0.0.1:8080/poc.html http://127.0.0.1:8080/poc.html

You would observe the UAF shown in asan.txt.

Suggested Fix

The minimal fix is to store cache entries with ref-counted pointer.

View on issue tracker