CVE-2026-102307
Overview
Background
- Dawn `BlitTextureToBuffer`
- Dawn’s implementation of WebGPU texture-to-buffer copies, which on some backends emulates the copy with a shader “blit” that loads texels and packs them into the destination buffer.
- `glReadPixels` readback path
- the OpenGL ES fallback that reads a texture region into a buffer; per the OpenGL ES 3.1 spec, only
RGBA32Uint/RGBA32Sintinteger formats are guaranteed supported with a matching data layout. - Compute-shader blit (`textureLoadGeneral` / `encodeVectorInU32General`)
- WGSL helpers, newly parameterized per texel type (
TexelVector4/TexelVector2aliased tof32/u32/i32), that deterministically load each texel and encode it intodst_buf. - Toggles `UseBlitForUintTextureToBufferCopy` / `UseBlitForSintTextureToBufferCopy`
- new toggles, enabled by default on OpenGL ES, that route integer texture formats through the compute blit instead of the readback path.
Root Cause Analysis
On the OpenGL ES backend, texture-to-buffer copies for integer (Uint/Sint) texture formats relied on a glReadPixels-style readback path, but the OpenGL ES 3.1 spec only guarantees that path with a matching data layout for RGBA32Uint/RGBA32Sint; for the other integer formats the readback was not guaranteed to produce correctly-laid-out data, so bytes in the destination buffer could be left with incorrect or uninitialized contents. Because a WebGPU buffer written by a copy is expected to be fully and correctly populated and is subsequently readable by the page, this exposed uninitialized/garbage backing memory to script (an “uninitialized resource”).
The fix adds compute-shader blit code paths for all integer format/dimension combinations (1D/2D/2DArray/3D, plus cube fallback) with format-specific encoders (kEncodeRGBA8UintInU32, kEncodeRGBA8SintInU32, kEncodeRG16UintInU32, kEncodeRG16SintInU32, kEncodeRGB10A2UintInU32, and per-channel 32-bit loads such as kLoadR32Uint/kLoadRGBA32Sint) that deterministically load every texel and pack it into dst_buf. Integer formats are now routed to the compute blit on OpenGL ES except RGBA32Uint/RGBA32Sint (kept on the guaranteed glReadPixels layout) and integer cube textures in compat mode (which fall back to per-face glReadPixels because texture_cube<u32>/texture_cube<i32> lack textureSampleLevel/textureLoad).
Attack Path
- Create an integer-format texture
A page creates a WebGPU texture in an affected integer format (e.g.
R8Uint,RG16Sint,RGB10A2Uint) on a device using the OpenGL ES backend. - Copy texture to buffer
The page issues a
copyTextureToBuffer(BlitTextureToBuffer) whose destination buffer is mappable/readable back to script. - Unreliable readback
Pre-patch, the copy uses the
glReadPixelspath that is not guaranteed for that format’s data layout on OpenGL ES, leaving some destination bytes incorrect or uninitialized rather than the intended texel data. - Read uninitialized memory
The page maps and reads the destination buffer, observing uninitialized/garbage backing memory (an information disclosure of uninitialized GPU/driver memory contents). Any cross-origin significance of the leaked bytes is
(inferred).
Impact Assessment
Toggles, CommandBufferGL, PhysicalDeviceGL, CommandEncoder, Buffer, Texture) are truncated in the diff and their exact roles are not shown.Changed Functions
| Function | Change | Notes |
|---|---|---|
switchsrc/dawn/native/BlitTextureToBuffer.cpp |
modified |
Files Changed
src/dawn/native/opengl/CommandBufferGL.cppsrc/dawn/native/Toggles.hsrc/dawn/native/opengl/PhysicalDeviceGL.cppsrc/dawn/native/BlitTextureToBuffer.cppsrc/dawn/native/Toggles.cppsrc/dawn/native/CommandEncoder.cppsrc/dawn/native/Buffer.cppsrc/dawn/tests/end2end/CopyTests.cppsrc/dawn/native/Texture.cpp
Audit Directions
- Backend readback paths not guaranteed by specAudit texture-to-buffer / copy paths that rely on
glReadPixels(or analogous driver readbacks) for formats/layouts the backend spec does not guarantee, since they may leave destination buffers partially or wrongly written. - Destination buffers not fully written by a copyReview copy emulations for format/dimension combinations that fall through to a default or skip, leaving mappable buffer memory uninitialized and readable by script.
- Integer / less-common format coverageCheck that
Uint/Sint, cube,1D, and3Dvariants are handled in every copy/blit switch, as these are frequent gaps that expose uninitialized resources.
Patch
From 4d90aa23afd490070ac38b6244aa875418a201b0 Mon Sep 17 00:00:00 2001 From: Shrek Shao <[email protected]> Date: Thu, 17 Sep 2026 14:03:27 -0700 Subject: [PATCH] [gl] Add compute blit for Sint and Uint texture formats This CL: - Adds toggles UseBlitForUintTextureToBufferCopy and UseBlitForSintTextureToBufferCopy, enabled by default on OpenGLES. - Implements compute shader blits for integer (Uint and Sint) textures in BlitTextureToBuffer. - Routes integer formats to compute shader blits on OpenGLES except RGBA32Uint and RGBA32Sint, which are the only integer formats guaranteed by the OpenGL ES 3.1 spec to be supported by glReadPixels with matching data layout. - Bypasses compute shader blit for integer cube textures in compat mode. In WGSL, texture_cube<u32> and texture_cube<i32> do not support textureSampleLevel or textureLoad, and textureGather only operates on mip level 0 for a single component. Therefore, integer cube textures fall back to glReadPixels via FramebufferTexture2D for each cube face. - Expands CopyTests_T2B and CopyTests_T2B_Compat format parameters to cover integer formats. Bug: 556959073, 562077184 Change-Id: Ica78ec08b6702ea386c7d95e36b5f2fd43a1b02b TAG=agy CONV=8a0c15f2-fbb5-4cb0-bd81-240982407cd0 Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/340755 Reviewed-by: Geoff Lang <[email protected]> Commit-Queue: Shrek Shao <[email protected]> --- diff --git a/src/dawn/native/BlitTextureToBuffer.cpp b/src/dawn/native/BlitTextureToBuffer.cpp index fcf48af..2f7b30d 100644 --- a/src/dawn/native/BlitTextureToBuffer.cpp +++ b/src/dawn/native/BlitTextureToBuffer.cpp @@ -129,7 +129,15 @@ @group(0) @binding(0) var src_tex : texture_cube<f32>; ); -constexpr std::string_view kUintTexture = DAWN_MULTILINE( +constexpr std::string_view kUintTexture1D = DAWN_MULTILINE( + fn textureLoadGeneral(tex: texture_1d<u32>, coords: vec3u, level: u32) -> vec4<u32> { + // + return textureLoad(tex, coords.x, level); + } + @group(0) @binding(0) var src_tex : texture_1d<u32>; +); + +constexpr std::string_view kUintTexture2D = DAWN_MULTILINE( fn textureLoadGeneral(tex: texture_2d<u32>, coords: vec3u, level: u32) -> vec4<u32> { // return textureLoad(tex, coords.xy, level); @@ -137,7 +145,7 @@ @group(0) @binding(0) var src_tex : texture_2d<u32>; ); -constexpr std::string_view kUintTextureArray = DAWN_MULTILINE( +constexpr std::string_view kUintTexture2DArray = DAWN_MULTILINE( fn textureLoadGeneral(tex: texture_2d_array<u32>, coords: vec3u, level: u32) -> vec4<u32> { // return textureLoad(tex, coords.xy, coords.z, level); @@ -145,6 +153,14 @@ @group(0) @binding(0) var src_tex : texture_2d_array<u32>; ); +constexpr std::string_view kUintTexture3D = DAWN_MULTILINE( + fn textureLoadGeneral(tex: texture_3d<u32>, coords: vec3u, level: u32) -> vec4<u32> { + // + return textureLoad(tex, coords, level); + } + @group(0) @binding(0) var src_tex : texture_3d<u32>; +); + // textureSampleLevel doesn't support texture_cube<u32> // Use textureGather as a workaround. // Always choose the texel with the smallest coord (stored in w component). @@ -158,6 +174,58 @@ @group(0) @binding(0) var src_tex : texture_cube<u32>; ); +constexpr std::string_view kSintTexture1D = DAWN_MULTILINE( + fn textureLoadGeneral(tex: texture_1d<i32>, coords: vec3u, level: u32) -> vec4<i32> { + // + return textureLoad(tex, coords.x, level); + } + @group(0) @binding(0) var src_tex : texture_1d<i32>; +); + +constexpr std::string_view kSintTexture2D = DAWN_MULTILINE( + fn textureLoadGeneral(tex: texture_2d<i32>, coords: vec3u, level: u32) -> vec4<i32> { + // + return textureLoad(tex, coords.xy, level); + } + @group(0) @binding(0) var src_tex : texture_2d<i32>; +); + +constexpr std::string_view kSintTexture2DArray = DAWN_MULTILINE( + fn textureLoadGeneral(tex: texture_2d_array<i32>, coords: vec3u, level: u32) -> vec4<i32> { + // + return textureLoad(tex, coords.xy, coords.z, level); + } + @group(0) @binding(0) var src_tex : texture_2d_array<i32>; +); + +constexpr std::string_view kSintTexture3D = DAWN_MULTILINE( + fn textureLoadGeneral(tex: texture_3d<i32>, coords: vec3u, level: u32) -> vec4<i32> { + // + return textureLoad(tex, coords, level); + } + @group(0) @binding(0) var src_tex : texture_3d<i32>; +); + +constexpr std::string_view kTexelVector4F32 = DAWN_MULTILINE( + alias TexelVector4 = vec4f; +); +constexpr std::string_view kTexelVector4U32 = DAWN_MULTILINE( + alias TexelVector4 = vec4<u32>; +); +constexpr std::string_view kTexelVector4I32 = DAWN_MULTILINE( + alias TexelVector4 = vec4<i32>; +); + +constexpr std::string_view kTexelVector2F32 = DAWN_MULTILINE( + alias TexelVector2 = vec2f; +); +constexpr std::string_view kTexelVector2U32 = DAWN_MULTILINE( + alias TexelVector2 = vec2<u32>; +); +constexpr std::string_view kTexelVector2I32 = DAWN_MULTILINE( + alias TexelVector2 = vec2<i32>; +); + constexpr std::string_view kEncodeRGBA8UnormInU32 = DAWN_MULTILINE( fn encodeVectorInU32General(v: vec4f) -> u32 { // @@ -172,6 +240,20 @@ } ); +constexpr std::string_view kEncodeRGBA8UintInU32 = DAWN_MULTILINE( + fn encodeVectorInU32General(v: vec4<u32>) -> u32 { + // + return pack4xU8(v); + } +); + +constexpr std::string_view kEncodeRGBA8SintInU32 = DAWN_MULTILINE( + fn encodeVectorInU32General(v: vec4<i32>) -> u32 { + // + return pack4xI8(v); + } +); + // Storing and swizzling bgra8unorm texel values and convert to u32. constexpr std::string_view kEncodeBGRA8UnormInU32 = DAWN_MULTILINE( fn encodeVectorInU32General(v: vec4f) -> u32 { @@ -187,6 +269,27 @@ } ); +constexpr std::string_view kEncodeRG16UintInU32 = DAWN_MULTILINE( + fn encodeVectorInU32General(v: vec2<u32>) -> u32 { + // + return (v.x & 0xffffu) | ((v.y & 0xffffu) << 16u); + } +); + +constexpr std::string_view kEncodeRG16SintInU32 = DAWN_MULTILINE( + fn encodeVectorInU32General(v: vec2<i32>) -> u32 { + // + return (bitcast<u32>(v.x) & 0xffffu) | ((bitcast<u32>(v.y) & 0xffffu) << 16u); + } +); + +constexpr std::string_view kEncodeRGB10A2UintInU32 = DAWN_MULTILINE( + fn encodeVectorInU32General(v: vec4<u32>) -> u32 { + // + return (v.r & 0x3ffu) | ((v.g & 0x3ffu) << 10u) | ((v.b & 0x3ffu) << 20u) | ((v.a & 0x3u) << 30u); + } +); + // Keep in sync with the WGSL struct below. struct Params { // copyExtent @@ -396,9 +499,9 @@ constexpr std::string_view kPackR8ToU32 = DAWN_MULTILINE( // Result bits to store into dst_buf var result: u32 = 0u; - // Storing xnorm8 texel values - // later called by pack4x8xnorm to convert to u32. - var v: vec4<f32>; + // Storing texel values + // later called by encodeVectorInU32General to convert to u32. + var v: TexelVector4; // dstBuf value is used for starting part. var mask: u32 = 0xffffffffu; @@ -534,9 +637,9 @@ constexpr std::string_view kPackRG8ToU32 = DAWN_MULTILINE( // Result bits to store into dst_buf var result: u32 = 0u; - // Storing snorm8 texel values - // later called by pack4x8xnorm to convert to u32. - var v: vec4<f32>; + // Storing texel values + // later called by encodeVectorInU32General to convert to u32. + var v: TexelVector4; // dstBuf value is used for starting part. var mask: u32 = 0xffffffffu; @@ -569,9 +672,9 @@ constexpr std::string_view kPackR16ToU32 = DAWN_MULTILINE( // Result bits to store into dst_buf var result: u32 = 0u; - // Storing half texel values - // later called by pack2x16unorm to convert to u32. - var v: vec2f; + // Storing half/integer texel values + // later called by encodeVectorInU32General to convert to u32. + var v: TexelVector2; // dstBuf value is used for starting part. var mask: u32 = 0xffffffffu; @@ -598,13 +701,13 @@ ); constexpr std::string_view kPackRG16ToU32 = DAWN_MULTILINE( // - let v: vec2f = textureLoadGeneral(src_tex, coord0, params.mipLevel).rg; + let v = textureLoadGeneral(src_tex, coord0, params.mipLevel).rg; let result = encodeVectorInU32General(v); ); // Load RGBA16 and pack to 2 uint4_t constexpr std::string_view kLoadRGBA16ToU32 = DAWN_MULTILINE( - let v: vec4f = textureLoadGeneral(src_tex, coord0, params.mipLevel); + let v = textureLoadGeneral(src_tex, coord0, params.mipLevel); // dstOffset is based on 8 bytes so we need to multiply by 2 to get uint32 offset. let uintOffset = dstOffset << 1; dst_buf[uintOffset] = encodeVectorInU32General(v.rg); @@ -747,6 +850,47 @@ dst_buf[uintOffset + 3u] = v.a; ); +constexpr std::string_view kLoadR32Uint = DAWN_MULTILINE( // + dst_buf[dstOffset] = textureLoadGeneral(src_tex, coord0, params.mipLevel).r; +); +constexpr std::string_view kLoadR32Sint = DAWN_MULTILINE( // + dst_buf[dstOffset] = bitcast<u32>(textureLoadGeneral(src_tex, coord0, params.mipLevel).r); +); +constexpr std::string_view kLoadRG32Uint = + DAWN_MULTILINE( + let v = textureLoadGeneral(src_tex, coord0, params.mipLevel); + // dstOffset is based on 8 bytes so we need to multiply by 2. + let uintOffset = dstOffset << 1; + dst_buf[uintOffset] = v.r; + dst_buf[uintOffset + 1u] = v.g; +); +constexpr std::string_view kLoadRG32Sint = + DAWN_MULTILINE( + let v = textureLoadGeneral(src_tex, coord0, params.mipLevel); + // dstOffset is based on 8 bytes so we need to multiply by 2. + let uintOffset = dstOffset << 1; + dst_buf[uintOffset] = bitcast<u32>(v.r); + dst_buf[uintOffset + 1u] = bitcast<u32>(v.g); +); +constexpr std::string_view kLoadRGBA32Uint = DAWN_MULTILINE( // + let v = textureLoadGeneral(src_tex, coord0, params.mipLevel); + // dstOffset is based on 16 bytes so we need to multiply by 4. + let uintOffset = dstOffset << 2; + dst_buf[uintOffset] = v.r; + dst_buf[uintOffset + 1u] = v.g; + dst_buf[uintOffset + 2u] = v.b; + dst_buf[uintOffset + 3u] = v.a; +); +constexpr std::string_view kLoadRGBA32Sint = DAWN_MULTILINE( // + let v = textureLoadGeneral(src_tex, coord0, params.mipLevel); + // dstOffset is based on 16 bytes so we need to multiply by 4. + let uintOffset = dstOffset << 2; + dst_buf[uintOffset] = bitcast<u32>(v.r); + dst_buf[uintOffset + 1u] = bitcast<u32>(v.g); + dst_buf[uintOffset + 2u] = bitcast<u32>(v.b); + dst_buf[uintOffset + 3u] = bitcast<u32>(v.a); +); + ResultOrError<Ref<ComputePipelineBase>> GetOrCreateTextureToBufferPipeline( DeviceBase* device, const TextureCopy& src, @@ -793,10 +937,10 @@ switch (viewDimension) { // Stencil cannot have e1D texture. case wgpu::TextureViewDimension::e2D: - shader += kUintTexture; + shader += kUintTexture2D; break; case wgpu::TextureViewDimension::e2DArray: - shader += kUintTextureArray; + shader += kUintTexture2DArray; break; case wgpu::TextureViewDimension::Cube: shader += kCubeCoordCommon; @@ -806,6 +950,42 @@ DAWN_UNREACHABLE(); } }; + auto AppendUintTextureHead = [&]() { + switch (viewDimension) { + case wgpu::TextureViewDimension::e1D: + shader += kUintTexture1D; + break; + case wgpu::TextureViewDimension::e2D: + shader += kUintTexture2D; + break; + case wgpu::TextureViewDimension::e2DArray: + shader += kUintTexture2DArray; + break; + case wgpu::TextureViewDimension::e3D: + shader += kUintTexture3D; + break; + default: + DAWN_UNREACHABLE(); + } + }; + auto AppendSintTextureHead = [&]() { + switch (viewDimension) { + case wgpu::TextureViewDimension::e1D: + shader += kSintTexture1D; + break; + case wgpu::TextureViewDimension::e2D: + shader += kSintTexture2D; + break; + case wgpu::TextureViewDimension::e2DArray: + shader += kSintTexture2DArray; + break; + case wgpu::TextureViewDimension::e3D: + shader += kSintTexture3D; + break; + default: + DAWN_UNREACHABLE(); + } + }; switch (format.format) { case wgpu::TextureFormat::R8Snorm: @@ -813,6 +993,7 @@ AppendFloatTextureHead(); shader += kDstBufferU32; shader += format.IsSnorm() ? kEncodeRGBA8SnormInU32 : kEncodeRGBA8UnormInU32; + shader += kTexelVector4F32; shader += kCommonHead; shader += kNonMultipleOf4OffsetStart; shader += kPackR8ToU32; @@ -824,6 +1005,7 @@ AppendFloatTextureHead(); shader += kDstBufferU32; shader += format.IsSnorm() ? kEncodeRGBA8SnormInU32 : kEncodeRGBA8UnormInU32; + shader += kTexelVector4F32; shader += kCommonHead; shader += kNonMultipleOf4OffsetStart; shader += kPackRG8ToU32; @@ -841,6 +1023,70 @@ shader += kCommonEnd; textureSampleType = wgpu::TextureSampleType::Float; break; + case wgpu::TextureFormat::R8Uint: + AppendUintTextureHead(); + shader += kDstBufferU32; + shader += kEncodeRGBA8UintInU32; + shader += kTexelVector4U32; + shader += kCommonHead; + shader += kNonMultipleOf4OffsetStart; + shader += kPackR8ToU32; + shader += kCommonEnd; + textureSampleType = wgpu::TextureSampleType::Uint; + break; + case wgpu::TextureFormat::R8Sint: + AppendSintTextureHead(); + shader += kDstBufferU32; + shader += kEncodeRGBA8SintInU32; + shader += kTexelVector4I32; + shader += kCommonHead; + shader += kNonMultipleOf4OffsetStart; + shader += kPackR8ToU32; + shader += kCommonEnd; + textureSampleType = wgpu::TextureSampleType::Sint; + break; + case wgpu::TextureFormat::RG8Uint: + AppendUintTextureHead(); + shader += kDstBufferU32; + shader += kEncodeRGBA8UintInU32; + shader += kTexelVector4U32; + shader += kCommonHead; + shader += kNonMultipleOf4OffsetStart; + shader += kPackRG8ToU32; + shader += kCommonEnd; + textureSampleType = wgpu::TextureSampleType::Uint; + break; + case wgpu::TextureFormat::RG8Sint: + AppendSintTextureHead(); + shader += kDstBufferU32; + shader += kEncodeRGBA8SintInU32; + shader += kTexelVector4I32; ... (file diff truncated)
Regression Test / PoC
diff --git a/src/dawn/tests/end2end/CopyTests.cpp b/src/dawn/tests/end2end/CopyTests.cpp
index 7dfc35e..5daed79 100644
--- a/src/dawn/tests/end2end/CopyTests.cpp
+++ b/src/dawn/tests/end2end/CopyTests.cpp
@@ -2238,31 +2238,36 @@
wgpu::TextureFormat::RGBA8Unorm,
+ // Also covers OpenGL compat Toggle::UseBlitForUintTextureToBufferCopy and
+ // Toggle::UseBlitForSintTextureToBufferCopy
wgpu::TextureFormat::R8Uint,
wgpu::TextureFormat::R8Sint,
-
+ wgpu::TextureFormat::RG8Uint,
+ wgpu::TextureFormat::RG8Sint,
+ wgpu::TextureFormat::RGBA8Uint,
+ wgpu::TextureFormat::RGBA8Sint,
wgpu::TextureFormat::R16Uint,
wgpu::TextureFormat::R16Sint,
- wgpu::TextureFormat::R16Float,
-
wgpu::TextureFormat::RG16Uint,
wgpu::TextureFormat::RG16Sint,
- wgpu::TextureFormat::RG16Float,
-
+ wgpu::TextureFormat::RGBA16Uint,
+ wgpu::TextureFormat::RGBA16Sint,
wgpu::TextureFormat::R32Uint,
wgpu::TextureFormat::R32Sint,
+ wgpu::TextureFormat::RG32Uint,
+ wgpu::TextureFormat::RG32Sint,
+ wgpu::TextureFormat::RGB10A2Uint,
+
+ wgpu::TextureFormat::RGBA32Uint,
+ wgpu::TextureFormat::RGBA32Sint,
// Also covers OpenGL compat Toggle::UseBlitForNonRGBAFloatTextureToBufferCopy
+ wgpu::TextureFormat::R16Float,
+ wgpu::TextureFormat::RG16Float,
wgpu::TextureFormat::R32Float,
wgpu::TextureFormat::RG32Float,
- wgpu::TextureFormat::RG32Uint,
- wgpu::TextureFormat::RG32Sint,
-
- wgpu::TextureFormat::RGBA16Uint,
- wgpu::TextureFormat::RGBA16Sint,
wgpu::TextureFormat::RGBA16Float,
-
wgpu::TextureFormat::RGBA32Float,
wgpu::TextureFormat::RGB10A2Unorm,
@@ -2456,6 +2461,28 @@
// Testing OpenGL compat Toggle::UseBlitForRGB9E5UfloatTextureCopy
wgpu::TextureFormat::RGB9E5Ufloat,
+
+ // Testing OpenGL compat Toggle::UseBlitForUintTextureToBufferCopy and
+ // Toggle::UseBlitForSintTextureToBufferCopy
+ wgpu::TextureFormat::R8Uint,
+ wgpu::TextureFormat::R8Sint,
+ wgpu::TextureFormat::RG8Uint,
+ wgpu::TextureFormat::RG8Sint,
+ wgpu::TextureFormat::RGBA8Uint,
+ wgpu::TextureFormat::RGBA8Sint,
+
+ wgpu::TextureFormat::R16Sint,
+ wgpu::TextureFormat::RG16Sint,
+ wgpu::TextureFormat::RGBA16Sint,
+
+ wgpu::TextureFormat::R32Uint,
+ wgpu::TextureFormat::R32Sint,
+ wgpu::TextureFormat::RG32Uint,
+ wgpu::TextureFormat::RG32Sint,
+ wgpu::TextureFormat::RGBA32Uint,
+ wgpu::TextureFormat::RGBA32Sint,
+
+ wgpu::TextureFormat::RGB10A2Uint,
});
// Test that copying an entire texture with 256-byte aligned dimensions works