CVE-2026-9930
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Psrc/dawn/tests/end2end/RenderPassTests.cpp |
modified |
Files Changed
src/dawn/native/metal/UtilsMetal.mmsrc/dawn/tests/end2end/RenderPassTests.cpp
Patch
From 72871431b55aa888c0cf5e0f8942421da125994a Mon Sep 17 00:00:00 2001 From: Kai Ninomiya <[email protected]> Date: Wed, 22 Apr 2026 15:13:34 -0700 Subject: [PATCH] [dawn][metal] Fix Metal r8/rg8 workaround for 3d slice rendering When depthSlice was added this workaround didn't get updated. Fixes: 501499832 Change-Id: I7810188ff55e75f2801ea211299aebd105872837 Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/304296 Reviewed-by: Loko Kung <[email protected]> Commit-Queue: Kai Ninomiya <[email protected]> --- diff --git a/src/dawn/native/metal/UtilsMetal.mm b/src/dawn/native/metal/UtilsMetal.mm index 5eb75b8..4fb05ec 100644 --- a/src/dawn/native/metal/UtilsMetal.mm +++ b/src/dawn/native/metal/UtilsMetal.mm @@ -84,6 +84,7 @@ id<MTLTexture> texture = nil; NSUInteger level; NSUInteger slice; + NSUInteger depthPlane; NSPRef<id<MTLTexture>> temporary; @@ -97,7 +98,7 @@ toTexture:texture destinationSlice:slice destinationLevel:level - destinationOrigin:MTLOriginMake(0, 0, 0)]; + destinationOrigin:MTLOriginMake(0, 0, depthPlane)]; } void CopyFromAttachmentToTemporary(CommandRecordingContext* commandContext) { @@ -105,7 +106,7 @@ copyFromTexture:texture sourceSlice:slice sourceLevel:level - sourceOrigin:MTLOriginMake(0, 0, 0) + sourceOrigin:MTLOriginMake(0, 0, depthPlane) sourceSize:MTLSizeMake([temporary.Get() width], [temporary.Get() height], 1) toTexture:temporary.Get() destinationSlice:0 @@ -118,12 +119,14 @@ ResultOrError<SavedMetalAttachment> SaveAttachmentCreateTemporary(Device* device, id<MTLTexture> attachmentTexture, NSUInteger attachmentLevel, - NSUInteger attachmentSlice) { + NSUInteger attachmentSlice, + NSUInteger attachmentDepthPlane) { // Save the attachment. SavedMetalAttachment result; result.texture = attachmentTexture; result.level = attachmentLevel; result.slice = attachmentSlice; + result.depthPlane = attachmentDepthPlane; // Create the temporary texture. NSRef<MTLTextureDescriptor> mtlDescRef = AcquireNSRef([MTLTextureDescriptor new]); @@ -155,13 +158,15 @@ Device* device, MTLRenderPassAttachmentDescriptor* attachment) { SavedMetalAttachment result; - DAWN_TRY_ASSIGN(result, SaveAttachmentCreateTemporary(device, attachment.texture, - attachment.level, attachment.slice)); + DAWN_TRY_ASSIGN(result, + SaveAttachmentCreateTemporary(device, attachment.texture, attachment.level, + attachment.slice, attachment.depthPlane)); // Replace the attachment with the temporary attachment.texture = result.temporary.Get(); attachment.level = 0; attachment.slice = 0; + attachment.depthPlane = 0; return result; } diff --git a/src/dawn/tests/end2end/RenderPassTests.cpp b/src/dawn/tests/end2end/RenderPassTests.cpp index f5a70bd..1cf0a85 100644 --- a/src/dawn/tests/end2end/RenderPassTests.cpp +++ b/src/dawn/tests/end2end/RenderPassTests.cpp @@ -234,6 +234,53 @@ EXPECT_BUFFER_U8_EQ(255, buf, 0); } +// Test that clearing one slice of an R8Unorm texture works. Regression test for crbug.com/501499832 +// with toggle metal_render_r8_rg8_unorm_small_mip_to_temp_texture and --enable-backend-validation. +TEST_P(RenderPassTest, ClearR8UnormDepthSlice) { + const uint32_t kLastMipLevel = 2; + + // Create the texture and buffer used for readback. + wgpu::TextureDescriptor texDesc; + texDesc.dimension = wgpu::TextureDimension::e3D; + texDesc.format = wgpu::TextureFormat::R8Unorm; + texDesc.usage = wgpu::TextureUsage::RenderAttachment | wgpu::TextureUsage::CopySrc; + texDesc.size = {32, 32, 8}; + texDesc.mipLevelCount = kLastMipLevel + 1; + wgpu::Texture tex = device.CreateTexture(&texDesc); + + wgpu::BufferDescriptor bufDesc; + bufDesc.size = 8; + bufDesc.usage = wgpu::BufferUsage::CopyDst | wgpu::BufferUsage::CopySrc; + wgpu::Buffer buf = device.CreateBuffer(&bufDesc); + + wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); + + // Clear the texture with a render pass. + { + wgpu::TextureViewDescriptor viewDesc; + viewDesc.baseMipLevel = kLastMipLevel; + + utils::ComboRenderPassDescriptor renderPass({tex.CreateView(&viewDesc)}); + renderPass.cColorAttachments[0].clearValue = {1.0f, 0.0f, 0.0f, 1.0f}; + renderPass.cColorAttachments[0].loadOp = wgpu::LoadOp::Clear; + renderPass.cColorAttachments[0].storeOp = wgpu::StoreOp::Store; + renderPass.cColorAttachments[0].depthSlice = 1; + wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&renderPass); + pass.End(); + } + + wgpu::CommandBuffer commands = encoder.Finish(); + queue.Submit(1, &commands); + + static uint8_t expected[]{ + // Slice 0 still initialized to 0.0 + 0, + // Slice 1 cleared to 1.0 + 255, + }; + EXPECT_TEXTURE_EQ(expected, tex, {0, 0, 0}, {1, 1, 2}, kLastMipLevel); +} + // Test that clearing a depth16unorm texture with multiple subresources works. This is a regression // test for dawn:1389 where Intel Metal devices fail to do that correctly, requiring a workaround. TEST_P(RenderPassTest, ClearMultisubresourceAfterWriteDepth16Unorm) { @@ -371,7 +418,7 @@ D3D12Backend({}, {"use_d3d12_render_pass"}), MetalBackend(), - // for dawn:1071 regression + // for crbug.com/40096166 and crbug.com/501499832 regressions MetalBackend({"metal_render_r8_rg8_unorm_small_mip_to_temp_texture"}), // for dawn:1389 regression
Regression Test / PoC
diff --git a/src/dawn/tests/end2end/RenderPassTests.cpp b/src/dawn/tests/end2end/RenderPassTests.cpp
index f5a70bd..1cf0a85 100644
--- a/src/dawn/tests/end2end/RenderPassTests.cpp
+++ b/src/dawn/tests/end2end/RenderPassTests.cpp
@@ -234,6 +234,53 @@
EXPECT_BUFFER_U8_EQ(255, buf, 0);
}
+// Test that clearing one slice of an R8Unorm texture works. Regression test for crbug.com/501499832
+// with toggle metal_render_r8_rg8_unorm_small_mip_to_temp_texture and --enable-backend-validation.
+TEST_P(RenderPassTest, ClearR8UnormDepthSlice) {
+ const uint32_t kLastMipLevel = 2;
+
+ // Create the texture and buffer used for readback.
+ wgpu::TextureDescriptor texDesc;
+ texDesc.dimension = wgpu::TextureDimension::e3D;
+ texDesc.format = wgpu::TextureFormat::R8Unorm;
+ texDesc.usage = wgpu::TextureUsage::RenderAttachment | wgpu::TextureUsage::CopySrc;
+ texDesc.size = {32, 32, 8};
+ texDesc.mipLevelCount = kLastMipLevel + 1;
+ wgpu::Texture tex = device.CreateTexture(&texDesc);
+
+ wgpu::BufferDescriptor bufDesc;
+ bufDesc.size = 8;
+ bufDesc.usage = wgpu::BufferUsage::CopyDst | wgpu::BufferUsage::CopySrc;
+ wgpu::Buffer buf = device.CreateBuffer(&bufDesc);
+
+ wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
+
+ // Clear the texture with a render pass.
+ {
+ wgpu::TextureViewDescriptor viewDesc;
+ viewDesc.baseMipLevel = kLastMipLevel;
+
+ utils::ComboRenderPassDescriptor renderPass({tex.CreateView(&viewDesc)});
+ renderPass.cColorAttachments[0].clearValue = {1.0f, 0.0f, 0.0f, 1.0f};
+ renderPass.cColorAttachments[0].loadOp = wgpu::LoadOp::Clear;
+ renderPass.cColorAttachments[0].storeOp = wgpu::StoreOp::Store;
+ renderPass.cColorAttachments[0].depthSlice = 1;
+ wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&renderPass);
+ pass.End();
+ }
+
+ wgpu::CommandBuffer commands = encoder.Finish();
+ queue.Submit(1, &commands);
+
+ static uint8_t expected[]{
+ // Slice 0 still initialized to 0.0
+ 0,
+ // Slice 1 cleared to 1.0
+ 255,
+ };
+ EXPECT_TEXTURE_EQ(expected, tex, {0, 0, 0}, {1, 1, 2}, kLastMipLevel);
+}
+
// Test that clearing a depth16unorm texture with multiple subresources works. This is a regression
// test for dawn:1389 where Intel Metal devices fail to do that correctly, requiring a workaround.
TEST_P(RenderPassTest, ClearMultisubresourceAfterWriteDepth16Unorm) {
@@ -371,7 +418,7 @@
D3D12Backend({}, {"use_d3d12_render_pass"}),
MetalBackend(),
- // for dawn:1071 regression
+ // for crbug.com/40096166 and crbug.com/501499832 regressions
MetalBackend({"metal_render_r8_rg8_unorm_small_mip_to_temp_texture"}),
// for dawn:1389 regression
Original Bug Report
Potential OOB GPU write in Dawn Metal backend via unreset depthPlane in texture workaround
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports without the Chrome Security team.
Overview: A logic error exists in Dawn’s Metal backend where the depthPlane property is not reset when applying the MetalRenderR8RG8UnormSmallMipToTempTexture workaround. If a 3D texture is used as a color attachment, this omission passes an attacker-controlled, non-zero depth slice to the Metal driver alongside a 1-depth temporary 2D texture. This mismatch causes the GPU driver to calculate an out-of-bounds memory offset during the render pass, potentially leading to arbitrary writes in GPU memory.
Affected files:
third_party/dawn/src/dawn/native/metal/UtilsMetal.mmthird_party/dawn/src/dawn/native/metal/TextureMTL.mmthird_party/dawn/src/dawn/native/metal/CommandBufferMTL.mm
Estimated timestamp from git blame: 2022-05-01
Summary
A vulnerability exists in Dawn’s Metal backend within the PatchAttachmentWithTemporary function. When the Toggle::MetalRenderR8RG8UnormSmallMipToTempTexture workaround is triggered (default-enabled for Intel GPUs on macOS), Dawn swaps the render attachment with a temporary 2D texture. However, it fails to reset the depthPlane property of the Metal render pass descriptor. When an attacker provides a 3D texture view and specifies a depthSlice, the stale depthPlane value is used by the Metal driver against the temporary 1-depth 2D texture, resulting in a potential out-of-bounds (OOB) write on the GPU.
Technical Details
Dawn permits 3D textures to be created with the RENDER_ATTACHMENT usage flag and supports rendering to specific slices via the depthSlice property in the render pass descriptor. During command translation, CreateMTLRenderPassDescriptor (CommandBufferMTL.mm:214) maps this depthSlice directly to the depthPlane property of the MTLRenderPassColorAttachmentDescriptor.
When EncodeMetalRenderPass (UtilsMetal.mm:845) detects a small mip level (>= 2) of an R8Unorm or RG8Unorm texture on Intel hardware, it triggers a workaround to patch the attachment using PatchAttachmentWithTemporary.
This function (UtilsMetal.mm:154) allocates a temporary MTLTextureType2D texture with a hardcoded depth of 1. It then updates the attachment descriptor in-place:
// Replace the attachment with the temporary
attachment.texture = result.temporary.Get();
attachment.level = 0;
attachment.slice = 0;
// BUG: attachment.depthPlane is not reset.
Because depthPlane is not reset to 0, the Metal driver receives a descriptor for a 2D texture with a potentially large depthPlane index. Without Metal validation layers enabled in release builds, the driver calculates the memory offset using base_address + (depthPlane * slice_stride). Since the temporary allocation is only 1 layer deep, the GPU will perform the render pass out-of-bounds of the temporary texture’s backing memory.
Further evidence that 3D textures were completely overlooked in this workaround can be found in SavedMetalAttachment. When copying the data back to the original texture (CopyFromTemporaryToAttachment), it hardcodes destinationOrigin:MTLOriginMake(0, 0, 0) and destinationSlice:0, ignoring the original depthPlane entirely.
Potential Exploit Steps
- Using WebGPU, create a 3D texture with format
r8unorm(orrg8unorm), usageRENDER_ATTACHMENT, andmipLevelCount >= 3. - Create a
GPUTextureViewtargeting this 3D texture withbaseMipLevel: 2(or greater) anddimension: "3d". - Begin a render pass, using this view as a color attachment and specifying an out-of-bounds
depthSlicemultiplier (e.g.,depthSlice: 3). This passes Dawn’s frontend validation because it is within the bounds of the original 3D texture. - Submit the commands. The Metal backend translates the
depthSlicetodepthPlane: 3. - The
MetalRenderR8RG8UnormSmallMipToTempTextureworkaround activates, swapping the 3D texture for a newly allocated 1-depth 2D texture, but leavingdepthPlane: 3. - The Metal driver executes the render pass. Because
depthPlaneis 3 on a 1-depth allocation, fragment shader outputs (or clear colors) are written to an out-of-bounds memory address on the GPU.
Note: These are suggested steps based on static code analysis; our tooling agent does not yet have the ability to run code to provide a working proof of concept.
Suggested Fix
- In
PatchAttachmentWithTemporary(third_party/dawn/src/dawn/native/metal/UtilsMetal.mm), explicitly reset thedepthPlaneproperty:
attachment.texture = result.temporary.Get();
attachment.level = 0;
attachment.slice = 0;
attachment.depthPlane = 0;
- To ensure correct functional behavior for 3D textures,
SavedMetalAttachmentshould be updated to store the originaldepthPlanevalue.CopyFromTemporaryToAttachmentandCopyFromAttachmentToTemporaryshould then use this saveddepthPlanevalue for the Z-coordinate of the origin or the destination slice, ensuring the workaround successfully copies data to and from the correct Z-slice of the original 3D texture.
Evaluated with Chrome root at commit: 096fc8fdbfacf2546485756d03f160a3d04fcc9b
Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.