Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in ANGLE
DescriptionUse after free in ANGLE
ComponentANGLE
Bug ClassUAF
Tracker500540958
Fix commit9a6c6d9c054c (angle/angle) +33/-2
CISA KEVNot listed
CreditedGoogle
Disclosed2026-05-27

Changed Functions

FunctionChangeNotes
TransformFeedbackTestVkEvent
src/tests/gl_tests/TransformFeedbackTest.cpp
modified

Files Changed

  • src/libANGLE/renderer/vulkan/ContextVk.cpp
  • src/tests/gl_tests/TransformFeedbackTest.cpp
From 9a6c6d9c054cd5126b72fe68b7f8792ab614616f Mon Sep 17 00:00:00 2001
From: Shahbaz Youssefi <[email protected]>
Date: Tue, 21 Apr 2026 15:56:44 -0400
Subject: [PATCH] Vulkan: Fix XFB emulation vs deleted buffer

Bug: chromium:500540958
Change-Id: I298100cbd77b9b70af7b46f0b229a8c539aa9d52
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7783628
Commit-Queue: Shahbaz Youssefi <[email protected]>
Reviewed-by: Amirali Abdolrashidi <[email protected]>
---

diff --git a/src/libANGLE/renderer/vulkan/ContextVk.cpp b/src/libANGLE/renderer/vulkan/ContextVk.cpp
index 1fcb849..85886a6 100644
--- a/src/libANGLE/renderer/vulkan/ContextVk.cpp
+++ b/src/libANGLE/renderer/vulkan/ContextVk.cpp
@@ -2948,7 +2948,7 @@
     const gl::ProgramExecutable *executable = mState.getProgramExecutable();
     ASSERT(executable);
 
-    if (!executable->hasTransformFeedbackOutput())
+    if (!executable->hasTransformFeedbackOutput() || !mState.isTransformFeedbackActive())
     {
         return angle::Result::Continue;
     }
diff --git a/src/tests/gl_tests/TransformFeedbackTest.cpp b/src/tests/gl_tests/TransformFeedbackTest.cpp
index 4e44d8c..c7e3f13 100644
--- a/src/tests/gl_tests/TransformFeedbackTest.cpp
+++ b/src/tests/gl_tests/TransformFeedbackTest.cpp
@@ -4855,7 +4855,7 @@
 }
 
 // Test that deleting a buffer bound to a transform feedback slot that is not used by the current
-// program.
+// program doesn't crash.
 TEST_P(TransformFeedbackTest, StaleBufferBinding)
 {
     std::vector<std::string> tfVaryings = {"gl_Position"};
@@ -4891,6 +4891,37 @@
     ASSERT_GL_NO_ERROR();
 }
 
+// Test that deleting a buffer bound to a transform feedback slot that is no longer active doesn't
+// crash.
+TEST_P(TransformFeedbackTest, StaleBufferBindingInactiveXfb)
+{
+    std::vector<std::string> tfVaryings = {"gl_Position"};
+    mProgram                            = CompileProgramWithTransformFeedback(
+        essl3_shaders::vs::Simple(), essl3_shaders::fs::Red(), tfVaryings, GL_INTERLEAVED_ATTRIBS);
+    ASSERT_NE(0u, mProgram);
+    glUseProgram(mProgram);
+
+    GLBuffer buf0;
+    glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, buf0);
+    glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, 1024, nullptr, GL_DYNAMIC_COPY);
+
+    glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, buf0);
+
+    // Draw once with the buffers, syncs initial state.
+    glBeginTransformFeedback(GL_POINTS);
+    glDrawArrays(GL_POINTS, 0, 1);
+    glEndTransformFeedback();
+
+    // Unbind and delete the buffer
+    glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, 0);
+    buf0.reset();
+
+    // Regular draw while TF inactive, but still using a program that was compiled with transform
+    // feedback.  It shouldn't crash.
+    glDrawArrays(GL_POINTS, 0, 1);
+    ASSERT_GL_NO_ERROR();
+}
+
 class TransformFeedbackTestVkEvent : public TransformFeedbackTest
 {};
 
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/src/tests/gl_tests/TransformFeedbackTest.cpp b/src/tests/gl_tests/TransformFeedbackTest.cpp
index 4e44d8c..c7e3f13 100644
--- a/src/tests/gl_tests/TransformFeedbackTest.cpp
+++ b/src/tests/gl_tests/TransformFeedbackTest.cpp
@@ -4855,7 +4855,7 @@
 }
 
 // Test that deleting a buffer bound to a transform feedback slot that is not used by the current
