Overview

Critical
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Dawn
DescriptionUse after free in Dawn
ComponentDawn
Bug ClassUAF
Tracker500609038
Fix commitcea524cfcd4c (dawn) +25/-10
CISA KEVNot listed
CreditedAnonymous
Disclosed2026-05-27

Changed Functions

FunctionChangeNotes
if
src/dawn/native/vulkan/BindGroupLayoutVk.cpp
modified
for
src/dawn/native/vulkan/BindGroupLayoutVk.cpp
modified

Files Changed

  • src/dawn/native/vulkan/BindGroupLayoutVk.cpp
  • src/dawn/native/vulkan/BindGroupLayoutVk.h
From cea524cfcd4c24764cc2a2dce19290ecb1031b27 Mon Sep 17 00:00:00 2001
From: Lokbondo Kung <lokokung@google.com>
Date: Fri, 10 Apr 2026 06:52:26 -0700
Subject: [PATCH] [native][vulkan] Wrap BindGroupLayout cache with a mutex.

- Because we dynamically create BGLs when initializing pipelines,
  and because pipelines may be created asynchronously, we need to
  make sure that access to the cache map doesn't mangle the data.

Bug: 500609038
Change-Id: I6ffc093773c1c8308bbc95f62da1f202622908db
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/301935
Auto-Submit: Loko Kung <lokokung@google.com>
Reviewed-by: Kyle Charbonneau <kylechar@google.com>
Commit-Queue: Kyle Charbonneau <kylechar@google.com>
---

diff --git a/src/dawn/native/vulkan/BindGroupLayoutVk.cpp b/src/dawn/native/vulkan/BindGroupLayoutVk.cpp
index cccb3f0..226e369 100644
--- a/src/dawn/native/vulkan/BindGroupLayoutVk.cpp
+++ b/src/dawn/native/vulkan/BindGroupLayoutVk.cpp
@@ -251,7 +251,7 @@
     DAWN_TRY(CheckVkSuccess(device->fn.CreateDescriptorSetLayout(device->GetVkDevice(), &createInfo,
                                                                  nullptr, &*mHandle),
                             "CreateDescriptorSetLayout"));
-    mSpecializations.insert({{}, mHandle});
+    mSpecializations->insert({{}, mHandle});
 
     SetLabelImpl();
 
@@ -260,8 +260,15 @@
 
 ResultOrError<VkDescriptorSetLayout> BindGroupLayout::GetOrCreateSpecializedHandle(
     const Specialization& specialization) {
-    if (auto it = mSpecializations.find(specialization); it != mSpecializations.end()) {
-        return it->second;
+    if (auto specialized = mSpecializations.ConstUse(
+            [&](auto specializations) -> std::optional<VkDescriptorSetLayout> {
+                if (auto it = specializations->find(specialization); it != specializations->end()) {
+                    return it->second;
+                }
+                return std::nullopt;
+            });
+        specialized) {
+        return *specialized;
     }
 
     Device* device = ToBackend(GetDevice());
@@ -282,8 +289,14 @@
                                                                  nullptr, &*specialized),
                             "CreateDescriptorSetLayout"));
 
