Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in Dawn
DescriptionInappropriate implementation in Dawn
ComponentDawn
Bug ClassLogic Error
Tracker500162791
Fix commitb2741fb026b4 (dawn) +142/-24
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Changed Functions

FunctionChangeNotes
if
src/dawn/native/vulkan/TextureVk.cpp
modified

Files Changed

  • src/dawn/native/vulkan/BindGroupVk.cpp
  • src/dawn/native/vulkan/ResourceTableVk.cpp
  • src/dawn/native/vulkan/TextureVk.cpp
  • src/dawn/native/vulkan/TextureVk.h
From b2741fb026b4652c0eec1198467e1969cf32c7f3 Mon Sep 17 00:00:00 2001
From: Brandon Jones <[email protected]>
Date: Thu, 23 Apr 2026 16:31:28 -0700
Subject: [PATCH] Vulkan: Handle non-depth/stencil multi-bit layout

Updates the VulkanImageLayout method and descriptor set creation
to detect and handle when multiple usages of a texture are needed.
This was already handled for depth/stencil textures but did not
take into account that a texture can be used as a Sampled texture
and a read-only storage texture at the same time, at which point
the appropriate Layout is VK_IMAGE_LAYOUT_GENERAL.

Bug: 500162791
Fixed: 500162791
Change-Id: Ie2ef32b4694b3336e9206b13432cb83704f25bf1
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/303495
Reviewed-by: Kai Ninomiya <[email protected]>
Reviewed-by: Corentin Wallez <[email protected]>
Commit-Queue: Brandon Jones <[email protected]>
---

diff --git a/src/dawn/native/vulkan/BindGroupVk.cpp b/src/dawn/native/vulkan/BindGroupVk.cpp
index cfcebfd..9cb316f 100644
--- a/src/dawn/native/vulkan/BindGroupVk.cpp
+++ b/src/dawn/native/vulkan/BindGroupVk.cpp
@@ -185,7 +185,7 @@
 
             writeImageInfo[writeIndex].imageView = handle;
             writeImageInfo[writeIndex].imageLayout =
-                VulkanImageLayout(view->GetFormat(), wgpu::TextureUsage::TextureBinding);
+                view->VulkanImageLayout(wgpu::TextureUsage::TextureBinding);
             write->pImageInfo = &writeImageInfo[writeIndex];
         });
     }
diff --git a/src/dawn/native/vulkan/ResourceTableVk.cpp b/src/dawn/native/vulkan/ResourceTableVk.cpp
index c9a1688..884a469 100644
--- a/src/dawn/native/vulkan/ResourceTableVk.cpp
+++ b/src/dawn/native/vulkan/ResourceTableVk.cpp
@@ -286,8 +286,8 @@
                 VkDescriptorImageInfo imageWrite = {
                     .sampler = ToBackend(unusedSampler)->GetHandle(),
                     .imageView = handle,
-                    .imageLayout = VulkanImageLayout(textureView->GetFormat(),
-                                                     wgpu::TextureUsage::TextureBinding),
+                    .imageLayout = ToBackend(textureView)
+                                       ->VulkanImageLayout(wgpu::TextureUsage::TextureBinding),
                 };
                 imageWrites.push_back(imageWrite);
                 arrayElements.push_back(uint32_t{diff.slot});
diff --git a/src/dawn/native/vulkan/TextureVk.cpp b/src/dawn/native/vulkan/TextureVk.cpp
index a6023d4..2cc076c 100644
--- a/src/dawn/native/vulkan/TextureVk.cpp
+++ b/src/dawn/native/vulkan/TextureVk.cpp
@@ -323,8 +323,8 @@
     barrier.pNext = nullptr;
     barrier.srcAccessMask = VulkanAccessFlags(lastUsage, format);
     barrier.dstAccessMask = VulkanAccessFlags(usage, format);
-    barrier.oldLayout = VulkanImageLayout(format, lastUsage);
-    barrier.newLayout = VulkanImageLayout(format, usage);
+    barrier.oldLayout = texture->VulkanImageLayout(lastUsage);
+    barrier.newLayout = texture->VulkanImageLayout(usage);
     barrier.image = texture->GetHandle();
     barrier.subresourceRange.aspectMask = VulkanAspectMask(range.aspects);
     barrier.subresourceRange.baseMipLevel = range.baseMipLevel;