-// program.
+// program doesn't crash.
 TEST_P(TransformFeedbackTest, StaleBufferBinding)
 {
     std::vector<std::string> tfVaryings = {"gl_Position"};
@@ -4891,6 +4891,37 @@
     ASSERT_GL_NO_ERROR();
 }
 
+// Test that deleting a buffer bound to a transform feedback slot that is no longer active doesn't
+// crash.
+TEST_P(TransformFeedbackTest, StaleBufferBindingInactiveXfb)
+{
+    std::vector<std::string> tfVaryings = {"gl_Position"};
+    mProgram                            = CompileProgramWithTransformFeedback(
+        essl3_shaders::vs::Simple(), essl3_shaders::fs::Red(), tfVaryings, GL_INTERLEAVED_ATTRIBS);
+    ASSERT_NE(0u, mProgram);
+    glUseProgram(mProgram);
+
+    GLBuffer buf0;
+    glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, buf0);
+    glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, 1024, nullptr, GL_DYNAMIC_COPY);
+
+    glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, buf0);
+
+    // Draw once with the buffers, syncs initial state.
+    glBeginTransformFeedback(GL_POINTS);
+    glDrawArrays(GL_POINTS, 0, 1);
+    glEndTransformFeedback();
+
+    // Unbind and delete the buffer
+    glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, 0);
+    buf0.reset();
+
+    // Regular draw while TF inactive, but still using a program that was compiled with transform
+    // feedback.  It shouldn't crash.
+    glDrawArrays(GL_POINTS, 0, 1);
+    ASSERT_GL_NO_ERROR();
+}
+
 class TransformFeedbackTestVkEvent : public TransformFeedbackTest
 {};
Loading diff…

Original Bug Report

reported by [email protected]

Use-After-Free in ANGLE Vulkan via stale Transform Feedback buffer pointers

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 potential Use-After-Free vulnerability exists in ANGLE’s Vulkan backend when Transform Feedback emulation is enabled. Raw pointers to vk::BufferHelper objects are cached but not cleared when transform feedback ends, leaving dangling pointers if the underlying buffer is deleted. Subsequent draw calls process dirty bits without checking if transform feedback is active, leading to dereferencing the dangling pointer and yielding powerful heap manipulation primitives in the GPU process.

Affected files:

  • third_party/angle/src/libANGLE/renderer/vulkan/TransformFeedbackVk.cpp
  • third_party/angle/src/libANGLE/renderer/vulkan/ContextVk.cpp
  • third_party/angle/src/libANGLE/renderer/vulkan/ProgramExecutableVk.cpp

Estimated timestamp from git blame: 2025-01-22

Summary

A potential Use-After-Free (UAF) vulnerability in ANGLE’s Vulkan backend can lead to a heap-based write primitive in the GPU process. The issue arises when transform feedback (XFB) emulation is used, as TransformFeedbackVk maintains stale raw pointers to buffer helpers that are not cleared when the underlying buffers are deleted. A missing state check in ContextVk allows these dangling pointers to be dereferenced during subsequent draw calls.

Technical Details

In ANGLE’s Vulkan backend, when XFB is emulated (typically on platforms lacking VK_EXT_transform_feedback but supporting vertexPipelineStoresAndAtomics), TransformFeedbackVk::initializeXFBVariables caches raw pointers to vk::BufferHelper objects in its mBufferHelpers array. These pointers are not cleared in TransformFeedbackVk::end(). Furthermore, while end() resets observer bindings (preventing ANGLE from tracking the buffer’s lifecycle), it leaves the raw pointers in the array intact.

