Medium watch CVSS 4.3 chrome Integer Overflow 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
4.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInteger overflow in ANGLE
DescriptionInteger overflow in ANGLE
ComponentANGLE
Sub-componentD3D11
Bug ClassInteger Overflow
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker497896137
Fix commit838c9be2bc21 (angle/angle) +69/-3
CWECWE-472
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:N
CISA KEVNot listed
Credited86ac1f1587b71893ed2ad792cd7dde32
Reported2026-03-30 · bounty $3000 (per vendor advisory)
Fix landed2026-04-09 (public git)
Disclosed2026-04-28
Patch gap19 days — fix public in source before the advisory

Background

Block-compressed texture
a format (e.g. GL_COMPRESSED_RGBA_BPTC_UNORM_EXT) that stores a 4x4 pixel block in a fixed number of bytes, so the byte size is a function of block count, not pixel count.
Deferred / robust resource initialization
the ANGLE behavior (always on in WebGL) that lazily zero-fills a texture’s backing store before first use, driven by the slow path of initializeContents in the D3D11 backend.
`ANGLE_CHECK_GL_MATH`
a macro that validates an arithmetic helper succeeded and raises GL_OUT_OF_MEMORY/GL_INVALID_VALUE on failure; a raw *= performed outside such a helper is unchecked.
`angle::CheckedNumeric`
a wrapper type that tracks overflow so a multiplication can be rejected via AssignIfValid instead of silently wrapping.

Root Cause Analysis

The deferred-init size computation in TextureD3D.cpp computed the zero-fill buffer size for a block-compressed 3D texture by calling computeRowPitch with a height of 1 (one row of blocks) and then doing imageBytes *= image->getHeight() * image->getDepth(). For a block-compressed format the pitch is already the byte size of a full row of 4x4 blocks, so multiplying by the full pixel getHeight() instead of the block-row count (height/4) overcounts by 4x, and the multiplication itself was an unchecked 32-bit GLuint operation. Per the shipped regression test, dimensions 2048x2048x320 produce 8192 * 2048 * 320 = 5,368,709,120, which wraps to 1,073,741,824 in 32-bit — a value smaller than the correct 1,342,177,280, so the buffer sized from it is too small and the zero-fill then reads/writes beyond it (the test asserts no ASAN error).

The fix routes compressed formats through InternalFormat::computeCompressedImageSize, which accounts for block dimensions, and wraps the uncompressed path in angle::CheckedNumeric so an overflowing product is caught by ANGLE_CHECK_GL_MATH rather than wrapping silently.

Key insight
The single core mistake was sizing a block-compressed image as rowPitch * pixelHeight * depth with an unchecked 32-bit multiply, conflating pixel height with block-row count (a 4x overcount) and allowing a 32-bit wrap; the fix uses the block-aware computeCompressedImageSize for compressed formats and CheckedNumeric overflow detection for the rest.

Attack Path

  1. Enable robust init The slow path is reached only when robust resource initialization is on, which is always the case for WebGL contexts, so content runs in a normal WebGL/WebGL2 page.
  2. Allocate a large compressed 3D texture JavaScript calls glTexStorage3D with a BPTC format and large dimensions (e.g. 2048x2048x320) so the computed byte count exceeds 2^32.
  3. Trigger deferred initialization A small glCompressedTexSubImage3D update forces ANGLE to lazily zero-fill the texture’s backing store via the deferred-init path.
  4. Undersized buffer The unchecked 32-bit multiply wraps to a value smaller than the real image size, so the zero-fill buffer is allocated too small.
  5. Out-of-bounds access The subsequent fill operates against the true (larger) extent, producing an out-of-bounds read/write that the regression test guards against with ASAN (inferred as to exact read-vs-write primitive, which the diff does not pin down).

Impact Assessment

An attacker-controlled web page can drive an integer overflow in ANGLE’s D3D11 deferred-init path, yielding an out-of-bounds memory access in the GPU process on Windows. The vendor advisory rates this medium; it does not report in-the-wild exploitation. Preconditions are a WebGL context (robust init implicitly enabled), the D3D11 backend, and GL_EXT_texture_compression_bptc support; any escalation beyond the OOB access (information disclosure or GPU-process compromise) is (inferred).

Changed Functions

FunctionChangeNotes
BPTCCompressedTextureTestES3WebGL
src/tests/gl_tests/BPTCCompressedTextureTest.cpp
modified

Files Changed

  • src/libANGLE/renderer/d3d/TextureD3D.cpp
  • src/tests/gl_tests/BPTCCompressedTextureTest.cpp

Audit Directions

  • Pixel-vs-block dimension confusion
    Search for size math on compressed formats that multiplies by getHeight()/getWidth() directly instead of block counts (height/blockHeight), especially any computeRowPitch(..., 1, ...) followed by a manual height multiply.
  • Unchecked integer products in size computation
    Flag raw *= or * on GLuint/32-bit size accumulators that are not wrapped in CheckedNumeric or guarded by ANGLE_CHECK_GL_MATH, particularly for 3D/array textures where three dimensions multiply.
  • Deferred/robust-init paths
    Review other backends’ lazy zero-fill routines (initializeContents and callers) for the same compressed-size assumption, since the bug only manifests on the robust-init slow path that WebGL always exercises.
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());
Loading diff…

Regression Test / PoC

shipped with the fix
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());
Loading diff…

Original Bug Report

reported by se...@gmail.com

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)

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

Credit

Please use 86ac1f1587b71893ed2ad792cd7dde32 as the credit for this vulnerability. Thank you.

View on issue tracker