Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactImproper quantity validation in Tint
DescriptionImproper quantity validation in Tint
ComponentTint
Bug ClassLogic Error
Tracker536446354
Fix commitad9154f67176 (dawn) +64/-0
CISA KEVNot listed
CreditedGoogle
Disclosed2026-09-08

Background

Tint
Dawn’s WGSL shader compiler that translates WebGPU shaders into backend-specific languages such as Metal Shading Language (MSL).
Workgroup storage
On-chip memory shared by all invocations in a compute workgroup, declared in WGSL with var<workgroup> and bounded by the maxComputeWorkgroupStorageSize limit.
`[[threadgroup(n)]]`
The MSL attribute Tint emits for threadgroup (workgroup) memory allocations, whose declared size is ty->Size() of the aggregated wrapper struct.
`DAWN_INTERNAL_ERROR_IF`
A Dawn macro that raises an internal (non-user) validation error when its condition holds, distinct from DAWN_INVALID_IF used for user-facing limit violations.

Root Cause Analysis

On the Metal backend, tint::msl::writer::Printer combines all var<workgroup> variables into a single MSL struct and reports its full size (ty->Size()) in the allocations/workgroup_allocations list, whereas Dawn’s front end validated only the separately computed storage_size. Because the wrapper struct’s size includes inter-member padding driven by member @align attributes, the true allocated size can exceed the front-end storage_size, so a shader whose storage_size fits under maxComputeWorkgroupStorageSize could still produce a Metal allocation that exceeds the device limit. The violated invariant is that the actual backend workgroup allocation must never exceed maxComputeWorkgroupStorageSize, an invariant the existing storage_size-only check failed to enforce.

The fix adds a second check in ShaderModuleMTL.mm that compares the reported workgroup_allocations.front() value against maxComputeWorkgroupStorageSize and fails pipeline creation with DAWN_INTERNAL_ERROR_IF when it is too large. The check works because it validates the real, post-aggregation allocation size rather than the divergent front-end estimate, closing the gap between what Dawn validates and what Metal actually allocates.

Key insight
The single core mistake was validating a front-end storage_size figure that diverged from the padded, aggregated wrapper-struct size Metal actually allocates; the fix directly validates the backend-reported workgroup_allocations size against maxComputeWorkgroupStorageSize, so the check now matches the true allocation.

Attack Path

  1. Craft a divergent shader Author a WGSL compute shader with a var<workgroup> struct member carrying a large @align (e.g. maxComputeWorkgroupStorageSize / 2) so padding inflates the wrapper struct.
  2. Pass front-end validation The front-end storage_size computes to roughly align + 32, which stays at or under maxComputeWorkgroupStorageSize and satisfies the old check.
  3. Aggregate into wrapper struct Tint’s MSL printer combines the workgroup variables into one struct whose ty->Size() reaches about 1.5 * maxComputeWorkgroupStorageSize.
  4. Over-allocate on Metal Dawn requests a threadgroup allocation exceeding the device limit because no check guarded the reported allocation size, reaching the driver with an oversized request.

Impact Assessment

An attacker supplying a malicious WGSL compute shader through WebGPU could cause Dawn to request a Metal threadgroup allocation larger than maxComputeWorkgroupStorageSize, bypassing the intended workgroup-storage limit during compute pipeline creation. This occurs in the GPU process on the Metal backend and requires only the ability to create a WebGPU compute pipeline with a crafted @align-inflated workgroup struct. The classified impact is a logic/quantity-validation error (medium severity); no memory-corruption primitive is established by the diff itself.

Changed Functions

FunctionChangeNotes
TEST_P
src/dawn/tests/end2end/ShaderValidationTests.cpp
modified

Files Changed

  • src/dawn/native/metal/ShaderModuleMTL.mm
  • src/dawn/tests/end2end/ShaderValidationTests.cpp
  • src/tint/lang/msl/writer/printer/printer.cc

