High chrome Type Confusion 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactType Confusion in Tint
DescriptionType Confusion in Tint
ComponentTint
Bug ClassType Confusion
Tracker517522769
Fix commit2a1121a43b90 (dawn) +161/-43
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-30

Changed Functions

FunctionChangeNotes
if
src/dawn/native/vulkan/RenderPipelineVk.cpp
modified
TEST_P
src/dawn/tests/end2end/ShaderIOPolyfillTests.cpp
modified

Files Changed

  • src/dawn/native/vulkan/RenderPipelineVk.cpp
  • src/dawn/native/vulkan/ShaderModuleVk.h
  • src/dawn/tests/BUILD.gn
  • src/dawn/tests/CMakeLists.txt
  • src/dawn/tests/end2end/ShaderIOPolyfillTests.cpp
From 2a1121a43b9013f529875a98d03d35f4ff2ec89b Mon Sep 17 00:00:00 2001
From: dan sinclair <dsinclair@chromium.org>
Date: Tue, 09 Jun 2026 05:01:46 -0700
Subject: [PATCH] Reland "Fix location selection for pixel center polyfill."

This reverts commit b85548b0d317e71ea6034ceedf0bf1ed6fd352bf.

There was an issue with the fuzzer setting the position center location
which is now fixed.

Fixed: 517522769
Change-Id: I85a63e65e7ea2ff5543e04b87009f4112afae1d1
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/314935
Auto-Submit: dan sinclair <dsinclair@chromium.org>
Reviewed-by: James Price <jrprice@google.com>
Commit-Queue: dan sinclair <dsinclair@chromium.org>
---

diff --git a/src/dawn/native/vulkan/RenderPipelineVk.cpp b/src/dawn/native/vulkan/RenderPipelineVk.cpp
index 184cf10..9de4480 100644
--- a/src/dawn/native/vulkan/RenderPipelineVk.cpp
+++ b/src/dawn/native/vulkan/RenderPipelineVk.cpp
@@ -27,7 +27,6 @@
 
 #include "src/dawn/native/vulkan/RenderPipelineVk.h"
 
-#include <memory>
 #include <string>
 #include <utility>
 #include <vector>
@@ -514,14 +513,37 @@
         return {};
     };
 
+    std::optional<uint32_t> pixelCenterPolyfillLocation = std::nullopt;
+    if (NeedsPixelCenterPolyfill()) {
+        const EntryPointMetadata* vtx = GetStage(SingleShaderStage::Vertex).metadata;
+        for (size_t i = 0; i < vtx->usedInterStageVariables.size(); ++i) {
+            if (vtx->usedInterStageVariables[i] == false) {
+                pixelCenterPolyfillLocation = uint32_t(i);
+                break;
+            }
+        }
+        if (!pixelCenterPolyfillLocation.has_value()) {
+            return DAWN_INTERNAL_ERROR(
+                "unable to find a free vertex location for the pixel center polyfill");
+        }
+
+        if (HasStage(SingleShaderStage::Fragment)) {
+            const EntryPointMetadata* frag = GetStage(SingleShaderStage::Fragment).metadata;
+            // Because the fragment stage must be a subset of the vertex stage, if the value was
+            // free in the vertex stage it _must_ be free in the fragment stage.
+            DAWN_ASSERT(frag->usedInterStageVariables[pixelCenterPolyfillLocation.value()] ==
+                        false);
+        }
+    }
+
     // Add the vertex stage that's always present.
     DAWN_TRY(AddShaderStage({
         .stage = &GetStage(SingleShaderStage::Vertex),
         .layout = layout,
         .immediateMask = GetImmediateMask(),
         .ycbcrExternalTextures = &specialization.ycbcrExternalTextures,
+        .polyfillPixelCenter = pixelCenterPolyfillLocation,
         .emitPointSize = GetPrimitiveTopology() == wgpu::PrimitiveTopology::PointList,
-        .polyfillPixelCenter = NeedsPixelCenterPolyfill(),
         .pipelineUsesFramebufferFetch = UsesFramebufferFetch(),
     }));
 
