Overview

Critical
Severity
9.6
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Dawn
DescriptionUse after free in Dawn
ComponentDawn
Sub-componentNative
Bug ClassUAF
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable
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.
Tracker500609038
Fix commitcea524cfcd4c (dawn) +25/-10
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedAnonymous
Reported2026-04-08 · bounty $11000 (per vendor advisory)
Fix landed2026-04-10 (public git)
Disclosed2026-05-27
Patch gap47 days — fix public in source before the advisory

Background

Dawn
Chromium’s cross-platform implementation of the WebGPU API, whose native/vulkan backend translates WebGPU objects into Vulkan primitives.
`BindGroupLayout`
The Vulkan-backend object that owns one or more VkDescriptorSetLayout handles describing how shader resources are bound.
Specialization cache
The mSpecializations hash map (absl::flat_hash_map<Specialization, VkDescriptorSetLayout>) that memoizes specialized descriptor set layouts so their lifetime matches mHandle.
`MutexProtected<T>`
A Dawn wrapper that guards a value behind a mutex, exposing Use and ConstUse accessors that run a lambda while holding the lock.

Root Cause Analysis

Pipelines in Dawn may be created asynchronously, and BindGroupLayout::GetOrCreateSpecializedHandle dynamically inserts specialized VkDescriptorSetLayout entries into the unsynchronized mSpecializations map when a pipeline is initialized. Because multiple threads could enter this path concurrently, two operations could mutate the absl::flat_hash_map at the same time, or a lookup could race with a rehash triggered by an insert on another thread. This data race can corrupt the map’s internal structure or leave a reader holding a reference to an element that has been moved or freed by a concurrent rehash, producing a use-after-free.

The fix wraps the map in MutexProtected<> so every read, insert, and teardown runs under a mutex via ConstUse/Use, serializing all access. The insert path now also handles the case where another thread won the race by destroying the redundant handle and returning the already-cached one, keeping exactly one live handle per specialization.

Key insight
The single mistake was mutating and reading a shared absl::flat_hash_map from concurrent asynchronous pipeline-creation threads without synchronization; the fix serializes every access through a MutexProtected<> wrapper and reconciles double-insert races so only one VkDescriptorSetLayout survives per key.

Attack Path

  1. Trigger async pipeline creation Script calls createRenderPipelineAsync / createComputePipelineAsync many times so Dawn spins up pipeline initialization on multiple worker threads sharing the same BindGroupLayout.
  2. Race the cache Two threads reach GetOrCreateSpecializedHandle for the same layout and both call mSpecializations.insert (or one inserts while another calls find) with no lock held.
  3. Corrupt the map A concurrent insert forces the absl::flat_hash_map to rehash and relocate its storage while another thread is dereferencing an iterator or stored reference into the old, freed buckets.
  4. Use freed memory The racing reader returns or operates on a VkDescriptorSetLayout reference backed by reclaimed heap memory, giving an attacker-influenced use-after-free.

Impact Assessment

An attacker running WebGPU-enabled JavaScript in a renderer (or GPU) process can provoke a heap use-after-free in Dawn’s Vulkan backend by racing asynchronous pipeline creation, potentially leading to memory corruption and code execution in that process. The preconditions are a Vulkan-backed WebGPU device and the ability to issue many concurrent asynchronous pipeline-creation requests to win the timing window.

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

Audit Directions

  • Unsynchronized caches on async paths
    Search Dawn backends for raw absl::flat_hash_map or other containers mutated during pipeline/shader creation that can run asynchronously without a MutexProtected<> guard.
  • Iterator/reference lifetime across mutation
    Flag code that holds an iterator or reference from find while another code path may insert/clear the same container, since rehashing invalidates it.
  • Double-insert reconciliation
    Review other get-or-create handle caches to confirm they destroy the losing duplicate Vulkan object on a race instead of leaking or freeing the shared one.
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