CVE-2025-10500
Overview
Background
- Dawn device teardown
- Dawn is Chrome’s WebGPU implementation, running in the GPU process. When a device is destroyed or lost,
src/dawn/native/Device.cppruns a set of “graceful teardown” helpers to drain GPU work and release resources before the device and its queue are destroyed. - `WaitForIdleForDestruction()`
- A queue helper that waits for already-submitted GPU commands to complete before destruction continues; its errors are ignored through
IgnoreErrors(...). - `AssumeCommandsComplete()` / `Tick(serial)`
AssumeCommandsComplete()forces the queue’s completed serial to equal the last submitted serial, andmQueue->Tick(mQueue->GetCompletedCommandSerial())then runs the frontend cleanup and callbacks for everything up to that serial.- `TickImpl()`
- The backend-specific (Vulkan, D3D, Metal and so on) per-tick hook that “cleans up resources” according to the in-code comment.
Root Cause Analysis
Verify before building a PoC
What the commit does not establish: Says the commit attributes the use-after-free to this ordering; the commit message only says it standardizes teardown order and defers reasoning to a bug comment. The freed-object mechanism is inferred, not shown in the diff.
The device teardown code in Device.cpp (the hunks do not name the enclosing functions) called the graceful teardown helpers in an inconsistent order. On the normal destruction path for a live device, it first waited for idle, then called mQueue->AssumeCommandsComplete() and mQueue->Tick(...) to retire everything up to the forced serial, and only afterwards called TickImpl() for one last backend cleanup. It then called AssumeCommandsComplete() a second time right before mQueue->Destroy().
The patch moves the final IgnoreErrors(TickImpl()) up so that it runs straight after WaitForIdleForDestruction() in the live-device case, before the forced-complete Tick. It also removes the second AssumeCommandsComplete() before mQueue->Destroy(), and adds a matching TickImpl() after WaitForIdleForDestruction() on the error-injection device-lost path. The commit attributes the use-after-free to this ordering, and its detailed reasoning is in a bug comment not included here. Our reading is that running the backend TickImpl() after the frontend had already forced completion and ticked could make backend cleanup act on objects the frontend tick had already released or finalised (inferred). Calling every teardown path as wait-for-idle, then TickImpl(), then force-complete and Tick keeps backend cleanup ahead of the frontend’s final release.
TickImpl()) and the frontend’s forced-completion Tick, and different paths broke it, which left a window for use of freed objects. The fix puts backend cleanup immediately after the idle wait on every path, before the queue is forced complete and destroyed.Attack Path
- Obtain a WebGPU device
A page uses WebGPU (
navigator.gpu→ adapter → device), which Dawn services in the GPU process. - Leave work or resources outstanding The page submits GPU work or creates resources whose release depends on queue serials and backend ticking (inferred; the diff does not show which object is affected).
- Trigger device destruction The page destroys the device or lets it be torn down, which enters the graceful teardown path for a live device.
- Out-of-order cleanup
Before the patch,
AssumeCommandsComplete()andTick()ran before the finalTickImpl(), so backend cleanup could touch objects already released by the frontend tick. That is a use-after-free in the GPU process (inferred mechanism).
Impact Assessment
ErrorInjectorEnabled()) only affects test and fuzzing builds. Memory corruption in the GPU process could be leveraged for code execution there and as a step toward a sandbox escape (inferred), but the patch demonstrates neither.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifsrc/dawn/native/Device.cpp |
modified |
Files Changed
src/dawn/native/Device.cpp
Audit Directions
- Teardown helper ordering across pathsIn Dawn backends, compare every path that ends a device (
Destroy, device loss,HandleError, adapter or instance shutdown) and check that wait-for-idle, backend tick, forced serial completion and object release always happen in the same order. - Forced serial completionAudit callers of
AssumeCommandsComplete()and of any code that advances completed serials artificially, since anything freed “at serial N” becomes eligible at once and may still be referenced by backend-pending work. - Backend `TickImpl` assumptionsReview each backend’s
TickImpl()(deferred deallocators, recycled command buffers, staging buffers) for references to frontend-owned objects that may already be destroyed during teardown.
Patch
From 1237c6eccd8822df3fcd76a10be27234d4ba9918 Mon Sep 17 00:00:00 2001
From: Lokbondo Kung <lokokung@google.com>
Date: Wed, 20 Aug 2025 05:01:25 -0700
Subject: [PATCH] [dawn][native] Standardize calling order for graceful device teardown.
- Updates all the code-paths to call the graceful device teardown
helpers in the same order.
- See https://g-issues.chromium.org/issues/435875050#comment13 for
breakdown of the logic for ordering it in this manner.
Bug: 435875050
Change-Id: Ife7dfcb2c333f9b9bbc2895b683974e96119e73f
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/258175
Auto-Submit: Loko Kung <lokokung@google.com>
Commit-Queue: Corentin Wallez <cwallez@chromium.org>
Reviewed-by: Corentin Wallez <cwallez@chromium.org>
---
diff --git a/src/dawn/native/Device.cpp b/src/dawn/native/Device.cpp
index dd0fe08..dc78fee 100644
--- a/src/dawn/native/Device.cpp
+++ b/src/dawn/native/Device.cpp
@@ -638,6 +638,10 @@
// complete before proceeding with destruction.
// Ignore errors so that we can continue with destruction
IgnoreErrors(mQueue->WaitForIdleForDestruction());
+
+ // Call TickImpl once last time to clean up resources
+ // Ignore errors so that we can continue with destruction
+ IgnoreErrors(TickImpl());
break;
case State::BeingDisconnected:
@@ -661,10 +665,6 @@
mQueue->AssumeCommandsComplete();
DAWN_ASSERT(mQueue->GetCompletedCommandSerial() == mQueue->GetLastSubmittedCommandSerial());
mQueue->Tick(mQueue->GetCompletedCommandSerial());
-
- // Call TickImpl once last time to clean up resources
- // Ignore errors so that we can continue with destruction
- IgnoreErrors(TickImpl());
}
// At this point GPU operations are always finished, so we are in the disconnected state.
@@ -682,7 +682,6 @@
// Note: mQueue is not released here since the application may still get it after calling
// Destroy() via APIGetQueue.
if (mQueue != nullptr) {
- mQueue->AssumeCommandsComplete();
mQueue->Destroy();
}
@@ -728,6 +727,7 @@
// Disconnected so we can detect this case in WaitForIdleForDestruction.
if (ErrorInjectorEnabled()) {
IgnoreErrors(mQueue->WaitForIdleForDestruction());
+ IgnoreErrors(TickImpl());
}
// A real device lost happened. Set the state to disconnected as the device cannot be
Original Bug Report
WebGPU dawn::native::d3d12::ResourceAllocatorManager::Tick Heap-Use-After-Free
Steps to reproduce the problem
Steps to reproduce
- Run open_PoC.bat (launches a local HTTP server and opens chromium)
- Navigates to PoC.html, The flag —no-sandbox is used for ASAN output. ⇒
./chrome.exe --no-sandbox http://localhost:8000/PoC.html - After a few reload cycles, the GPU process crashes. On ASan builds, you will see a heap-use-after-free
open_PoC.bat
@echo off
cd /d %~dp0
start "" python -m http.server 8000
timeout /t 2 >nul
start "" http://localhost:8000/PoC.html
Problem Description
Description
This issue is a heap-use-after-free bug in the Dawn D3D12 backend of Chromium/WebGPU, observed only on Windows systems without a discrete GPU (e.g., integrated GPU or GPU-less notebook environments), triggered by resource lifetime mismanagement during asynchronous GPU command execution.
Impact
When specific WebGPU operations are performed (creating large depth/stencil textures, creating incompatible texture views, and issuing copyTextureToBuffer with intentionally misaligned parameters), the GPU process consistently crashes.
Root Cause
The root cause lies in third_party/dawn/src/dawn/native/d3d12/ResourceAllocatorManagerD3D12.cpp:
void ResourceAllocatorManager::Tick(ExecutionSerial completedSerial) {
for (ResourceHeapAllocation& allocation :
mAllocationsToDelete.IterateUpTo(completedSerial)) {
if (allocation.GetInfo().mMethod == AllocationMethod::kSubAllocated) {
FreeSubAllocatedMemory(allocation);
}
}
mAllocationsToDelete.ClearUpTo(completedSerial);
mHeapsToDelete.ClearUpTo(completedSerial);
}
Tick() is called with a completedSerial that Dawn believes corresponds to all finished GPU work. However, in certain timing conditions, commands that still reference these Heap objects may not yet be fully retired on the GPU. This allows the manager to clear and free heap resources prematurely.
Later, when ResidencyManager::EnsureHeapsAreResident() iterates over those heap pointers to lock residency before executing a new command list, it dereferences stale pointers to already-freed memory. This results in a reproducible use-after-free read (and potentially write) condition.
mitigation suggestion
Use-after-free can be mitigated by introducing a reference counting mechanism for D3D12 Heaps:
retain the Heap while any command list or resource still references it, and only free when the ref-count drops to zero after GPU work completion.
How to Fix
The root cause is that ResourceAllocatorManager::Tick() may free ResourceHeapAllocation objects that are still referenced by in-flight GPU commands.
To fix this, introdue reference counting (or strong ownership tracking) for heap allocations instead of relying solely on completedSerial.
Key steps:
- Add a ref-count to ResourceHeapAllocation / Heap objects
- Increment the ref-count when a command list or texture/buffer references a heap.
- Decrement the ref-count only after the GPU has signaled completion of the work using that heap.
- Modify Tick() to defer freeing
-
In
Tick(), instead of callingFreeSubAllocatedMemory()andClearUpTo()immediately,check whether the ref-count for each
allocationhas reached 0. -
Only free and clear entries when no active references remain.
-
- Validation
- Optionally add debug assertions (in ASan/Debug builds) to ensure that no heap is accessed after free.
- This prevents use-after-free bugs when commands complete later than
completedSerialsuggests.
Pseudo-code adjustment:
void ResourceAllocatorManager::Tick(ExecutionSerial completedSerial) {
for (ResourceHeapAllocation& allocation :
mAllocationsToDelete.IterateUpTo(completedSerial)) {
if (allocation.refCount == 0 &&
allocation.GetInfo().mMethod == AllocationMethod::kSubAllocated) {
FreeSubAllocatedMemory(allocation);
} else {
// Still in use: defer deletion
DeferAllocation(allocation);
}
}
// Clear only those heaps that are confirmed unused
mAllocationsToDelete.RemoveIf([](auto& alloc) { return alloc.refCount == 0; });
mHeapsToDelete.RemoveIf([](auto& heap) { return heap.refCount == 0; });
}
This ensures that heap objects will not be freed prematurely, eliminating the race that leads to the use-after-free.
Summary
WebGPU dawn::native::d3d12::ResourceAllocatorManager::Tick Heap-Use-After-Free
Custom Questions
Type of crash:
see ASan
Crash state:
ASan stack trace
Heap-use-after-free (read) detected by ASan.
READ of size 4 at 0x12a6a8aec854 thread T0
#0 0x7ff8e93af829 in dawn::native::d3d12::Pageable::IsResidencyLocked(void) const C:\b\s\w\ir\cache\builder\src\third_party\dawn\src\dawn\native\d3d12\PageableD3D12.cpp:86:12
#1 0x7ff8e93c8159 in dawn::native::d3d12::ResidencyManager::EnsureHeapsAreResident(class dawn::native::d3d12::Heap **, unsigned __int64) C:\b\s\w\ir\cache\builder\src\third_party\dawn\src\dawn\native\d3d12\ResidencyManagerD3D12.cpp:263:19
#2 0x7ff8e9398acf in dawn::native::d3d12::CommandRecordingContext::ExecuteCommandList(class dawn::native::d3d12::Device *, struct ID3D12CommandQueue *) C:\b\s\w\ir\cache\builder\src\third_party\dawn\src\dawn\native\d3d12\CommandRecordingContext.cpp:77:5
#3 0x7ff8e93bee55 in dawn::native::d3d12::Queue::SubmitPendingCommandsImpl(void) C:\b\s\w\ir\cache\builder\src\third_party\dawn\src\dawn\native\d3d12\QueueD3D12.cpp:133:5
#4 0x7ff8e9163d98 in dawn::native::ExecutionQueueBase::SubmitPendingCommands(void) C:\b\s\w\ir\cache\builder\src\third_party\dawn\src\dawn\native\ExecutionQueue.cpp:112:19
#5 0x7ff8e93a4938 in dawn::native::d3d12::Device::TickImpl(void) C:\b\s\w\ir\cache\builder\src\third_party\dawn\src\dawn\native\d3d12\DeviceD3D12.cpp:366:5
#6 0x7ff8e90c37d9 in dawn::native::DeviceBase::Destroy(void) C:\b\s\w\ir\cache\builder\src\third_party\dawn\src\dawn\native\Device.cpp:665:22
#7 0x7ff8e8fa2258 in dawn::native::NativeDeviceDestroy(struct WGPUDeviceImpl *) C:\b\s\w\ir\cache\builder\src\out\069a-Win_ASan_Releas\gen\third_party\dawn\src\dawn\native\ProcTable.cpp:921:15
...
freed by thread T0 here:
#0 0x7ff96150c584 (C:\Users\wlsrb\Desktop\bugs\chromium-140.0.7317.0-win64-asan\clang_rt.asan_dynamic-x86_64.dll+0x18005c584)
#1 0x7ff8e93af1ce in dawn::native::d3d12::Heap::`scalar deleting dtor'(unsigned int) C:\b\s\w\ir\cache\builder\src\third_party\dawn\src\dawn\native\d3d12\HeapD3D12.h:42:7
#2 0x7ff8e6f83124 in std::__Cr::default_delete<perfetto::internal::TracingMuxerImpl::ConsumerImpl>::operator() C:\b\s\w\ir\cache\builder\src\third_party\libc++\src\include\__memory\unique_ptr.h:77
#3 0x7ff8e6f83124 in std::__Cr::unique_ptr<perfetto::internal::TracingMuxerImpl::ConsumerImpl,std::__Cr::default_delete<perfetto::internal::TracingMuxerImpl::ConsumerImpl> >::reset C:\b\s\w\ir\cache\builder\src\third_party\libc++\src\include\__memory\unique_ptr.h:290
Reporter credit:
Giunash (Gyujeong Jin) of BoB 14th
Additional Data
Category: Security
Chrome Channel: Dev
Regression: N/A \