@@ -532,7 +554,7 @@
             .layout = layout,
             .immediateMask = GetImmediateMask(),
             .ycbcrExternalTextures = &specialization.ycbcrExternalTextures,
-            .polyfillPixelCenter = NeedsPixelCenterPolyfill(),
+            .polyfillPixelCenter = pixelCenterPolyfillLocation,
             .pipelineUsesFramebufferFetch = UsesFramebufferFetch(),
             .needsMultisampledFramebufferFetch = UseSampleRateShading() && UsesFramebufferFetch(),
         }));
diff --git a/src/dawn/native/vulkan/ShaderModuleVk.h b/src/dawn/native/vulkan/ShaderModuleVk.h
index 337af93..693d9fb 100644
--- a/src/dawn/native/vulkan/ShaderModuleVk.h
+++ b/src/dawn/native/vulkan/ShaderModuleVk.h
@@ -72,8 +72,8 @@
         ImmediateMask immediateMask;
         raw_ptr<const absl::flat_hash_set<APIBindPoint>> ycbcrExternalTextures;
 
+        std::optional<uint32_t> polyfillPixelCenter = std::nullopt;
         bool emitPointSize = false;
-        bool polyfillPixelCenter = false;
         bool pipelineUsesFramebufferFetch = false;
         bool needsMultisampledFramebufferFetch = false;
     };
diff --git a/src/dawn/tests/BUILD.gn b/src/dawn/tests/BUILD.gn
index 5e533fc..73d213a 100644
--- a/src/dawn/tests/BUILD.gn
+++ b/src/dawn/tests/BUILD.gn
@@ -648,6 +648,7 @@
     "end2end/ShaderAtomicTests.cpp",
     "end2end/ShaderBuiltinPartialConstArgsErrorTests.cpp",
     "end2end/ShaderF16Tests.cpp",
+    "end2end/ShaderIOPolyfillTests.cpp",
     "end2end/ShaderModuleCachingTests.cpp",
     "end2end/ShaderPrintTests.cpp",
     "end2end/ShaderTests.cpp",
diff --git a/src/dawn/tests/CMakeLists.txt b/src/dawn/tests/CMakeLists.txt
index ec77cff..9b64d0b 100644
--- a/src/dawn/tests/CMakeLists.txt
+++ b/src/dawn/tests/CMakeLists.txt
@@ -160,6 +160,7 @@
     "end2end/ShaderAtomicTests.cpp"
     "end2end/ShaderBuiltinPartialConstArgsErrorTests.cpp"
     "end2end/ShaderF16Tests.cpp"
+    "end2end/ShaderIOPolyfillTests.cpp"
     "end2end/ShaderModuleCachingTests.cpp"
     "end2end/ShaderPrintTests.cpp"
     "end2end/ShaderTests.cpp"
