Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in ANGLE
DescriptionUse after free in ANGLE
ComponentANGLE
Bug ClassUAF
Tracker498804020
Fix commita9554b2e5af8 (angle/angle) +25/-16
CISA KEVNot listed
CreditedGoogle
Disclosed2026-05-27

Files Changed

  • src/libANGLE/renderer/vulkan/vk_cache_utils.cpp
  • src/libANGLE/renderer/vulkan/vk_cache_utils.h
  • src/libANGLE/renderer/vulkan/vk_helpers.cpp
  • src/libANGLE/renderer/vulkan/vk_utils.cpp
  • src/libANGLE/renderer/vulkan/vk_utils.h
From a9554b2e5af8c3a7f3ca9052dca8060e880dbffd Mon Sep 17 00:00:00 2001
From: Shahbaz Youssefi <syoussefi@chromium.org>
Date: Mon, 04 May 2026 14:14:49 -0400
Subject: [PATCH] Reland "Vulkan: Use 64-bit resource serials"

This is a reland of commit d0875fb5c481dfc471ab0a6b4911dbc50c20987a

Original change's description:
> Vulkan: Use 64-bit resource serials
>
> This makes serial wraparound impossible until the next species evolves.
> If an instance of ANGLE is running till then, it might crash.
>
> So you know, remember to restart every million years.
>
> Bug: chromium:498804020
> Change-Id: I1baf152196fed0f228f40deaa452abca19224bbf
> Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7812906
> Commit-Queue: Shahbaz Youssefi <syoussefi@chromium.org>
> Auto-Submit: Shahbaz Youssefi <syoussefi@chromium.org>
> Reviewed-by: Charlie Lao <cclao@google.com>

Bug: chromium:498804020
Change-Id: I18457e9be05b6d16c9f32776824997be73c8af27
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7818820
Commit-Queue: Shahbaz Youssefi <syoussefi@chromium.org>
Reviewed-by: Charlie Lao <cclao@google.com>
---

diff --git a/src/libANGLE/renderer/vulkan/vk_cache_utils.cpp b/src/libANGLE/renderer/vulkan/vk_cache_utils.cpp
index 5faeab1..7ac75d9 100644
--- a/src/libANGLE/renderer/vulkan/vk_cache_utils.cpp
+++ b/src/libANGLE/renderer/vulkan/vk_cache_utils.cpp
@@ -5345,6 +5345,7 @@
 
 void FramebufferDesc::reset()
 {
+    mPadding                  = 0;
     mMaxIndex                 = 0;
     mHasColorFramebufferFetch = false;
     mLayerCount               = 0;
diff --git a/src/libANGLE/renderer/vulkan/vk_cache_utils.h b/src/libANGLE/renderer/vulkan/vk_cache_utils.h
index c2f8080..1680a58 100644
--- a/src/libANGLE/renderer/vulkan/vk_cache_utils.h
+++ b/src/libANGLE/renderer/vulkan/vk_cache_utils.h
@@ -1740,6 +1740,8 @@
     return *this;
 }
 
+ANGLE_ENABLE_STRUCT_PADDING_WARNINGS
+
 struct ImageSubresourceRange
 {
     // GL max is 1000 (fits in 10 bits).
@@ -1780,8 +1782,11 @@
 {
     ImageOrBufferViewSerial viewSerial;
     ImageSubresourceRange subresource;
+    uint32_t padding;
 };
 
+static_assert(sizeof(ImageOrBufferViewSubresourceSerial) == 16, "Size check failed");
+
 inline bool operator==(const ImageOrBufferViewSubresourceSerial &a,
                        const ImageOrBufferViewSubresourceSerial &b)
 {
@@ -1789,7 +1794,7 @@
 }
 
 constexpr ImageOrBufferViewSubresourceSerial kInvalidImageOrBufferViewSubresourceSerial = {
-    kInvalidImageOrBufferViewSerial, kInvalidImageSubresourceRange};
+    kInvalidImageOrBufferViewSerial, kInvalidImageSubresourceRange, 0};
 
 // Always starts with array element zero, with descriptorCount descriptors.
 struct WriteDescriptorDesc
@@ -1804,13 +1809,15 @@
 
 struct DescriptorInfoDesc
 {
-    uint32_t samplerOrBufferSerialOrStorageFormat;
-    uint32_t imageViewSerialOrOffset;
+    uint64_t samplerOrBufferSerialOrStorageFormat;
+    uint64_t imageViewSerialOrOffset;
     uint32_t imageLayoutOrRange;
     uint32_t imageSubresourceRange;
 };
 
-static_assert(sizeof(DescriptorInfoDesc) == 16, "Size mismatch");
+static_assert(sizeof(DescriptorInfoDesc) == 24, "Size mismatch");
+
+ANGLE_DISABLE_STRUCT_PADDING_WARNINGS
 
 // Generic description of a descriptor set. Used as a key when indexing descriptor set caches. The
 // key storage is an angle:FixedVector. Beyond a certain fixed size we'll end up using heap memory