Audit Directions

  • Front-end vs backend size divergence
    Audit every place a validation limit is checked against a computed size and confirm the checked figure matches the size the backend actually allocates after padding, alignment, or struct aggregation.
  • Alignment-driven padding
    Review handling of @align and struct layout in shader translation for cases where member alignment inflates aggregate size beyond naive summation of member sizes.
  • Per-backend invariant assumptions
    Look for backend transforms (like Metal’s single-struct combining) whose structural assumptions, such as allocations.size() == 1, must stay synchronized with validation code in Dawn native.
From ad9154f671769f495365deceded6c8db701a1449 Mon Sep 17 00:00:00 2001
From: dan sinclair <[email protected]>
Date: Wed, 29 Jul 2026 16:23:45 -0700
Subject: [PATCH] [metal] Validate metal allocation size

On Metal Tint will take the workgroup memory variables and combine them
into a single struct. This means that the size of that struct can be
larger then the `storage_size` reported. The full size is reported in
the `allocations` list.

This cl adds an extra check into dawn against the allocation size along
side the existing check for the storage size. The check was duplicated
in order to allow us to differentiate the "error caused by user
settings" vs "error caused by how we end up creating the internal
structure".

Fixed: 536446354
Change-Id: I5cb0dc9589c3ba0531e024e60217d289aebbce65
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/327438
Commit-Queue: dan sinclair <[email protected]>
Reviewed-by: Kai Ninomiya <[email protected]>
---

diff --git a/src/dawn/native/metal/ShaderModuleMTL.mm b/src/dawn/native/metal/ShaderModuleMTL.mm
index 4b4967b..5afc2c8 100644
--- a/src/dawn/native/metal/ShaderModuleMTL.mm
+++ b/src/dawn/native/metal/ShaderModuleMTL.mm
@@ -421,6 +421,22 @@
                                     result->workgroup_info, r.usesSubgroupMatrix, r.maxSubgroupSize,
                                     r.limits, r.adapterSupportedLimits.UnsafeGetValue()));
 