diff --git a/src/dawn/tests/end2end/ShaderIOPolyfillTests.cpp b/src/dawn/tests/end2end/ShaderIOPolyfillTests.cpp
new file mode 100644
index 0000000..20501ff
--- /dev/null
+++ b/src/dawn/tests/end2end/ShaderIOPolyfillTests.cpp
@@ -0,0 +1,111 @@
+// Copyright 2026 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include <utility>
+
+#include "src/dawn/tests/DawnTest.h"
+#include "src/dawn/utils/ComboRenderPipelineDescriptor.h"
+#include "src/dawn/utils/WGPUHelpers.h"
+
+namespace dawn {
+namespace {
+
+using ShaderIOPolyfillTests = DawnTest;
+
+// https://crbug.com/517522769
+TEST_P(ShaderIOPolyfillTests, DivergentLocations) {
+    wgpu::ShaderModule vsModule = utils::CreateShaderModule(device, R"(
+        struct VOut {
+            @builtin(position) pos : vec4f,
+            @location(0) a : vec4f,
+            @location(1) @interpolate(flat) b : i32,
+        };
+        @vertex fn vs(@builtin(vertex_index) vi : u32) -> VOut {
+            var p = array<vec2f, 3>(
+                vec2f(-1, -1),
+                vec2f( 3, -1),
+                vec2f(-1,  3));
+            var o : VOut;
+            o.pos = vec4f(p[vi], 0, 1);
+            o.a   = vec4f(1, 0, 0, 1);
+            o.b   = 0x7eadbeef;
+            return o;
+        }
+    )");
+
+    wgpu::ShaderModule fsModule = utils::CreateShaderModule(device, R"(
+        @fragment fn fs(
+            @location(0) a : vec4f,
+            @builtin(sample_index) s : u32,
+            @builtin(position) p : vec4f) -> @location(0) vec4f {
+            return a + vec4f(p.z, f32(s) * 0, 0, 0);
+        }
+    )");
+
+    utils::ComboRenderPipelineDescriptor pipelineDesc;
+    pipelineDesc.vertex.module = std::move(vsModule);
+    pipelineDesc.cFragment.module = std::move(fsModule);
+    pipelineDesc.multisample.count = 4;
+    pipelineDesc.cTargets[0].format = wgpu::TextureFormat::RGBA8Unorm;
+
+    wgpu::RenderPipeline pipeline = device.CreateRenderPipeline(&pipelineDesc);
+
+    wgpu::TextureDescriptor msaaDesc;
+    msaaDesc.size = {64, 64, 1};
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/src/dawn/tests/BUILD.gn b/src/dawn/tests/BUILD.gn
index 5e533fc..73d213a 100644
--- a/src/dawn/tests/BUILD.gn
+++ b/src/dawn/tests/BUILD.gn
@@ -648,6 +648,7 @@
     "end2end/ShaderAtomicTests.cpp",
     "end2end/ShaderBuiltinPartialConstArgsErrorTests.cpp",
     "end2end/ShaderF16Tests.cpp",
+    "end2end/ShaderIOPolyfillTests.cpp",
     "end2end/ShaderModuleCachingTests.cpp",
     "end2end/ShaderPrintTests.cpp",
     "end2end/ShaderTests.cpp",
diff --git a/src/dawn/tests/CMakeLists.txt b/src/dawn/tests/CMakeLists.txt
index ec77cff..9b64d0b 100644
--- a/src/dawn/tests/CMakeLists.txt
+++ b/src/dawn/tests/CMakeLists.txt
@@ -160,6 +160,7 @@
     "end2end/ShaderAtomicTests.cpp"
     "end2end/ShaderBuiltinPartialConstArgsErrorTests.cpp"
     "end2end/ShaderF16Tests.cpp"
+    "end2end/ShaderIOPolyfillTests.cpp"
     "end2end/ShaderModuleCachingTests.cpp"
     "end2end/ShaderPrintTests.cpp"
     "end2end/ShaderTests.cpp"
diff --git a/src/dawn/tests/end2end/ShaderIOPolyfillTests.cpp b/src/dawn/tests/end2end/ShaderIOPolyfillTests.cpp
new file mode 100644
index 0000000..20501ff
--- /dev/null
+++ b/src/dawn/tests/end2end/ShaderIOPolyfillTests.cpp
@@ -0,0 +1,111 @@
+// Copyright 2026 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include <utility>
+
+#include "src/dawn/tests/DawnTest.h"
+#include "src/dawn/utils/ComboRenderPipelineDescriptor.h"
+#include "src/dawn/utils/WGPUHelpers.h"
+
+namespace dawn {
+namespace {
+
+using ShaderIOPolyfillTests = DawnTest;
+
+// https://crbug.com/517522769
+TEST_P(ShaderIOPolyfillTests, DivergentLocations) {
+    wgpu::ShaderModule vsModule = utils::CreateShaderModule(device, R"(
+        struct VOut {
+            @builtin(position) pos : vec4f,
+            @location(0) a : vec4f,
+            @location(1) @interpolate(flat) b : i32,
+        };
+        @vertex fn vs(@builtin(vertex_index) vi : u32) -> VOut {
+            var p = array<vec2f, 3>(
+                vec2f(-1, -1),
+                vec2f( 3, -1),
+                vec2f(-1,  3));
+            var o : VOut;
+            o.pos = vec4f(p[vi], 0, 1);
+            o.a   = vec4f(1, 0, 0, 1);
+            o.b   = 0x7eadbeef;
+            return o;
+        }
+    )");
+
+    wgpu::ShaderModule fsModule = utils::CreateShaderModule(device, R"(
+        @fragment fn fs(
+            @location(0) a : vec4f,
+            @builtin(sample_index) s : u32,
+            @builtin(position) p : vec4f) -> @location(0) vec4f {
+            return a + vec4f(p.z, f32(s) * 0, 0, 0);
+        }
+    )");
+
+    utils::ComboRenderPipelineDescriptor pipelineDesc;
+    pipelineDesc.vertex.module = std::move(vsModule);
+    pipelineDesc.cFragment.module = std::move(fsModule);
+    pipelineDesc.multisample.count = 4;
+    pipelineDesc.cTargets[0].format = wgpu::TextureFormat::RGBA8Unorm;
+
+    wgpu::RenderPipeline pipeline = device.CreateRenderPipeline(&pipelineDesc);
+
+    wgpu::TextureDescriptor msaaDesc;
+    msaaDesc.size = {64, 64, 1};
+    msaaDesc.sampleCount = 4;
+    msaaDesc.format = wgpu::TextureFormat::RGBA8Unorm;
+    msaaDesc.usage = wgpu::TextureUsage::RenderAttachment;
+    wgpu::Texture msaa = device.CreateTexture(&msaaDesc);
+
+    wgpu::TextureDescriptor resolveDesc;
+    resolveDesc.size = {64, 64, 1};
+    resolveDesc.format = wgpu::TextureFormat::RGBA8Unorm;
+    resolveDesc.usage = wgpu::TextureUsage::RenderAttachment | wgpu::TextureUsage::CopySrc;
+    wgpu::Texture resolve = device.CreateTexture(&resolveDesc);
+
+    wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
+    utils::ComboRenderPassDescriptor renderPass({msaa.CreateView()});
+    renderPass.cColorAttachments[0].resolveTarget = resolve.CreateView();
+    renderPass.cColorAttachments[0].clearValue = {0.0, 0.0, 0.0, 0.0};
+    renderPass.cColorAttachments[0].loadOp = wgpu::LoadOp::Clear;
+    renderPass.cColorAttachments[0].storeOp = wgpu::StoreOp::Discard;
+
+    wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&renderPass);
+    pass.SetPipeline(pipeline);
+    pass.Draw(3);
+    pass.End();
+
+    wgpu::CommandBuffer commands = encoder.Finish();
+    queue.Submit(1, &commands);
+
+    EXPECT_PIXEL_RGBA8_EQ(utils::RGBA8::kRed, resolve, 32, 32);
+}
+
+DAWN_INSTANTIATE_TEST(ShaderIOPolyfillTests, VulkanBackend());
+
+}  // namespace
+}  // namespace dawn
diff --git a/src/tint/lang/spirv/writer/raise/shader_io_test.cc b/src/tint/lang/spirv/writer/raise/shader_io_test.cc
index 08928cc..0b79059 100644
--- a/src/tint/lang/spirv/writer/raise/shader_io_test.cc
+++ b/src/tint/lang/spirv/writer/raise/shader_io_test.cc
@@ -1422,7 +1422,7 @@
 
     core::ir::transform::ImmediateDataLayout immediate_data;
     ShaderIOConfig config{immediate_data};
-    config.polyfill_pixel_center = true;
+    config.polyfill_pixel_center = 1;
     Run(ShaderIO, config);
 
     EXPECT_EQ(expect, str());
@@ -1457,7 +1457,7 @@
 $B1: {  # root
   %foo_position_Input:ptr<__in, vec4<f32>, read> = var undef @builtin(position)
   %foo_sample_index_Input:ptr<__in, u32, read> = var undef @interpolate(flat) @builtin(sample_index)
-  %foo_loc0_Input:ptr<__in, vec4<f32>, read> = var undef @location(0) @interpolate(perspective, center)
+  %foo_loc1_Input:ptr<__in, vec4<f32>, read> = var undef @location(1) @interpolate(perspective, center)
 }
 
 %foo_inner = func(%position:vec4<f32>, %idx:u32):void {
@@ -1472,7 +1472,7 @@
     %10:vec2<f32> = swizzle %9, xy
     %11:vec2<f32> = floor %10
     %12:vec2<f32> = add %11, vec2<f32>(0.5f)
-    %13:vec4<f32> = spirv.interpolate_at_offset %foo_loc0_Input, vec2<f32>(0.0f)
+    %13:vec4<f32> = spirv.interpolate_at_offset %foo_loc1_Input, vec2<f32>(0.0f)
     %14:f32 = swizzle %13, z
     %15:f32 = swizzle %13, w
     %16:f32 = div %14, %15
@@ -1487,7 +1487,7 @@
 
     core::ir::transform::ImmediateDataLayout immediate_data;
     ShaderIOConfig config{immediate_data};
-    config.polyfill_pixel_center = true;
+    config.polyfill_pixel_center = 1;
     Run(ShaderIO, config);
 
     EXPECT_EQ(expect, str());
@@ -1552,7 +1552,7 @@
 
     core::ir::transform::ImmediateDataLayout immediate_data;
     ShaderIOConfig config{immediate_data};
-    config.polyfill_pixel_center = true;
+    config.polyfill_pixel_center = 1;
     Run(ShaderIO, config);
 
     EXPECT_EQ(expect, str());
@@ -1641,7 +1641,7 @@
 
     core::ir::transform::ImmediateDataLayout immediate_data;
     ShaderIOConfig config{immediate_data};
-    config.polyfill_pixel_center = true;
+    config.polyfill_pixel_center = 0;
     Run(ShaderIO, config);
 
     EXPECT_EQ(expect, str());
@@ -1725,7 +1725,7 @@
 
     core::ir::transform::ImmediateDataLayout immediate_data;
     ShaderIOConfig config{immediate_data};
-    config.polyfill_pixel_center = true;
+    config.polyfill_pixel_center = 0;
     Run(ShaderIO, config);
 
     EXPECT_EQ(expect, str());
@@ -1792,7 +1792,7 @@
 
     core::ir::transform::ImmediateDataLayout immediate_data;
     ShaderIOConfig config{immediate_data};
-    config.polyfill_pixel_center = true;
+    config.polyfill_pixel_center = 1;
     Run(ShaderIO, config);
 
     EXPECT_EQ(expect, str());
diff --git a/src/tint/lang/spirv/writer/writer_test.cc b/src/tint/lang/spirv/writer/writer_test.cc
index 9d2b1c0..0dc44b4 100644
--- a/src/tint/lang/spirv/writer/writer_test.cc
+++ b/src/tint/lang/spirv/writer/writer_test.cc
@@ -457,7 +457,7 @@
     });
 
     Options options;
-    options.polyfill_pixel_center = true;
+    options.polyfill_pixel_center = 0;
     auto result = Generate(options);
     ASSERT_EQ(result, Success) << result.Failure() << output_;
     EXPECT_INST(R"(
Loading diff…

Original Bug Report

reported by vm...@google.com

Vulkan driver interface mismatch due to divergent pixel-center polyfill locations

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: When the pixel-center polyfill is active, Dawn/Tint independently assigns the polyfill interpolant location for each shader stage by scanning for the lowest unused location index. Because WebGPU allows fragment inputs to be a strict subset of vertex outputs, the vertex and fragment compilers can resolve different locations for the polyfill variable. This discrepancy can result in a mismatched SPIR-V interface being passed directly to the Vulkan driver, potentially triggering undefined behavior or memory corruption in the GPU process.

Affected files:

  • third_party/dawn/src/tint/lang/spirv/writer/raise/shader_io.cc

Estimated timestamp from git blame: 2025-11-13

Description

When polyfill_pixel_center is enabled (for instance, when multisample.count > 1 and the fragment shader uses @builtin(position) alongside a sample-interpolated input or @builtin(sample_index)), the function AddCenterPosInterpolant in third_party/dawn/src/tint/lang/spirv/writer/raise/shader_io.cc is invoked once per shader stage.

This function selects the smallest location index absent from the current stage’s input/output list:

        std::set<uint32_t> existing_locations;
        for (auto io : entries) {
            if (io.attributes.location.has_value()) {
                existing_locations.insert(io.attributes.location.value());
            }
        }
        uint32_t free_location = 0u;
        for (uint32_t i = 0u; i < (existing_locations.size() + 1); i++) {
            if (existing_locations.find(i) == existing_locations.end()) {
                free_location = i;
                break;
            }
        }

However, WebGPU validation permits the fragment stage inputs to be a strict subset of the vertex stage outputs (as validated in ValidateInterStageMatching in RenderPipeline.cpp). Because stages are compiled in isolation without sharing location allocation metadata, the vertex and fragment compilation passes can resolve different “smallest free” locations for the center_pos polyfill variable.

Potential Attack Scenario

An attacker seeking to trigger this mismatch would potentially perform the following steps from web content:

  1. Request a WebGPU adapter and device on a platform using the Vulkan backend (e.g., Android, Linux, or Windows).
  2. Create a shader module containing a vertex shader that outputs multiple locations (e.g., location 0 and location 1):
    struct VOut {
        @builtin(position) p: vec4<f32>,
        @location(0) a: vec4<f32>,
        @location(1) @interpolate(flat) b: i32
    };
    @vertex fn vs() -> VOut { ... }
    
  3. Create a fragment shader module that only consumes a subset of those outputs (e.g., only location 0) and uses @builtin(sample_index) (or sample interpolation) and @builtin(position) to trigger the pixel-center polyfill:
    @fragment fn fs(
        @builtin(position) p: vec4<f32>,
        @builtin(sample_index) s: u32,
        @location(0) a: vec4<f32>
    ) -> @location(0) vec4<f32> { 
        return p + a;
    } 
    
  4. Create a render pipeline with multisample: {count: 4} using these modules.

During compilation:

  • The vertex shader’s polyfill scans locations {0, 1} and assigns center_pos to Location 2.
  • The fragment shader’s polyfill scans location {0} and assigns center_pos to Location 1.

Consequently, the vertex shader outputs a flat i32 at Location 1, whereas the fragment shader expects a NoPerspective vec4<f32> at Location 1. This mismatch violates Vulkan’s inter-stage interface-matching rules (VUID-RuntimeSpirv-OpEntryPoint-07754 and VUID-RuntimeSpirv-OpEntryPoint-08743).

When the mismatched SPIR-V modules are sent directly to vkCreateGraphicsPipelines, it can lead to Vulkan driver undefined behavior. Depending on the driver and hardware, this could cause pipeline creation failure, garbage interpolation reads, or potential memory corruption inside the GPU process.

Note: These steps are based on source code analysis. Our tooling currently does not have the ability to run code or verify runtime behavior.

Suggested Remediation

Instead of resolving the polyfill location dynamically within each stage independently, Dawn should establish a unified inter-stage location assignment strategy during the pipeline compilation setup, or Tint should reserve a specific, universally agreed-upon location slot for internal polyfills that does not conflict with user-declared locations.

Evaluated with Chrome root at commit: 5133b93d189b383c37805b1cf3a9d2dbfe8d7379


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