CVE-2026-93374
Overview
Background
- Dawn
- Chrome’s native implementation of the WebGPU API;
src/dawn/nativebridges JavaScript WebGPU calls onto backends such as Vulkan. - Device loss
- a WebGPU
GPUDevicebecoming permanently unusable (DeviceLost), which can be triggered by driver errors, TDR, or unrecoverable validation faults. - `vkDeviceWaitIdle`/WaitIdle
- the Vulkan operation that blocks until all outstanding GPU work on a device finishes; destroying device-owned objects (
VkDevicechildren) while the GPU still references them is undefined behavior. - Device state machine
dawn::native::DeviceBaseinternalmState(Alive,BeingDisconnected,Disconnected) gating whether the app may submit new work and whether CPU/GPU resources may be torn down.
Root Cause Analysis
On a natural DeviceLost, dawn::native::DeviceBase::HandleError took an early deviceDefinitelyStopped shortcut that set mState = Disconnected immediately, treating the device as fully stopped the instant loss was observed. Because the state jumped straight to Disconnected, Dawn skipped the required drain of outstanding GPU work and proceeded to tear down VkDevice-owned objects while the Vulkan device — or an in-progress lost-device WaitIdle — could still reference them. Per the Vulkan spec, all queue and device work must be idle before device objects are destroyed, so freeing them mid-flight is a use-after-free in the GPU process.
The fix removes the shortcut and instead compare_exchanges mState from Alive to BeingDisconnected (acq_rel), so validation blocks any new submissions while the GPU timeline drains. Only after the WaitIdle completes does the device advance to Disconnected and destroy CPU-side structures, guaranteeing no object is freed while still in use. A new VulkanSleepAfterLostDeviceWait toggle (default on for the affected vendor) inserts a driver sleep after WaitIdle to paper over a driver race where WaitIdle returns before the GPU has truly quiesced.
BeingDisconnected state is essential: it blocks new work while still waiting for the GPU timeline to actually drain before any Vulkan object is destroyed.Attack Path
- Trigger device loss
malicious page issues WebGPU work that provokes a
DeviceLoston the vulnerable Vulkan vendor. - Immediate Disconnected
old
HandleErrorshortcut setsmState = Disconnected, declaring the device stopped without draining GPU work. - Premature teardown
Dawn destroys
VkDevice-owned objects while outstanding GPU work / a lost-deviceWaitIdlestill references them. - Dangling GPU reference the freed Vulkan objects are used-after-free in the GPU process as the driver races to quiesce.
- Corruption/control the attacker grooms the freed allocations to steer the UAF toward memory corruption in the GPU process.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifsrc/dawn/native/Device.cpp |
modified |
Files Changed
src/dawn/native/Device.cppsrc/dawn/native/Device.hsrc/dawn/native/Toggles.hsrc/dawn/native/vulkan/PhysicalDeviceVk.cppsrc/dawn/native/Toggles.cppsrc/dawn/native/vulkan/QueueVk.cpp
Audit Directions
- Instant-terminal state transitions on lossflag any error/loss handler that jumps a resource to a final
Disconnected/Destroyedstate before outstanding async or GPU work is confirmed drained; require an intermediate quiescing state. - Object destruction ordering vs. GPU idleaudit all backends (
Metal,D3D12) for teardown paths that destroy device-owned objects without a completedWaitIdle/fence wait, especially on the error/lost path rather than the normal path. - Driver-race workaroundsreview WaitIdle-return assumptions; where a toggle like
VulkanSleepAfterLostDeviceWaitis needed, verify the drain is actually complete rather than relying on the API return alone.
Patch
From 292e993b4a84d9a4affba538643ad376a8f36064 Mon Sep 17 00:00:00 2001
From: Albin Bernhardsson <albin.bernhardsson@arm.com>
Date: Mon, 22 Jun 2026 09:55:52 -0700
Subject: [PATCH] Vulkan: Fix device loss handling
Always wait for the device work to finish and destroy its objects before
destroying the device, even when the device is lost. This is required in
Vulkan according to spec.
Bug: 500417361
Change-Id: I89cedcd15b660a1a4e9a62099fc22e56c1c37a37
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/314796
Reviewed-by: Corentin Wallez <cwallez@chromium.org>
Reviewed-by: David Neto <dneto@google.com>
Commit-Queue: David Neto <dneto@google.com>
---
diff --git a/src/dawn/native/Device.cpp b/src/dawn/native/Device.cpp
index dc2cda7..6c73110 100644
--- a/src/dawn/native/Device.cpp
+++ b/src/dawn/native/Device.cpp
@@ -668,14 +668,8 @@
AppendDeviceLostMessage(error.get());
}
- // If ErrorInjectorEnabled() then this may be an injected DeviceLost, not a real one.
- bool deviceDefinitelyStopped = type == InternalErrorType::DeviceLost && !ErrorInjectorEnabled();
- if (deviceDefinitelyStopped) {
- // If the device was lost naturally, the backend device has already stopped.
- mState = State::Disconnected;
- }
-
InternalErrorType allowedErrors = InternalErrorType::Validation | additionalAllowedErrors;
+
if (!(allowedErrors & type)) {
// If we receive an error which we did not explicitly allow, assume the backend can't
// recover and lose the device now. Cleanup for the device will be deferred until the last
@@ -684,9 +678,14 @@
allowedErrors);
// Handle the remainder of this error as if it caused a device lost.
+ type = InternalErrorType::DeviceLost;
+ }
+
+ if (type == InternalErrorType::DeviceLost) {
+ // Transition to a non-alive state if we are currently alive so that the application can no
+ // longer use the device.
State prev = State::Alive;
mState.compare_exchange_strong(prev, State::BeingDisconnected, std::memory_order::acq_rel);
- type = InternalErrorType::DeviceLost;
}
// Re-enable validation on device loss or OOM to avoid unpredictable behaviors afterwards.
diff --git a/src/dawn/native/Device.h b/src/dawn/native/Device.h
index 5d4a5cb..1689ea2 100644
--- a/src/dawn/native/Device.h
+++ b/src/dawn/native/Device.h
@@ -344,7 +344,7 @@
// - Alive: the device is usable and might have work happening on the GPU timeline.
// - BeingDisconnected: the device is no longer usable because we are waiting for all
// work on the GPU timeline to finish. (this is to make validation prevent the
- // application from adding more work during the transition from Available to
+ // application from adding more work during the transition from Alive to
// Disconnected)
// - Disconnected: there is no longer work happening on the GPU timeline and the CPU data
// structures can be safely destroyed without additional synchronization.
diff --git a/src/dawn/native/Toggles.cpp b/src/dawn/native/Toggles.cpp
index 5ade3d6..5db3805 100644
--- a/src/dawn/native/Toggles.cpp
+++ b/src/dawn/native/Toggles.cpp
@@ -804,6 +804,10 @@
{"metal_fix_u32_div_mod",
"Workaround a driver bug on Apple Silicon with u32 div and mod operations.",
"https://crbug.com/517225032", ToggleStage::Device}},
+ {Toggle::VulkanSleepAfterLostDeviceWait,
+ {"vulkan_sleep_after_lost_device_wait",
+ "Insert an arbitrary sleep after WaitIdle functions on device loss.",
+ "https://crbug.com/500417361", ToggleStage::Device}},
{Toggle::WaitIsThreadSafe,
{"wait_is_thread_safe",
"WaitFor* functions are thread-safe and can be called without the device-lock if implicit "
diff --git a/src/dawn/native/Toggles.h b/src/dawn/native/Toggles.h
index 2d1020e..9e71492 100644
--- a/src/dawn/native/Toggles.h
+++ b/src/dawn/native/Toggles.h
@@ -191,6 +191,7 @@
VulkanForceStaticSamplersForExternalTextures,
D3D12UseHLSL2021,
MetalFixU32DivMod,
+ VulkanSleepAfterLostDeviceWait,
// Once all backends have been updated to be thread safe for waiting, we can remove this toggle.
WaitIsThreadSafe,
diff --git a/src/dawn/native/vulkan/PhysicalDeviceVk.cpp b/src/dawn/native/vulkan/PhysicalDeviceVk.cpp
index e38794b..aab9be9 100644
--- a/src/dawn/native/vulkan/PhysicalDeviceVk.cpp
+++ b/src/dawn/native/vulkan/PhysicalDeviceVk.cpp
@@ -1083,6 +1083,10 @@
// instructions as matrix elements instead of a source/dest pointee elements.
// See crbug.com/460209126
deviceToggles->Default(Toggle::VulkanCooperativeMatrixStrideIsMatrixElements, true);
+
+ // dawn:500417361
+ // TODO: Add details once available.
+ deviceToggles->Default(Toggle::VulkanSleepAfterLostDeviceWait, true);
}
if (IsAndroidSamsung() || IsAndroidQualcomm() || IsAndroidHuawei()) {
diff --git a/src/dawn/native/vulkan/QueueVk.cpp b/src/dawn/native/vulkan/QueueVk.cpp
index 36c7433..642bf58 100644
--- a/src/dawn/native/vulkan/QueueVk.cpp
+++ b/src/dawn/native/vulkan/QueueVk.cpp
@@ -49,6 +49,7 @@
#include "src/dawn/native/vulkan/VulkanError.h"
#include "src/dawn/platform/metrics/HistogramMacros.h"
#include "src/dawn/platform/tracing/TraceEvent.h"
+#include "src/dawn/utils/SystemUtils.h"
#include "src/utils/compiler.h"
namespace dawn::native::vulkan {
@@ -177,8 +178,12 @@
// Ignore the result of QueueWaitIdle: it can return OOM which we can't really do anything
// about, Device lost, which means workloads running on the GPU are no longer accessible
// (so they are as good as waited on) or success.
- [[maybe_unused]] VkResult waitIdleResult =
- VkResult::WrapUnsafe(device->fn.QueueWaitIdle(mQueue));
+ VkResult waitIdleResult = VkResult::WrapUnsafe(device->fn.QueueWaitIdle(mQueue));
+
+ if (waitIdleResult == VK_ERROR_DEVICE_LOST &&
+ GetDevice()->IsToggleEnabled(Toggle::VulkanSleepAfterLostDeviceWait)) {
+ dawn::utils::USleep(1000);
+ }
DAWN_TRY(WaitForQueueSerial(GetLastSubmittedCommandSerial(),
std::numeric_limits<Nanoseconds>::max()));