CVE-2026-7340
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
BPTCCompressedTextureTestES3WebGLsrc/tests/gl_tests/BPTCCompressedTextureTest.cpp |
modified |
Files Changed
src/libANGLE/renderer/d3d/TextureD3D.cppsrc/tests/gl_tests/BPTCCompressedTextureTest.cpp
Patch
From 838c9be2bc21df9ab804428d53bf61fa906be4b4 Mon Sep 17 00:00:00 2001
From: Shrek Shao <shrekshao@google.com>
Date: Thu, 02 Apr 2026 15:41:06 -0700
Subject: [PATCH] D3D11: Fix overflow in compressed 3D texture deferred-init
The ANGLE D3D11 backend computed the zero-fill buffer size for
block-compressed 3D textures using an unchecked
32-bit multiplication that did not account for block height.
This produced a value four times larger than the actual
compressed image size and could overflow for sufficiently large
textures.
This fix:
1. Uses InternalFormat::computeCompressedImageSize to correctly
calculate the required buffer size for compressed textures,
taking block dimensions into account.
2. Employs angle::CheckedNumeric for uncompressed texture size
calculations to prevent potential integer overflows.
3. Adds a regression test DeferredInit3DOverflow
that uses large texture dimensions (2048x2048x320) to verify
there is no ASAN error.
Bug: b/497896137
Change-Id: I76f0fb008af34e8ac78870318e608d16ed4ddd93
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7728091
Reviewed-by: Shahbaz Youssefi <syoussefi@chromium.org>
Commit-Queue: Shahbaz Youssefi <syoussefi@chromium.org>
Auto-Submit: Shrek Shao <shrekshao@google.com>
---
diff --git a/src/libANGLE/renderer/d3d/TextureD3D.cpp b/src/libANGLE/renderer/d3d/TextureD3D.cpp
index 6d36548..be59fad 100644
--- a/src/libANGLE/renderer/d3d/TextureD3D.cpp
+++ b/src/libANGLE/renderer/d3d/TextureD3D.cpp
@@ -947,9 +947,23 @@
const auto &formatInfo = gl::GetSizedInternalFormatInfo(image->getInternalFormat());
GLuint imageBytes = 0;
- ANGLE_CHECK_GL_MATH(contextD3D, formatInfo.computeRowPitch(formatInfo.type, image->getWidth(),
- 1, 0, &imageBytes));
- imageBytes *= image->getHeight() * image->getDepth();
+ if (formatInfo.compressed)
+ {
+ ANGLE_CHECK_GL_MATH(
+ contextD3D, formatInfo.computeCompressedImageSize(
+ gl::Extents(image->getWidth(), image->getHeight(), image->getDepth()),
+ &imageBytes));
+ }
+ else
+ {
+ ANGLE_CHECK_GL_MATH(contextD3D, formatInfo.computeRowPitch(
+ formatInfo.type, image->getWidth(), 1, 0, &imageBytes));
+
+ angle::CheckedNumeric<GLuint> checkedImageBytes(imageBytes);
+ checkedImageBytes *= image->getHeight();
+ checkedImageBytes *= image->getDepth();
+ ANGLE_CHECK_GL_MATH(contextD3D, checkedImageBytes.AssignIfValid(&imageBytes));
+ }
gl::PixelUnpackState zeroDataUnpackState;
zeroDataUnpackState.alignment = 1;
diff --git a/src/tests/gl_tests/BPTCCompressedTextureTest.cpp b/src/tests/gl_tests/BPTCCompressedTextureTest.cpp
index dbac858..7368a85 100644
--- a/src/tests/gl_tests/BPTCCompressedTextureTest.cpp
+++ b/src/tests/gl_tests/BPTCCompressedTextureTest.cpp
@@ -452,3 +452,55 @@
GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(BPTCCompressedTextureTestES3);
ANGLE_INSTANTIATE_TEST_ES3(BPTCCompressedTextureTestES3);
+
+class BPTCCompressedTextureTestES3WebGL : public BPTCCompressedTextureTestES3
+{
+ protected:
+ BPTCCompressedTextureTestES3WebGL()
+ {
+ setWebGLCompatibilityEnabled(true);
+ setRobustResourceInit(true);
+ }
+};
+
+// Test that initializing a large 3D BPTC texture doesn't overflow the size calculation.
+// This is a regression test for a bug where the size was computed as (width/4 * 16) * height *
+// depth instead of (width/4 * 16) * (height/4) * depth.
+TEST_P(BPTCCompressedTextureTestES3WebGL, DeferredInit3DOverflow)
+{
+ ANGLE_SKIP_TEST_IF(!IsGLExtensionEnabled("GL_EXT_texture_compression_bptc"));
+
+ // The overflow happens in the D3D11 backend.
+ // The dimensions 2048x2048x320 were reported to trigger it.
+ // 2048/4 * 16 = 8192 (row pitch)
+ // 8192 * 2048 * 320 = 5,368,709,120, which wraps to 1,073,741,824 in 32-bit GLuint.
+ // The correct size is 1,342,177,280 (1.25 GB).
+ // Since the wrapped buggy size is smaller than the correct size, it triggers an OOB read.
+ // 1.25 GB is large enough that it might trigger GL_OUT_OF_MEMORY on some systems.
+
+ GLTexture tex;
+ glBindTexture(GL_TEXTURE_3D, tex);
+ {
+ ScopedIgnorePlatformMessages ignore;
+ glTexStorage3D(GL_TEXTURE_3D, 1, GL_COMPRESSED_RGBA_BPTC_UNORM_EXT, 2048, 2048, 320);
+ }
+ GLenum err = glGetError();
+ // Allow GL_OUT_OF_MEMORY as the texture is large.
+ ASSERT_TRUE(err == GL_NO_ERROR || err == GL_OUT_OF_MEMORY);
+
+ if (err != GL_OUT_OF_MEMORY)
+ {
+ // Trigger deferred initialization by updating a small sub-region.
+ std::vector<GLubyte> data(16, 0);
+ glCompressedTexSubImage3D(GL_TEXTURE_3D, 0, 0, 0, 0, 4, 4, 1,
+ GL_COMPRESSED_RGBA_BPTC_UNORM_EXT, 16, data.data());
+ err = glGetError();
+ EXPECT_TRUE(err == GL_NO_ERROR || err == GL_OUT_OF_MEMORY);
+ }
+}
+
+GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(BPTCCompressedTextureTestES3WebGL);
+// The overflow happens in the "slow path" of initializeContents, which is only called if
+// robust resource initialization is enabled. Since it is always enabled in WebGL, we
+// enable it here to reproduce the bug.
+ANGLE_INSTANTIATE_TEST(BPTCCompressedTextureTestES3WebGL, ES3_D3D11());
Regression Test / PoC
diff --git a/src/tests/gl_tests/BPTCCompressedTextureTest.cpp b/src/tests/gl_tests/BPTCCompressedTextureTest.cpp
index dbac858..7368a85 100644
--- a/src/tests/gl_tests/BPTCCompressedTextureTest.cpp
+++ b/src/tests/gl_tests/BPTCCompressedTextureTest.cpp
@@ -452,3 +452,55 @@
GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(BPTCCompressedTextureTestES3);
ANGLE_INSTANTIATE_TEST_ES3(BPTCCompressedTextureTestES3);
+
+class BPTCCompressedTextureTestES3WebGL : public BPTCCompressedTextureTestES3
+{
+ protected:
+ BPTCCompressedTextureTestES3WebGL()
+ {
+ setWebGLCompatibilityEnabled(true);
+ setRobustResourceInit(true);
+ }
+};
+
+// Test that initializing a large 3D BPTC texture doesn't overflow the size calculation.
+// This is a regression test for a bug where the size was computed as (width/4 * 16) * height *
+// depth instead of (width/4 * 16) * (height/4) * depth.
+TEST_P(BPTCCompressedTextureTestES3WebGL, DeferredInit3DOverflow)
+{
+ ANGLE_SKIP_TEST_IF(!IsGLExtensionEnabled("GL_EXT_texture_compression_bptc"));
+
+ // The overflow happens in the D3D11 backend.
+ // The dimensions 2048x2048x320 were reported to trigger it.
+ // 2048/4 * 16 = 8192 (row pitch)
+ // 8192 * 2048 * 320 = 5,368,709,120, which wraps to 1,073,741,824 in 32-bit GLuint.
+ // The correct size is 1,342,177,280 (1.25 GB).
+ // Since the wrapped buggy size is smaller than the correct size, it triggers an OOB read.
+ // 1.25 GB is large enough that it might trigger GL_OUT_OF_MEMORY on some systems.
+
+ GLTexture tex;
+ glBindTexture(GL_TEXTURE_3D, tex);
+ {
+ ScopedIgnorePlatformMessages ignore;
+ glTexStorage3D(GL_TEXTURE_3D, 1, GL_COMPRESSED_RGBA_BPTC_UNORM_EXT, 2048, 2048, 320);
+ }
+ GLenum err = glGetError();
+ // Allow GL_OUT_OF_MEMORY as the texture is large.
+ ASSERT_TRUE(err == GL_NO_ERROR || err == GL_OUT_OF_MEMORY);
+
+ if (err != GL_OUT_OF_MEMORY)
+ {
+ // Trigger deferred initialization by updating a small sub-region.
+ std::vector<GLubyte> data(16, 0);
+ glCompressedTexSubImage3D(GL_TEXTURE_3D, 0, 0, 0, 0, 4, 4, 1,
+ GL_COMPRESSED_RGBA_BPTC_UNORM_EXT, 16, data.data());
+ err = glGetError();
+ EXPECT_TRUE(err == GL_NO_ERROR || err == GL_OUT_OF_MEMORY);
+ }
+}
+
+GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(BPTCCompressedTextureTestES3WebGL);
+// The overflow happens in the "slow path" of initializeContents, which is only called if
+// robust resource initialization is enabled. Since it is always enabled in WebGL, we
+// enable it here to reproduce the bug.
+ANGLE_INSTANTIATE_TEST(BPTCCompressedTextureTestES3WebGL, ES3_D3D11());
Original Bug Report
Integer overflow in ANGLE D3D11 compressed 3D texture deferred-init leads to heap OOB read in GPU process
Summary
The ANGLE D3D11 backend computes the zero-fill buffer size for block-compressed 3D textures using an unchecked 32-bit multiplication that does not account for block height, producing a value four times larger than the actual compressed image size. For sufficiently large textures the product wraps around in a GLuint, and the subsequent LoadCompressedToNative memcpy reads the correct (larger) number of bytes from the undersized buffer. Any WebGL2 page can trigger this through standard API calls (texStorage3D followed by a partial compressedTexSubImage3D), causing a heap-buffer-overflow read in the GPU process with no source modifications required. Platform: Windows (D3D11 ANGLE backend). Any GPU with BPTC (BC6H/BC7) support, which is standard on all Direct3D 11 hardware.
Bisect
Introducing Commit: 05b35b210ef3dcdf7e3260d192ce51b602b6f3a7 (ANGLE repo)
- Date: 2017-10-03
- Author: Jamie Madill (jmadill@chromium.org)
This commit introduced TextureD3D::initializeContents with the slow-path zero-fill logic. The function computes imageBytes as computeRowPitch(...) * height * depth, which for block-compressed formats treats height as a pixel count rather than a block count, inflating the result by blockHeight (4 for BPTC). The computation has been present in every subsequent revision of this function.
Root Cause
When a compressed 3D texture has deferred initialization pending and the fast render-target-clear path is unavailable (which is always the case for block-compressed formats since they have no renderable format), TextureD3D::initializeContents falls through to a slow path that allocates a zero-filled buffer and loads it into the texture through the normal compressed data upload path.
The slow path computes the buffer size as follows:
// third_party/angle/src/libANGLE/renderer/d3d/TextureD3D.cpp
GLuint imageBytes = 0;
ANGLE_CHECK_GL_MATH(contextD3D, formatInfo.computeRowPitch(formatInfo.type, image->getWidth(),
1, 0, &imageBytes));
imageBytes *= image->getHeight() * image->getDepth();
For BPTC (4x4 blocks, 16 bytes per block), computeRowPitch correctly returns (width / 4) * 16. However, the subsequent multiplication uses the raw pixel height rather than the block-row count height / 4. The result is exactly four times larger than the true compressed image size. The multiplication is performed in GLuint (32-bit unsigned) arithmetic with no overflow check.
The validation layer in ValidateCompressedTexImage3D computes the correct compressed size through computeCompressedImageSize, which properly divides by blockHeight. The two computations therefore disagree on image size for any 3D compressed texture.
For a 2048x2048x320 BPTC texture, the correct compressed size is 512 * 512 * 320 * 16 = 1,342,177,280 bytes. The initializeContents formula produces 8192 * 2048 * 320 = 5,368,709,120, which wraps to 1,073,741,824 in GLuint. Context::getZeroFilledBuffer allocates this smaller buffer. Image11::loadData then recomputes the correct inputImageSize and issues a memcpy of that length from the undersized source, reading past the end of the allocation.
The deferred-init state is reached through standard WebGL2 API usage. Calling texStorage3D with a compressed internal format allocates immutable storage and marks all levels as MayNeedInit under robust resource initialization. A subsequent compressedTexSubImage3D on a small sub-region triggers ensureSubImageInitialized, which calls initializeContents for the entire mip level. The BPTC format on D3D11 has texFormat set to a valid DXGI format but rtvFormat set to DXGI_FORMAT_UNKNOWN, ensuring the fast clear path is never taken and the vulnerable slow path is always exercised.
Reproduce
This issue was tested on Chromium commit cd400b8c25335a1e2a8f3676367a21b1c82e1df1 running on Windows 11 with an NVIDIA GeForce RTX 4060 Ti. The bug is in the ANGLE D3D11 backend and requires only the default Windows GPU configuration with BPTC (BC6H/BC7) texture compression support, which is standard on all Direct3D 11 hardware. No source modifications are required to reproduce.
Configure an ASAN build with the following args.gn in out/asan, then build.
is_asan = true
is_debug = false
dcheck_always_on = false
target_cpu = "x64"
autoninja -C out/asan chrome
Launch Chrome and open the PoC page(poc.html uses absolute path).
$ out\asan\chrome.exe --user-data-dir=/tmp/poc /path/to/poc.html 2>/tmp/asan.txt
$ cat /tmp/asan.txt
The PoC page creates a WebGL2 context, obtains the EXT_texture_compression_bptc extension, and calls texStorage3D with GL_TEXTURE_3D, COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT_EXT, and dimensions 2048x2048x320. This allocates immutable compressed storage with all levels marked for deferred initialization. A subsequent compressedTexSubImage3D on a 4x4x1 sub-region triggers ensureSubImageInitialized, which calls TextureD3D::initializeContents. The overflowing size formula causes allocation of an undersized buffer, and the subsequent memcpy reads past its end. The GPU process crashes with a heap-buffer-overflow reported by AddressSanitizer.
AddressSanitizer reports the following in the GPU process:
=================================================================
==23284==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x12b328cb1800 at pc 0x7ffaa14db36c bp 0x00b7e95fd4a0 sp 0x00b7e95fd4e8
READ of size 1342177280 at 0x12b328cb1800 thread T0
==23284==*** WARNING: Failed to initialize DbgHelp! ***
==23284==*** Most likely this means that the app is already ***
==23284==*** using DbgHelp, possibly with incompatible flags. ***
==23284==*** Due to technical reasons, symbolization might crash ***
==23284==*** or produce wrong results. ***
#0 0x7ffaa14db36b in _asan_memcpy+0x25b (D:\src\chromium\src\out\asan\clang_rt.asan_dynamic-x86_64.dll+0x18004b36b)
#1 0x7ffa968870f3 in angle::LoadCompressedToNative<4,4,1,16> D:\src\chromium\src\third_party\angle\src\image_util\loadimage.inc:389
#2 0x7ffa9696efb4 in rx::Image11::loadData D:\src\chromium\src\third_party\angle\src\libANGLE\renderer\d3d\d3d11\Image11.cpp:308
#3 0x7ffa96aa7be1 in rx::TextureD3D::initializeContents D:\src\chromium\src\third_party\angle\src\libANGLE\renderer\d3d\TextureD3D.cpp:968
#4 0x7ffa9668789c in gl::FramebufferAttachmentObject::initializeContents D:\src\chromium\src\third_party\angle\src\libANGLE\FramebufferAttachment.cpp:365
#5 0x7ffa967a52e8 in gl::Texture::setCompressedSubImage D:\src\chromium\src\third_party\angle\src\libANGLE\Texture.cpp:1494
#6 0x7ffa96611e67 in gl::Context::compressedTexSubImage3D D:\src\chromium\src\third_party\angle\src\libANGLE\Context.cpp:5804
......
The full ASAN log is provided in asan.txt.
References
- TextureD3D::initializeContents slow-path size computation
- ValidateCompressedTexImage3D correct size computation
- LoadCompressedToNative memcpy with correct size
- Image11::loadData computing correct inputDepthPitch
Credit
Please use 86ac1f1587b71893ed2ad792cd7dde32 as the credit for this vulnerability. Thank you.
- https://source.chromium.org/chromium/chromium/src/+/main:third_party/angle/src/image_util/loadimage.inc;l=389
- https://source.chromium.org/chromium/chromium/src/+/main:third_party/angle/src/libANGLE/renderer/d3d/TextureD3D.cpp;l=949-952
- https://source.chromium.org/chromium/chromium/src/+/main:third_party/angle/src/libANGLE/renderer/d3d/d3d11/Image11.cpp;l=308
- https://source.chromium.org/chromium/chromium/src/+/main:third_party/angle/src/libANGLE/validationES3.cpp;l=1889