When a buffer bound to transform feedback is subsequently deleted, the BufferVk object and its internal vk::BufferHelper are freed from the heap. At this point, mBufferHelpers contains a dangling pointer.

When a subsequent draw call is issued with a program that declares XFB outputs, ContextVk::handleDirtyGraphicsTransformFeedbackBuffersEmulation is executed due to a pending dirty bit. Critically, this function lacks the !mState.isTransformFeedbackActive() guard that is present in the extension-based variant (handleDirtyGraphicsTransformFeedbackBuffersExtension). Because the dirty bit is re-armed after XFB ends, this path executes even when transform feedback is inactive.

The execution flow proceeds as follows:

  1. ContextVk::handleDirtyGraphicsTransformFeedbackBuffersEmulation calls ProgramExecutableVk::updateUniformsAndXfbDescInfo.
  2. If a descriptor set cache miss occurs, TransformFeedbackVk::onNewDescriptorSet is invoked.
  3. TransformFeedbackVk::onNewDescriptorSet iterates through mBufferHelpers. Since the pointer was never nullified, it calls onNewDescriptorSet on the stale, dangling pointer.
  4. This invokes vk::SharedCacheKeyManager::addKey operating on the freed memory.

Exploitability

If an attacker grooms the heap to reclaim the freed BufferVk slot, they can control the vk::BufferHelper object’s data, including its mDescriptorSetCacheManager. Within this manager, the attacker can craft the mLastAddedSharedCacheKey object (a SharedPtr).

When addKey executes mLastAddedSharedCacheKey = key;, SharedPtr::operator= is called. This function reads the attacker-controlled mRefCounted pointer and calls releaseRef(). This provides three powerful exploitation primitives:

  • Arbitrary 32-bit decrement: mRefCounted->getAndReleaseRef() decrements a 32-bit integer at the attacker-specified mRefCounted address.
  • Arbitrary free: SafeDelete(mRefCounted) calls delete on the attacker-specified address.
  • Potential Execution: If the pre-decrement value is 1, mRefCounted->get().destroy(mDevice) is called, potentially yielding an arbitrary virtual method call or function pointer execution depending on the underlying object type.

Because ANGLE relies on raw C++ pointers (vk::BufferHelper*) and is excluded from Chromium’s MiraclePtr (BackupRefPtr) protections, these raw pointer manipulations directly bypass memory safety mitigations. Triggerable from WebGL2, this provides a highly exploitable Remote Code Execution (RCE) primitive within the GPU process.

Suggested Attacker Steps

Note: These are potential steps as our tooling agent cannot execute code to verify the exploit chain.

  1. Initialize a WebGL2 context.
  2. Compile and link a program with transform feedback varyings.
  3. Create a buffer, bind it to a transform feedback index, and start transform feedback.
  4. End transform feedback (glEndTransformFeedback).
  5. Delete the buffer (glDeleteBuffers) to leave a dangling pointer in TransformFeedbackVk::mBufferHelpers.
  6. Perform heap grooming to reclaim the freed memory with attacker-controlled fake vk::BufferHelper data.
  7. Execute a draw call with the same program to trigger the emulation dirty bit handler while XFB is inactive, forcing a descriptor set cache miss to trigger the UAF dereference and subsequent SharedPtr manipulation.

Suggested Fix

  1. Ensure that TransformFeedbackVk::mBufferHelpers are cleared in TransformFeedbackVk::end() or when a buffer is detached in TransformFeedbackVk::bindIndexedBuffer.
  2. Add an early return to ContextVk::handleDirtyGraphicsTransformFeedbackBuffersEmulation if transform feedback is inactive, similar to the existing logic in ContextVk::handleDirtyGraphicsTransformFeedbackBuffersExtension:
if (!executable->hasTransformFeedbackOutput() || !mState.isTransformFeedbackActive())
{
    return angle::Result::Continue;
}

Evaluated with Chrome root at commit: 137d451a126685dd5010e6609db9f6d4a78d8234


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