@@ -2266,11 +2273,12 @@
     // Used by SharedFramebufferCacheKey to indicate if this cache key is valid or not.
     uint16_t mIsValid : 1;
 
+    uint32_t mPadding;
+
     FramebufferAttachmentArray<ImageOrBufferViewSubresourceSerial> mSerials;
 };
 
-constexpr size_t kFramebufferDescSize = sizeof(FramebufferDesc);
-static_assert(kFramebufferDescSize == 156, "Size check failed");
+static_assert(sizeof(FramebufferDesc) == 312, "Size check failed");
 
 // Disable warnings about struct padding.
 ANGLE_DISABLE_STRUCT_PADDING_WARNINGS
@@ -2450,7 +2458,7 @@
     {                                                            \
         size_t operator()(const rx::vk::Type##Serial &key) const \
         {                                                        \
-            return key.getValue();                               \
+            return std::hash<uint64_t>()(key.getValue());        \
         }                                                        \
     };
 
diff --git a/src/libANGLE/renderer/vulkan/vk_helpers.cpp b/src/libANGLE/renderer/vulkan/vk_helpers.cpp
index a0f9149..b70b7d7 100644
--- a/src/libANGLE/renderer/vulkan/vk_helpers.cpp
+++ b/src/libANGLE/renderer/vulkan/vk_helpers.cpp
@@ -12819,7 +12819,7 @@
 {
     ASSERT(mImageViewSerial.valid());
 
-    ImageOrBufferViewSubresourceSerial serial;
+    ImageOrBufferViewSubresourceSerial serial = {};
     serial.viewSerial  = mImageViewSerial;
     serial.subresource = MakeImageSubresourceReadRange(levelGL, levelCount, layer, layerMode,
                                                        readColorspace, mWriteColorspace);
diff --git a/src/libANGLE/renderer/vulkan/vk_utils.cpp b/src/libANGLE/renderer/vulkan/vk_utils.cpp
index 5d30b62..765b3e2 100644
--- a/src/libANGLE/renderer/vulkan/vk_utils.cpp
+++ b/src/libANGLE/renderer/vulkan/vk_utils.cpp
@@ -1051,9 +1051,9 @@
 
 ResourceSerialFactory::~ResourceSerialFactory() {}
 
-uint32_t ResourceSerialFactory::issueSerial()
+uint64_t ResourceSerialFactory::issueSerial()
 {
-    uint32_t newSerial = ++mCurrentUniqueSerial;
+    uint64_t newSerial = ++mCurrentUniqueSerial;
     // make sure serial does not wrap
     ASSERT(newSerial > 0);
     return newSerial;
diff --git a/src/libANGLE/renderer/vulkan/vk_utils.h b/src/libANGLE/renderer/vulkan/vk_utils.h
index 2a561a1..9c2b533 100644
--- a/src/libANGLE/renderer/vulkan/vk_utils.h
+++ b/src/libANGLE/renderer/vulkan/vk_utils.h
@@ -1274,7 +1274,7 @@
     {                                                                         \
       public:                                                                 \
         constexpr Type##Serial() : mSerial(kInvalid) {}                       \
-        constexpr explicit Type##Serial(uint32_t serial) : mSerial(serial) {} \
+        constexpr explicit Type##Serial(uint64_t serial) : mSerial(serial) {} \
                                                                               \
         constexpr bool operator==(const Type##Serial &other) const            \
         {                                                                     \
@@ -1286,7 +1286,7 @@
             ASSERT(mSerial != kInvalid || other.mSerial != kInvalid);         \
             return mSerial != other.mSerial;                                  \
         }                                                                     \
-        constexpr uint32_t getValue() const                                   \
+        constexpr uint64_t getValue() const                                   \
         {                                                                     \
             return mSerial;                                                   \
         }                                                                     \
@@ -1296,8 +1296,8 @@
         }                                                                     \
                                                                               \
       private:                                                                \
-        uint32_t mSerial;                                                     \
-        static constexpr uint32_t kInvalid = 0;                               \
+        uint64_t mSerial;                                                     \
+        static constexpr uint64_t kInvalid = 0;                               \
     };                                                                        \
     static constexpr Type##Serial kInvalid##Type##Serial = Type##Serial();
 
@@ -1314,10 +1314,10 @@
     ANGLE_VK_SERIAL_OP(ANGLE_DECLARE_GEN_VK_SERIAL)
 
   private:
-    uint32_t issueSerial();
+    uint64_t issueSerial();
 
     // Kept atomic so it can be accessed from multiple Context threads at once.
-    std::atomic<uint32_t> mCurrentUniqueSerial;
+    std::atomic<uint64_t> mCurrentUniqueSerial;
 };
 
 #if defined(ANGLE_ENABLE_PERF_COUNTER_OUTPUT)
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential UAF in ANGLE Vulkan via 32-bit ResourceSerial wrap and stale dynamic descriptor set

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 security team.

Overview: A Use-After-Free (UAF) vulnerability exists in ANGLE’s Vulkan backend due to a 32-bit serial counter wraparound combined with skipped cache invalidation for dynamic uniform buffers. An attacker can force a 32-bit serial wraparound to trigger a descriptor set cache collision, causing the Vulkan driver to dereference a destroyed VkBuffer handle. This results in memory corruption within the highly privileged GPU process.

Affected files:

  • src/libANGLE/renderer/vulkan/vk_utils.cpp
  • src/libANGLE/renderer/vulkan/ProgramExecutableVk.cpp
  • src/libANGLE/renderer/vulkan/vk_cache_utils.cpp
  • src/libANGLE/renderer/vulkan/vk_helpers.cpp
  • src/libANGLE/renderer/vulkan/Suballocation.cpp

Estimated timestamp from git blame: 2026-02-13

Technical Details

A combination of three architectural behaviors in ANGLE’s Vulkan backend allows for a potential driver-level Use-After-Free or cross-resource memory corruption triggered from a WebGL context.

1. 32-bit Serial Wraparound (vk_utils.cpp) ResourceSerialFactory::issueSerial() increments a 32-bit atomic counter (mCurrentUniqueSerial) to generate unique IDs for resources like buffers and images. While it contains an ASSERT(newSerial > 0), this assertion compiles out in release builds. After 2^32 increments, the counter wraps, allowing new resources to be assigned serial numbers previously used by destroyed resources.

2. Intentional Skip of Cache Invalidation (ProgramExecutableVk.cpp) When using dynamic uniform buffer descriptors (VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC), ANGLE deliberately skips registering descriptor set cache entries with the buffer’s DescriptorSetCacheManager (lines 2140-2161). The code explicitly notes that maintaining this cache is too expensive, acknowledging that cached descriptor sets will not be immediately destroyed when the underlying BufferBlock is destroyed. Instead, it relies on an LRU cache that evicts entries after 10 frames of inactivity.

3. Cache Key Collision (vk_cache_utils.cpp) The DescriptorSetDesc used as a cache key identifies buffers by their 32-bit BlockSerial rather than their actual opaque Vulkan VkBuffer handle. Consequently, if a new buffer is assigned a serial number that matches a destroyed buffer, a cache hit will occur even if the underlying Vulkan handles differ.

Potential Exploit Scenario

An attacker can potentially trigger a Use-After-Free via the following steps:

  1. Prime the Cache: Create a GL buffer (Buffer A) and bind it to a shader program as a uniform buffer. This causes ANGLE to allocate a VkDescriptorSet (Set D) containing the VkBuffer handle for Buffer A and cache it using Buffer A’s serial.
  2. Destroy the Buffer: Delete Buffer A in JavaScript. ANGLE destroys the underlying VkBuffer. However, due to the intentional invalidation skip (Issue 2), Set D remains in the LRU cache, referencing the destroyed handle.
  3. Defeat LRU Eviction & Wrap Serial: The attacker initiates a tight while(true) loop across multiple Web Workers, repeatedly calling gl.bufferData with empty arrays to trigger new suballocations and serial generation.
    • By doing this in a tight loop without yielding to the browser event loop, no frame boundaries are triggered (ANGLE’s currentFrame count does not increment).
    • Because the frame count does not increase, Set D’s age never reaches the 10-frame staleness threshold (kDescriptorSetCacheRetireAge), rendering it immune to eviction.
    • The loop eventually forces the 32-bit mCurrentUniqueSerial to wrap around (approximately 4.3 billion calls).
  4. Trigger Collision: The attacker stops the loop and creates a new buffer (Buffer B). Due to the wraparound, Buffer B is assigned the same serial number previously held by Buffer A.
  5. Trigger UAF: Use Buffer B with the original shader program. The descriptor set cache returns the stale Set D because the serial-based keys perfectly match. ANGLE skips updating the descriptor set, and the Vulkan driver binds Set D, which still contains the destroyed handle for Buffer A.
  6. Result: The driver dereferences the destroyed handle during rendering. Because the handle is an opaque Vulkan type and the dereference happens at the driver level, MiraclePtr provides no protection, leading to memory corruption in the GPU process.

Note: Our tooling agent cannot run code, so these steps are suggested based on static code analysis.

Suggested Fix

Upgrade the mCurrentUniqueSerial counter in ResourceSerialFactory to a 64-bit atomic counter (std::atomic<uint64_t>). This guarantees the serial space will never wrap around during the lifetime of a renderer process, permanently preventing the cache collision.

Evaluated with Chrome root at commit: ff3d2b74fa39431785bd60e51463b08fcc71ee33


Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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