CVE-2026-6310
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifsrc/dawn/native/ExecutionQueue.cpp |
modified | |
forsrc/dawn/native/ExecutionQueue.cpp |
modified |
Files Changed
src/dawn/native/ExecutionQueue.cppsrc/dawn/native/ExecutionQueue.h
Patch
From 7c11e118870577ccf42a84108fed8890c71c69e7 Mon Sep 17 00:00:00 2001 From: Lokbondo Kung <[email protected]> Date: Tue, 07 Apr 2026 19:22:22 -0700 Subject: [PATCH] [dawn][native] Check for waiting for idle before updating serials. - In the ExecutionQueue, we need to make sure to check whether a thread is waiting for idle prior to updating the completed serial. Otherwise, as the bug below points out, it's possible for the thread that's waiting for idle (which just waits for the completed serial to reach a certain value), to complete and destroy the Queue before the rest of the UpdateSerial call completes. Bug: 497969820 Change-Id: I7b9dba50f4ccb1aa8dfced122801e17db3ee4e0e Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/300595 Reviewed-by: Kai Ninomiya <[email protected]> Commit-Queue: Loko Kung <[email protected]> --- diff --git a/src/dawn/native/ExecutionQueue.cpp b/src/dawn/native/ExecutionQueue.cpp index 8be4e70..2bb5a6a 100644 --- a/src/dawn/native/ExecutionQueue.cpp +++ b/src/dawn/native/ExecutionQueue.cpp @@ -253,20 +253,21 @@ } void ExecutionQueueBase::UpdateCompletedSerialToInternal(QueuePriority priority, - ExecutionSerial completedSerial, - bool forceTasks) { + ExecutionSerial newCompletedSerial, + bool forceTasksForDestroy) { QueuePriorityArray<std::vector<Ref<SerialProcessor>>>* processors = nullptr; std::vector<Task> tasks; - // We update the completed serial as soon as possible before waiting for callback rights so - // that we almost always process as many callbacks as possible. - ExecutionSerial serial = mCompletedSerial.Use([&](auto old) { - *old = std::max(*old, static_cast<uint64_t>(completedSerial)); - return ExecutionSerial(*old); - }); - - mState.Use<NotifyType::None>([&](auto state) { - if (state->mWaitingForIdle && !forceTasks) { + // Note that we need to determine whether we are waiting for idle before updating the completed + // serial because some backends WaitForIdleForDestructionImpl may be implemented via a call to + // WaitForQueueSerial which (by default without overrides), waits on the completed serial value. + // If we updated the serial value before checking the other pieces of state, a thread destroying + // the Queue calling WaitForIdleForDestruction, could end up being woken up and destroying the + // Queue device before the rest of this function completes. By checking the state first before + // updating the serial, however, we avoid waking up the thread that's waiting for idle until we + // have completed using the queue. + bool waitingForIdle = mState.Use<NotifyType::None>([&](auto state) { + if (state->mWaitingForIdle && !forceTasksForDestroy) { // If we are waiting for idle, then the callbacks will be fired there. It is currently // necessary to avoid calling the callbacks in this function and doing it in the // |WaitForIdleForDestruction| call because |WaitForIdleForDestruction| is called while @@ -274,8 +275,11 @@ // device lock. As a result, if the main thread is waiting for idle, and another thread // is trying to update the completed serial and call callbacks, it could deadlock. Once // we update |WaitForIdleForDestruction| to release the device lock on the wait, we may - // be able to simplify the code here. - return; + // be able to simplify the code here. Note that skipping this when + // |forceTasksForDestroy| is currently ok because that branch is only called when we are + // also holding the device lock, either via a Destroy or via an error that is being + // handled. + return true; } // Wait until we can exclusively call callbacks. @@ -284,16 +288,22 @@ // Call all callbacks that for the given priority and anything of higher priority as well. processors = &state->mWaitingProcessors; for (QueuePriority p = QueuePriority::Highest; p >= priority; p -= 1) { - PopWaitingTasksInto(serial, state->mWaitingTasks[p], tasks); + PopWaitingTasksInto(newCompletedSerial, state->mWaitingTasks[p], tasks); } state->mCallingCallbacks = true; + return false; + }); + + // Update the serial now that we know whether we are waiting for idle. + mCompletedSerial.Use([&](auto completedSerial) { + *completedSerial = std::max(*completedSerial, static_cast<uint64_t>(newCompletedSerial)); }); // Always call the processors before processing individual tasks. if (processors) { for (QueuePriority p = QueuePriority::Highest; p >= priority; p -= 1) { for (auto& processor : (*processors)[p]) { - processor->UpdateCompletedSerialTo(serial); + processor->UpdateCompletedSerialTo(newCompletedSerial); } } } @@ -304,7 +314,9 @@ task(); } - mState->mCallingCallbacks = false; + if (!waitingForIdle) { + mState->mCallingCallbacks = false; + } } MaybeError ExecutionQueueBase::EnsureCommandsFlushed(ExecutionSerial serial) { diff --git a/src/dawn/native/ExecutionQueue.h b/src/dawn/native/ExecutionQueue.h index 9140f9e..c0bfd75 100644 --- a/src/dawn/native/ExecutionQueue.h +++ b/src/dawn/native/ExecutionQueue.h @@ -183,7 +183,7 @@ void UpdateCompletedSerialToInternal(QueuePriority priority, ExecutionSerial completedSerial, - bool forceTasks = false); + bool forceTasksForDestroy = false); // |mCompletedSerial| tracks the last completed command serial that the fence has returned. // |mLastSubmittedSerial| tracks the last submitted command serial.
Original Bug Report
Potential UAF in Dawn Metal completion handler via race condition during Queue destruction
Project Fortify, an experimental security project, has identified the following potential security issue.
Overview: A race condition in Dawn’s Metal backend can lead to a potential Use-After-Free (UAF) vulnerability when a Queue is destroyed. An Objective-C completion handler captures a raw this pointer to the Queue object, which can be freed by the main thread before the background handler finishes executing. A compromised renderer could potentially exploit this to achieve arbitrary code execution in the highly privileged GPU process.
Affected files:
third_party/dawn/src/dawn/native/ExecutionQueue.cppthird_party/dawn/src/dawn/native/metal/QueueMTL.mmthird_party/dawn/src/dawn/native/ExecutionQueue.h
Estimated timestamp from git blame: 2026-03-02
Vulnerability Details
A potential Use-After-Free (UAF) vulnerability exists in the Dawn GPU process when using the Metal backend on macOS.
In third_party/dawn/src/dawn/native/metal/QueueMTL.mm, the SubmitPendingCommandBuffer function registers an MTLCommandBuffer completion handler. This Objective-C block inherently captures the C++ this pointer as a raw pointer, bypassing Dawn’s Ref<T> reference counting and MiraclePtr (raw_ptr<T>) protections:
// QueueMTL.mm
[*pendingCommands addCompletedHandler:^(id<MTLCommandBuffer>) {
// `this` is captured as a raw pointer
this->UpdateCompletedSerialTo(QueuePriority::Lowest, pendingSerial);
}];
When the GPU command finishes, this block executes on a background Metal driver thread, calling ExecutionQueueBase::UpdateCompletedSerialToInternal in ExecutionQueue.cpp. This function relies on two distinct mutex-protected scopes:
mCompletedSerial.Use(...): Updates the completed serial and signals a condition variable (notify_all()).mState.Use(...): Accesses internal queue state and fires waiting callbacks/processors.
A race condition occurs because signaling the condition variable at the end of the first scope can wake up the main thread if it is currently waiting in WaitForIdleForDestruction (invoked during device teardown).
If the main thread wakes up, it can complete the destruction of the Device and Queue. It sets mQueue = nullptr, dropping the last reference to the queue, causing it to be deleted. If the background thread is preempted by the OS exactly between the two lock scopes, it will resume execution with a dangling this pointer when it attempts to enter the mState.Use(...) scope.
Suggested Steps to Trigger
(Note: These are potential steps as our tooling agent does not yet have the ability to run or verify arbitrary exploit code.)
- Setup: From a compromised renderer process, create a WebGPU
DeviceandQueue. - Submit Work: Submit a WebGPU command buffer that takes a predictable amount of time to execute.
- Trigger Teardown: Immediately drop all renderer-side references to the WebGPU device, prompting the GPU process to call
DeviceBase::WillDropLastExternalRef()and subsequentlyWaitForIdleForDestruction(). - Heap Spray: Rapidly spray the GPU process heap via other WebGPU allocations (e.g., buffer creations) sized perfectly to reclaim the
Queueobject’s memory once it is freed. - Win the Race: The background thread signals the condition variable and the OS preempts it. The main thread wakes up, frees the
Queue, and the attacker’s heap spray reclaims the memory, setting up a fakemStateobject with a forgedmWaitingProcessorsvector. - Hijack Control Flow: The background thread resumes, accesses the sprayed
mStatememory, and iterates over the fakemWaitingProcessors. It executesprocessor->UpdateCompletedSerialTo(serial);, which is a virtual function call. This dereferences the attacker-controlled vtable, leading to arbitrary Remote Code Execution (RCE) in the GPU process.
Suggested Fix
To fix this issue, the Objective-C completion handler must keep the Queue object alive for the duration of its execution, or safely check if it has been destroyed.
The most straightforward fix is to capture a strongly-referenced Ref<Queue> inside the Objective-C block instead of relying on the implicit raw this pointer:
// In QueueMTL.mm
Ref<Queue> selfRef = this;
[*pendingCommands addCompletedHandler:^(id<MTLCommandBuffer>) {
selfRef->UpdateCompletedSerialTo(QueuePriority::Lowest, pendingSerial);
}];
This ensures the Queue’s reference count remains elevated until the completion handler fully finishes executing, preventing the main thread from deleting the object prematurely.
Evaluated with Chrome root at commit: a9cbf6e8b275fe4147435aa905f3b7f5a656f5f0
Results from 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.