@@ -719,26 +719,46 @@
 // Chooses which Vulkan image layout should be used for the given Dawn usage. Note that this
 // layout must match the layout given to various Vulkan operations as well as the layout given
 // to descriptor set writes.
-VkImageLayout VulkanImageLayout(const Format& format, wgpu::TextureUsage usage) {
+VkImageLayout VulkanImageLayout(const Format& format,
+                                wgpu::TextureUsage usage,
+                                wgpu::TextureUsage allowedUsage = wgpu::TextureUsage::None) {
     if (usage == wgpu::TextureUsage::None) {
         return VK_IMAGE_LAYOUT_UNDEFINED;
     }
 
-    if (!wgpu::HasZeroOrOneBits(usage)) {
-        // sampled | (some sort of readonly depth-stencil aspect) is the only possible multi-bit
-        // usage, if more appear we will need additional special-casing.
-        DAWN_ASSERT(IsSubset(
-            usage, wgpu::TextureUsage::TextureBinding | kDepthReadOnlyStencilWritableAttachment |
-                       kDepthWritableStencilReadOnlyAttachment | kReadOnlyRenderAttachment));
+    if (allowedUsage == wgpu::TextureUsage::None) {
+        allowedUsage = usage;
+    }
 
-        if (IsSubset(kDepthReadOnlyStencilWritableAttachment, usage)) {
-            return VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_STENCIL_ATTACHMENT_OPTIMAL;
-        } else if (IsSubset(kDepthWritableStencilReadOnlyAttachment, usage)) {
-            return VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_STENCIL_READ_ONLY_OPTIMAL;
+    if (!wgpu::HasZeroOrOneBits(usage)) {
+        // (sampled | (some sort of readonly depth-stencil aspect)) or
+        // (sampled | readonly storage) are the only possible multi-bit usages,
+        // if more appear we will need additional special-casing.
+
+        if (format.HasDepthOrStencil()) {
+            DAWN_ASSERT(IsSubset(usage, wgpu::TextureUsage::TextureBinding |
+                                            kDepthReadOnlyStencilWritableAttachment |
+                                            kDepthWritableStencilReadOnlyAttachment |
+                                            kReadOnlyRenderAttachment));
+
+            if (IsSubset(kDepthReadOnlyStencilWritableAttachment, usage)) {
+                return VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_STENCIL_ATTACHMENT_OPTIMAL;
+            } else if (IsSubset(kDepthWritableStencilReadOnlyAttachment, usage)) {
+                return VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_STENCIL_READ_ONLY_OPTIMAL;
+            } else if (IsSubset(kReadOnlyRenderAttachment, usage)) {
+                return VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL;
+            }
+
+            DAWN_UNREACHABLE();
         } else {
             DAWN_ASSERT(
-                IsSubset(usage, kReadOnlyRenderAttachment | wgpu::TextureUsage::TextureBinding));
-            return VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL;
+                IsSubset(usage, wgpu::TextureUsage::TextureBinding | kReadOnlyStorageTexture));
+
+            if (usage & kReadOnlyStorageTexture) {
+                return VK_IMAGE_LAYOUT_GENERAL;
+            }
+
+            DAWN_UNREACHABLE();
         }
     }
 
@@ -756,6 +776,12 @@
             if (format.HasDepthOrStencil() && format.IsRenderable()) {
                 return VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL;
             }
+            // Sampled textures can be used simultaneously as read only storage. If storage usage is
+            // allowed fall back to VK_IMAGE_LAYOUT_GENERAL.
+            // TODO(crbug.com/392121643): Investigate potential optimizations.
+            if (allowedUsage & (wgpu::TextureUsage::StorageBinding | kReadOnlyStorageTexture)) {
+                return VK_IMAGE_LAYOUT_GENERAL;
+            }
             return VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
 
             // Vulkan texture copy functions require the image to be in _one_  known layout.
@@ -916,6 +942,10 @@
     mIsExternalSwapChainTexture = isSwapChainTexture;
 }
 
+VkImageLayout Texture::VulkanImageLayout(wgpu::TextureUsage usage) const {
+    return dawn::native::vulkan::VulkanImageLayout(GetFormat(), usage, GetUsage());
+}
+
 void Texture::SetLabelImpl() {
     SetLabelHelper("Dawn_InternalTexture");
 }
@@ -1416,8 +1446,7 @@
                                         uint32_t arrayLayer,
                                         uint32_t mipLevel) const {
     DAWN_ASSERT(GetFormat().aspects == Aspect::Color);
-    return VulkanImageLayout(GetFormat(),
-                             mSubresourceLastSyncInfos.Get(aspect, arrayLayer, mipLevel).usage);
+    return VulkanImageLayout(mSubresourceLastSyncInfos.Get(aspect, arrayLayer, mipLevel).usage);
 }
 
 bool Texture::UseCombinedAspects() const {
@@ -2304,6 +2333,10 @@
     return mIsYCbCrFilterable;
 }
 
+VkImageLayout TextureView::VulkanImageLayout(wgpu::TextureUsage usage) const {
+    return dawn::native::vulkan::VulkanImageLayout(GetFormat(), usage, GetUsage());
+}
+
 void TextureView::SetLabelImpl() {
     SetDebugName(ToBackend(GetDevice()), mHandle, "Dawn_TextureView", GetLabel());
 }
diff --git a/src/dawn/native/vulkan/TextureVk.h b/src/dawn/native/vulkan/TextureVk.h
index 889778e..aab7142 100644
--- a/src/dawn/native/vulkan/TextureVk.h
+++ b/src/dawn/native/vulkan/TextureVk.h
@@ -58,7 +58,6 @@
                                           wgpu::TextureUsage usage,
                                           const Format& format,
                                           uint32_t sampleCount);
