CVE-2026-17790
Overview
Files Changed
src/libANGLE/renderer/d3d/d3d11/Image11.cppsrc/tests/gl_tests/RobustResourceInitTest.cpp
Patch
From ef3be7c543c104ebf82db7fa8262f3ca51a338d1 Mon Sep 17 00:00:00 2001
From: wangra <wangra@google.com>
Date: Mon, 08 Jun 2026 11:46:43 -0400
Subject: [PATCH] D3D11: Fix resource type mismatch in Image11 copy
Validates that the source and destination resource types match in
copyFromFramebuffer before choosing the fast CopySubresourceRegion path.
If they mismatch, falls back to the slow conversion path.
Test: angle_end2end_tests --gtest_filter="*CopyTexSubImage3DTarget3DSource2DMatches*"
Bug: b/513919931
Change-Id: I1db80aedeeb07da570d8fe7ced822a97b692300c
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7909051
Reviewed-by: Geoff Lang <geofflang@chromium.org>
Reviewed-by: Shahbaz Youssefi <syoussefi@chromium.org>
Commit-Queue: Ran Wang <wangra@google.com>
---
diff --git a/src/libANGLE/renderer/d3d/d3d11/Image11.cpp b/src/libANGLE/renderer/d3d/d3d11/Image11.cpp
index 29f52c5..542030e 100644
--- a/src/libANGLE/renderer/d3d/d3d11/Image11.cpp
+++ b/src/libANGLE/renderer/d3d/d3d11/Image11.cpp
@@ -389,15 +389,16 @@
const auto &d3d11Format =
d3d11::Format::Get(sourceInternalFormat, mRenderer->getRenderer11DeviceCaps());
- if (d3d11Format.texFormat == mDXGIFormat && sourceInternalFormat == mInternalFormat)
+ RenderTarget11 *rt11 = nullptr;
+ ANGLE_TRY(srcAttachment->getRenderTarget(context, 0, &rt11));
+ ASSERT(rt11->getTexture().get());
+
+ TextureHelper11 textureHelper = rt11->getTexture();
+ unsigned int sourceSubResource = rt11->getSubresourceIndex();
+
+ if (d3d11Format.texFormat == mDXGIFormat && sourceInternalFormat == mInternalFormat &&
+ textureHelper.is3D() == (mType == gl::TextureType::_3D))
{
- RenderTarget11 *rt11 = nullptr;
- ANGLE_TRY(srcAttachment->getRenderTarget(context, 0, &rt11));
- ASSERT(rt11->getTexture().get());
-
- TextureHelper11 textureHelper = rt11->getTexture();
- unsigned int sourceSubResource = rt11->getSubresourceIndex();
-
const int z = textureHelper.is3D() ? srcAttachment->layer() : 0;
gl::Box sourceBox(sourceArea.x, sourceArea.y, z, sourceArea.width, sourceArea.height, 1);
return copyWithoutConversion(context, destOffset, sourceBox, textureHelper,
diff --git a/src/tests/gl_tests/RobustResourceInitTest.cpp b/src/tests/gl_tests/RobustResourceInitTest.cpp
index 3369af2..6d0436a 100644
--- a/src/tests/gl_tests/RobustResourceInitTest.cpp
+++ b/src/tests/gl_tests/RobustResourceInitTest.cpp
@@ -1264,6 +1264,43 @@
EXPECT_EQ(data, pixels);
}
+// Test that glCopyTexSubImage3D from a 2D texture to a 3D texture works correctly when formats
+// match
+TEST_P(RobustResourceInitTestES3, CopyTexSubImage3DTarget3DSource2DMatches)
+{
+ ANGLE_SKIP_TEST_IF(!hasGLExtension());
+
+ constexpr GLint kTextureLayer = 0;
+ constexpr GLint kTextureWidth = 2;
+ constexpr GLint kTextureHeight = 2;
+ constexpr GLint kTextureDepth = 2;
+ constexpr size_t kTextureDataSize = kTextureWidth * kTextureHeight * 4;
+
+ GLTexture texture2D;
+ glBindTexture(GL_TEXTURE_2D, texture2D);
+ constexpr std::array<uint8_t, kTextureDataSize> data = {{0x01, 0x02, 0x03, 0x04, 0x05, 0x06,
+ 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C,
+ 0x0D, 0x0E, 0x0F, 0x10}};
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kTextureWidth, kTextureHeight, 0, GL_RGBA,
+ GL_UNSIGNED_BYTE, data.data());
+
+ GLFramebuffer fbo;
+ glBindFramebuffer(GL_FRAMEBUFFER, fbo);
+ glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture2D, 0);
+ ASSERT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+
+ GLTexture texture3D;
+ glBindTexture(GL_TEXTURE_3D, texture3D);
+ glTexStorage3D(GL_TEXTURE_3D, 1, GL_RGBA8, kTextureWidth, kTextureHeight, kTextureDepth);
+ glCopyTexSubImage3D(GL_TEXTURE_3D, 0, 0, 0, kTextureLayer, 0, 0, kTextureWidth, kTextureHeight);
+
+ glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, texture3D, 0, kTextureLayer);
+ std::array<uint8_t, kTextureDataSize> pixels;
+ glReadPixels(0, 0, kTextureWidth, kTextureHeight, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
+ ASSERT_GL_NO_ERROR();
+ EXPECT_EQ(data, pixels);
+}
+
// Test that binding an EGL surface to a texture does not cause it to be cleared.
TEST_P(RobustResourceInitTestES3, BindTexImage)
{
Regression Test / PoC
diff --git a/src/tests/gl_tests/RobustResourceInitTest.cpp b/src/tests/gl_tests/RobustResourceInitTest.cpp
index 3369af2..6d0436a 100644
--- a/src/tests/gl_tests/RobustResourceInitTest.cpp
+++ b/src/tests/gl_tests/RobustResourceInitTest.cpp
@@ -1264,6 +1264,43 @@
EXPECT_EQ(data, pixels);
}
+// Test that glCopyTexSubImage3D from a 2D texture to a 3D texture works correctly when formats
+// match
+TEST_P(RobustResourceInitTestES3, CopyTexSubImage3DTarget3DSource2DMatches)
+{
+ ANGLE_SKIP_TEST_IF(!hasGLExtension());
+
+ constexpr GLint kTextureLayer = 0;
+ constexpr GLint kTextureWidth = 2;
+ constexpr GLint kTextureHeight = 2;
+ constexpr GLint kTextureDepth = 2;
+ constexpr size_t kTextureDataSize = kTextureWidth * kTextureHeight * 4;
+
+ GLTexture texture2D;
+ glBindTexture(GL_TEXTURE_2D, texture2D);
+ constexpr std::array<uint8_t, kTextureDataSize> data = {{0x01, 0x02, 0x03, 0x04, 0x05, 0x06,
+ 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C,
+ 0x0D, 0x0E, 0x0F, 0x10}};
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kTextureWidth, kTextureHeight, 0, GL_RGBA,
+ GL_UNSIGNED_BYTE, data.data());
+
+ GLFramebuffer fbo;
+ glBindFramebuffer(GL_FRAMEBUFFER, fbo);
+ glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture2D, 0);
+ ASSERT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+
+ GLTexture texture3D;
+ glBindTexture(GL_TEXTURE_3D, texture3D);
+ glTexStorage3D(GL_TEXTURE_3D, 1, GL_RGBA8, kTextureWidth, kTextureHeight, kTextureDepth);
+ glCopyTexSubImage3D(GL_TEXTURE_3D, 0, 0, 0, kTextureLayer, 0, 0, kTextureWidth, kTextureHeight);
+
+ glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, texture3D, 0, kTextureLayer);
+ std::array<uint8_t, kTextureDataSize> pixels;
+ glReadPixels(0, 0, kTextureWidth, kTextureHeight, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
+ ASSERT_GL_NO_ERROR();
+ EXPECT_EQ(data, pixels);
+}
+
// Test that binding an EGL surface to a texture does not cause it to be cleared.
TEST_P(RobustResourceInitTestES3, BindTexImage)
{
Original Bug Report
Potential GPU memory disclosure in ANGLE D3D11 via resource type mismatch in Image11
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. 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 ANGLE’s D3D11 backend allows WebGL2 content to read uninitialized GPU memory by triggering a mismatched resource copy. The D3D11 runtime silently drops copy operations between different resource types (e.g., Texture2D to Texture3D), while ANGLE erroneously marks the destination as initialized with valid data. This can lead to the disclosure of stale GPU memory residue from the shared GPU process on Windows.
Affected files:
third_party/angle/src/libANGLE/renderer/d3d/d3d11/Image11.cpp
Estimated timestamp from git blame: 2016-02-01
Summary
A potential information disclosure vulnerability exists in the ANGLE D3D11 backend. The issue occurs when performing a framebuffer copy (e.g., gl.copyTexSubImage3D) where the source attachment and destination texture have mismatched D3D11 resource dimensions. ANGLE fails to validate that the resource types match before calling ID3D11DeviceContext::CopySubresourceRegion, leading to a silent failure in the D3D11 runtime and the subsequent use of uninitialized staging memory.
Root Cause Analysis
In third_party/angle/src/libANGLE/renderer/d3d/d3d11/Image11.cpp, the method copyWithoutConversion issues a copy command between a source resource (determined by the read framebuffer) and a destination staging texture.
If the destination is a 3D texture and the source is a 2D renderbuffer or texture, ANGLE attempts to copy from a Texture2D to a Texture3D staging resource. According to D3D11 specifications, CopySubresourceRegion requires source and destination resources to be of the same type. When they mismatch, the D3D11 runtime silently drops the operation.
Because ANGLE does not detect this failure, it proceeds to mark the image as ‘dirty’ (mDirty = true). In production builds of Chrome, staging textures are allocated without zero-initialization (as mInitializeAllocations is typically false outside of debug environments). Consequently, when the texture is later read back via gl.readPixels, the attacker receives the stale memory residue remaining in the staging resource from the GPU driver’s allocator.
Furthermore, ANGLE’s robust resource initialization is bypassed in this scenario. If the copy covers the full extent of the destination level (e.g., a 3D texture with depth 1), the frontend marks the resource as initialized without performing a manual zero-fill, assuming the copy will overwrite all bytes.
Potential Attack Sequence
An attacker might follow these steps to trigger the issue:
- Initialize a WebGL2 context on Windows (D3D11 backend).
- Create a 2D RGBA8 Renderbuffer and attach it to a read framebuffer.
- Create a 3D RGBA8 texture with a depth of 1.
- Call
gl.copyTexSubImage3Dto copy from the 2D source to the 3D destination. The D3D11 copy is silently dropped. - Read the 3D texture back using
gl.readPixelsor by sampling it in a shader. The resulting data will contain uninitialized GPU memory residue from the shared GPU process.
Suggested Fix
In third_party/angle/src/libANGLE/renderer/d3d/d3d11/Image11.cpp, validate that the textureHelper resource type matches the mStagingTexture type before calling CopySubresourceRegion. If they mismatch, the staging texture should be redefined to match the source type, or a fallback copy path should be used. A similar check is already implemented in Buffer11.cpp to prevent this class of issue.
Evaluated with Chrome root at commit: 1a8d40fc44df2088d5945c0bf53584038aa1614a
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.