+                if (!result->workgroup_allocations.empty()) {
+                    DAWN_ASSERT(result->workgroup_allocations.size() == 1);
+
+                    uint32_t maxComputeWorkgroupStorageSize =
+                        r.limits.maxComputeWorkgroupStorageSize;
+                    uint64_t size = result->workgroup_allocations.front();
+                    DAWN_INTERNAL_ERROR_IF(
+                        size > maxComputeWorkgroupStorageSize,
+                        "The total combined workgroup storage (%u bytes) size with all workgroup "
+                        "variables combined into a single structure is larger than the maximum "
+                        "allowed (%u bytes).%s",
+                        size, maxComputeWorkgroupStorageSize,
+                        DAWN_INCREASE_LIMIT_MESSAGE(r.adapterSupportedLimits.UnsafeGetValue(),
+                                                    maxComputeWorkgroupStorageSize, size));
+                }
+
                 if (result->workgroup_info.subgroup_size.has_value()) {
                     uint32_t explicitSubgroupSize = result->workgroup_info.subgroup_size.value();
                     DAWN_INVALID_IF(
diff --git a/src/dawn/tests/end2end/ShaderValidationTests.cpp b/src/dawn/tests/end2end/ShaderValidationTests.cpp
index 328178e..bd75152 100644
--- a/src/dawn/tests/end2end/ShaderValidationTests.cpp
+++ b/src/dawn/tests/end2end/ShaderValidationTests.cpp
@@ -417,6 +417,48 @@
     CheckPipelineWithWorkgroupStorage(false, (UINT32_MAX - 3) / 4);
 }
 
+// Test workgroup storage size validation with large @align to ensure that front-end validation
+// limits are correctly enforced and do not diverge from the backend's wrapper struct size (such as
+// on Metal where a wrapper struct aggregates all workgroup variables).
+// https://crbug.com/dawn/536446354
+TEST_P(WorkgroupSizeValidationTest, WorkgroupAlignValidationDivergence) {
+    // The metal backend is the only one that combines the workgroup variables into a distinct
+    // structure which causes the padding issue.
+    DAWN_SUPPRESS_TEST_IF(!IsMetal());
+
+    const auto& supportedLimits = GetSupportedLimits();
+    uint32_t maxComputeWorkgroupStorageSize = supportedLimits.maxComputeWorkgroupStorageSize;
+
+    // We choose the alignment 'align' to be maxComputeWorkgroupStorageSize / 2.
+    // The wrapper struct size ty->Size() will be 1.5 * maxComputeWorkgroupStorageSize, which
+    // exceeds maxComputeWorkgroupStorageSize. The divergent front-end calculation of storage_size
+    // would be maxComputeWorkgroupStorageSize / 2 + 32, which is <= maxComputeWorkgroupStorageSize.
+    uint32_t align = maxComputeWorkgroupStorageSize / 2;
+
+    std::ostringstream ss;
+    ss << R"(
+         struct S {
+             @align()"
+       << align << R"() x : f32,
+         }
+         var<workgroup> a : f32;
+         var<workgroup> b : S;
+         var<workgroup> c : f32;
+         @compute @workgroup_size(1) fn main() {
+             _ = a + b.x + c;
+         }
+     )";
+
+    wgpu::ComputePipelineDescriptor desc;
+    desc.compute.module = utils::CreateShaderModule(device, ss.str().c_str());
+
+    // Because the padding makes the actual workgroup storage size (1.5 *
+    // maxComputeWorkgroupStorageSize) exceed maxComputeWorkgroupStorageSize, creating this pipeline
+    // should fail pipeline creation on Metal (the test is suppressed on other backends). This
+    // failure is an internal error due to the size of our buffer allocation
+    ASSERT_DEVICE_ERROR(device.CreateComputePipeline(&desc));
+}
+
 // TODO(crbug.com/462151326): Fix pipeline creation error of the inner layer to surface up properly
 // in WebGPUBackend.
 DAWN_INSTANTIATE_TEST(WorkgroupSizeValidationTest,
diff --git a/src/tint/lang/msl/writer/printer/printer.cc b/src/tint/lang/msl/writer/printer/printer.cc
index 2faddee..c430a35 100644
--- a/src/tint/lang/msl/writer/printer/printer.cc
+++ b/src/tint/lang/msl/writer/printer/printer.cc
@@ -465,6 +465,12 @@
                     out << " [[threadgroup(" << allocations.size() << ")]]";
                     allocations.push_back(ty->Size());
 
+                    // Because we combine the workgroup memory into a single struct, we should only
+                    // ever get a single allocation. If we change this we need to update the
+                    // corresponding validation in ShaderModuleMTL which checks the allocation size
+                    // against the available compute workgroup memory size.
+                    TINT_ASSERT(allocations.size() == 1);
+
                     // Currently type is always a struct, if this changes in the future we'll need
                     // to update this to handle non-struct data as well.
                     TINT_IR_ASSERT(ir_, ty->Is<core::type::Struct>());
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/src/dawn/tests/end2end/ShaderValidationTests.cpp b/src/dawn/tests/end2end/ShaderValidationTests.cpp
index 328178e..bd75152 100644
--- a/src/dawn/tests/end2end/ShaderValidationTests.cpp
+++ b/src/dawn/tests/end2end/ShaderValidationTests.cpp
@@ -417,6 +417,48 @@
     CheckPipelineWithWorkgroupStorage(false, (UINT32_MAX - 3) / 4);
 }
 
