High patch-now ~ plausible CVSS 8.8 chrome UAF 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Dawn
DescriptionUse after free in Dawn
ComponentDawn
Sub-componentDawn
Bug ClassUAF
Action tierpatch-now — corruption primitive, web-reachable in default config (plausible) — upheld by independent review by AI
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Independent review by AIPlausible
Reviewer notes: 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.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker435875050
Fix commit1237c6eccd88 (dawn) +5/-5
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedGiunash (Gyujeong Jin)
Reported2025-08-03 · bounty $15000 (per vendor advisory)
Fix landed2025-08-20 (public git)
Disclosed2025-09-17
Patch gap28 days — fix public in source before the advisory

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.cpp runs 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, and mQueue->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

This analysis is consistent with the fixing commit but not fully provable from it: part of the vulnerable path lives in code the fix commit does not touch. Confirm the trigger and the sink against the source before relying on it for exploit development.
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.

Key insight
Graceful teardown relied on an implicit ordering between backend cleanup (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

  1. Obtain a WebGPU device A page uses WebGPU (navigator.gpu → adapter → device), which Dawn services in the GPU process.
  2. 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).
  3. Trigger device destruction The page destroys the device or lets it be torn down, which enters the graceful teardown path for a live device.
  4. Out-of-order cleanup Before the patch, AssumeCommandsComplete() and Tick() ran before the final TickImpl(), 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

The vendor rates this use-after-free in Dawn as High severity, and the metadata gives no in-the-wild exploitation status. It is reachable from web content wherever WebGPU is enabled, which is the default on several desktop platforms, though not all. The diff suggests it depends on device teardown order and possibly on the timing of outstanding GPU work, and neither a compromised renderer nor a special flag appears to be required. The error-injection hunk (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

FunctionChangeNotes
if
src/dawn/native/Device.cpp
modified

Files Changed

  • src/dawn/native/Device.cpp

Audit Directions

  • Teardown helper ordering across paths
    In 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 completion
    Audit 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` assumptions
    Review each backend’s TickImpl() (deferred deallocators, recycled command buffers, staging buffers) for references to frontend-owned objects that may already be destroyed during teardown.
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
Loading diff…

Original Bug Report

reported by wl...@gmail.com

WebGPU dawn::native::d3d12::ResourceAllocatorManager::Tick Heap-Use-After-Free

Steps to reproduce the problem

Steps to reproduce

  1. Run open_PoC.bat (launches a local HTTP server and opens chromium)
  2. Navigates to PoC.html, The flag —no-sandbox is used for ASAN output. ⇒ ./chrome.exe --no-sandbox http://localhost:8000/PoC.html
  3. 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:

  1. 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.
  2. Modify Tick() to defer freeing
    • In Tick(), instead of calling FreeSubAllocatedMemory() and ClearUpTo() immediately,

      check whether the ref-count for each allocation has reached 0.

    • Only free and clear entries when no active references remain.

  3. 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 completedSerial suggests.

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 \

View on issue tracker
Uploaded PoC files
Links in the report