CVE-2026-9927
Overview
Background
- ANGLE Vulkan backend
- services WebGL/GLES in Chrome’s GPU process by translating GL state into Vulkan.
ContextVkapplies pending GL state to the Vulkan command buffer via “dirty bit” handlers before each draw. - Transform Feedback (XFB)
- a GLES3 feature capturing vertex-shader outputs into buffer objects between
glBeginTransformFeedback/glEndTransformFeedback. ANGLE has two code paths: a native one (VK_EXT_transform_feedback) and an emulation path used when the extension is absent butvertexPipelineStoresAndAtomicsis available. - `TransformFeedbackVk::mBufferHelpers`
- an array of raw
vk::BufferHelper*pointers into the buffers bound as XFB targets. These are plain C++ pointers and, per the report, are not nulled when XFB ends or a buffer is deleted — so they can dangle. - Dirty-bit handlers
handleDirtyGraphicsTransformFeedbackBuffersExtensionand...Emulation. The extension handler already early-returns when XFB is inactive; the emulation handler did not. ANGLE raw pointers are outside Chromium’s MiraclePtr/BackupRefPtr, so a danglingvk::BufferHelper*is a classic exploitable UAF.
Root Cause Analysis
The invariant is that XFB buffer state (and the raw vk::BufferHelper* pointers cached for it) must only be consumed while a transform-feedback session is actually active. ContextVk::handleDirtyGraphicsTransformFeedbackBuffersEmulation in src/libANGLE/renderer/vulkan/ContextVk.cpp violated this: it gated only on !executable->hasTransformFeedbackOutput(), returning early only when the bound program declares no XFB outputs. It did not check mState.isTransformFeedbackActive(). Because the transform-feedback dirty bit is re-armed after glEndTransformFeedback, this handler executes on a subsequent ordinary draw even when XFB is inactive — a program compiled with XFB varyings is enough. At that point mBufferHelpers may hold a pointer to a vk::BufferHelper whose owning BufferVk was freed by glDeleteBuffers, and the handler walks those stale pointers (via updateUniformsAndXfbDescInfo → TransformFeedbackVk::onNewDescriptorSet → vk::SharedCacheKeyManager::addKey), dereferencing freed memory.
The patch is a one-line guard in that handler: the early-return condition becomes if (!executable->hasTransformFeedbackOutput() || !mState.isTransformFeedbackActive()), mirroring the check already present in the extension variant. Once XFB is inactive the emulation handler returns immediately and never touches the (possibly dangling) mBufferHelpers entries. The added regression test TransformFeedbackTest.StaleBufferBindingInactiveXfb reproduces exactly this: draw once with XFB active, end XFB, unbind and delete the buffer, then do a normal draw with the same XFB-compiled program — which must no longer crash. Note the fix does not clear mBufferHelpers; it prevents the stale array from ever being read while inactive, so the dangling pointers are rendered unreachable rather than nulled.
glEndTransformFeedback draw dereference pointers left dangling by a deleted buffer.Attack Path
- Renderer (attacker) initialise a WebGL2 context that lands on ANGLE’s XFB emulation path (extension absent; e.g. configured/driver conditions where emulation is used).
- Arm XFB state
compile/link a program with transform-feedback varyings, create a buffer, bind it to a TF index,
glBeginTransformFeedback, draw to populatemBufferHelpers. - Deactivate + free
glEndTransformFeedback, thenglDeleteBuffersthe TF buffer. TheBufferVk/vk::BufferHelperis freed but its pointer remains inmBufferHelpers(raw pointer, not cleared). - Groom (inferred)
reclaim the freed
vk::BufferHelperslot with attacker-controlled data, forging itsmDescriptorSetCacheManager/mLastAddedSharedCacheKey. - Trigger
issue a normal
glDrawArrayswith the same XFB-compiled program; the re-armed dirty bit runshandleDirtyGraphicsTransformFeedbackBuffersEmulationwhile XFB is inactive, forcing a descriptor-set cache miss that dereferences the stale pointer. - Primitive (inferred)
SharedPtr::operator=on the forged key yields an arbitrary 32-bit decrement / arbitrarydelete/ potential virtual-call, the documented route to GPU-process RCE.
Impact Assessment
isTransformFeedbackActive() guard and that an inactive-XFB draw could reach freed buffer-helper memory (confirmed as a crash by the regression test). The escalation to the 32-bit-decrement / arbitrary-free / virtual-call primitives and RCE is (inferred) from the report — the freed-object reuse and SharedPtr manipulation are not shown by this diff. Because ANGLE’s vk::BufferHelper* pointers are excluded from MiraclePtr, the UAF is not downgraded to a safe crash. Gating: the victim program must be compiled with XFB varyings and the backend must use XFB emulation.Changed Functions
| Function | Change | Notes |
|---|---|---|
TransformFeedbackTestVkEventsrc/tests/gl_tests/TransformFeedbackTest.cpp |
modified |
Files Changed
src/libANGLE/renderer/vulkan/ContextVk.cppsrc/tests/gl_tests/TransformFeedbackTest.cpp
Audit Directions
- Stale raw pointers in `mBufferHelpers`the fix hides, rather than clears, the dangling entries. Audit whether
TransformFeedbackVk::end()/bindIndexedBuffershould null these pointers so any other consumer (observer teardown, descriptor rebuild) is also safe. - Dirty-bit paritycompare every
handleDirty*Emulationhandler against its*Extensioncounterpart for missing active-state/liveness guards; this bug was precisely such an asymmetry. - Other cached backend pointersreview ANGLE Vulkan objects caching raw pointers to buffer/image helpers across GL object deletion (e.g. vertex-array, UBO, SSBO binding caches) for the same not-cleared-on-delete pattern.
- MiraclePtr exclusionANGLE’s reliance on raw
vk::*Helper*pointers is the exploitability multiplier; flag candidates forraw_ptrmigration.
Patch
From 9a6c6d9c054cd5126b72fe68b7f8792ab614616f Mon Sep 17 00:00:00 2001
From: Shahbaz Youssefi <syoussefi@chromium.org>
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 <syoussefi@chromium.org>
Reviewed-by: Amirali Abdolrashidi <abdolrashidi@google.com>
---
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
{};
Regression Test / PoC
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
{};
Original Bug Report
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.cppthird_party/angle/src/libANGLE/renderer/vulkan/ContextVk.cppthird_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:
ContextVk::handleDirtyGraphicsTransformFeedbackBuffersEmulationcallsProgramExecutableVk::updateUniformsAndXfbDescInfo.- If a descriptor set cache miss occurs,
TransformFeedbackVk::onNewDescriptorSetis invoked. TransformFeedbackVk::onNewDescriptorSetiterates throughmBufferHelpers. Since the pointer was never nullified, it callsonNewDescriptorSeton the stale, dangling pointer.- This invokes
vk::SharedCacheKeyManager::addKeyoperating 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-specifiedmRefCountedaddress. - Arbitrary free:
SafeDelete(mRefCounted)callsdeleteon 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.
- Initialize a WebGL2 context.
- Compile and link a program with transform feedback varyings.
- Create a buffer, bind it to a transform feedback index, and start transform feedback.
- End transform feedback (
glEndTransformFeedback). - Delete the buffer (
glDeleteBuffers) to leave a dangling pointer inTransformFeedbackVk::mBufferHelpers. - Perform heap grooming to reclaim the freed memory with attacker-controlled fake
vk::BufferHelperdata. - 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
SharedPtrmanipulation.
Suggested Fix
- Ensure that
TransformFeedbackVk::mBufferHelpersare cleared inTransformFeedbackVk::end()or when a buffer is detached inTransformFeedbackVk::bindIndexedBuffer. - Add an early return to
ContextVk::handleDirtyGraphicsTransformFeedbackBuffersEmulationif transform feedback is inactive, similar to the existing logic inContextVk::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.