+// Test workgroup storage size validation with large @align to ensure that front-end validation
+// limits are correctly enforced and do not diverge from the backend's wrapper struct size (such as
+// on Metal where a wrapper struct aggregates all workgroup variables).
+// https://crbug.com/dawn/536446354
+TEST_P(WorkgroupSizeValidationTest, WorkgroupAlignValidationDivergence) {
+    // The metal backend is the only one that combines the workgroup variables into a distinct
+    // structure which causes the padding issue.
+    DAWN_SUPPRESS_TEST_IF(!IsMetal());
+
+    const auto& supportedLimits = GetSupportedLimits();
+    uint32_t maxComputeWorkgroupStorageSize = supportedLimits.maxComputeWorkgroupStorageSize;
+
+    // We choose the alignment 'align' to be maxComputeWorkgroupStorageSize / 2.
+    // The wrapper struct size ty->Size() will be 1.5 * maxComputeWorkgroupStorageSize, which
+    // exceeds maxComputeWorkgroupStorageSize. The divergent front-end calculation of storage_size
+    // would be maxComputeWorkgroupStorageSize / 2 + 32, which is <= maxComputeWorkgroupStorageSize.
+    uint32_t align = maxComputeWorkgroupStorageSize / 2;
+
+    std::ostringstream ss;
+    ss << R"(
+         struct S {
+             @align()"
+       << align << R"() x : f32,
+         }
+         var<workgroup> a : f32;
+         var<workgroup> b : S;
+         var<workgroup> c : f32;
+         @compute @workgroup_size(1) fn main() {
+             _ = a + b.x + c;
+         }
+     )";
+
+    wgpu::ComputePipelineDescriptor desc;
+    desc.compute.module = utils::CreateShaderModule(device, ss.str().c_str());
+
+    // Because the padding makes the actual workgroup storage size (1.5 *
+    // maxComputeWorkgroupStorageSize) exceed maxComputeWorkgroupStorageSize, creating this pipeline
+    // should fail pipeline creation on Metal (the test is suppressed on other backends). This
+    // failure is an internal error due to the size of our buffer allocation
+    ASSERT_DEVICE_ERROR(device.CreateComputePipeline(&desc));
+}
+
 // TODO(crbug.com/462151326): Fix pipeline creation error of the inner layer to surface up properly
 // in WebGPUBackend.
 DAWN_INSTANTIATE_TEST(WorkgroupSizeValidationTest,
Loading diff…

Original Bug Report

reported by [email protected]

Potential Metal API Validation Bypass via Divergent WGSL Workgroup Padding in Dawn

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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: Dawn’s Metal backend uses divergent logic when calculating and validating workgroup memory sizes for WGSL shaders. By heavily padding a struct via the @align attribute, an attacker can pass Dawn’s size validation checks but force the backend to allocate an out-of-bounds threadgroup memory length. This results in a potential Metal API precondition violation that triggers undefined driver behavior in the sandboxed GPU process.

Affected files:

  • third_party/dawn/src/tint/lang/msl/writer/printer/printer.cc
  • third_party/dawn/src/dawn/native/metal/ComputePipelineMTL.mm
  • third_party/dawn/src/tint/lang/msl/writer/raise/module_scope_vars.cc
  • third_party/dawn/src/tint/lang/core/type/manager.cc
  • third_party/dawn/src/tint/lang/wgsl/resolver/resolver.cc
  • third_party/dawn/src/dawn/native/ShaderModule.cpp

Estimated timestamp from git blame: 2024-12-11

1. Summary of the Issue (Meant for Human Triage)

In Dawn’s Metal backend, a discrepancy in how workgroup memory sizes are calculated allows attacker-controlled WGSL shaders to potentially bypass front-end validation limits. Tint’s MSL backend computes two divergent values for workgroup storage:

  1. workgroup_info.storage_size: Computed by iterating over individual variables and summing their sizes. This smaller value is validated against the WebGPU device’s limit (maxComputeWorkgroupStorageSize).
  2. workgroup_allocations[0]: Represents the actual size of a generated wrapper struct that aggregates all var<workgroup> variables. This larger value is passed directly to the Metal driver via [MTLComputeCommandEncoder setThreadgroupMemoryLength:atIndex:].

Because WGSL allows unbounded @align(N) attributes, Tint’s struct layout engine introduces substantial inter-member padding when packing variables into the wrapper struct. This padding is included in the final struct Size(), but is completely ignored by the validated storage_size. An attacker can supply a shader that passes the front-end validation check, but forces the GPU process to call setThreadgroupMemoryLength with a value exceeding the hardware driver limit (MTLDevice.maxThreadgroupMemoryLength).

This violates Apple’s API preconditions and causes undefined driver behavior or validation layer crashes. However, because Tint omits explicit MSL padding for kWorkgroup structs, the emitted MSL shader operates on a dense layout and does not perform out-of-bounds memory accesses. Consequently, this is a spec-invalid driver call without memory corruption, meriting a Medium (S2) severity in the sandboxed GPU process.


2. Proof-of-Concept & Detailed Execution Flow

Note: These are potential execution steps as our tooling agent has verified this statically but cannot dynamically execute a payload.

Step 1: Attacker WGSL Payload Construction An attacker constructs a WGSL compute shader containing a custom struct with a massive @align(N) attribute, and interleaves standard variables with this struct:

struct S { @align(16384) x : f32 };
var<workgroup> a : f32;
var<workgroup> b : S;
var<workgroup> c : f32;
@compute @workgroup_size(1) fn main() { _ = a + b.x + c; }

Step 2: WGSL Resolution and Unbounded @align Dawn delegates WGSL compilation to Tint. In third_party/dawn/src/tint/lang/wgsl/resolver/resolver.cc:4300-4329, Tint parses the @align(16384) attribute. Tint enforces that the alignment is a power of two (tint::IsPowerOfTwo), but applies no explicit upper-bound cap. The value 16384 is permitted.

Step 3: Wrapper Struct Layout Calculation To comply with MSL’s limits on threadgroup buffers, Tint’s ModuleScopeVars transform bundles the workgroup variables (a, b, c) into a single wrapper struct. In third_party/dawn/src/tint/lang/core/type/manager.cc:414-433, the layout manager computes the memory layout. When it processes b (alignment 16384), it executes offset = tint::RoundUp(align, current_size). This injects a massive 16380-byte padding gap between a and b. The final wrapper struct size (ty->Size()) is computed as 49152.

Step 4: Divergent Storage Size Computation During MSL generation (third_party/dawn/src/tint/lang/msl/writer/printer/printer.cc:453-481), Tint populates the driver allocation size:

auto& allocations = result_.workgroup_allocations;
allocations.push_back(ty->Size()); // Stores 49152

However, it independently calculates workgroup_info.storage_size using a divergent formula that iterates through the original individual variables, completely ignoring the layout manager’s inter-member offset padding:

for (auto& mem : ty->As<core::type::Struct>()->Members()) {
    auto mem_ty = mem->Type();
    uint64_t align = mem_ty->Align();
    uint64_t size = mem_ty->Size();
    result_.workgroup_info.storage_size +=
        tint::RoundUp(static_cast<uint64_t>(16u), tint::RoundUp(align, size));
}

This strictly calculates 16 + 16384 + 16 = 16416 bytes.

Step 5: Validation Bypass Control returns to Dawn’s Metal ShaderModule (third_party/dawn/src/dawn/native/ShaderModule.cpp:1405-1412). Dawn compares workgroupInfo.storage_size (16416) against limits.maxComputeWorkgroupStorageSize (e.g., 32768 on an Apple4 GPU). Since 16416 is less than 32768, the DAWN_INVALID_IF check passes successfully.

Step 6: Driver API Contract Violation (The Sink) When the attacker invokes pass.dispatchWorkgroups(), the GPU process executes ComputePipeline::Encode (third_party/dawn/src/dawn/native/metal/ComputePipelineMTL.mm:97-107). The backend loops over mWorkgroupAllocations and directly calls:

[encoder setThreadgroupMemoryLength:rounded atIndex:i]; // length = 49152

Apple’s documentation strictly defines that this length must be less than or equal to MTLDevice.maxThreadgroupMemoryLength (32768). Passing 49152 violates the API precondition, triggering an assertion in the Metal Validation Layer or undefined behavior in the release OS driver.

Mitigating Factor (No Shader OOB Execution) In third_party/dawn/src/tint/lang/msl/writer/printer/printer.cc:1648-1815, EmitStructType omits generating explicit tint_pad padding bytes for structs unless they are in host_shareable_structs_. FindHostShareableStructs explicitly skips kWorkgroup address spaces. As a result, the generated MSL uses a tightly packed natural C++ layout. The compiled shader only accesses the first ~12 bytes of the threadgroup memory. Even if the driver under-allocates memory due to the precondition violation, no out-of-bounds reads or writes occur from the shader execution side.

Suggested Fix The storage_size calculation in third_party/dawn/src/tint/lang/msl/writer/printer/printer.cc should be updated to match the final bundled wrapper struct’s size (ty->Size()). Alternatively, Dawn’s validation in ShaderModule.cpp should validate the values within workgroup_allocations against maxComputeWorkgroupStorageSize before passing them to the Metal driver.


3. Technical Verification Details (Automated Audit Logs)

> The vulnerability report is 100% accurate and correctly identifies a validation gap in Dawn’s Metal backend. Tint computes workgroup_info.storage_size using a per-variable sum (validated against maxComputeWorkgroupStorageSize in ShaderModule.cpp), but the Metal backend uses workgroup_allocations, populated with the wrapper struct’s Size(). Because WGSL allows unbounded @align(N) attributes, ty->Size() can be arbitrarily inflated by inter-member padding. This allows an attacker to bypass Dawn’s validation and pass an oversized length to [MTLComputeCommandEncoder setThreadgroupMemoryLength:atIndex:] in ComputePipelineMTL.mm. > Per the KB ‘GPU/media driver-sink triage’ guide, length is a bounds-bearing parameter. However, because Tint omits explicit padding for non-host-shareable structs (like kWorkgroup), the emitted MSL shader uses a natural layout and does not perform out-of-bounds memory accesses. Thus, the issue is a spec-invalid driver call without memory corruption. According to the severity guidelines, a GPU validating-layer gap where a spec-invalid value reaches the driver but no OOB write is demonstrated (or where drivers cleanly tolerate it) is rated Medium (S2). Furthermore, the Metal backend runs in the GPU process, which is sandboxed on macOS/iOS, eliminating any path to S0.

Codebase Investigation Traces:

  • Divergent calculations: Verified in third_party/dawn/src/tint/lang/msl/writer/printer/printer.cc:460-489. ty->Size() is appended to allocations (line 466), while result_.workgroup_info.storage_size rounds and sums individual members (line 486).
  • @align bounds missing: Verified in third_party/dawn/src/tint/lang/wgsl/resolver/resolver.cc:4300-4329. The validation uses tint::IsPowerOfTwo(value), but lacks an explicit upper bound (the only limits are the maximum possible u32 value and the 0xffffffff size limits on struct offset/size lines 4413/4433).
  • Device Limits: third_party/dawn/src/dawn/native/metal/PhysicalDeviceMTL.mm:849 sets maxTotalThreadgroupMemory up to 32768u on modern Apple hardware. Line 947 initializes limits->v1.maxComputeWorkgroupStorageSize to this same value.
  • Padding Mitigation Proof: Investigated FindHostShareableStructs (printer.cc:235-281). Parameters are filtered by core::IsHostShareable(ptr->AddressSpace()). According to third_party/dawn/src/tint/lang/core/enums.h:127-131, kWorkgroup is explicitly excluded from returning true. Thus, EmitStructType does not generate tint_pad bytes for the wrapper struct, confirming natural C++ layout and preventing out-of-bounds shader execution.

Evaluated with Chrome root at commit: b96d2ec58f4f5f92b540a723966b199d6e9951b4


Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:

  • If you are familiar with the severity guidelines, you may adjust the severity.
  • If this is a false positive, and there’s no work to be done, please close as WAI.
  • If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.

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