Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in ANGLE
DescriptionUse after free in ANGLE
ComponentANGLE
Bug ClassUAF
Tracker502812366
Fix commit052c3437f454 (angle/angle) +108/-129
CISA KEVNot listed
CreditedGoogle
Disclosed2026-05-27

Changed Functions

FunctionChangeNotes
ContextVk
src/libANGLE/renderer/vulkan/ContextVk.h
modified

Files Changed

  • src/libANGLE/renderer/vulkan/ContextVk.cpp
  • src/libANGLE/renderer/vulkan/ContextVk.h
  • src/libANGLE/renderer/vulkan/VertexArrayVk.cpp
From 052c3437f454f97bfd3d7d959e25eef336413cb3 Mon Sep 17 00:00:00 2001
From: Charlie Lao <[email protected]>
Date: Thu, 14 May 2026 14:39:03 -0700
Subject: [PATCH] Vukan: Don't update default attributes for unused attributes

This change is follow up of https://crrev.com/c/7822999. It optimizes
how default vertex attributes (the constant values used when an
attribute is disabled) are handled in the Vulkan backend . The primary
goal is to avoid unnecessary updates for attributes that are not
actually used by the current program.

The attributes can come from two different sources: If vertex array
attribute is enabled, it should come from vertex array. Otherwise it
should come from context. And we want store these values in one array so
that we can send it down to driver without extra data copy. Right now we
store this mCurrentArray*** in VertexArrayVk, which causes a bug that
when a different vertex array is bound, the default attributes is not
being updated and may have a stale pointer that causes UAF, which
https://crrev.com/c/7822999 fixed. The other possible solution was to
store mCurrentArray*** in ContextVk, but after prototyping I noticed
that it is no simpler since now you have to deal with update vertex
array's attributes for every VAO bind, even though it made default
attribute easier. This CL focused on keeping mCurrentArray*** in
VertexArrayVk, but try to minimize the default attributes update
associated with previous fix.

Key Changes: Before this CL,
ContextVk::handleDirtyGraphicsDefaultAttribs will check if VAO has
changed, if yes, it calls updateDefaultAttrib for *all* attributes that
are disabled. That will allocate memory from ContextVk::
mStreamedVertexBuffers (per attribute DynamicBuffer). Since most of
attributes are unused, this is a bit waste. This CL try to avoid that by
detecting the exact default attribute that has stale buffer pointer and
set it to mEmptyBuffer. To make this possible, VertexArrayVk now
maintains mCurrentDefaultAttribsMask to track which attributes are
currently set up as default/constant attributes. When VAO binding
changes ( gl::state::DIRTY_BIT_VERTEX_ARRAY_BINDING), the new VAO's
mCurrentDefaultAttribsMask is split into two:
context->getActiveDefaultAttribsMask() is going to be updated by
ContextVk::handleDirtyGraphicsDefaultAttribs(). The remaining bits is
now explicitly set to mEmptyBuffer by calling
VertexArrayVk::syncDirtyDisabledAttribs().

The code previously used mDefaultAttribsGeneration to track when default
attribute buffers needed to be refreshed globally. This has been removed
with this explicit tracking of which attributes are using default
attributes.

Bug: chromium:502812366
Change-Id: Ic25da400a81c202bfc6e250a79b2d0b19ecc7986
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7851199
Reviewed-by: Yuxin Hu <[email protected]>
Commit-Queue: Charlie Lao <[email protected]>
Reviewed-by: Shahbaz Youssefi <[email protected]>
---

diff --git a/src/libANGLE/renderer/vulkan/ContextVk.cpp b/src/libANGLE/renderer/vulkan/ContextVk.cpp
index 8b267c0..1657315 100644
--- a/src/libANGLE/renderer/vulkan/ContextVk.cpp
+++ b/src/libANGLE/renderer/vulkan/ContextVk.cpp
@@ -828,7 +828,6 @@
       mFlipViewportForDrawFramebuffer(false),
       mFlipViewportForReadFramebuffer(false),
       mIsAnyHostVisibleBufferWritten(false),
-      mDefaultAttribsGeneration(mDefaultAttribsGenerationFactory.generate()),
       mImageWithTileMemory(nullptr),
       mCurrentQueueSerialIndex(kInvalidQueueSerialIndex),
       mInitialContextPriority(renderer->getDriverPriority(GetContextPriority(state))),
