CVE-2026-7901
Overview
Files Changed
src/libANGLE/renderer/metal/mtl_library_cache.hsrc/libANGLE/renderer/metal/mtl_library_cache.mm
Patch
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;
}
Original Bug Report
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.
- http://127.0.0.1:8080/poc.html
- https://chromium-review.googlesource.com/c/angle/angle/+/4614362
- https://source.chromium.org/chromium/chromium/src/+/main:third_party/angle/src/libANGLE/renderer/metal/ProgramExecutableMtl.mm;l=349
- https://source.chromium.org/chromium/chromium/src/+/main:third_party/angle/src/libANGLE/renderer/metal/mtl_library_cache.mm;l=135
- https://source.chromium.org/chromium/chromium/src/+/main:third_party/angle/src/libANGLE/renderer/metal/mtl_library_cache.mm;l=55
- https://source.chromium.org/chromium/chromium/src/+/main:third_party/angle/src/libANGLE/renderer/metal/mtl_library_cache.mm;l=88
- https://source.chromium.org/chromium/chromium/src/+/main:third_party/angle/src/libANGLE/renderer/metal/mtl_library_cache.mm;l=99
- https://storage.googleapis.com/chromium-browser-asan/mac-release-arm64/asan-mac-release-1606755.zip