CVE-2026-4453
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifsrc/dawn/native/metal/BufferMTL.mm |
modified | |
ifsrc/dawn/native/metal/CommandBufferMTL.mm |
modified |
Files Changed
src/dawn/native/Limits.cppsrc/dawn/native/metal/BufferMTL.mmsrc/dawn/native/metal/CommandBufferMTL.mmsrc/dawn/native/metal/PhysicalDeviceMTL.mmsrc/dawn/tests/BUILD.gn
Patch
From 12f4bc468e7a724285bfc9aac2e4fc3f2162c423 Mon Sep 17 00:00:00 2001 From: Kai Ninomiya <[email protected]> Date: Mon, 09 Mar 2026 14:53:56 -0700 Subject: [PATCH] [dawn][metal] Fix robustness issues around buffer lengths being u32 On Metal, the sizes of storage buffers (and vertex buffers when transformed into storage buffers for vertex pulling for robustness) are passed to MSL in an array of u32 values. Thus, such bindings cannot be larger than 4GiB-1. There is no separate limit on vertex buffer binding size (as there is for storage buffer bindings), so in order to make this safe - without significantly changing how buffer sizes are passed - this also reduces maxBufferSize by 4 bytes to 4GiB-4. This should have minimal impact on apps, but in order to raise it back to 4GiB we can either: - Pass buffer size minus one (i.e. the max byte index value) into the shader so the minus-one step doesn't have to happen inside the shader - Or just pass sizes as u64. The tests are verified to fail without the fix, with the exception of VertexBuffer_ZeroSizeRemaining which exists to help test later when we raise the limit again. Test: MetalBufferRobustnessTest.* Fixed: 488400770 Change-Id: I3b4207b63ba641271b098a964734f70446595814 Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/295255 Reviewed-by: Loko Kung <[email protected]> Commit-Queue: Kai Ninomiya <[email protected]> Reviewed-by: Corentin Wallez <[email protected]> --- diff --git a/src/dawn/native/Limits.cpp b/src/dawn/native/Limits.cpp index fceae29..ae06f6d 100644 --- a/src/dawn/native/Limits.cpp +++ b/src/dawn/native/Limits.cpp @@ -54,15 +54,16 @@ X(v1, Maximum, maxComputeWorkgroupSizeZ, 64, 64, 64) \ X(v1, Maximum, maxComputeWorkgroupsPerDimension, 65535, 65535, 65535) -// Tiers are 128MB, 256MB, 512MB, 1GB, 2GB-4, 4GB-4. -// compat tier0 tier1 -#define LIMITS_STORAGE_BUFFER_BINDING_SIZE(X) \ - X(v1, Maximum, maxStorageBufferBindingSize, 134217728, 134217728, 268435456, 536870912, 1073741824, 2147483644, 4294967292) +static constexpr uint64_t MiB = 1'048'576; +static constexpr uint64_t GiB = 1'073'741'824; -// Tiers are 256MB, 1GB, 2GB, 4GB. -// compat tier0 tier1 -#define LIMITS_MAX_BUFFER_SIZE(X) \ - X(v1, Maximum, maxBufferSize, 0x10000000, 0x10000000, 0x40000000, 0x80000000, 0x100000000) +// compat tier0 tier1 tier2 tier3 tier4 tier5 +#define LIMITS_STORAGE_BUFFER_BINDING_SIZE(X) \ + X(v1, Maximum, maxStorageBufferBindingSize, 128 * MiB, 128 * MiB, 256 * MiB, 512 * MiB, 1 * GiB, 2 * GiB - 4, 4 * GiB - 4) + +// compat tier0 tier1 tier2 tier3 +#define LIMITS_MAX_BUFFER_SIZE(X) \ + X(v1, Maximum, maxBufferSize, 256 * MiB, 256 * MiB, 1 * GiB, 2 * GiB, 4 * GiB - 4) // Tiers for limits related to resource bindings. // Note that changing these limits may require updating hard-coded constants common/Constants.h. diff --git a/src/dawn/native/metal/BufferMTL.mm b/src/dawn/native/metal/BufferMTL.mm index c7c8741..7fd2a0b 100644 --- a/src/dawn/native/metal/BufferMTL.mm +++ b/src/dawn/native/metal/BufferMTL.mm @@ -89,8 +89,8 @@ } // The vertex pulling transform requires at least 4 bytes in the buffer. - // 0-sized vertex buffer bindings are allowed, so we always need an additional 4 bytes - // after the end. + // Zero-sized vertex buffer bindings at the very end of the buffer are + // allowed, so we always need an additional 4 bytes after the end. NSUInteger extraBytes = 0u; if ((GetInternalUsage() & wgpu::BufferUsage::Vertex) != 0) { extraBytes = 4u; @@ -103,13 +103,19 @@ std::max(static_cast<NSUInteger>(GetSize()) + extraBytes, NSUInteger(4)); if (currentSize > std::numeric_limits<NSUInteger>::max() - alignment) { - // Alignment would overlow. + // Alignment would overflow. return DAWN_OUT_OF_MEMORY_ERROR("Buffer allocation is too large"); } currentSize = Align(currentSize, alignment); uint64_t maxBufferSize = QueryMaxBufferLength(ToBackend(GetDevice())->GetMTLDevice()); if (currentSize > maxBufferSize) { + // Note if this is a vertex buffer, this will result in an OutOfMemory error even when there + // is otherwise enough memory (e.g. a storage buffer of the same size would succeed). + // TODO(crbug.com/488400770): Find some way to avoid falsely signalling to the app that + // there is high memory pressure. (Note, this won't happen if Metal's max buffer size is + // greater than maxBufferSize+4, as it is on M1+ when limit tiering is enabled.) + return DAWN_OUT_OF_MEMORY_ERROR("Buffer allocation is too large"); } diff --git a/src/dawn/native/metal/CommandBufferMTL.mm b/src/dawn/native/metal/CommandBufferMTL.mm index 8cd6c61..810a8ac 100644 --- a/src/dawn/native/metal/CommandBufferMTL.mm +++ b/src/dawn/native/metal/CommandBufferMTL.mm @@ -445,6 +445,12 @@ // length of storage buffers and apply them to the reserved "immediate blocks" when // needed for a draw or a dispatch. struct StorageBufferLengthTracker { + StorageBufferLengthTracker() = delete; + explicit StorageBufferLengthTracker(DeviceBase* device) { + // Lengths are stored as uint32_t. Make sure that's OK for the device. + DAWN_ASSERT(device->GetLimits().v1.maxBufferSize <= std::numeric_limits<uint32_t>::max()); + } + wgpu::ShaderStage dirtyStages = wgpu::ShaderStage::None; // The lengths of buffers are stored as 32bit integers because that is the width the @@ -560,6 +566,9 @@ // Update storage buffer length data that are needed and changed. for (auto stage : IterateStages(lengthTracker->dirtyStages)) { + // Sizes must be > 0, otherwise we'll do min(index, bufferSize - 1) and underflow. + // TODO(crbug.com/488400770): Should be able to assert that, but Graphite violates it. + WriteImmediateBlocks(StageBit(stage), bufferSizeOffset / kImmediateConstantElementByteSize, lengthTracker->data[stage].data(), lengthTracker->dataSize[stage]); @@ -759,6 +768,10 @@ const BufferBinding& binding = group->GetBindingAsBufferBinding(bindingIndex); ToBackend(binding.buffer)->TrackUsage(); + // Check to make sure sizes will fit into uint32_t below. + // TODO(crbug.com/488400770): Warnings for implicit narrowing below are missing. + DAWN_ASSERT(binding.size <= std::numeric_limits<uint32_t>::max()); + if (hasVertStage) { mLengthTracker->data[SingleShaderStage::Vertex][vertIndex] = binding.size; mLengthTracker->dirtyStages |= wgpu::ShaderStage::Vertex; @@ -921,9 +934,16 @@ mVertexBuffers[slot] = mtlBuffer; mVertexBufferOffsets[slot] = offset; - DAWN_ASSERT(buffer->GetSize() < std::numeric_limits<uint32_t>::max()); - mVertexBufferBindingSizes[slot] = - static_cast<uint32_t>(buffer->GetAllocatedSize() - offset); + DAWN_ASSERT(buffer->GetSize() >= offset); + // The binding size for a vertex buffer must always be at least 4 so we can do clamping. + uint64_t bindingSize = std::max(4ull, buffer->GetSize() - offset); + // (BufferMTL reserves an extra 4 bytes for us in case we're at the very end of the buffer.) + DAWN_ASSERT(offset + bindingSize <= buffer->GetAllocatedSize()); + + // Check to make sure sizes will fit into uint32_t for the shader. + DAWN_CHECK(bindingSize <= std::numeric_limits<uint32_t>::max()); + mVertexBufferBindingSizes[slot] = static_cast<uint32_t>(bindingSize); + mDirtyVertexBuffers.set(slot); } @@ -1534,7 +1554,7 @@ const ComputePassResourceUsage& resourceUsage) { uint64_t currentDispatch = 0; ComputePipeline* lastPipeline = nullptr; - StorageBufferLengthTracker storageBufferLengths = {}; + StorageBufferLengthTracker storageBufferLengths{GetDevice()}; BindGroupTracker bindGroups(&storageBufferLengths, GetDevice()->IsToggleEnabled(Toggle::MetalUseArgumentBuffers)); @@ -1726,7 +1746,7 @@ bool didDrawInCurrentOcclusionQuery = false; - StorageBufferLengthTracker storageBufferLengths = {}; + StorageBufferLengthTracker storageBufferLengths{GetDevice()}; VertexBufferTracker vertexBuffers(&storageBufferLengths); BindGroupTracker bindGroups(&storageBufferLengths, GetDevice()->IsToggleEnabled(Toggle::MetalUseArgumentBuffers)); diff --git a/src/dawn/native/metal/PhysicalDeviceMTL.mm b/src/dawn/native/metal/PhysicalDeviceMTL.mm index 01ce8c3..216b08b 100644 --- a/src/dawn/native/metal/PhysicalDeviceMTL.mm +++ b/src/dawn/native/metal/PhysicalDeviceMTL.mm @@ -921,7 +921,9 @@ limits->v1.minUniformBufferOffsetAlignment = mtlLimits.minBufferOffsetAlignment; limits->v1.minStorageBufferOffsetAlignment = mtlLimits.minBufferOffsetAlignment; - uint64_t maxBufferSize = Buffer::QueryMaxBufferLength(*mDevice); + // Hard limit at UINT32_MAX because we pass storage (and vertex) buffer sizes to MSL as u32. + uint64_t maxBufferSize = std::min(static_cast<uint64_t>(std::numeric_limits<uint32_t>::max()), + Buffer::QueryMaxBufferLength(*mDevice)); limits->v1.maxBufferSize = maxBufferSize; // Metal has no documented limit on the size of a binding. Use the maximum diff --git a/src/dawn/tests/BUILD.gn b/src/dawn/tests/BUILD.gn index 704d6d5..6d1f264 100644 --- a/src/dawn/tests/BUILD.gn +++ b/src/dawn/tests/BUILD.gn @@ -622,6 +622,7 @@ "end2end/BindingArrayTests.cpp", "end2end/BufferHostMappedPointerTests.cpp", "end2end/BufferHostMappedPointerTests.h", + "end2end/BufferRobustnessTests.cpp", "end2end/BufferTests.cpp", "end2end/BufferZeroInitTests.cpp", "end2end/ClipDistancesTests.cpp",
Regression Test / PoC
diff --git a/src/dawn/tests/BUILD.gn b/src/dawn/tests/BUILD.gn
index 704d6d5..6d1f264 100644
--- a/src/dawn/tests/BUILD.gn
+++ b/src/dawn/tests/BUILD.gn
@@ -622,6 +622,7 @@
"end2end/BindingArrayTests.cpp",
"end2end/BufferHostMappedPointerTests.cpp",
"end2end/BufferHostMappedPointerTests.h",
+ "end2end/BufferRobustnessTests.cpp",
"end2end/BufferTests.cpp",
"end2end/BufferZeroInitTests.cpp",
"end2end/ClipDistancesTests.cpp",
diff --git a/src/dawn/tests/CMakeLists.txt b/src/dawn/tests/CMakeLists.txt
index a992319..01c0609 100644
--- a/src/dawn/tests/CMakeLists.txt
+++ b/src/dawn/tests/CMakeLists.txt
@@ -53,6 +53,7 @@
"end2end/BindingArrayTests.cpp"
"end2end/BufferHostMappedPointerTests.cpp"
"end2end/BufferHostMappedPointerTests.h"
+ "end2end/BufferRobustnessTests.cpp"
"end2end/BufferTests.cpp"
"end2end/BufferZeroInitTests.cpp"
"end2end/ClipDistancesTests.cpp"
diff --git a/src/dawn/tests/end2end/ArchTierLimitsExhaustive.cpp b/src/dawn/tests/end2end/ArchTierLimitsExhaustive.cpp
index 45d54f3..2e5be76 100644
--- a/src/dawn/tests/end2end/ArchTierLimitsExhaustive.cpp
+++ b/src/dawn/tests/end2end/ArchTierLimitsExhaustive.cpp
@@ -182,13 +182,13 @@
device_map["Metal_AMD_Radeon_Pro_560X"] = { 16384, 16384, 2048, 2048, 4, 24, 1000, 10, 8, 48, 16, 10, 8, 12, 65536, 2147483644, 256, 256, 8, 2147483648, 30, 2048, 28, 8, 128, 32768, 1024, 1024, 1024, 64, 65535, 64, 10, 8, 10, 8,};
// Apple
-device_map["Metal_Apple_M2"] = { 16384, 16384, 2048, 2048, 4, 24, 1000, 10, 8, 48, 16, 10, 8, 12, 65536, 4294967292, 256, 256, 8, 4294967296, 30, 2048, 28, 8, 128, 32768, 1024, 1024, 1024, 64, 65535, 64, 10, 8, 10, 8,};
+device_map["Metal_Apple_M2"] = { 16384, 16384, 2048, 2048, 4, 24, 1000, 10, 8, 48, 16, 10, 8, 12, 65536, 4294967292, 256, 256, 8, 4294967292, 30, 2048, 28, 8, 128, 32768, 1024, 1024, 1024, 64, 65535, 64, 10, 8, 10, 8,};
// ARM
device_map["OpenGLES_Mali_G78_compat"] = { 8192, 8192, 2048, 256, 4, 24, 1000, 8, 4, 16, 16, 8, 4, 12, 65536, 2147483644, 256, 256, 8, 2147483648, 30, 2048, 16, 8, 128, 32768, 256, 256, 256, 64, 65535, 64, 0, 0, 4, 4,};
device_map["OpenGLES_Mali_G78_compat_alt1"] = { 8192, 8192, 2048, 256, 4, 24, 1000, 8, 4, 16, 16, 8, 4, 12, 65536, 268435456, 256, 256, 8, 2147483648, 30, 2048, 16, 8, 128, 32768, 256, 256, 256, 64, 65535, 64, 0, 0, 4, 4,};
device_map["Vulkan_Mali_G78"] = { 16384, 16384, 2048, 2048, 4, 24, 1000, 10, 8, 48, 16, 16, 8, 12, 65536, 2147483644, 256, 256, 8, 2147483648, 30, 2048, 28, 8, 128, 32768, 256, 256, 256, 64, 65535, 64, 16, 8, 16, 8,};
-device_map["Vulkan_Mali_G78_alt1"] = { 16384, 16384, 2048, 2048, 4, 24, 1000, 10, 8, 48, 16, 16, 8, 12, 65536, 268435456, 256, 256, 8, 4294967296, 30, 2048, 28, 8, 128, 32768, 256, 256, 256, 64, 65535, 64, 16, 8, 16, 8,};
+device_map["Vulkan_Mali_G78_alt1"] = { 16384, 16384, 2048, 2048, 4, 24, 1000, 10, 8, 48, 16, 16, 8, 12, 65536, 268435456, 256, 256, 8, 4294967292, 30, 2048, 28, 8, 128, 32768, 256, 256, 256, 64, 65535, 64, 16, 8, 16, 8,};
// Intel
device_map["D3D11_Intel_R__UHD_Graphics_630"] = { 16384, 16384, 2048, 2048, 4, 24, 1000, 10, 8, 48, 16, 16, 8, 12, 65536, 2147483644, 256, 256, 8, 2147483648, 30, 2048, 28, 8, 128, 32768, 1024, 1024, 1024, 64, 65535, 64, 16, 8, 16, 8,};
@@ -196,15 +196,15 @@
device_map["Metal_Intel_R__UHD_Graphics_630"] = { 16384, 16384, 2048, 2048, 4, 24, 1000, 10, 8, 48, 16, 10, 8, 12, 65536, 2147483644, 256, 256, 8, 2147483648, 30, 2048, 28, 8, 128, 32768, 1024, 1024, 1024, 64, 65535, 64, 10, 8, 10, 8,};
device_map["OpenGLES_ANGLE__Intel__Intel_R__UHD_Graphics_630__0x00009BC5__Direct3D11_vs_5_0_ps_5_0__D3D11_31_0_101_2127__compat"]
= { 16384, 16384, 2048, 2048, 4, 24, 1000, 10, 8, 16, 16, 16, 8, 12, 65536, 134217728, 256, 256, 8, 2147483648, 16, 2048, 16, 8, 128, 32768, 1024, 1024, 1024, 64, 65535, 0, 16, 8, 16, 8,};
-device_map["Vulkan_Intel_R__UHD_Graphics_630"] = { 16384, 16384, 2048, 2048, 4, 24, 1000, 10, 8, 48, 16, 16, 8, 12, 65536, 536870912, 256, 256, 8, 4294967296, 30, 2048, 28, 8, 128, 32768, 256, 256, 256, 64, 65535, 64, 16, 8, 16, 8,};
-device_map["Vulkan_Intel_R__UHD_Graphics_630__CML_GT2"] = { 16384, 16384, 2048, 2048, 4, 24, 1000, 8, 4, 16, 16, 16, 4, 12, 65536, 4294967292, 256, 256, 8, 4294967296, 16, 2048, 16, 8, 128, 65536, 256, 256, 256, 64, 65535, 64, 16, 4, 16, 4,};
+device_map["Vulkan_Intel_R__UHD_Graphics_630"] = { 16384, 16384, 2048, 2048, 4, 24, 1000, 10, 8, 48, 16, 16, 8, 12, 65536, 536870912, 256, 256, 8, 4294967292, 30, 2048, 28, 8, 128, 32768, 256, 256, 256, 64, 65535, 64, 16, 8, 16, 8,};
+device_map["Vulkan_Intel_R__UHD_Graphics_630__CML_GT2"] = { 16384, 16384, 2048, 2048, 4, 24, 1000, 8, 4, 16, 16, 16, 4, 12, 65536, 4294967292, 256, 256, 8, 4294967292, 16, 2048, 16, 8, 128, 65536, 256, 256, 256, 64, 65535, 64, 16, 4, 16, 4,};
device_map["D3D11_Intel_R__UHD_Graphics_770"] = { 16384, 16384, 2048, 2048, 4, 24, 1000, 10, 8, 48, 16, 16, 8, 12, 65536, 2147483644, 256, 256, 8, 2147483648, 30, 2048, 28, 8, 128, 32768, 1024, 1024, 1024, 64, 65535, 64, 16, 8, 16, 8,};
device_map["D3D12_Intel_R__UHD_Graphics_770"] = { 16384, 16384, 2048, 2048, 4, 24, 1000, 10, 8, 48, 16, 16, 8, 12, 65536, 2147483644, 256, 256, 8, 2147483648, 30, 2048, 28, 8, 128, 32768, 1024, 1024, 1024, 64, 65535, 64, 16, 8, 16, 8,};
device_map["OpenGLES_ANGLE__Intel__Intel_R__UHD_Graphics_770__0x00004680__Direct3D11_vs_5_0_ps_5_0__D3D11_31_0_101_5333__compat"]
= { 16384, 16384, 2048, 2048, 4, 24, 1000, 10, 8, 16, 16, 16, 8, 12, 65536, 134217728, 256, 256, 8, 2147483648, 16, 2048, 16, 8, 128, 32768, 1024, 1024, 1024, 64, 65535, 64, 16, 8, 16, 8,};
-device_map["Vulkan_Intel_R__UHD_Graphics_770"] = { 16384, 16384, 2048, 2048, 4, 24, 1000, 10, 8, 48, 16, 16, 8, 12, 65536, 536870912, 256, 256, 8, 4294967296, 30, 2048, 28, 8, 128, 32768, 256, 256, 256, 64, 65535, 64, 16, 8, 16, 8,};
-device_map["Vulkan_Intel_R__UHD_Graphics_770__ADL_S_GT1"] = { 16384, 16384, 2048, 2048, 4, 24, 1000, 8, 4, 16, 16, 16, 4, 12, 65536, 4294967292, 256, 256, 8, 4294967296, 16, 2048, 16, 8, 128, 65536, 1024, 1024, 1024, 64, 65535, 64, 16, 4, 16, 4,};
-device_map["Vulkan_Intel_R__Iris_R__Xe_Graphics__TGL_GT2"] = { 16384, 16384, 2048, 2048, 4, 24, 1000, 8, 4, 16, 16, 16, 4, 12, 65536, 4294967292, 256, 256, 8, 4294967296, 16, 2048, 28, 8, 128, 65536, 1024, 1024, 1024, 64, 65535, 64, 16, 4, 16, 4,};
+device_map["Vulkan_Intel_R__UHD_Graphics_770"] = { 16384, 16384, 2048, 2048, 4, 24, 1000, 10, 8, 48, 16, 16, 8, 12, 65536, 536870912, 256, 256, 8, 4294967292, 30, 2048, 28, 8, 128, 32768, 256, 256, 256, 64, 65535, 64, 16, 8, 16, 8,};
+device_map["Vulkan_Intel_R__UHD_Graphics_770__ADL_S_GT1"] = { 16384, 16384, 2048, 2048, 4, 24, 1000, 8, 4, 16, 16, 16, 4, 12, 65536, 4294967292, 256, 256, 8, 4294967292, 16, 2048, 16, 8, 128, 65536, 1024, 1024, 1024, 64, 65535, 64, 16, 4, 16, 4,};
+device_map["Vulkan_Intel_R__Iris_R__Xe_Graphics__TGL_GT2"] = { 16384, 16384, 2048, 2048, 4, 24, 1000, 8, 4, 16, 16, 16, 4, 12, 65536, 4294967292, 256, 256, 8, 4294967292, 16, 2048, 28, 8, 128, 65536, 1024, 1024, 1024, 64, 65535, 64, 16, 4, 16, 4,};
// llvmpipe
device_map["Vulkan_llvmpipe__LLVM_19_1_7__256_bits"] = { 16384, 16384, 2048, 2048, 4, 24, 1000, 10, 8, 48, 16, 16, 8, 12, 65536, 134217728, 256, 256, 8, 2147483648, 30, 2048, 28, 8, 128, 32768, 1024, 1024, 1024, 64, 65535, 64, 16, 8, 16, 8,};
@@ -223,7 +223,7 @@
= { 16384, 16384, 2048, 2048, 4, 24, 1000, 10, 8, 16, 16, 16, 8, 12, 65536, 134217728, 256, 256, 8, 2147483648, 16, 2048, 16, 8, 128, 32768, 1024, 1024, 1024, 64, 65535, 64, 16, 8, 16, 8,};
device_map["OpenGLES_ANGLE__NVIDIA__NVIDIA_GeForce_GTX_1660__0x00002184__Direct3D11_vs_5_0_ps_5_0__D3D11_32_0_15_7602__compat"]
= { 16384, 16384, 2048, 2048, 4, 24, 1000, 10, 8, 16, 16, 16, 8, 12, 65536, 134217728, 256, 256, 8, 2147483648, 16, 2048, 16, 8, 128, 32768, 1024, 1024, 1024, 64, 65535, 64, 16, 8, 16, 8,};
-device_map["Vulkan_NVIDIA_GeForce_GTX_1660"] = { 16384, 16384, 2048, 2048, 4, 24, 1000, 10, 8, 48, 16, 16, 8, 12, 65536, 2147483644, 256, 256, 8, 4294967296, 30, 2048, 28, 8, 128, 49152, 1024, 1024, 1024, 64, 65535, 64, 16, 8, 16, 8,};
+device_map["Vulkan_NVIDIA_GeForce_GTX_1660"] = { 16384, 16384, 2048, 2048, 4, 24, 1000, 10, 8, 48, 16, 16, 8, 12, 65536, 2147483644, 256, 256, 8, 4294967292, 30, 2048, 28, 8, 128, 49152, 1024, 1024, 1024, 64, 65535, 64, 16, 8, 16, 8,};
// Qualcomm
device_map["OpenGLES_Adreno__TM__640_compat"] = { 16384, 16384, 2048, 2048, 4, 24, 1000, 8, 4, 16, 16, 8, 4, 12, 65536, 536870912, 256, 256, 8, 2147483648, 30, 2048, 16, 8, 128, 32768, 1024, 1024, 1024, 64, 65535, 64, 0, 4, 4, 4,};
diff --git a/src/dawn/tests/end2end/BufferRobustnessTests.cpp b/src/dawn/tests/end2end/BufferRobustnessTests.cpp
new file mode 100644
index 0000000..65476ea
--- /dev/null
+++ b/src/dawn/tests/end2end/BufferRobustnessTests.cpp
@@ -0,0 +1,239 @@
+// 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 <string>
+#include <vector>
+
+#include "dawn/tests/DawnTest.h"
+#include "dawn/utils/ComboRenderPipelineDescriptor.h"
+#include "dawn/utils/WGPUHelpers.h"
+
+namespace dawn {
+namespace {
+
+class MetalBufferRobustnessTest : public DawnTest {
+ protected:
+ void GetRequiredLimits(const dawn::utils::ComboLimits& supported,
+ dawn::utils::ComboLimits& required) override {
+ required.maxBufferSize = supported.maxBufferSize;
+ required.maxStorageBufferBindingSize = supported.maxStorageBufferBindingSize;
+ }
+
+ std::vector<wgpu::FeatureName> GetRequiredFeatures() override {
+ return {wgpu::FeatureName::IndirectFirstInstance};
+ }
+
+ wgpu::Buffer CreateBuffer(uint64_t size, wgpu::BufferUsage usage) {
+ wgpu::BufferDescriptor descriptor;
+ descriptor.size = size;
+ descriptor.usage = usage;
+ return device.CreateBuffer(&descriptor);
+ }
+
+ enum class BindType { Vertex, Storage };
+
+ void TestBuffer(BindType bindType,
+ uint64_t bufferSize,
+ uint64_t bindingOffset,
+ uint32_t firstVertex,
+ std::array<uint32_t, 4> expected) {
+ DAWN_TEST_UNSUPPORTED_IF(deviceLimits.maxBufferSize < bufferSize);
+
+ constexpr uint32_t kNumChecks = expected.size();
+
+ // Create a vertex buffer containing known data. We expect the out-of-bounds access to be
+ // clamped so just populate the very end of the buffer with known data.
+ wgpu::Buffer testBuffer =
+ CreateBuffer(bufferSize, wgpu::BufferUsage::Vertex | wgpu::BufferUsage::Storage |
+ wgpu::BufferUsage::CopyDst);
+ constexpr uint32_t kKnownData = 0xAAAAAAAA;
+ queue.WriteBuffer(testBuffer, bufferSize - sizeof(kKnownData), &kKnownData,
+ sizeof(kKnownData));
+
+ // Draw one point to each output pixel, containing the value we got from the vertex buffer.
+ wgpu::ShaderModule shader = utils::CreateShaderModule(device, absl::StrFormat(R"(
+ // Common code
+
+ const kNumChecks: u32 = %u;
+
+ struct VOut { @builtin(position) pos: vec4f, @location(0) @interpolate(flat) val: u32 }
+
+ fn vsCommon(instanceIndex: u32, val: u32) -> VOut {
+ var o: VOut;
+ o.pos = vec4f((f32(instanceIndex) + 0.5) / f32(kNumChecks) * 2 - 1, 0, 0, 1);
+ o.val = val;
+ return o;
+ }
+
+ @fragment fn fs(i: VOut) -> @location(0) u32 {
+ return i.val;
+ }
+
+ // Vertex buffer test
+
+ struct VIn { @location(0) val: u32 }
+
+ @vertex fn vsVertexBufferTest(v: VIn,
+ @builtin(instance_index) instanceIndex: u32) -> VOut {
+ return vsCommon(instanceIndex, v.val);
+ }
+
+ // Storage buffer test
+
+ @group(0) @binding(0) var<storage, read> buf: array<u32>;
+
+ @vertex fn vsStorageBufferTest(@builtin(vertex_index) vertexIndex: u32,
+ @builtin(instance_index) instanceIndex: u32) -> VOut {
+ return vsCommon(instanceIndex, buf[vertexIndex]);
+ }
+ )",
+ kNumChecks));
+
+ utils::ComboRenderPipelineDescriptor pipelineDesc;
+ pipelineDesc.vertex.module = shader;
+ if (bindType == BindType::Vertex) {
+ pipelineDesc.vertex.entryPoint = "vsVertexBufferTest";
+ pipelineDesc.vertex.bufferCount = 1;
+ pipelineDesc.cAttributes[0].format = wgpu::VertexFormat::Uint32;
+ pipelineDesc.cAttributes[0].shaderLocation = 0;
+ pipelineDesc.cBuffers[0].arrayStride = sizeof(uint32_t);
+ pipelineDesc.cBuffers[0].attributes = pipelineDesc.cAttributes.data();
+ pipelineDesc.cBuffers[0].attributeCount = 1;
+ } else {
+ pipelineDesc.vertex.entryPoint = "vsStorageBufferTest";
+ }
+ pipelineDesc.cFragment.module = shader;
+ pipelineDesc.cTargets[0].format = wgpu::TextureFormat::R32Uint;
+ pipelineDesc.primitive.topology = wgpu::PrimitiveTopology::PointList;
+ wgpu::RenderPipeline pipeline = device.CreateRenderPipeline(&pipelineDesc);
+
+ wgpu::TextureDescriptor textureDesc;
+ textureDesc.size = {kNumChecks, 1, 1};
+ textureDesc.format = wgpu::TextureFormat::R32Uint;
+ textureDesc.usage = wgpu::TextureUsage::RenderAttachment | wgpu::TextureUsage::CopySrc;
+ wgpu::Texture texture = device.CreateTexture(&textureDesc);
+
+ // Generate indirect draw data.
+ struct Draw {
+ uint32_t vertexCount, instanceCount, firstVertex, firstInstance;
+ };
+ std::array<Draw, kNumChecks> indirectData;
+ for (uint32_t i = 0; i < kNumChecks; ++i) {
+ // One check at each vertex offset. Uses the instance_index to pass the output position.
+ indirectData[i] = {1, 1, firstVertex + i, i};
+ }
+ wgpu::Buffer indirectBuffer = utils::CreateBufferFromData(
+ device, indirectData.data(), indirectData.size() * sizeof(Draw),
+ wgpu::BufferUsage::Indirect);
+
+ wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
+ {
+ utils::ComboRenderPassDescriptor renderPass({texture.CreateView()});
+ renderPass.cColorAttachments[0].loadOp = wgpu::LoadOp::Clear;
+ // Initial value indicating there's a bug in the test and points aren't drawn correctly
+ renderPass.cColorAttachments[0].clearValue = {7, 7, 7, 7};
+ wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&renderPass);
+ {
+ pass.SetPipeline(pipeline);
+ if (bindType == BindType::Vertex) {
+ pass.SetVertexBuffer(0, testBuffer, bindingOffset);
+ } else {
+ wgpu::BindGroup bg =
+ utils::MakeBindGroup(device, pipeline.GetBindGroupLayout(0),
+ {
+ {0, testBuffer, bindingOffset},
+ });
+ pass.SetBindGroup(0, bg);
+ }
+ // Indirect draw avoids the CPU-side validation of the vertex buffer binding size.
+ for (uint32_t i = 0; i < kNumChecks; ++i) {
+ pass.DrawIndirect(indirectBuffer, i * sizeof(Draw));
+ }
+ }
+ pass.End();
+ }
+ wgpu::CommandBuffer commands = encoder.Finish();
+ queue.Submit(1, &commands);
+
+ // Check if any of the vertices failed.
+ EXPECT_TEXTURE_EQ(expected.data(), texture, {0, 0}, textureDesc.size);
+
+ testBuffer.Destroy();
+ }
+};
+
+// Regression test for crbug.com/488400770.
+// Test that vertex buffer robustness works even with a vertex buffer around 4GB: buffer sizes are
+// passed to MSL as u32, so they risk overflowing to 0. If that happens this test should both fail
+// and trigger a Metal Shader Validation layer error. (As of this writing, that itself won't fail
+// the test, but it will cause the OOB access to return 0.)
+TEST_P(MetalBufferRobustnessTest, VertexBuffer_Under4GB) {
+ // Implementation adds an extra 4B at the end, in case the buffer is bound with offset=size, so
+ // there will be space at the end to clamp into. This should result in a 4GiB MTLBuffer, but the
+ // bound size should still be 4GiB-4 which fits in u32. If the buffer size is not passed to MSL
+ // correctly, it can overflow to 0, and clamp the the access to the u32[] vertex buffer to
+ // 0 - 1 = UINT32_MAX, which allows access to 12GiB of space past the end of the buffer.
+ uint32_t bufferSizeInts = 0x4000'0000 - 1;
+ uint64_t bufferSize = static_cast<uint64_t>(bufferSizeInts) * sizeof(uint32_t);
+ TestBuffer(BindType::Vertex, bufferSize, 0, bufferSizeInts - 1,
+ {0xAAAAAAAA, 0xAAAAAAAA, 0xAAAAAAAA, 0xAAAAAAAA});
+}
+
+// Regression test for crbug.com/488400770.
+// If the actual size is 4GiB, then even passing the correct size to MSL for clamping would
+// result in the bug. (As of this writing, this test will skip itself.)
+TEST_P(MetalBufferRobustnessTest, VertexBuffer_4GB) {
+ uint32_t kBufferSizeInts = 0x4000'0000;
+ uint64_t kBufferSize = static_cast<uint64_t>(kBufferSizeInts) * sizeof(uint32_t);
+ TestBuffer(BindType::Vertex, kBufferSize, 0, kBufferSizeInts - 1,
+ {0xAAAAAAAA, 0xAAAAAAAA, 0xAAAAAAAA, 0xAAAAAAAA});
+}
+
+// If we bind the buffer with offset=size so that there's no space at the end (the binding size is
+// 0), we expect to read the padding (which should be 0). Note unfortunately, in this case, we can't
+// tell if the Metal shader validation layer caught an OOB, except by looking at stderr manually.
+TEST_P(MetalBufferRobustnessTest, VertexBuffer_ZeroSizeRemaining) {
+ uint32_t kBufferSizeInts = 0x4000'0000 - 1;
... (truncated)
Original Bug Report
Security: Cross-tab GPU memory exfiltration via uint64 to uint32 truncation in Dawn's Metal vertex buffer length tracking
Report description
Security: Cross-tab GPU memory exfiltration via uint64 to uint32 truncation in Dawn’s Metal vertex buffer length tracking
Bug location
Where do you want to report your vulnerability?
Chrome VRP β Report security issues affecting the Chrome browser. See program rules
The problem
Please describe the technical details of the vulnerability
Summary
A uint64_t to uint32_t truncation in Dawn’s Metal backend (CommandBufferMTL.mm:924) causes vertex buffer sizes β₯ 4GB to be recorded as 0. This effectively disables the Tint robustness clamp for vertex buffer accesses (arrayLength() = 0 β min(idx, 0 - 1) = no-op). Combined with drawIndirect() to bypass CPU-side firstVertex validation, this allows GPU out-of-bounds reads past the buffer boundary. Apple Silicon GPUs lack hardware buffer robustness, so OOB reads return actual adjacent GPU memory. Cross-tab data exfiltration confirmed on M4 Pro.
Affected Version
- Component: Dawn / WebGPU (Metal backend)
- Tested on: Chrome stable, macOS (Apple M4 Pro)
- Other affected: Chrome Canary, Chromium (any version with Metal WebGPU backend)
- Platform: macOS with Apple Silicon (M1/M2/M3/M4)
Root Cause
The truncation
In src/dawn/native/metal/CommandBufferMTL.mm, vertex buffer binding sizes are stored as uint32_t. When GetAllocatedSize() - offset β₯ 0x100000000, the static_cast<uint32_t>() silently truncates the value:
// Line 923: DEBUG-ONLY assert β checks the WRONG VALUE
DAWN_ASSERT(buffer->GetSize() < std::numeric_limits<uint32_t>::max());
// ^^^^^^^^^^^^^^^^ Checks GetSize() (user-requested), not GetAllocatedSize()!
// Line 924-925: Silent uint64βuint32 truncation
mVertexBufferBindingSizes[slot] =
static_cast<uint32_t>(buffer->GetAllocatedSize() - offset);
// ^^^^^^^^^^^^^^^^^^^^^^ GetAllocatedSize() = 0x100000000 β truncates to 0
Optimal trigger: size = 0xFFFFFFFC (4GB - 4)
The size 0xFFFFFFFC is optimal because it also bypasses the debug-only DAWN_ASSERT on line 923:
GetSize() = 0xFFFFFFFCβ user-requested buffer sizeextraBytes = 4β Metal adds extra bytes for vertex buffers (BufferMTL.mm:96)currentSize = max(0xFFFFFFFC + 4, 4) = 0x100000000Align(0x100000000, 4) = 0x100000000(alignment=4 on macOS for fillBuffer)mAllocatedSize = 0x100000000(exactly 4GB)- Line 923:
DAWN_ASSERT(0xFFFFFFFC < 0xFFFFFFFF)β TRUE β assert passes even in debug! - Line 924:
static_cast<uint32_t>(0x100000000 - 0) = 0β truncated to zero
Robustness bypass chain
The truncated value of 0 propagates through:
mVertexBufferBindingSizes[slot] = 0
β StorageBufferLengthTracker::data[Vertex][metalIndex] = 0 (line 946-947)
β MSL shader: tint_storage_buffer_sizes[N].x = 0
β arrayLength() = 0
β Robustness clamp: min(idx, arrayLength() - 1)
= min(idx, 0 - 1)
= min(idx, 0xFFFFFFFF) β u32 underflow
= idx β NO CLAMPING
The vertex pulling transform (vertex_pulling.cc:222) creates the buffer as var<storage, read>, and the robustness transform (robustness.cc:310-312) computes arrayLength() - 1 without guarding against zero.
Debug assert is doubly wrong
The DAWN_ASSERT on line 923 has two independent bugs:
- Checks the wrong value: It checks
GetSize()(user-requested), but the truncation is onGetAllocatedSize()(which includes alignment + extra bytes) - Uses wrong comparison:
GetSize() = 0xFFFFFFFC < UINT32_MAX = 0xFFFFFFFFis TRUE, so the assert passes even whenGetAllocatedSize() = 0x100000000overflows uint32
The bug is invisible in debug builds.
Proof of Concept
Attack overview
The exploit uses two HTML files:
victim.htmlβ Opened in a separate tab, sprays GPU memory with 512MB of identifiable marker patterns (0xD1_XX_XX_XX)exploit_cross_tab_s4_imageSuccess.htmlβ Creates a 4GB-4 vertex buffer (triggering the truncation), then usesdrawIndirect()to read GPU memory beyond the buffer boundary
bypass size validation using drawIndirect() instead of draw()
A direct draw(N, 1, OOB_FIRST_VERTEX, 0) call is rejected by Dawn’s CPU-side validation (CommandBufferStateTracker.cpp:399-437), which checks (firstVertex + vertexCount - 1) * arrayStride + lastStride against the bound buffer size. However, drawIndirect() puts firstVertex in a GPU buffer β the CPU cannot inspect it:
// CPU validation CANNOT see firstVertex in an indirect buffer
const params = new Uint32Array([
256, // vertexCount
1, // instanceCount
OOB_FIRST_VERTEX, // firstVertex β CPU can't validate this!
0, // firstInstance
]);
device.queue.writeBuffer(indirectBuffer, 0, params);
pass.drawIndirect(indirectBuffer, 0);
The GPU’s indirect draw validation shader does not check vertex buffer bounds β it only validates draw parameter consistency. The vertex pulling shader reads from the buffer using arrayLength=0 β no robustness clamping β OOB read.
Data exfiltration chain
GPU OOB read (vertex buffer[firstVertex + vid])
β vertex shader: v.data = OOB u32 value
β fragment shader: encode as RGBA color
β color attachment texture
β copyTextureToBuffer
β mapAsync(MAP_READ)
β CPU JavaScript receives leaked data
Each drawIndirect() call reads 256 u32 values from GPU memory past the buffer boundary. The attacker controls the offset via firstVertex.
Minimal reproduction
// 1. Create 4GB-4 vertex buffer (triggers truncation)
const victimBuffer = device.createBuffer({
size: 0xFFFFFFFC,
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
});
// 2. Metal internally: allocatedSize = 0x100000000
// mVertexBufferBindingSizes[0] = (uint32_t)(0x100000000) = 0
// arrayLength() = 0 β robustness clamp = min(idx, 0xFFFFFFFF)
// 3. Vertex shader reads @location(0) data: u32 from vertex buffer
// Vertex pulling loads buffer[firstVertex + vid] with NO clamping
// 4. drawIndirect bypasses CPU vertex range validation
const indirectParams = new Uint32Array([
256, // vertexCount
1, // instanceCount
0x3FFFFFFF + 0x1000000, // firstVertex: 16MB past buffer end
0, // firstInstance
]);
device.queue.writeBuffer(indirectBuffer, 0, indirectParams);
pass.setVertexBuffer(0, victimBuffer);
pass.drawIndirect(indirectBuffer, 0);
Suggested Fix
Option 1: Widen the storage type (comprehensive fix)
Change StorageBufferLengthTracker::data and mVertexBufferBindingSizes from uint32_t to uint64_t. Update the Tint MSL writer to emit 64-bit buffer sizes. This prevents truncation for any buffer size.
Option 2: Clamp to UINT32_MAX (minimal fix)
// Replace line 924-925:
uint64_t size64 = buffer->GetAllocatedSize() - offset;
mVertexBufferBindingSizes[slot] = static_cast<uint32_t>(
std::min(size64, static_cast<uint64_t>(std::numeric_limits<uint32_t>::max())));
Option 3: Validation reject
Add explicit validation in setVertexBuffer to reject buffers where allocatedSize - offset > UINT32_MAX:
DAWN_INVALID_IF(buffer->GetAllocatedSize() - offset > std::numeric_limits<uint32_t>::max(),
"Vertex buffer allocated size minus offset exceeds uint32 maximum.");
Also fix the assert
Regardless of which fix is chosen, the DAWN_ASSERT on line 923 should be corrected:
// Fix: check GetAllocatedSize(), not GetSize()
DAWN_ASSERT(buffer->GetAllocatedSize() - offset <= std::numeric_limits<uint32_t>::max());
Reproduction Steps
- Environment: macOS with Apple Silicon (M1/M2/M3/M4), Chrome stable (no flags)
- Open
victim.htmlin one Chrome tab β click “Spray 512MB” β wait for completion - Open
exploit_cross_tab_s4_imageSuccess.htmlin another Chrome tab (same Chrome window = same GPU process) - Click “1. Self-Test” to verify the pipeline works (should show 256/256 matching in-bounds reads)
- Click “2. Cross-Tab Exploit” to scan 512MB past the 4GB buffer boundary
- Alternatively, click “5. Auto-Hunt” for automated retry with VA layout jittering
Expected outcome: The exploit detects victim buffer signatures (0xD1_XX_XX_XX) in the OOB scan. May require multiple attempts due to GPU VA layout variation.
Version Information
- Chrome: Stable channel (145.0.7632.117)
- OS: macOS, Apple M4 Pro
- Memory: 24GB unified (4GB GPU allocation succeeds)
Impact analysis
Cross-tab / cross-origin GPU memory disclosure
Chrome’s GPU process is shared across all tabs. All WebGPU contexts on the same GPU device share the same GPU virtual address space. By reading past the 4GB buffer boundary, the attacker can access:
- Other tabs’ WebGPU buffer contents
- Compositor framebuffer pixels
- GPU virtual address metadata
This is cross-origin information disclosure without any user interaction.
The cause
What version of Chrome have you found the security issue in?
145.0.7632.117 stable
Is the security issue related to a crash?
No, it is not related to a crash.
Choose the type of vulnerability
Information Leak
How would you like to be publicly acknowledged for your report?
sweetchip