Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Dawn
DescriptionUse after free in Dawn
ComponentDawn
Bug ClassUAF
Tracker435875050
Fix commit1237c6eccd88 (dawn) +5/-5
CISA KEVNot listed
CreditedGiunash (Gyujeong Jin)
Disclosed2025-09-17

Changed Functions

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

Files Changed

  • src/dawn/native/Device.cpp
From 1237c6eccd8822df3fcd76a10be27234d4ba9918 Mon Sep 17 00:00:00 2001
From: Lokbondo Kung <[email protected]>
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 <[email protected]>
Commit-Queue: Corentin Wallez <[email protected]>
Reviewed-by: Corentin Wallez <[email protected]>
---

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 [email protected]

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
Links in the report