-VkImageLayout VulkanImageLayout(const Format& format, wgpu::TextureUsage usage);
 VkImageLayout VulkanImageLayoutForDepthStencilAttachment(const Format& format,
                                                          bool depthReadOnly,
                                                          bool stencilReadOnly);
@@ -117,6 +116,8 @@
 
     void SetIsExternalSwapchainTexture(bool isSwapChainTexture);
 
+    VkImageLayout VulkanImageLayout(wgpu::TextureUsage usage) const;
+
     // Dawn API
     void SetLabelImpl() override;
 
@@ -334,6 +335,8 @@
 
     bool IsYCbCrFilterable() const override;
 
+    VkImageLayout VulkanImageLayout(wgpu::TextureUsage usage) const;
+
     // Unique per-device.
     uint64_t GetTextureViewId() const { return mTextureViewId; }
 
diff --git a/src/dawn/tests/end2end/StorageTextureTests.cpp b/src/dawn/tests/end2end/StorageTextureTests.cpp
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/src/dawn/tests/end2end/StorageTextureTests.cpp b/src/dawn/tests/end2end/StorageTextureTests.cpp
index 64a69c6..475b5d8 100644
--- a/src/dawn/tests/end2end/StorageTextureTests.cpp
+++ b/src/dawn/tests/end2end/StorageTextureTests.cpp
@@ -1719,9 +1719,6 @@
 // TEXTURE_BASE_LEVEL and TEXTURE_MAX_LEVEL. If we mistakenly apply the workaround
 // to read only textures then this test will fail.
 TEST_P(ReadWriteStorageTextureTests, ReadMipLevel2AsBothTextureBindingAndStorageBinding) {
-    // This asserts in TextureVK.cpp, see https://crbug.com/392121643
-    DAWN_SUPPRESS_TEST_IF(IsVulkan());
-
     wgpu::ShaderModule csModule = utils::CreateShaderModule(device, R"(
         @binding(0) @group(0) var<storage, read_write> buf : array<vec4u>;
         @binding(1) @group(0) var t_in: texture_2d<f32>;
@@ -1796,6 +1793,91 @@
     EXPECT_BUFFER_U32_RANGE_EQ(expectedData, storageBuffer, 0, 4);
 }
 
+// Tests reading from both a TEXTURE_BINDING and a STORAGE_BINDING from the same
+// texture at the same time, but bound in different bind groups. Almost identical
+// to the previous test, but covers an edge case discovered during development
+// where the internal Vulkan layout selected for the texture was correct if both
+// uses were in a single bind group, but wrong if both uses were in separate groups.
+TEST_P(ReadWriteStorageTextureTests,
+       ReadMipLevel2AsBothTextureBindingAndStorageBindingSeparateGroups) {
+    wgpu::ShaderModule csModule = utils::CreateShaderModule(device, R"(
+        @binding(0) @group(0) var<storage, read_write> buf : array<vec4u>;
+        @binding(1) @group(0) var t_in: texture_2d<f32>;
+        @binding(0) @group(1) var s_in: texture_storage_2d<rgba8unorm, read>;
+
+        @compute @workgroup_size(1) fn cs() {
+          buf[0] = vec4u(
+            u32(textureLoad(t_in, vec2u(0), 0).r * 255),
+            u32(textureLoad(s_in, vec2u(0)).r * 255),
+            123,
+            456,
+          );
+        }
+    )");
+
+    wgpu::ComputePipelineDescriptor pipelineDescriptor;
+    pipelineDescriptor.layout = nullptr;
+    pipelineDescriptor.compute.module = csModule;
+    wgpu::ComputePipeline pipeline = device.CreateComputePipeline(&pipelineDescriptor);
+
+    wgpu::BufferDescriptor bufferDesc;
+    bufferDesc.size = 16;
+    bufferDesc.usage = wgpu::BufferUsage::Storage | wgpu::BufferUsage::CopySrc;
+    wgpu::Buffer storageBuffer = device.CreateBuffer(&bufferDesc);
+
+    // make a 3 mip level texture
+    wgpu::TextureDescriptor textureDesc;
+    textureDesc.format = wgpu::TextureFormat::RGBA8Unorm;
+    textureDesc.size = {4, 1};
+    textureDesc.mipLevelCount = 3;
+    textureDesc.usage = wgpu::TextureUsage::StorageBinding | wgpu::TextureUsage::TextureBinding |
+                        wgpu::TextureUsage::CopySrc | wgpu::TextureUsage::CopyDst;
+    wgpu::Texture texture = device.CreateTexture(&textureDesc);
+
+    // put 1 in first mip, 2 in 2nd, 3 in 3rd.
+    for (uint32_t mipLevel = 0; mipLevel < 3; ++mipLevel) {
+        uint32_t width = 4 >> mipLevel;
+        uint32_t bytesPerRow = width * 4;
+        wgpu::Extent3D copySize({width, 1, 1});
+        wgpu::TexelCopyTextureInfo texelCopyTextureInfo =
+            utils::CreateTexelCopyTextureInfo(texture, mipLevel, {0, 0, 0});
+        wgpu::TexelCopyBufferLayout texelCopyBufferLayout =
+            utils::CreateTexelCopyBufferLayout(0, bytesPerRow);
+        std::vector<uint8_t> data(bytesPerRow, mipLevel + 1);
+        queue.WriteTexture(&texelCopyTextureInfo, data.data(), bytesPerRow, &texelCopyBufferLayout,
+                           &copySize);
+    }
+
+    // View mip level 2
+    wgpu::TextureViewDescriptor textureViewDesc;
+    textureViewDesc.baseMipLevel = 2;
+    textureViewDesc.mipLevelCount = 1;
+    wgpu::TextureView view = texture.CreateView(&textureViewDesc);
+    wgpu::BindGroup bindGroup0 = utils::MakeBindGroup(device, pipeline.GetBindGroupLayout(0),
+                                                      {{0, storageBuffer}, {1, view}});
+
+    wgpu::BindGroup bindGroup1 =
+        utils::MakeBindGroup(device, pipeline.GetBindGroupLayout(1), {{0, view}});
+
+    wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
+    wgpu::ComputePassEncoder computeEncoder = encoder.BeginComputePass();
+
+    computeEncoder.SetBindGroup(0, bindGroup0);
+    computeEncoder.SetBindGroup(1, bindGroup1);
+    computeEncoder.SetPipeline(pipeline);
+    computeEncoder.DispatchWorkgroups(1);
+
+    computeEncoder.End();
+
+    wgpu::CommandBuffer commandBuffer = encoder.Finish();
+    queue.Submit(1, &commandBuffer);
+
+    // expect 3 from reading through the texture binding and
+    // also 3 from reading through the storage binding.
+    static uint32_t expectedData[]{3, 3, 123, 456};
+    EXPECT_BUFFER_U32_RANGE_EQ(expectedData, storageBuffer, 0, 4);
+}
+
 // Tests reading from mip level 1 via TEXTURE_BINDING and write to mip level 2 via
 // STORAGE_BINDING at the same time.
 TEST_P(ReadWriteStorageTextureTests, ReadMipLevel1AndWriteLevel2AtTheSameTime) {
Loading diff…

Original Bug Report

reported by [email protected]

GPU memory disclosure via unhandled multi-bit layout in Vulkan backend

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 security team.

Overview: Dawn’s Vulkan backend fails to handle color textures bound simultaneously as a sampled texture and a read-only storage texture. This causes an assertion failure that is bypassed in Release builds, leading the driver to transition the color texture into an invalid depth/stencil layout. This layout mismatch can confuse GPU driver compression and caching, potentially allowing cross-origin GPU memory disclosure.

Affected files:

  • third_party/dawn/src/dawn/native/vulkan/TextureVk.cpp

Estimated timestamp from git blame: 2023-10-25

Description

A potential vulnerability exists in Dawn’s Vulkan backend where a valid WebGPU resource usage combination causes an invalid Vulkan image layout transition. This issue can be triggered by binding a single color texture view simultaneously as a sampled texture (TextureBinding) and a read-only storage texture (kReadOnlyStorageTexture).

While WebGPU resource tracking correctly identifies this combination as a valid, read-only usage (in PassResourceUsageTracker::AddBindGroup and CommandValidation.cpp), the Vulkan backend’s layout selection logic in VulkanImageLayout() (third_party/dawn/src/dawn/native/vulkan/TextureVk.cpp) is not equipped to handle it.

When VulkanImageLayout() receives the multi-bit usage TextureBinding | kReadOnlyStorageTexture, it checks !wgpu::HasZeroOrOneBits(usage). It assumes any multi-bit usage must be a specific depth/stencil read-only combination and asserts this via DAWN_ASSERT.

However, in Chrome Release builds compiled with Clang, DAWN_ASSERT expands to __builtin_assume(). When the assertion fails, __builtin_assume(false) invokes compiler-level undefined behavior without trapping. Execution falls through the depth/stencil specific if-else blocks and hits the final else statement, incorrectly returning VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL for a standard color texture.

This results in Dawn recording a vkCmdPipelineBarrier that transitions a color texture into a depth/stencil layout (violating Vulkan VUID VUID-VkImageMemoryBarrier-oldLayout-01210). Furthermore, the Bind Group descriptors are populated with conflicting layouts (SHADER_READ_ONLY_OPTIMAL and GENERAL), violating VUID VUID-VkDescriptorImageInfo-imageLayout-00344 during shader execution.

Impact

This issue allows a malicious web page to intentionally cause a severe state mismatch in the Vulkan driver. Because GPU drivers rely on explicit layouts to manage proprietary memory compression (e.g., AMD DCC, ARM AFBC) and cache hierarchies, forcing a color texture into a depth/stencil layout while accessing it via general/shader-read descriptors forces the GPU into an undefined state. This class of layout confusion is a known vector for cross-resource GPU memory disclosure and driver-level memory corruption, potentially leading to a GPU Sandbox Escape.

Potential Reproduction Steps

Note: These are potential steps based on code analysis; a working Proof of Concept has not been fully verified via execution.

  1. On a platform using the Vulkan backend (e.g., Android, Linux, ChromeOS), obtain a WebGPU device.
  2. Create a GPUTexture with a color format (e.g., rgba8unorm) and usage GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.STORAGE_BINDING.
  3. Create a GPUBindGroupLayout defining two entries: one as a sampled texture (texture: {sampleType: 'unfilterable-float'}) and one as a read-only storage texture (storageTexture: {access: 'read-only', format: 'rgba8unorm'}).
  4. Create a GPUBindGroup and bind the exact same texture view to both entries.
  5. Create a compute pipeline that reads from both bindings.
  6. Record a command buffer dispatching this pipeline and submit it. The driver will attempt an invalid layout transition.

Suggested Fix

Update VulkanImageLayout() in third_party/dawn/src/dawn/native/vulkan/TextureVk.cpp to properly handle the combination of TextureBinding and kReadOnlyStorageTexture. When this specific multi-bit usage is detected on a color format, the function should return VK_IMAGE_LAYOUT_GENERAL (as it is required for storage operations, even read-only, and is compatible with sampled reads). The DAWN_ASSERT should also be updated to permit this specific subset.

Evaluated with Chrome root at commit: f200f57a19490707ff8bc7aa5de3cbc443a3afad


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