-    mSpecializations.insert({specialization, specialized});
-    return specialized;
+    return mSpecializations.Use([&](auto specializations) -> ResultOrError<VkDescriptorSetLayout> {
+        auto [it, inserted] = specializations->insert({specialization, specialized});
+        if (!inserted) {
+            device->fn.DestroyDescriptorSetLayout(device->GetVkDevice(), specialized, nullptr);
+            return it->second;
+        }
+        return specialized;
+    });
 }
 
 void BindGroupLayout::DestroyImpl(DestroyReason reason) {
@@ -293,10 +306,12 @@
 
     // DescriptorSetLayout aren't used by execution on the GPU and can be deleted at any time,
     // so we can destroy mHandle immediately instead of using the FencedDeleter.
-    for (auto& [_, handle] : mSpecializations) {
-        device->fn.DestroyDescriptorSetLayout(device->GetVkDevice(), handle, nullptr);
-    }
-    mSpecializations.clear();
+    mSpecializations.Use([&](auto specializations) {
+        for (auto& [_, handle] : *specializations) {
+            device->fn.DestroyDescriptorSetLayout(device->GetVkDevice(), handle, nullptr);
+        }
+        specializations->clear();
+    });
 
     // Handled in the loop above already.
     mHandle = VK_NULL_HANDLE;
diff --git a/src/dawn/native/vulkan/BindGroupLayoutVk.h b/src/dawn/native/vulkan/BindGroupLayoutVk.h
index d61a813..1e164e6 100644
--- a/src/dawn/native/vulkan/BindGroupLayoutVk.h
+++ b/src/dawn/native/vulkan/BindGroupLayoutVk.h
@@ -117,7 +117,7 @@
     // Caches VkDescriptorSetLayouts for specializations so that the lifetime guarantees are the
     // same as for mHandle. Note that the noop specialization has mHandle cached directly, but
     // mHandle is also kept separate for efficiency when creating BindGroups.
-    absl::flat_hash_map<Specialization, VkDescriptorSetLayout> mSpecializations;
+    MutexProtected<absl::flat_hash_map<Specialization, VkDescriptorSetLayout>> mSpecializations;
 
     // Maps from indices of texture entries that are paired with static samplers
     // to indices of the entries of their respective samplers.
Loading diff…

Original Bug Report

reported by ki...@gmail.com

heap-use-after-free in dawn::native::vulkan::BindGroupLayout::GetOrCreateSpecializedHandle

VULNERABILITY DETAILS

WebGPU supports compiling compute pipelines asynchronously via the createComputePipelineAsync API. This spawns background task worker threads in ThreadPoolForeg to initialize the pipeline in the backend. During backend layout construction, BindGroupLayout leverages an absl::flat_hash_map named mSpecializations to cache and specialize Vulkan descriptor sets and layouts based on ExternalTexture parameters.

In BindGroupLayout::GetOrCreateSpecializedHandle, the asynchronous background thread invokes find to look up an existing layout for the given specialization [0]. However, there is no synchronization or mutex protecting concurrent accesses to this absl::flat_hash_map.

Concurrently, the front-end JS can trigger operations on the main GPU thread, such as executing queue.submit with alternating ExternalTexture bindings to continuously incur cache misses on newly allocated combinations. Because of the intentional cache misses, GetOrCreateSpecializedHandle executes on the main GPU thread and frequently inserts new items into the same hash map [1].

ResultOrError<VkDescriptorSetLayout> BindGroupLayout::GetOrCreateSpecializedHandle(
    const Specialization& specialization) {
    if (auto it = mSpecializations.find(specialization); it != mSpecializations.end()) { // [0]
        return it->second; // [2]
    }

// ...

    VkDescriptorSetLayout specialized;
    DAWN_TRY(
        CheckVkSuccess(device->fn.CreateDescriptorSetLayout(device->GetVkDevice(), &createInfo,
                                                            nullptr, &*specialized),
                       "CreateDescriptorSetLayout"));

    mSpecializations.insert({specialization, specialized}); // [1]
    return specialized;
}

This unsynchronized insert operation can intermittently trigger an internal target container reallocation rehash, which drops the backing nodes. If the worker thread performs mSpecializations.find() at the same time and attempts to dereference the iterator it->second [2], a data race ensues, leading to heap-use-after-free.

[0] https://source.chromium.org/chromium/chromium/src/+/main:third_party/dawn/src/dawn/native/vulkan/BindGroupLayoutVk.cpp;drc=35a1c65f9e2f12819540d824d414ae0109bb1b10;l=263

[1] https://source.chromium.org/chromium/chromium/src/+/main:third_party/dawn/src/dawn/native/vulkan/BindGroupLayoutVk.cpp;drc=35a1c65f9e2f12819540d824d414ae0109bb1b10;l=285

[2] https://source.chromium.org/chromium/chromium/src/+/main:third_party/dawn/src/dawn/native/vulkan/BindGroupLayoutVk.cpp;drc=35a1c65f9e2f12819540d824d414ae0109bb1b10;l=264

BISECTION

Introduced by Dawn upstream commit [0] which added runtime specialization of pipelines and the absl::flat_hash_map caching mechanism without mutex protection.

This regression was rolled into Chromium in commit [1].

[0] https://dawn.googlesource.com/dawn/+/66c4d93dc91932933c4bd66ff6afba1562d51259 ([YUV AHB] Add runtime specialization of pipelines)

[1] https://chromium.googlesource.com/chromium/src/+/5ba6a05e0af48d7b5a38703d0844fea105c5e1fc (Roll Dawn from b263aff3bd77 to 7ae353ac9aa9 (5 revisions))

VERSION

Chrome Version: HEAD

Operating System: Linux

REPRODUCTION CASE

poc.patch is a simple patch that introduces a 50ms artificial sleep in the GPU process to widen the data race window, ensuring the race is won reliably.

  1. Apply poc.patch, then build Chromium with ASan.
  2. Host the poc.html on an HTTP server.
  3. Run Chrome against the PoC.
$ python3 -m http.server
$ ./out/asan/chrome --enable-dawn-features=vulkan_force_static_samplers_for_external_textures "http://localhost:8000/poc.html"

CRASH INFORMATION

Type of crash: GPU process

Crash log: see the attached asan.txt ASan trace.

CREDIT INFORMATION

Reporter credit: Anonymous

View on issue tracker