CVE-2026-9927
Overview
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
Patch
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 {};
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.