CVE-2026-95283
Overview
Background
- Tint
- The WebGPU shader compiler inside Dawn that translates WGSL into an internal IR and lowers it to a backend shading language.
- `Functional` validator
- The Tint IR validation pass (
src/tint/lang/core/ir/functional_validator.cc) that checks module-level invariants before code generation. - `AddressSpace::kPrivate`
- The WGSL private address space, whose module-scope variables are per-invocation storage the driver must allocate for every shader invocation.
- `internal_limits`
- A header of compile-time constants (
src/tint/utils/internal_limits.h) that bound resource sizes Tint will accept in a shader.
Root Cause Analysis
Verify before building a PoC
What the commit does not establish: Diff only adds an 8MB combined private-variable cap; commit says excessive size causes 'problems for some drivers'. Driver allocation/offset overflow is stated as fact but not shown; advisory places the overflow in Tint, not driver code.
Before the patch, the Functional validator checked individual module-scope variables but never accumulated the combined size of all variables declared in AddressSpace::kPrivate. An attacker-supplied WGSL shader could therefore declare many private variables (or private variables of very large aggregate types) whose combined StoreType()->Size() was effectively unbounded. Because private storage is materialized per shader invocation, some GPU drivers allocated or computed offsets against this excessive combined size, leading to a buffer overflow when the driver’s allocation or addressing assumptions were exceeded. The validator had no counterpart to a per-shader private budget, so the oversized layout passed validation and was handed to the backend and ultimately the driver.
The fix introduces a running counter total_private_bytes_ and rejects the module once the sum crosses kMaxCombinedPrivateVariableSize (8 MB). This shifts the failure from an out-of-bounds condition deep in a driver to a clean validation error in Tint.
Attack Path
- Craft a malicious shader An attacker authors a WGSL shader containing many or very large module-scope variables in the private address space.
- Submit via WebGPU
The shader is compiled through a
GPUDevice.createShaderModule/pipeline call from web content or a compromised renderer. - Pass validation
The pre-patch
Functionalvalidator accepts the module because no combined private-size limit exists. - Trigger driver allocation The oversized private layout is lowered to the backend and the GPU driver allocates or indexes per-invocation private storage.
- Overflow The driver’s size or offset assumptions are exceeded, causing an out-of-bounds write during allocation or addressing.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifsrc/tint/lang/core/ir/functional_validator.cc |
modified | |
TEST_Fsrc/tint/lang/core/ir/validator_value_test.cc |
modified | |
forsrc/tint/lang/core/ir/validator_value_test.cc |
modified |
Files Changed
src/tint/utils/internal_limits.hsrc/tint/lang/core/ir/functional_validator.hsrc/tint/lang/core/ir/validator_value_test.ccsrc/tint/lang/core/ir/functional_validator.cc
Audit Directions
- Unbounded accumulationLook for validators that check per-item limits but never sum sizes/counts across items in a module, buffer, or address space.
- Size handed to driversAudit any computed size or offset (
StoreType()->Size()and similar) that flows into backend or driver allocation without an enforced upper bound. - Per-address-space budgetsVerify that workgroup, storage, and other address spaces have combined-size checks analogous to the new private-space limit, since the same accumulation gap may exist elsewhere.
Patch
From 023b7514b6a0025a5218ed283f3c63a4a1ff082d Mon Sep 17 00:00:00 2001
From: James Price <jrprice@google.com>
Date: Wed, 26 Aug 2026 10:54:37 -0700
Subject: [PATCH] [tint] Limit combined size of all private variables
Excessive combined private variable size is causing problems for some
drivers. Limit to 8MB for now, which is well above the minmax limit of
8KB that WGSL requires for a single declaration.
Fixed: 543141419
Change-Id: I152d8f504185417b0ae9a954e3be42b40b15763f
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/336255
Reviewed-by: dan sinclair <dsinclair@chromium.org>
Auto-Submit: James Price <jrprice@google.com>
Commit-Queue: James Price <jrprice@google.com>
---
diff --git a/src/tint/lang/core/ir/functional_validator.cc b/src/tint/lang/core/ir/functional_validator.cc
index 2692da0..6d9863c 100644
--- a/src/tint/lang/core/ir/functional_validator.cc
+++ b/src/tint/lang/core/ir/functional_validator.cc
@@ -803,6 +803,15 @@
}
}
}
+
+ if (mv->AddressSpace() == AddressSpace::kPrivate) {
+ total_private_bytes_ += mv->StoreType()->Size();
+ if (total_private_bytes_ > internal_limits::kMaxCombinedPrivateVariableSize) {
+ AddError(var) << "total size of private address-space variables exceeds "
+ << internal_limits::kMaxCombinedPrivateVariableSize << " bytes";
+ return;
+ }
+ }
}
void Functional::CheckLet(const Let* l) {
diff --git a/src/tint/lang/core/ir/functional_validator.h b/src/tint/lang/core/ir/functional_validator.h
index 929e787..c5608eb 100644
--- a/src/tint/lang/core/ir/functional_validator.h
+++ b/src/tint/lang/core/ir/functional_validator.h
@@ -224,6 +224,7 @@
Hashmap<const Loop*, const Continue*, 4> first_continues_;
Hashset<std::string, 4> entry_point_names_;
Hashmap<const core::type::Type*, uint64_t, 16> elements_counts_;
+ uint64_t total_private_bytes_ = 0;
};
} // namespace tint::core::ir::validator
diff --git a/src/tint/utils/internal_limits.h b/src/tint/utils/internal_limits.h
index 56a1f9c..a66645c 100644
--- a/src/tint/utils/internal_limits.h
+++ b/src/tint/utils/internal_limits.h
@@ -62,6 +62,10 @@
// expects to work.
constexpr int64_t kMaxTemporaryStorageSize = 10LL * 1024 * 1024;
+// Limits the combined size of all private variables in the shader.
+// The WGSL spec has a min-max for a single variable of 8KB, so this 8MB limit is well above that.
+constexpr int64_t kMaxCombinedPrivateVariableSize = 8LL * 1024 * 1024;
+
// The maximum number of locations allowed for shader IO.
// This hard limit is derived from spirv-val (kMaxLocations in
// https://github.com/KhronosGroup/SPIRV-Tools/blob/main/source/val/validate_interfaces.cpp).
diff --git a/src/tint/lang/core/ir/validator_value_test.cc b/src/tint/lang/core/ir/validator_value_test.cc
index 8c36322..25fb4c6 100644
--- a/src/tint/lang/core/ir/validator_value_test.cc
+++ b/src/tint/lang/core/ir/validator_value_test.cc
@@ -477,6 +477,21 @@
<< res.Failure();
}
+TEST_F(IR_ValidatorTest, Var_CombinedPrivateSizeExceedsLimit) {
+ b.Append(mod.root_block, [&] {
+ for (uint32_t i = 0; i < 128; i++) {
+ b.Var(ty.ptr<private_>(ty.array<vec4u, 8000u>()));
+ }
+ });
+
+ auto res = ir::Validate(mod);
+ ASSERT_NE(res, Success);
+ EXPECT_THAT(
+ res.Failure().reason,
+ testing::HasSubstr("total size of private address-space variables exceeds 8388608 bytes"))
+ << res.Failure();
+}
+
TEST_F(IR_ValidatorTest, Var_HandleMissingBindingPoint) {
auto* v = b.Var(ty.ptr<handle, i32>());
mod.root_block->Append(v);
Regression Test / PoC
diff --git a/src/tint/lang/core/ir/validator_value_test.cc b/src/tint/lang/core/ir/validator_value_test.cc
index 8c36322..25fb4c6 100644
--- a/src/tint/lang/core/ir/validator_value_test.cc
+++ b/src/tint/lang/core/ir/validator_value_test.cc
@@ -477,6 +477,21 @@
<< res.Failure();
}
+TEST_F(IR_ValidatorTest, Var_CombinedPrivateSizeExceedsLimit) {
+ b.Append(mod.root_block, [&] {
+ for (uint32_t i = 0; i < 128; i++) {
+ b.Var(ty.ptr<private_>(ty.array<vec4u, 8000u>()));
+ }
+ });
+
+ auto res = ir::Validate(mod);
+ ASSERT_NE(res, Success);
+ EXPECT_THAT(
+ res.Failure().reason,
+ testing::HasSubstr("total size of private address-space variables exceeds 8388608 bytes"))
+ << res.Failure();
+}
+
TEST_F(IR_ValidatorTest, Var_HandleMissingBindingPoint) {
auto* v = b.Var(ty.ptr<handle, i32>());
mod.root_block->Append(v);