CVE-2026-79149
Overview
Files Changed
src/libANGLE/renderer/vulkan/vk_helpers.cppsrc/libANGLE/renderer/vulkan/vk_helpers.hsrc/tests/gl_tests/VulkanDescriptorSetTest.cpp
Patch
From f56a8e4f5f2852b9f58bbe1a9e79414b5246640c Mon Sep 17 00:00:00 2001 From: Charlie Lao <[email protected]> Date: Tue, 21 Jul 2026 15:17:29 -0700 Subject: [PATCH] Vulkan: Fix UAF in DescriptorPoolHelper via unsafe WeakPtr upgrade When context runs into error during submission, we may bail out early, leaving some DescriptorSetHelpers has unsubmitted ResourceUse. This cause potential UAF later on when context is destroyed. This CL prevents such UAF possibility by force finish DescriptorPoolHelper::mPendingGarbageList. This should have no behavior change during normal context runs where submission went through successfully. Bug: b/536636648 Change-Id: I608ed2e61796aefc86d1a806472cfd70c2b8c719 Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/8127746 Commit-Queue: Charlie Lao <[email protected]> Reviewed-by: Shahbaz Youssefi <[email protected]> Reviewed-by: Yuxin Hu <[email protected]> --- diff --git a/src/libANGLE/renderer/vulkan/vk_helpers.cpp b/src/libANGLE/renderer/vulkan/vk_helpers.cpp index c80590c..afb5746 100644 --- a/src/libANGLE/renderer/vulkan/vk_helpers.cpp +++ b/src/libANGLE/renderer/vulkan/vk_helpers.cpp @@ -3678,6 +3678,15 @@ } } +void DescriptorPoolHelper::forceFinishPendingGarbage() +{ + while (!mPendingGarbageList.empty()) + { + mFinishedGarbageList.push_back(std::move(mPendingGarbageList.front())); + mPendingGarbageList.pop_front(); + } +} + bool DescriptorPoolHelper::recycleFromGarbage(Renderer *renderer, DescriptorSetPointer *descriptorSetOut) { @@ -3800,7 +3809,11 @@ for (DescriptorPoolPointer &pool : mDescriptorPools) { - pool->cleanupPendingGarbage(); + // Usually all pending garbage should have been finished when DynamicDescriptorPool is + // destroyed. But when context runs into error and submit code path early out, we could have + // unfinished garbage in the pending list. So force all pending garbage to be cleared rather + // than being left in place until the pool itself is deleted. + pool->forceFinishPendingGarbage(); pool->destroyGarbage(); ASSERT(pool.unique()); } diff --git a/src/libANGLE/renderer/vulkan/vk_helpers.h b/src/libANGLE/renderer/vulkan/vk_helpers.h index b92f33d..fce9a22 100644 --- a/src/libANGLE/renderer/vulkan/vk_helpers.h +++ b/src/libANGLE/renderer/vulkan/vk_helpers.h @@ -296,6 +296,7 @@ bool recycleFromGarbage(Renderer *renderer, DescriptorSetPointer *descriptorSetOut); void destroyGarbage(); void cleanupPendingGarbage(); + void forceFinishPendingGarbage(); bool hasValidDescriptorSet() const { return mValidDescriptorSets != 0; } bool canDestroy() const { return mValidDescriptorSets == 0 && mPendingGarbageList.empty(); } diff --git a/src/tests/gl_tests/VulkanDescriptorSetTest.cpp b/src/tests/gl_tests/VulkanDescriptorSetTest.cpp index 72a57ca..af9adaf 100644 --- a/src/tests/gl_tests/VulkanDescriptorSetTest.cpp +++ b/src/tests/gl_tests/VulkanDescriptorSetTest.cpp @@ -208,8 +208,43 @@ mDescriptorSetLayoutCache.destroy(contextVk->getRenderer()); } +// Verify that DynamicDescriptorPool::destroy tears down cleanly even when descriptor sets that +// were returned to the pool are still tagged with an outstanding queue serial. +TEST_P(VulkanDescriptorSetLayoutDescTest, DestroyWithPendingGarbage) +{ + rx::ContextVk *contextVk = hackANGLE(); + + rx::vk::DescriptorSetLayoutDesc desc; + addBindings({0}, &desc); + + rx::vk::DescriptorSetLayoutPtr descriptorSetLayout; + angle::Result result = + mDescriptorSetLayoutCache.getDescriptorSetLayout(contextVk, desc, &descriptorSetLayout); + ASSERT_EQ(result, angle::Result::Continue); + + VkDescriptorPoolSize poolSize = {VK_DESCRIPTOR_TYPE_SAMPLER, 1}; + rx::vk::DynamicDescriptorPool dynamicPool; + result = dynamicPool.init(contextVk, &poolSize, 1, *descriptorSetLayout); + ASSERT_EQ(result, angle::Result::Continue); + + rx::vk::DescriptorSetPointer descriptorSet; + result = dynamicPool.allocateDescriptorSet(contextVk, *descriptorSetLayout, &descriptorSet); + ASSERT_EQ(result, angle::Result::Continue); + ASSERT_TRUE(descriptorSet); + + // Tag the descriptor set with a queue serial that has not been reached so that it stays on the + // pool's pending garbage list once released. + descriptorSet->setQueueSerial(rx::QueueSerial(0, rx::Serial::Infinite())); + descriptorSet.reset(); + + dynamicPool.destroy(contextVk->getDevice()); + + descriptorSetLayout.reset(); + mDescriptorSetLayoutCache.destroy(contextVk->getRenderer()); +} + ANGLE_INSTANTIATE_TEST(VulkanDescriptorSetTest, ES31_VULKAN(), ES31_VULKAN_SWIFTSHADER()); -ANGLE_INSTANTIATE_TEST(VulkanDescriptorSetLayoutDescTest, ES31_VULKAN()); +ANGLE_INSTANTIATE_TEST(VulkanDescriptorSetLayoutDescTest, ES31_VULKAN(), ES31_VULKAN_SWIFTSHADER()); GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(VulkanDescriptorSetLayoutDescTest); } // namespace
Regression Test / PoC
diff --git a/src/tests/gl_tests/VulkanDescriptorSetTest.cpp b/src/tests/gl_tests/VulkanDescriptorSetTest.cpp
index 72a57ca..af9adaf 100644
--- a/src/tests/gl_tests/VulkanDescriptorSetTest.cpp
+++ b/src/tests/gl_tests/VulkanDescriptorSetTest.cpp
@@ -208,8 +208,43 @@
mDescriptorSetLayoutCache.destroy(contextVk->getRenderer());
}
+// Verify that DynamicDescriptorPool::destroy tears down cleanly even when descriptor sets that
+// were returned to the pool are still tagged with an outstanding queue serial.
+TEST_P(VulkanDescriptorSetLayoutDescTest, DestroyWithPendingGarbage)
+{
+ rx::ContextVk *contextVk = hackANGLE();
+
+ rx::vk::DescriptorSetLayoutDesc desc;
+ addBindings({0}, &desc);
+
+ rx::vk::DescriptorSetLayoutPtr descriptorSetLayout;
+ angle::Result result =
+ mDescriptorSetLayoutCache.getDescriptorSetLayout(contextVk, desc, &descriptorSetLayout);
+ ASSERT_EQ(result, angle::Result::Continue);
+
+ VkDescriptorPoolSize poolSize = {VK_DESCRIPTOR_TYPE_SAMPLER, 1};
+ rx::vk::DynamicDescriptorPool dynamicPool;
+ result = dynamicPool.init(contextVk, &poolSize, 1, *descriptorSetLayout);
+ ASSERT_EQ(result, angle::Result::Continue);
+
+ rx::vk::DescriptorSetPointer descriptorSet;
+ result = dynamicPool.allocateDescriptorSet(contextVk, *descriptorSetLayout, &descriptorSet);
+ ASSERT_EQ(result, angle::Result::Continue);
+ ASSERT_TRUE(descriptorSet);
+
+ // Tag the descriptor set with a queue serial that has not been reached so that it stays on the
+ // pool's pending garbage list once released.
+ descriptorSet->setQueueSerial(rx::QueueSerial(0, rx::Serial::Infinite()));
+ descriptorSet.reset();
+
+ dynamicPool.destroy(contextVk->getDevice());
+
+ descriptorSetLayout.reset();
+ mDescriptorSetLayoutCache.destroy(contextVk->getRenderer());
+}
+
ANGLE_INSTANTIATE_TEST(VulkanDescriptorSetTest, ES31_VULKAN(), ES31_VULKAN_SWIFTSHADER());
-ANGLE_INSTANTIATE_TEST(VulkanDescriptorSetLayoutDescTest, ES31_VULKAN());
+ANGLE_INSTANTIATE_TEST(VulkanDescriptorSetLayoutDescTest, ES31_VULKAN(), ES31_VULKAN_SWIFTSHADER());
GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(VulkanDescriptorSetLayoutDescTest);
} // namespace
Original Bug Report
Potential GPU-HOST UAF/double-free in ANGLE DescriptorPoolHelper via unsafe WeakPtr upgrade
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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A potential re-entrant double-free vulnerability exists in the ANGLE Vulkan backend’s descriptor set pool management. When a context is destroyed following a failed command submission, an unsafe WeakPtr-to-SharedPtr upgrade resurrects a parent allocation mid-delete. This results in a Use-After-Free write and a double-free on the host heap within the GPU process.
Affected files:
third_party/angle/src/libANGLE/renderer/vulkan/vk_helpers.cppthird_party/angle/src/libANGLE/renderer/vulkan/vk_utils.h
Estimated timestamp from git blame: 2024-12-02
1. Summary of the Issue (Meant for Human Triage)
A potential severe memory-safety vulnerability exists in the ANGLE Vulkan backend due to unsafe object lifetime management and the lack of release-build validation checks when upgrading weak pointers. Specifically, DescriptorSetHelper::destroy() upgrades a vk::WeakPtr<DescriptorPoolHelper> to a SharedPtr to return descriptor sets to the pool’s garbage list. In release builds, this upgrade lacks checks to verify if the underlying RefCounted object is currently being destroyed or has already been freed, as the assertIsReferenced() check compiles to a no-op.
Under certain conditions—such as context destruction following a failed vkQueueSubmit—a DynamicDescriptorPool will be cleared while still holding undrained pending garbage. This initiates a recursive destruction sequence: deleting the DescriptorPoolHelper calls the destructor on its std::deque of garbage items, which invokes DescriptorSetHelper::destroy(). This function resurrects the mid-destruction RefCounted<DescriptorPoolHelper> allocation (re-incrementing the refcount from 0 to 1), mutates the std::deque under active destruction, and subsequently invokes delete on the same RefCounted object a second time when the temporary SharedPtr goes out of scope.
The result is a re-entrant host-heap double-free / Use-After-Free (UAF) in the GPU process. Because ANGLE does not utilize MiraclePtr (BackupRefPtr) protection for these ref-counted structures, this heap corruption is exploitable. On platforms like Android where the GPU process is unsandboxed, this could lead to a browser-tier compromise.
Note: These steps are based on exhaustive static analysis of the codebase. Our tooling agent does not yet have the ability to run code to produce a live proof-of-concept.
2. Proof-of-Concept & Detailed Execution Flow
The potential vulnerability is triggered through the following sequence of events an attacker might control:
Step 1: Allocating Descriptor Sets with GPU Conversion
A WebGL page issues draw calls that require vertex format conversion on the GPU (e.g., using gl.vertexAttribPointer with non-normalized gl.SHORT values).
VertexArrayVk::convertVertexBufferGPU->UtilsVk::allocateDescriptorSetWithLayout(third_party/angle/src/libANGLE/renderer/vulkan/UtilsVk.cpp:5189).commandBufferHelper->retainResource(descriptorSet.get())stamps aDescriptorSetHelperwith the current queue serial (e.g.,N).- The local
DescriptorSetPointergoes out of scope, callingDescriptorSetHelper::destroy(), which places it into themPendingGarbageListof the pool viaemplace_back.
Step 2: Inducing a Vulkan Submission Failure The attacker induces a GPU hang (via an expensive fragment shader) or exhausts Vulkan device memory, causing the next command buffer submission to fail.
Step 3: Incomplete Serial Tracking on Submit Failure
During the context destruction (ContextVk::onDestroy in third_party/angle/src/libANGLE/renderer/vulkan/ContextVk.cpp:1250), finishImpl() attempts to submit pending command buffers.
vkQueueSubmitfails and returnsVK_ERROR_DEVICE_LOSTorVK_ERROR_OUT_OF_DEVICE_MEMORYinsideCommandQueue::queueSubmitLocked(CommandQueue.cpp:1024).- Wrapped in
ANGLE_VK_TRY, the error propagates immediately, skippingpushInFlightBatchLocked()atCommandQueue.cpp:1029. - The batch serial
Nnever entersmInFlightCommands. WhenhandleDeviceLost()executes, it only advancesmLastCompletedSerialsfrom the existingmInFlightCommandsqueue. SerialNis left uncompleted.
Step 4: Failure to Clear Pending Garbage on Teardown
During teardown of DynamicDescriptorPool (vk_helpers.cpp:3793-3798):
for (DescriptorPoolPointer &pool : mDescriptorPools)
{
pool->cleanupPendingGarbage();
pool->destroyGarbage();
ASSERT(pool.unique()); // Debug only
}
mDescriptorPools.clear();
pool->cleanupPendingGarbage()(vk_helpers.cpp:3659-3671) halts immediately becausemRenderer->hasResourceUseFinished(garbage->getResourceUse())evaluates to false for serialN.pool->destroyGarbage()only flushes elements inmFinishedGarbageList.- The pending list remains fully intact.
mDescriptorPools.clear()is called, releasing the lastSharedPtr<DescriptorPoolHelper>.
Step 5: Re-entrant Destruction and Re-referencing
mDescriptorPools.clear()triggers~SharedPtr()->releaseRef()->SafeDelete(mRefCounted)->delete mRefCounted->~DescriptorPoolHelper->~std::deque<DescriptorSetPointer> (mPendingGarbageList).- While destructing the elements of
mPendingGarbageList,~SharedPtr<DescriptorSetHelper>runs, invokingDescriptorSetHelper::destroy(device)(vk_helpers.cpp:3560-3573):
void DescriptorSetHelper::destroy(VkDevice device)
{
if (valid())
{
DescriptorPoolPointer pool(device, mPool); // WeakPtr -> SharedPtr Upgrade!
DescriptorSetPointer garbage(device, std::move(*this));
pool->addPendingGarbage(std::move(garbage)); // Corrupts std::deque mid-destruction
ASSERT(!valid());
}
}
- The constructor for upgrading
WeakPtrtoSharedPtr(vk_utils.h:854-864) runs:
SharedPtr(VkDevice device, const WeakPtr<T> &other)
: mRefCounted(other.mRefCounted), mDevice(device)
{
if (mRefCounted)
{
mRefCounted->assertIsReferenced(); // Maps to ASSERT(mRefCount != 0)
mRefCounted->addRef(); // Resurrects mid-delete object!
}
}
- Critical Flaw: In Release builds,
ASSERTexpands toANGLE_EAT_STREAM_PARAMETERS << !(condition). BecauseANGLE_EAT_STREAM_PARAMETERSuses the compile-time constant ternary operatortrue ? static_cast<void>(0) : ...(third_party/angle/src/common/log_utils.h:260), theassertIsReferenced()check compiles out to zero instructions. The refcount is unconditionally incremented from0to1on a heap allocation currently executing its owndeleteoperator. pool->addPendingGarbage(...)invokesemplace_backon themPendingGarbageList(std::deque), which is actively executing its own~deque(), causing a UAF write.- Finally, when the local
poolobject goes out of scope at the end ofDescriptorSetHelper::destroy(),~SharedPtr()drops the refcount back to0, executingSafeDelete(mRefCounted)for a second time. This nesteddeleteon the sameRefCountedobject triggers a catastrophic double-free.
Suggested Fix:
- Modify
DynamicDescriptorPool::destroyto unconditionally force-drain and clearmPendingGarbageListwhen tearing down, regardless of whetherhasResourceUseFinishedreturns true (since the context is being destroyed). - Alternatively, advance
mLastCompletedSerialsfor dropped batches duringvkQueueSubmitfailures so that standard garbage collection processes them. - Revisit the ANGLE
WeakPtrimplementation to include a distinct weak reference count or a validity boolean rather than relying entirely on a debug-onlyASSERTto detect dead objects.
3. Technical Verification Details (Automated Audit Logs - Reviewers may skip this section)
> The vulnerability is a valid GPU-HOST-UAF (double-free) in the ANGLE Vulkan backend. The root cause is that DescriptorSetHelper::destroy() resurrects a DescriptorPoolHelper object during its own destruction by upgrading a WeakPtr to a SharedPtr (which lacks a runtime validity check in release builds). This path is reachable if a WebGL page issues a draw that requires format conversion and subsequently induces a vkQueueSubmit failure (e.g., via GPU hang or memory exhaustion). The failed batch’s queue serial is never pushed to mInFlightCommands, so handleDeviceLost() never marks it as completed. During ContextVk::onDestroy, the mPendingGarbageList fails to drain. When mDescriptorPools.clear() triggers delete on the pool’s RefCounted object, the undrained std::deque elements are destructed, calling DescriptorSetHelper::destroy(). This resurrects the mid-delete pool, calls emplace_back on the std::deque currently in its destructor (a heap UAF write), and upon the SharedPtr going out of scope, calls delete a second time on the same RefCounted object (double-free).
>
> ANGLE does not use MiraclePtr (BRP) for these ref-counted structures, so the UAF is unprotected. The Vulkan backend is default on Android, where the GPU process is UNSANDBOXED, meaning a memory corruption is treated as browser-tier. Per the severity guidelines, a GPU-process write primitive reachable by web content alone (A-SERVER) on an unsandboxed platform warrants Critical (S0). However, because triggering this requires inducing a device-lost or OOM condition and exploiting the issue during the teardown of the lost device before the GPU process is recycled, a -1 downgrade is applied for trigger complexity (similar to ‘Requiring browser shutdown’). Thus, the final severity is High (S1).
Automated Codebase Analysis Confirmations:
vkQueueSubmitError Handling: Codebase investigator tracedCommandQueue::queueSubmitLocked(CommandQueue.cpp:953-1035). ThevkQueueSubmitcall is wrapped inANGLE_VK_TRY(vk_utils.h:1775-1784), which early-returnsangle::Result::Stopon failure. The subsequentpushInFlightBatchLocked(line 1029) is permanently skipped.- Garbage Collection Halt: Codebase investigator traced
DescriptorPoolHelper::cleanupPendingGarbage(vk_helpers.cpp:3664). It breaks the loop early ifmRenderer->hasResourceUseFinished(garbage->getResourceUse())is false, leaving the pending deque populated. - ASSERT compilation: Codebase investigator verified
log_utils.h:260-271. WhenANGLE_ENABLE_ASSERTSis disabled (Release config),ASSERTmaps toANGLE_EAT_STREAM_PARAMETERS, which utilizestrue ? static_cast<void>(0) : .... The compiler completely optimizes away the condition. Therefore,mRefCounted->assertIsReferenced()inside theWeakPtr->SharedPtrconstructor (vk_utils.h:854) is definitively a no-op, permitting the 0->1 reference count increment on a deleted object. - MiraclePtr Status: ANGLE’s
vk::WeakPtrandvk::SharedPtrinternally utilize rawRefCountedStorage *mRefCountedandVkDevice mDevicefields, notraw_ptr. They fall outside the scope of PartitionAlloc’s BackupRefPtr protection.
Evaluated with Chrome root at commit: b96d2ec58f4f5f92b540a723966b199d6e9951b4
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
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.