Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Dawn
DescriptionUse after free in Dawn
ComponentDawn
Bug ClassUAF
Tracker501789156
Fix commit9f959a0d28d9 (dawn) +20/-5
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Changed Functions

FunctionChangeNotes
for
src/dawn/native/vulkan/QueueVk.cpp
modified

Files Changed

  • src/dawn/native/vulkan/QueueVk.cpp
  • src/dawn/native/vulkan/QueueVk.h
From 9f959a0d28d9da35507213936a1a3863d6f0f5d7 Mon Sep 17 00:00:00 2001
From: Brandon Jones <bajones@chromium.org>
Date: Tue, 14 Apr 2026 14:09:13 -0700
Subject: [PATCH] Vulkan: Avoid potential double-free of VkCommandPool

Ensures that the RecordingContext for a Queue is recreated
immediately after the command pool/buffers it contained are
scheduled for a reset in a SerialTask. This ensures they won't be
accidentally double-freed if the OnAfterSubmit fails for any of
the specialSyncTextures the recording context contains.

Bug: 501789156
Fixed: 501789156
Change-Id: I22a8d4ed09c0382807ae904ea4af19036a0e8773
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/302415
Reviewed-by: Loko Kung <lokokung@google.com>
Commit-Queue: Brandon Jones <bajones@chromium.org>
Auto-Submit: Brandon Jones <bajones@chromium.org>
Reviewed-by: Kai Ninomiya <kainino@chromium.org>
---

diff --git a/src/dawn/native/vulkan/QueueVk.cpp b/src/dawn/native/vulkan/QueueVk.cpp
index 512a28f..13bbdd3 100644
--- a/src/dawn/native/vulkan/QueueVk.cpp
+++ b/src/dawn/native/vulkan/QueueVk.cpp
@@ -31,6 +31,7 @@
 #include <optional>
 #include <utility>
 
+#include "absl/cleanup/cleanup.h"
 #include "dawn/common/Math.h"
 #include "dawn/native/Buffer.h"
 #include "dawn/native/CommandValidation.h"
@@ -292,6 +293,14 @@
 
     Device* device = ToBackend(GetDevice());
 
+    // Ensure that after calling this method we have a fresh recording context even if one of the
+    // DAWN_TRY calls below fail.
+    absl::Cleanup recycleContext = [&]() {
+        [[maybe_unused]] bool hadError = GetDevice()->ConsumedError(
+            RecycleRecordingContext(), "Recycling recording context after submit failed for %s",
+            this);
+    };
+
     if (!mRecordingContext.mappableBuffersForEagerTransition.empty()) {
         // Transition mappable buffers back to map usages with the submit.
         Buffer::TransitionMappableBuffersEagerly(
@@ -338,8 +347,17 @@
         device->GetFencedDeleter()->DeleteWhenUnused(semaphore);
     }
     IncrementLastSubmittedCommandSerial();
+    mFencesInFlight->emplace_back(fence, GetLastSubmittedCommandSerial());
+
+    for (auto texture : mRecordingContext.specialSyncTextures) {
+        DAWN_TRY(texture->OnAfterSubmit());
+    }
+
+    return {};
+}
+
+MaybeError Queue::RecycleRecordingContext() {
     ExecutionSerial lastSubmittedSerial = GetLastSubmittedCommandSerial();
-    mFencesInFlight->emplace_back(fence, lastSubmittedSerial);
 
     for (size_t i = 0; i < mRecordingContext.commandBufferList.size(); ++i) {
         CommandPoolAndBuffer commands = {mRecordingContext.commandPoolList[i],
@@ -360,10 +378,6 @@
         });
     }
 
-    for (auto texture : mRecordingContext.specialSyncTextures) {
-        DAWN_TRY(texture->OnAfterSubmit());
-    }
-
     mRecordingContext = CommandRecordingContext();
     DAWN_TRY(PrepareRecordingContext());
 
diff --git a/src/dawn/native/vulkan/QueueVk.h b/src/dawn/native/vulkan/QueueVk.h
index 6f8159c..96a5b64 100644
--- a/src/dawn/native/vulkan/QueueVk.h
+++ b/src/dawn/native/vulkan/QueueVk.h
@@ -69,6 +69,7 @@
     void ForceEventualFlushOfCommands() override;
     MaybeError WaitForIdleForDestructionImpl() override;
     MaybeError SubmitPendingCommandsImpl() override;
+    MaybeError RecycleRecordingContext();
     void DestroyImpl(DestroyReason reason) override;
 
     // Dawn API
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential Double-Free of VkCommandPool in Dawn Vulkan backend via error path

Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports without the Chrome Security team.

Overview: A logic error in Dawn’s Vulkan backend allows a VkCommandPool to be registered for destruction twice if an error occurs during OnAfterSubmit(). This leads to a potential double-free of driver-allocated memory, which could allow for remote code execution in the GPU process.

