CVE-2026-9874
Overview
Background
- Dawn
- Chromium’s cross-platform implementation of the WebGPU API, whose
native/vulkanbackend translates WebGPU objects into Vulkan primitives. - `BindGroupLayout`
- The Vulkan-backend object that owns one or more
VkDescriptorSetLayouthandles describing how shader resources are bound. - Specialization cache
- The
mSpecializationshash map (absl::flat_hash_map<Specialization, VkDescriptorSetLayout>) that memoizes specialized descriptor set layouts so their lifetime matchesmHandle. - `MutexProtected<T>`
- A Dawn wrapper that guards a value behind a mutex, exposing
UseandConstUseaccessors 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.
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
- Trigger async pipeline creation
Script calls
createRenderPipelineAsync/createComputePipelineAsyncmany times so Dawn spins up pipeline initialization on multiple worker threads sharing the sameBindGroupLayout. - Race the cache
Two threads reach
GetOrCreateSpecializedHandlefor the same layout and both callmSpecializations.insert(or one inserts while another callsfind) with no lock held. - Corrupt the map
A concurrent insert forces the
absl::flat_hash_mapto rehash and relocate its storage while another thread is dereferencing an iterator or stored reference into the old, freed buckets. - Use freed memory
The racing reader returns or operates on a
VkDescriptorSetLayoutreference backed by reclaimed heap memory, giving an attacker-influenced use-after-free.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifsrc/dawn/native/vulkan/BindGroupLayoutVk.cpp |
modified | |
forsrc/dawn/native/vulkan/BindGroupLayoutVk.cpp |
modified |
Files Changed
src/dawn/native/vulkan/BindGroupLayoutVk.cppsrc/dawn/native/vulkan/BindGroupLayoutVk.h
Audit Directions
- Unsynchronized caches on async pathsSearch Dawn backends for raw
absl::flat_hash_mapor other containers mutated during pipeline/shader creation that can run asynchronously without aMutexProtected<>guard. - Iterator/reference lifetime across mutationFlag code that holds an iterator or reference from
findwhile another code path mayinsert/clearthe same container, since rehashing invalidates it. - Double-insert reconciliationReview 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.
Patch
From cea524cfcd4c24764cc2a2dce19290ecb1031b27 Mon Sep 17 00:00:00 2001 From: Lokbondo Kung <[email protected]> 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 <[email protected]> Reviewed-by: Kyle Charbonneau <[email protected]> Commit-Queue: Kyle Charbonneau <[email protected]> --- 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.
Original Bug Report
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.
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.
- Apply
poc.patch, then build Chromium with ASan. - Host the
poc.htmlon an HTTP server. - 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
- http://localhost:8000/poc.html
- https://chromium.googlesource.com/chromium/src/+/5ba6a05e0af48d7b5a38703d0844fea105c5e1fc
- https://dawn.googlesource.com/dawn/+/66c4d93dc91932933c4bd66ff6afba1562d51259
- https://source.chromium.org/chromium/chromium/src/+/main:third_party/dawn/src/dawn/native/vulkan/BindGroupLayoutVk.cpp;drc=35a1c65f9e2f12819540d824d414ae0109bb1b10;l=263
- https://source.chromium.org/chromium/chromium/src/+/main:third_party/dawn/src/dawn/native/vulkan/BindGroupLayoutVk.cpp;drc=35a1c65f9e2f12819540d824d414ae0109bb1b10;l=264
- https://source.chromium.org/chromium/chromium/src/+/main:third_party/dawn/src/dawn/native/vulkan/BindGroupLayoutVk.cpp;drc=35a1c65f9e2f12819540d824d414ae0109bb1b10;l=285