@@ -2140,25 +2139,11 @@
 {
     VertexArrayVk *vertexArrayVk = getVertexArray();
 
-    gl::AttributesMask attribsMask;
-    if (vertexArrayVk->getDefaultAttribsGeneration() != mDefaultAttribsGeneration)
-    {
-        vertexArrayVk->setDefaultAttribsGeneration(mDefaultAttribsGeneration);
-        attribsMask = ~vertexArrayVk->getCurrentEnabledAttribsMask();
-    }
-    else
-    {
-        attribsMask = mDirtyDefaultAttribsMask;
-        attribsMask &= ~vertexArrayVk->getCurrentEnabledAttribsMask();
-        attribsMask &= mState.getProgramExecutable()->getAttributesMask();
-    }
+    mDirtyDefaultAttribsMask &= ~vertexArrayVk->getCurrentEnabledAttribsMask();
+    mDirtyDefaultAttribsMask &= mState.getProgramExecutable()->getAttributesMask();
 
-    for (size_t attribIndex : attribsMask)
-    {
-        ANGLE_TRY(vertexArrayVk->updateDefaultAttrib(this, attribIndex));
-    }
-
-    ANGLE_TRY(onVertexArrayChange(attribsMask));
+    ANGLE_TRY(vertexArrayVk->updateDefaultAttribs(this, mDirtyDefaultAttribsMask));
+    ANGLE_TRY(onVertexArrayChange(mDirtyDefaultAttribsMask));
 
     mDirtyDefaultAttribsMask.reset();
     return angle::Result::Continue;
@@ -5769,10 +5754,10 @@
                 break;
             case gl::state::DIRTY_BIT_VERTEX_ARRAY_BINDING:
             {
-                if (vertexArrayVk->getDefaultAttribsGeneration() != mDefaultAttribsGeneration)
-                {
-                    mGraphicsDirtyBits.set(DIRTY_BIT_DEFAULT_ATTRIBS);
-                }
+                gl::AttributesMask staleDefaultAttribsMask =
+                    vertexArrayVk->getCurrentDefaultAttribsMask() &
+                    ~programExecutable->getAttributesMask();
+                vertexArrayVk->syncDirtyDisabledAttribs(this, staleDefaultAttribsMask);
                 invalidateDefaultAttributes(context->getActiveDefaultAttribsMask());
                 ANGLE_TRY(onVertexArrayChange(vertexArrayVk->getCurrentEnabledAttribsMask()));
                 ANGLE_TRY(onIndexBufferChange(vertexArrayVk->getCurrentElementArrayBuffer()));
@@ -7782,12 +7767,6 @@
     return angle::Result::Continue;
 }
 
-void ContextVk::invalidateDefaultAttribute(size_t attribIndex)
-{
-    mDirtyDefaultAttribsMask.set(attribIndex);
-    mGraphicsDirtyBits.set(DIRTY_BIT_DEFAULT_ATTRIBS);
-}
-
 void ContextVk::invalidateDefaultAttributes(const gl::AttributesMask &dirtyMask)
 {
     if (dirtyMask.any())
diff --git a/src/libANGLE/renderer/vulkan/ContextVk.h b/src/libANGLE/renderer/vulkan/ContextVk.h
index 63a682b..b9f41a5 100644
--- a/src/libANGLE/renderer/vulkan/ContextVk.h
+++ b/src/libANGLE/renderer/vulkan/ContextVk.h
@@ -91,8 +91,6 @@
 class ContextVk : public ContextImpl, public vk::Context, public MultisampleTextureInitializer
 {
   public:
-    using VertexArrayGeneration = UniqueSerial;
-
     ContextVk(const gl::State &state, gl::ErrorSet *errorSet, vk::Renderer *renderer);
     ~ContextVk() override;
 
@@ -429,7 +427,6 @@
 
     angle::Result onVertexArrayChange(const gl::AttributesMask dirtyAttribBits);
 
-    void invalidateDefaultAttribute(size_t attribIndex);
     void invalidateDefaultAttributes(const gl::AttributesMask &dirtyMask);
     angle::Result onFramebufferChange(FramebufferVk *framebufferVk, gl::Command command);
     void onDrawFramebufferRenderPassDescChange(FramebufferVk *framebufferVk,
@@ -802,8 +799,6 @@
         if (newBufferOut)
         {
             mHasInFlightStreamedVertexBuffers.set(attribIndex);
-            mDefaultAttribsGeneration = mDefaultAttribsGenerationFactory.generate();
-            mGraphicsDirtyBits.set(DIRTY_BIT_DEFAULT_ATTRIBS);
         }
         return angle::Result::Continue;
     }
@@ -1577,10 +1572,6 @@
     // "Current Value" aka default vertex attribute state.
     gl::AttributesMask mDirtyDefaultAttribsMask;
 
-    // Tracks if default vertex attribute buffers have been invalidated.
-    UniqueSerialFactory mDefaultAttribsGenerationFactory;
-    VertexArrayGeneration mDefaultAttribsGeneration;
-
     // DynamicBuffers for streaming vertex data from client memory pointer as well as for default
     // attributes. mHasInFlightStreamedVertexBuffers indicates if the dynamic buffer has any
     // in-flight buffer or not that we need to release at submission time.
diff --git a/src/libANGLE/renderer/vulkan/VertexArrayVk.cpp b/src/libANGLE/renderer/vulkan/VertexArrayVk.cpp
index 723062c..5a1162a 100644
--- a/src/libANGLE/renderer/vulkan/VertexArrayVk.cpp
+++ b/src/libANGLE/renderer/vulkan/VertexArrayVk.cpp
@@ -948,6 +948,9 @@
     vk::Renderer *renderer = contextVk->getRenderer();
     contextVk->getPerfCounters().vertexArraySyncStateCalls++;
 
+    gl::AttributesMask prevEnabledAttribsMask = mCurrentEnabledAttribsMask;
+    mCurrentEnabledAttribsMask                = mState.getEnabledAttributesMask();
+
     const std::vector<gl::VertexAttribute> &attribs = mState.getVertexAttributes();
     const std::vector<gl::VertexBinding> &bindings  = mState.getVertexBindings();
 
@@ -1016,9 +1019,9 @@
     for (size_t attribIndex : enabledNonStreamAttribDirtyBits)
     {
         // This will also update mNeedsConversionAttribMask
-        ANGLE_TRY(syncDirtyEnabledNonStreamingAttrib(contextVk, attribs[attribIndex],
-                                                     bindings[attribs[attribIndex].bindingIndex],
-                                                     attribIndex, (*attribBits)[attribIndex]));
+        syncDirtyEnabledNonStreamingAttrib(contextVk, attribs[attribIndex],
+                                           bindings[attribs[attribIndex].bindingIndex], attribIndex,
+                                           (*attribBits)[attribIndex]);
     }
 
     // Sync all enabled and streaming attributes that are dirty
@@ -1027,9 +1030,9 @@
     for (size_t attribIndex : enabledStreamAttribDirtyBits)
     {
         // This will also update mNeedsConversionAttribMask
-        ANGLE_TRY(syncDirtyEnabledStreamingAttrib(contextVk, attribs[attribIndex],
-                                                  bindings[attribs[attribIndex].bindingIndex],
Loading diff…

Original Bug Report

reported by [email protected]

Potential Heap-Use-After-Free in ANGLE Vulkan VertexArrayVk

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 go/chrome-ai-generated-security-bugs-faq for more information.

Overview: A potential heap-use-after-free exists in ANGLE’s Vulkan backend due to dangling raw pointers in VertexArrayVk. When a DynamicBuffer resizes, old BufferHelper objects are deleted, but stale pointers remain cached in mCurrentArrayBuffers. Subsequent draw calls can dereference these pointers, providing an arbitrary memory increment primitive and potential driver-level exploitation.

Affected files:

  • third_party/angle/src/libANGLE/renderer/vulkan/VertexArrayVk.cpp
  • third_party/angle/src/libANGLE/renderer/vulkan/vk_helpers.cpp
  • third_party/angle/src/libANGLE/renderer/vulkan/ContextVk.cpp
  • third_party/angle/src/libANGLE/renderer/vulkan/VertexArrayVk.h

Estimated timestamp from git blame: 2025-09-22

Summary

A potential heap-use-after-free (UAF) vulnerability has been identified in the ANGLE Vulkan backend. The issue originates from how default vertex attributes are cached. When a program uses an attribute that is disabled on the bound Vertex Array Object (VAO), ANGLE allocates a BufferHelper for the default value and stores a raw pointer to it in VertexArrayVk::mCurrentArrayBuffers. If the underlying DynamicBuffer is later forced to resize, the old BufferHelper objects are deleted from the heap during a command flush. However, the VertexArrayVk cache is not updated. A subsequent draw call can dereference this dangling pointer, allowing an attacker who grooms the heap to forge a BufferHelper object and achieve arbitrary code execution in the unsandboxed GPU process.

Technical Details

  1. Allocation: During ContextVk::setupDraw, if a program uses a disabled attribute A, VertexArrayVk::updateDefaultAttrib is called. This allocates memory via DynamicBuffer::allocate, returning a raw BufferHelper* which is cached in VertexArrayVk::mCurrentArrayBuffers[A].
  2. The Free: If the attacker forces the DynamicBuffer for attribute A to require a larger block size (e.g., via a large instanced draw call), the old BufferHelper is moved to an in-flight list. Upon a Vulkan command flush (ContextVk::flushAndSubmitCommands), DynamicBuffer::updateQueueSerialAndReleaseInFlightBuffers detects the size mismatch and clears the list. This destroys the std::unique_ptr holding the BufferHelper, calling operator delete on the heap object. The raw pointer in mCurrentArrayBuffers[A] is now dangling.
  3. The Use: The attacker issues a new draw call using a different program that does not use attribute A, but uses an attribute B where B > A. Because A is not active, updateDefaultAttrib is skipped, leaving the dangling pointer in place. However, ContextVk calls mRenderPassCommands->buffersVertexAttribRead(..., maxAttrib). Since the new program’s maxAttrib is B + 1, the loop iterates over index A and dereferences the stale pointer, calling buffer->recordReadBarrier(...).

Impact and Exploitability

By grooming the heap, an attacker can replace the freed BufferHelper (approx 488 bytes) with controlled data. When recordReadBarrier is called on the forged object, it reads a forged mCurrentWriteEvent handle.

  1. Arbitrary Increment: The forged handle is passed to eventCollector->emplace_back, invoking RefCountedEvent’s copy constructor, which calls mHandle->addRef(). This performs an atomic 32-bit increment (mRefCount++) at the attacker-supplied memory address.
  2. Driver-Level Execution: Later, when the event collector is cleared, the forged event’s destructor calls vkDestroyEvent(device, mHandle->event, nullptr). Passing an attacker-controlled handle to the Vulkan driver can allow the attacker to free arbitrary driver memory or hijack execution, leading to full Remote Code Execution (RCE) in the GPU process. Note that the GPU process is unsandboxed on Android.

mCurrentArrayBuffers is a standard std::array of raw pointers, so it is not protected by MiraclePtr/BRP.

Potential Attacker Steps (Suggested)

Note: Our tooling agent does not have the ability to run code, these are suggested steps based on static analysis.

  1. Create a WebGL2 context and a VAO.
  2. Compile Program 1 (uses attribute 0) and Program 2 (uses attribute 1).
  3. Disable attribute 0 (glDisableVertexAttribArray(0)).
  4. Call glUseProgram(Program 1) and glDrawArrays(...) to cache a default BufferHelper pointer for attribute 0.
  5. Force a resize of the DynamicBuffer for attribute 0 (e.g., enable it, set a divisor, and draw a massive number of instances).
  6. Trigger a flush (e.g., glReadPixels) to free the old BufferHelper object.
  7. Groom the heap with controlled data to forge the BufferHelper structure at the freed location.
  8. Call glUseProgram(Program 2) and glDrawArrays(...) to trigger the UAF and execute the exploit primitives.

Suggested Fix

Replace the raw pointers in gl::AttribArray<vk::BufferHelper *> mCurrentArrayBuffers with a safer mechanism. Options include:

  • Using BackupRefPtr (e.g., base::raw_ptr<vk::BufferHelper>) to immediately mitigate the exploitability by turning the UAF into a safe crash.
  • Having VertexArrayVk observe the destruction of BufferHelper objects or strictly clearing mCurrentArrayBuffers for indices that are not active in the current program during setupDraw.

Evaluated with Chrome root at commit: c0eb5541aebfa4ea08806eaf6e94bcc69f87ab2f


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