CVE-2026-10976
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifsrc/dawn/native/opengl/UtilsGL.cpp |
modified | |
ifsrc/dawn/tests/end2end/CopyTests.cpp |
modified | |
CopyTests_T2T_Compatsrc/dawn/tests/end2end/CopyTests.cpp |
modified | |
TEST_Psrc/dawn/tests/end2end/CopyTests.cpp |
modified | |
forsrc/dawn/tests/end2end/CopyTests.cpp |
modified |
Files Changed
src/dawn/native/opengl/UtilsGL.cppsrc/dawn/tests/end2end/CopyTests.cpp
Patch
From edd70be7add4999d00965d16f6a7ca173334fff3 Mon Sep 17 00:00:00 2001
From: Brandon Jones <bajones@chromium.org>
Date: Fri, 15 May 2026 11:53:11 -0700
Subject: [PATCH] OpenGL: Fix copy offsets for cubemaps
Ensures that copies to and from cubemaps take the source and dest
z origin into account when performing the copy.
Bug: 513249847
Fixes: 513249847
Change-Id: Ie6772184dfec4d9ed7ec7b23afb814670ebb47fd
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/308838
Reviewed-by: Stephen White <senorblanco@chromium.org>
Commit-Queue: Brandon Jones <bajones@chromium.org>
---
diff --git a/src/dawn/native/opengl/UtilsGL.cpp b/src/dawn/native/opengl/UtilsGL.cpp
index 42a161c..32eccdc 100644
--- a/src/dawn/native/opengl/UtilsGL.cpp
+++ b/src/dawn/native/opengl/UtilsGL.cpp
@@ -140,6 +140,10 @@
if (srcTarget == GL_TEXTURE_2D) {
DAWN_GL_TRY(gl, FramebufferTexture2D(GL_READ_FRAMEBUFFER, glAttachment, srcTarget,
srcHandle, srcLevel));
+ } else if (srcTarget == GL_TEXTURE_CUBE_MAP) {
+ GLenum target = GL_TEXTURE_CUBE_MAP_POSITIVE_X + src.z + layer;
+ DAWN_GL_TRY(gl, FramebufferTexture2D(GL_READ_FRAMEBUFFER, glAttachment, target,
+ srcHandle, srcLevel));
} else {
DAWN_GL_TRY(gl, FramebufferTextureLayer(GL_READ_FRAMEBUFFER, glAttachment,
srcHandle, srcLevel, src.z + layer));
@@ -148,7 +152,7 @@
DAWN_GL_TRY(gl, FramebufferTexture2D(GL_DRAW_FRAMEBUFFER, glAttachment, dstTarget,
dstHandle, dstLevel));
} else if (dstTarget == GL_TEXTURE_CUBE_MAP) {
- GLenum target = GL_TEXTURE_CUBE_MAP_POSITIVE_X + layer;
+ GLenum target = GL_TEXTURE_CUBE_MAP_POSITIVE_X + dst.z + layer;
DAWN_GL_TRY(gl, FramebufferTexture2D(GL_DRAW_FRAMEBUFFER, glAttachment, target,
dstHandle, dstLevel));
} else {
diff --git a/src/dawn/tests/end2end/CopyTests.cpp b/src/dawn/tests/end2end/CopyTests.cpp
index 5d7a02b..64da8d2 100644
--- a/src/dawn/tests/end2end/CopyTests.cpp
+++ b/src/dawn/tests/end2end/CopyTests.cpp
@@ -813,12 +813,16 @@
return {wgpu::FeatureName::DawnInternalUsages};
}
- void DoTest(const TextureSpec& srcSpec,
- const TextureSpec& dstSpec,
- const wgpu::Extent3D& copySize,
- wgpu::TextureDimension srcDimension,
- wgpu::TextureDimension dstDimension,
- bool copyWithinSameTexture = false) {
+ void DoTest(
+ const TextureSpec& srcSpec,
+ const TextureSpec& dstSpec,
+ const wgpu::Extent3D& copySize,
+ wgpu::TextureDimension srcDimension,
+ wgpu::TextureDimension dstDimension,
+ bool copyWithinSameTexture = false,
+ wgpu::TextureViewDimension srcBindingViewDimension = wgpu::TextureViewDimension::Undefined,
+ wgpu::TextureViewDimension dstBindingViewDimension =
+ wgpu::TextureViewDimension::Undefined) {
const wgpu::TextureFormat format = srcSpec.format;
wgpu::TextureDescriptor srcDescriptor;
@@ -828,6 +832,15 @@
srcDescriptor.format = format;
srcDescriptor.mipLevelCount = srcSpec.levelCount;
srcDescriptor.usage = wgpu::TextureUsage::CopyDst | wgpu::TextureUsage::CopySrc;
+
+ // Test cube texture copy for compat.
+ wgpu::TextureBindingViewDimension srcTextureBindingViewDimensionDesc;
+ if (srcBindingViewDimension != wgpu::TextureViewDimension::Undefined) {
+ srcTextureBindingViewDimensionDesc.textureBindingViewDimension =
+ srcBindingViewDimension;
+ srcDescriptor.nextInChain = &srcTextureBindingViewDimensionDesc;
+ }
+
wgpu::Texture srcTexture = this->device.CreateTexture(&srcDescriptor);
wgpu::Texture dstTexture;
@@ -841,6 +854,15 @@
dstDescriptor.format = dstSpec.format;
dstDescriptor.mipLevelCount = dstSpec.levelCount;
dstDescriptor.usage = wgpu::TextureUsage::CopySrc | wgpu::TextureUsage::CopyDst;
+
+ // Test cube texture copy for compat.
+ wgpu::TextureBindingViewDimension dstTextureBindingViewDimension;
+ if (dstBindingViewDimension != wgpu::TextureViewDimension::Undefined) {
+ dstTextureBindingViewDimension.textureBindingViewDimension =
+ dstBindingViewDimension;
+ dstDescriptor.nextInChain = &dstTextureBindingViewDimension;
+ }
+
dstTexture = this->device.CreateTexture(&dstDescriptor);
}
@@ -3622,6 +3644,97 @@
{wgpu::TextureFormat::RGBA8Unorm, wgpu::TextureFormat::RGBA8UnormSrgb,
wgpu::TextureFormat::BGRA8Unorm, wgpu::TextureFormat::BGRA8UnormSrgb});
+// Test copying 2d texture arrays with binding view dimension set to cube.
+class CopyTests_T2T_Compat : public CopyTests_T2T {
+ protected:
+ void SetUp() override {
+ CopyTests_T2T::SetUp();
+ DAWN_TEST_UNSUPPORTED_IF(!IsCompatibilityMode());
+ }
+};
+
+TEST_P(CopyTests_T2T_Compat, TextureCubeToCubeOffset) {
+ constexpr uint32_t kWidth = 32;
+ constexpr uint32_t kHeight = 32;
+ constexpr uint32_t kLayers = 6;
+ constexpr uint32_t kCopyLayerCount = 2;
+
+ TextureSpec defaultTextureSpec;
+ defaultTextureSpec.textureSize = {kWidth, kHeight, kLayers};
+
+ for (uint32_t i = 0; i < kLayers - kCopyLayerCount; ++i) {
+ TextureSpec srcTextureSpec = defaultTextureSpec;
+ srcTextureSpec.copyOrigin = {0, 0, i};
+
+ for (uint32_t j = 0; j < kLayers - kCopyLayerCount; ++j) {
+ TextureSpec dstTextureSpec = defaultTextureSpec;
+ dstTextureSpec.copyOrigin = {0, 0, j};
+
+ DoTest(srcTextureSpec, dstTextureSpec, {kWidth, kHeight, kCopyLayerCount},
+ wgpu::TextureDimension::e2D, wgpu::TextureDimension::e2D, false,
+ wgpu::TextureViewDimension::Cube, wgpu::TextureViewDimension::Cube);
+ }
+ }
+}
+
+TEST_P(CopyTests_T2T_Compat, Texture2DToCubeOffset) {
+ constexpr uint32_t kWidth = 32;
+ constexpr uint32_t kHeight = 32;
+ constexpr uint32_t kLayers = 6;
+ constexpr uint32_t kCopyLayerCount = 2;
+
+ TextureSpec defaultTextureSpec;
+ defaultTextureSpec.textureSize = {kWidth, kHeight, kLayers};
+
+ for (uint32_t i = 0; i < kLayers - kCopyLayerCount; ++i) {
+ TextureSpec srcTextureSpec = defaultTextureSpec;
+ srcTextureSpec.copyOrigin = {0, 0, i};
+
+ for (uint32_t j = 0; j < kLayers - kCopyLayerCount; ++j) {
+ TextureSpec dstTextureSpec = defaultTextureSpec;
+ dstTextureSpec.copyOrigin = {0, 0, j};
+
+ DoTest(srcTextureSpec, dstTextureSpec, {kWidth, kHeight, kCopyLayerCount},
+ wgpu::TextureDimension::e2D, wgpu::TextureDimension::e2D, false,
+ wgpu::TextureViewDimension::Undefined, wgpu::TextureViewDimension::Cube);
+ }
+ }
+}
+
+TEST_P(CopyTests_T2T_Compat, TextureCubeTo2DOffset) {
+ constexpr uint32_t kWidth = 32;
+ constexpr uint32_t kHeight = 32;
+ constexpr uint32_t kLayers = 6;
+ constexpr uint32_t kCopyLayerCount = 2;
+
+ TextureSpec defaultTextureSpec;
+ defaultTextureSpec.textureSize = {kWidth, kHeight, kLayers};
+
+ for (uint32_t i = 0; i < kLayers - kCopyLayerCount; ++i) {
+ TextureSpec srcTextureSpec = defaultTextureSpec;
+ srcTextureSpec.copyOrigin = {0, 0, i};
+
+ for (uint32_t j = 0; j < kLayers - kCopyLayerCount; ++j) {
+ TextureSpec dstTextureSpec = defaultTextureSpec;
+ dstTextureSpec.copyOrigin = {0, 0, j};
+
+ DoTest(srcTextureSpec, dstTextureSpec, {kWidth, kHeight, kCopyLayerCount},
+ wgpu::TextureDimension::e2D, wgpu::TextureDimension::e2D, false,
+ wgpu::TextureViewDimension::Cube, wgpu::TextureViewDimension::Undefined);
+ }
+ }
+}
+
+DAWN_INSTANTIATE_TEST_P(CopyTests_T2T_Compat,
+ {
+ D3D11Backend(),
+ D3D11Backend({"d3d11_disable_map_on_default_buffers"}),
+ OpenGLBackend(),
+ OpenGLESBackend(),
+ OpenGLESBackend({"gl_defer"}),
+ },
+ {wgpu::TextureFormat::RGBA8Unorm, wgpu::TextureFormat::BGRA8Unorm});
+
static constexpr uint64_t kSmallBufferSize = 4;
static constexpr uint64_t kLargeBufferSize = 1 << 16;
Regression Test / PoC
diff --git a/src/dawn/tests/end2end/CopyTests.cpp b/src/dawn/tests/end2end/CopyTests.cpp
index 5d7a02b..64da8d2 100644
--- a/src/dawn/tests/end2end/CopyTests.cpp
+++ b/src/dawn/tests/end2end/CopyTests.cpp
@@ -813,12 +813,16 @@
return {wgpu::FeatureName::DawnInternalUsages};
}
- void DoTest(const TextureSpec& srcSpec,
- const TextureSpec& dstSpec,
- const wgpu::Extent3D& copySize,
- wgpu::TextureDimension srcDimension,
- wgpu::TextureDimension dstDimension,
- bool copyWithinSameTexture = false) {
+ void DoTest(
+ const TextureSpec& srcSpec,
+ const TextureSpec& dstSpec,
+ const wgpu::Extent3D& copySize,
+ wgpu::TextureDimension srcDimension,
+ wgpu::TextureDimension dstDimension,
+ bool copyWithinSameTexture = false,
+ wgpu::TextureViewDimension srcBindingViewDimension = wgpu::TextureViewDimension::Undefined,
+ wgpu::TextureViewDimension dstBindingViewDimension =
+ wgpu::TextureViewDimension::Undefined) {
const wgpu::TextureFormat format = srcSpec.format;
wgpu::TextureDescriptor srcDescriptor;
@@ -828,6 +832,15 @@
srcDescriptor.format = format;
srcDescriptor.mipLevelCount = srcSpec.levelCount;
srcDescriptor.usage = wgpu::TextureUsage::CopyDst | wgpu::TextureUsage::CopySrc;
+
+ // Test cube texture copy for compat.
+ wgpu::TextureBindingViewDimension srcTextureBindingViewDimensionDesc;
+ if (srcBindingViewDimension != wgpu::TextureViewDimension::Undefined) {
+ srcTextureBindingViewDimensionDesc.textureBindingViewDimension =
+ srcBindingViewDimension;
+ srcDescriptor.nextInChain = &srcTextureBindingViewDimensionDesc;
+ }
+
wgpu::Texture srcTexture = this->device.CreateTexture(&srcDescriptor);
wgpu::Texture dstTexture;
@@ -841,6 +854,15 @@
dstDescriptor.format = dstSpec.format;
dstDescriptor.mipLevelCount = dstSpec.levelCount;
dstDescriptor.usage = wgpu::TextureUsage::CopySrc | wgpu::TextureUsage::CopyDst;
+
+ // Test cube texture copy for compat.
+ wgpu::TextureBindingViewDimension dstTextureBindingViewDimension;
+ if (dstBindingViewDimension != wgpu::TextureViewDimension::Undefined) {
+ dstTextureBindingViewDimension.textureBindingViewDimension =
+ dstBindingViewDimension;
+ dstDescriptor.nextInChain = &dstTextureBindingViewDimension;
+ }
+
dstTexture = this->device.CreateTexture(&dstDescriptor);
}
@@ -3622,6 +3644,97 @@
{wgpu::TextureFormat::RGBA8Unorm, wgpu::TextureFormat::RGBA8UnormSrgb,
wgpu::TextureFormat::BGRA8Unorm, wgpu::TextureFormat::BGRA8UnormSrgb});
+// Test copying 2d texture arrays with binding view dimension set to cube.
+class CopyTests_T2T_Compat : public CopyTests_T2T {
+ protected:
+ void SetUp() override {
+ CopyTests_T2T::SetUp();
+ DAWN_TEST_UNSUPPORTED_IF(!IsCompatibilityMode());
+ }
+};
+
+TEST_P(CopyTests_T2T_Compat, TextureCubeToCubeOffset) {
+ constexpr uint32_t kWidth = 32;
+ constexpr uint32_t kHeight = 32;
+ constexpr uint32_t kLayers = 6;
+ constexpr uint32_t kCopyLayerCount = 2;
+
+ TextureSpec defaultTextureSpec;
+ defaultTextureSpec.textureSize = {kWidth, kHeight, kLayers};
+
+ for (uint32_t i = 0; i < kLayers - kCopyLayerCount; ++i) {
+ TextureSpec srcTextureSpec = defaultTextureSpec;
+ srcTextureSpec.copyOrigin = {0, 0, i};
+
+ for (uint32_t j = 0; j < kLayers - kCopyLayerCount; ++j) {
+ TextureSpec dstTextureSpec = defaultTextureSpec;
+ dstTextureSpec.copyOrigin = {0, 0, j};
+
+ DoTest(srcTextureSpec, dstTextureSpec, {kWidth, kHeight, kCopyLayerCount},
+ wgpu::TextureDimension::e2D, wgpu::TextureDimension::e2D, false,
+ wgpu::TextureViewDimension::Cube, wgpu::TextureViewDimension::Cube);
+ }
+ }
+}
+
+TEST_P(CopyTests_T2T_Compat, Texture2DToCubeOffset) {
+ constexpr uint32_t kWidth = 32;
+ constexpr uint32_t kHeight = 32;
+ constexpr uint32_t kLayers = 6;
+ constexpr uint32_t kCopyLayerCount = 2;
+
+ TextureSpec defaultTextureSpec;
+ defaultTextureSpec.textureSize = {kWidth, kHeight, kLayers};
+
+ for (uint32_t i = 0; i < kLayers - kCopyLayerCount; ++i) {
+ TextureSpec srcTextureSpec = defaultTextureSpec;
+ srcTextureSpec.copyOrigin = {0, 0, i};
+
+ for (uint32_t j = 0; j < kLayers - kCopyLayerCount; ++j) {
+ TextureSpec dstTextureSpec = defaultTextureSpec;
+ dstTextureSpec.copyOrigin = {0, 0, j};
+
+ DoTest(srcTextureSpec, dstTextureSpec, {kWidth, kHeight, kCopyLayerCount},
+ wgpu::TextureDimension::e2D, wgpu::TextureDimension::e2D, false,
+ wgpu::TextureViewDimension::Undefined, wgpu::TextureViewDimension::Cube);
+ }
+ }
+}
+
+TEST_P(CopyTests_T2T_Compat, TextureCubeTo2DOffset) {
+ constexpr uint32_t kWidth = 32;
+ constexpr uint32_t kHeight = 32;
+ constexpr uint32_t kLayers = 6;
+ constexpr uint32_t kCopyLayerCount = 2;
+
+ TextureSpec defaultTextureSpec;
+ defaultTextureSpec.textureSize = {kWidth, kHeight, kLayers};
+
+ for (uint32_t i = 0; i < kLayers - kCopyLayerCount; ++i) {
+ TextureSpec srcTextureSpec = defaultTextureSpec;
+ srcTextureSpec.copyOrigin = {0, 0, i};
+
+ for (uint32_t j = 0; j < kLayers - kCopyLayerCount; ++j) {
+ TextureSpec dstTextureSpec = defaultTextureSpec;
+ dstTextureSpec.copyOrigin = {0, 0, j};
+
+ DoTest(srcTextureSpec, dstTextureSpec, {kWidth, kHeight, kCopyLayerCount},
+ wgpu::TextureDimension::e2D, wgpu::TextureDimension::e2D, false,
+ wgpu::TextureViewDimension::Cube, wgpu::TextureViewDimension::Undefined);
+ }
+ }
+}
+
+DAWN_INSTANTIATE_TEST_P(CopyTests_T2T_Compat,
+ {
+ D3D11Backend(),
+ D3D11Backend({"d3d11_disable_map_on_default_buffers"}),
+ OpenGLBackend(),
+ OpenGLESBackend(),
+ OpenGLESBackend({"gl_defer"}),
+ },
+ {wgpu::TextureFormat::RGBA8Unorm, wgpu::TextureFormat::BGRA8Unorm});
+
static constexpr uint64_t kSmallBufferSize = 4;
static constexpr uint64_t kLargeBufferSize = 1 << 16;
Original Bug Report
Information Leak in Dawn OpenGL Backend due to Incorrect Cube Map Blit Fallback
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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A logic error in Dawn’s OpenGL ES 3.1 blit fallback for cube maps can result in destination subresources being incorrectly marked as initialized. This leads to a potential cross-origin information leak of uninitialized GPU memory when reading back texture data.
Affected files:
third_party/dawn/src/dawn/native/opengl/UtilsGL.cppthird_party/dawn/src/dawn/native/opengl/CommandBufferGL.cppthird_party/dawn/src/dawn/native/CommandBuffer.cppthird_party/dawn/src/dawn/native/opengl/TextureGL.cpp
Estimated timestamp from git blame: 2022-03-25
Summary
A potential cross-origin information leak exists in Dawn’s OpenGL backend when utilizing the CopyImageSubData fallback path. This fallback is active on hardware supporting only OpenGL ES 3.1 (standard in WebGPU Compatibility Mode on many Android devices). Two logic errors in the fallback implementation result in either incorrect data being written to cube map faces or the copy operation failing entirely. Because Dawn updates the initialization state of the destination texture before the backend copy attempt, this allows a renderer to read back uninitialized GPU heap memory.
Root Cause Analysis
The issues are located in third_party/dawn/src/dawn/native/opengl/UtilsGL.cpp within the CopyImageSubData fallback (triggered when native glCopyImageSubData is unavailable).
1. Destination Face Indexing Error (Bug A)
In UtilsGL.cpp:151, the code calculates the target cube map face for the destination attachment by ignoring the dst.z offset:
GLenum target = GL_TEXTURE_CUBE_MAP_POSITIVE_X + layer;
If an attacker targets a specific face (e.g., z=3), the loop binds Face 0 to the Draw Framebuffer instead, leaving the intended face unmodified.
2. Invalid Source Attachment (Bug B)
In UtilsGL.cpp:144, if the source is a cube map, the fallback uses glFramebufferTextureLayer. On OpenGL ES 3.1, this function is only valid for 3D and 2D Array textures. Using it with a cube map generates a GL_INVALID_OPERATION error. In release builds, the DAWN_GL_TRY macro (defined in UtilsGL.h) expands to a bare call that ignores GL errors, causing the blit to silently fail or behave unpredictably.
Exploitation Path (Potential)
In third_party/dawn/src/dawn/native/opengl/CommandBufferGL.cpp:1063-1073, Dawn checks if a copy covers the full subresource. For cube maps (represented as 2D textures with layers), IsCompleteSubresourceCopiedTo only validates width and height. If these match, Dawn marks the destination subresource as initialized before calling the backend CopyImageSubData.
An attacker could potential trigger this via the following steps:
- Initialize a WebGPU device in Compatibility Mode (default on many Android GLES devices).
- Create a destination cube map texture.
- Issue a
copyTextureToTexturecommand targeting a specific face (e.g.,z=3) covering the full width and height of that face. - Due to the bugs above, Face 3 remains uninitialized, but Dawn’s metadata now considers it initialized.
- Perform a
copyTextureToBufferreadback of Face 3. Dawn skips the security-critical ’lazy clear’ (zero-fill) and returns the stale contents of the GPU memory heap.
Suggested Fix
- Correct the destination face calculation in
UtilsGL.cppto include thedst.zoffset:GLenum target = GL_TEXTURE_CUBE_MAP_POSITIVE_X + dst.z + layer;. - Ensure the source attachment binding correctly handles cube map faces on GLES 3.1 by using
glFramebufferTexture2Dwith the appropriate face target instead ofglFramebufferTextureLayer. - Consider moving the
SetIsSubresourceContentInitializedcall to occur only after a successful backend copy, or add robust error checking for the blit fallback.
Evaluated with Chrome root at commit: b3153093eb3c78c3e88ccf562bcbc20437a04b0e
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.