Affected files:

  • third_party/dawn/src/dawn/native/vulkan/QueueVk.cpp
  • third_party/dawn/src/dawn/native/ExecutionQueue.cpp
  • third_party/dawn/src/dawn/native/Device.cpp
  • third_party/dawn/src/dawn/native/vulkan/TextureVk.cpp

Estimated timestamp from git blame: 2026-03-02

Summary

A logic error in the error-handling path of dawn::native::vulkan::Queue::SubmitPendingCommandsImpl can lead to a double-free of a VkCommandPool and VkCommandBuffer handle. This occurs because an early return bypasses the reset of the recording context, causing the same Vulkan handles to be destroyed once by a scheduled cleanup task and a second time during device destruction. This vulnerability could potentially be triggered from a compromised renderer process and may lead to remote code execution in the GPU process.

Technical Details

In third_party/dawn/src/dawn/native/vulkan/QueueVk.cpp, the SubmitPendingCommandsImpl function handles the submission of recorded command buffers to the Vulkan queue. The issue unfolds as follows:

  1. Task Registration: After a successful vkQueueSubmit, the function registers a TrackSerialTask (line 347). This task captures the current VkCommandPool and VkCommandBuffer by value. When the task executes, it intends to reset the command pool and push it into mUnusedCommands for recycling. If the reset fails, it destroys the pool directly.
  2. Error Path & State Leak: The function then calls texture->OnAfterSubmit() for special sync textures (line 364). If this fails (e.g., due to file descriptor exhaustion when exporting a semaphore), the DAWN_TRY macro forces an early return. Crucially, this early return skips the code that resets mRecordingContext (line 367). As a result, mRecordingContext.used remains true, and it retains the exact Vulkan handles that were just scheduled for recycling in the task.
  3. Device Destruction Sequence: The propagated error causes the device to transition to a lost state and initiate an emergency shutdown via mQueue->WaitForIdleForDestruction().
  4. First Registration for Destruction: Inside WaitForIdleForDestructionImpl() (QueueVk.cpp:168), the code checks if mRecordingContext.used is true. Because of the state leak in step 2, it is true. The function then constructs a CommandPoolAndBuffer from the stale context and pushes it into mUnusedCommands for cleanup during shutdown.
  5. First Free: Next, the execution queue drains pending tasks, including the TrackSerialTask from step 1. Inside the task, vkResetCommandPool is called but fails because the device is now in a lost state (VK_ERROR_DEVICE_LOST). As per its error handling (QueueVk.cpp:355), the task calls DestroyCommandPoolAndBuffer, which legitimately frees the Vulkan handles.
  6. Second Free (Double-Free/UAF): Finally, during Queue::DestroyImpl (QueueVk.cpp:426), Dawn iterates over mUnusedCommands. It encounters the entry that was pushed in step 4. It calls DestroyCommandPoolAndBuffer on it, passing the exact same Vulkan handles that were already freed in step 5.

Impact

Double-destruction of a Vulkan handle is undefined behavior. In most Vulkan driver implementations, VkCommandPool represents a heap allocation, and this sequence results in a double-free in the driver’s memory allocator. Additionally, the second call to vkFreeCommandBuffers will attempt to dereference the already-freed pool handle, leading to a Use-After-Free (UAF) condition.

Because VkCommandPool handles represent driver-internal memory, this corruption occurs completely outside of the PartitionAlloc heap and is not protected by MiraclePtr. On platforms where the GPU process has elevated privileges or a weaker sandbox (such as Android), this provides a potential path for a compromised renderer to achieve a sandbox escape.

Suggested Reproduction Steps

Note: These are theoretical steps; our tooling agent does not currently run code to verify them.

  1. From a compromised renderer, create a WebGPU device using the Vulkan backend.
  2. Create or import a shared texture (e.g., via importExternalTexture or a SharedImage-backed texture) so it is managed as an ImportedTextureBase.
  3. Deliberately exhaust the file descriptor (FD) table in the GPU process (e.g., by repeatedly creating AHardwareBuffer-backed SharedImages via the GPU channel without releasing them).
  4. Submit a command buffer using the shared texture. The initial vkQueueSubmit will succeed, but the subsequent OnAfterSubmit will fail when vkGetSemaphoreFdKHR cannot allocate a new FD, triggering the early return.
  5. The device will transition to a lost state and initiate destruction, executing the double-free during cleanup.

Suggested Fix

Ensure that mRecordingContext is always properly reset, even on error paths. This could be achieved by using an RAII helper (scope guard) within SubmitPendingCommandsImpl to guarantee that mRecordingContext = CommandRecordingContext(); executes regardless of how the function exits, preventing stale handles from being picked up during destruction.

Evaluated with Chrome root at commit: 096fc8fdbfacf2546485756d03f160a3d04fcc9b


Results 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.

View on issue tracker