CVE-2026-4452
Overview
Background
- ANGLE D3D11 backend
- ANGLE’s translation of OpenGL ES/WebGL calls to Direct3D 11; on Windows it is the default backend Chrome uses for WebGL in the GPU process.
- `GL_LINE_LOOP` / `GL_TRIANGLE_FAN`
- primitive topologies D3D11 does not support natively, so
Renderer11builds a new 32-bit index buffer (mLineLoopIB,mTriangleFanIB) from the app’s indices inmScratchIndexDataBufferto emulate them. - Index widening
- when the source element type is narrower than 32 bits (e.g.
GL_UNSIGNED_BYTE), each index becomes a 4-byteGLuint, so the byte size of the emulated buffer is about 4x the source buffer. - `reserveBufferSpace`
- the index-buffer helper that reserves the requested number of bytes before ANGLE copies the widened indices into it.
Root Cause Analysis
In the D3D11 line-loop and triangle-fan index paths in Renderer11.cpp (the enclosing function names are not shown in the hunks), the byte size of the widened index buffer was computed as sizeof(GLuint) * mScratchIndexDataBuffer.size() and narrowed with static_cast<unsigned int>. If a large GL_UNSIGNED_BYTE index buffer is used, the widened byte count can go past 32 bits, so the cast truncates it (or it exceeds INT_MAX when later read as a signed size). That passes a wrong, much smaller size to mLineLoopIB->reserveBufferSpace/mTriangleFanIB->reserveBufferSpace while the full scratch buffer still holds the larger data. The regression tests use count = 805306368 (0x30000000), which gives 0xC0000000 bytes, above INT_MAX.
The fix computes the size in uint64_t spaceNeeded64. Before the reservation, ANGLE_CHECK rejects any value above std::numeric_limits<int>::max() with GL_OUT_OF_MEMORY and returns early. Only then is the value narrowed to int spaceNeeded.
The patch does not show what happens after the reservation (the copy of the widened indices), but an undersized reservation followed by a full-length copy would be a heap buffer overflow (inferred).
GL_OUT_OF_MEMORY before reserving whenever it exceeds INT_MAX.Attack Path
- Create a huge narrow index buffer
WebGL/GLES content allocates an element array buffer of roughly 805 million
GL_UNSIGNED_BYTEindices (as inDISABLED_LargeLoopUnsignedByte/DISABLED_LargeFanUnsignedByte). - Draw with an emulated topology
The content calls
glDrawElementswithGL_LINE_LOOPorGL_TRIANGLE_FAN, which on the D3D11 backend forcesRenderer11to widen the indices to 32 bits inmScratchIndexDataBuffer. - Size computation wraps
sizeof(GLuint) * size()(orsize() * sizeof(unsigned int)) is cast tounsigned int, so values aboveINT_MAXor 32 bits produce a wrong size forreserveBufferSpace. - Undersized buffer is written The widened indices are then copied into a buffer reserved with the wrong size, which could give out-of-bounds writes in the GPU process (inferred; the copy is not in the diff).
Impact Assessment
Renderer11.cpp) and a draw with GL_LINE_LOOP or GL_TRIANGLE_FAN from a very large index buffer of an element type narrower than 32 bits. That plausibly makes it reachable from ordinary web content through WebGL in a stock build, provided the GPU process can allocate the multi-gigabyte buffers involved (inferred). The vendor advisory rates it high severity (“Integer overflow in ANGLE”) and does not state in-the-wild exploitation.Files Changed
src/libANGLE/renderer/d3d/d3d11/Renderer11.cppsrc/tests/gl_tests/LineLoopTest.cpp
Audit Directions
- Widening and expansion multipliersLook for other places where ANGLE backends convert narrow index or vertex data to wider formats (index conversion, primitive restart, vertex format conversion in D3D9/Vulkan/Metal) and confirm the
size * elementSizeproduct is checked before being narrowed tounsigned intorint. - Emulated topologies with extra elementsLine-loop emulation adds a closing index and fan emulation expands to
3 * (n - 2)indices; check every helper that sizes those outputs (for exampleGetTriFanIndices, line-loop helpers for draw arrays) for multiplication overflow. - `static_cast` narrowing of byte countsSearch ANGLE for
static_cast<unsigned int>(... * sizeof(...))andstatic_cast<GLuint>/intapplied tosize_tbyte counts on the way into allocation orreserveBufferSpace-style APIs.
Patch
From 4b7aace914924f634c4148c41e6fc87198867fa5 Mon Sep 17 00:00:00 2001 From: Stephen White <[email protected]> Date: Wed, 04 Mar 2026 15:44:30 -0500 Subject: [PATCH] D3D11: fix overflow in line loop and triangle fan If an index buffer is created of element type less than 32bit (e.g., GL_UNSIGNED_BYTE) and used in a line loop or triangle fan, D3D11 must widen it to 32-bit. This can cause an overflow if the widened size in bytes is greater than INT_MAX. The fix is to detect the overflow and abort. Bug: chromium:487977696 Change-Id: I57b1dcc9b3d968da88282164a5f92386500c6205 Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7637780 Reviewed-by: Geoff Lang <[email protected]> Commit-Queue: Stephen White <[email protected]> --- diff --git a/src/libANGLE/renderer/d3d/d3d11/Renderer11.cpp b/src/libANGLE/renderer/d3d/d3d11/Renderer11.cpp index 60c00fe..ec644eb 100644 --- a/src/libANGLE/renderer/d3d/d3d11/Renderer11.cpp +++ b/src/libANGLE/renderer/d3d/d3d11/Renderer11.cpp @@ -2083,8 +2083,14 @@ { return angle::Result::Continue; } - unsigned int spaceNeeded = - static_cast<unsigned int>(sizeof(GLuint) * mScratchIndexDataBuffer.size()); + + uint64_t spaceNeeded64 = sizeof(GLuint) * mScratchIndexDataBuffer.size(); + ANGLE_CHECK(GetImplAs<Context11>(context), spaceNeeded64 <= std::numeric_limits<int>::max(), + "Failed to create a 32-bit looping index buffer for " + "a GL_LINE_LOOP of <32-bit element type; too many indices required.", + GL_OUT_OF_MEMORY); + int spaceNeeded = static_cast<int>(spaceNeeded64); + ANGLE_TRY( mLineLoopIB->reserveBufferSpace(context, spaceNeeded, gl::DrawElementsType::UnsignedInt)); @@ -2171,8 +2177,12 @@ GetTriFanIndices(indexPointer, type, count, glState.isPrimitiveRestartEnabled(), &mScratchIndexDataBuffer); - const unsigned int spaceNeeded = - static_cast<unsigned int>(mScratchIndexDataBuffer.size() * sizeof(unsigned int)); + uint64_t spaceNeeded64 = mScratchIndexDataBuffer.size() * sizeof(unsigned int); + ANGLE_CHECK(GetImplAs<Context11>(context), spaceNeeded64 <= std::numeric_limits<int>::max(), + "Failed to create a 32-bit looping index buffer for " + "a GL_TRIANGLE_FAN of <32-bit element type; too many indices required.", + GL_OUT_OF_MEMORY); + int spaceNeeded = static_cast<int>(spaceNeeded64); ANGLE_TRY(mTriangleFanIB->reserveBufferSpace(context, spaceNeeded, gl::DrawElementsType::UnsignedInt)); diff --git a/src/tests/gl_tests/LineLoopTest.cpp b/src/tests/gl_tests/LineLoopTest.cpp index 696dde3..2dc6551 100644 --- a/src/tests/gl_tests/LineLoopTest.cpp +++ b/src/tests/gl_tests/LineLoopTest.cpp @@ -546,6 +546,46 @@ EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::green); } +// Test an edge case in the D3D11 backend where a large index array of GL_UNSIGNED_BYTE +// drawn as a GL_LINE_LOOP is widened to 32-bits internally and overflows. +// Disabled because it slow and triggers an internal error. +TEST_P(LineLoopTestES3, DISABLED_LargeLoopUnsignedByte) +{ + size_t count = 805306368; + + glLinkProgram(mProgram); + glUseProgram(mProgram); + ASSERT_GL_NO_ERROR(); + + GLBuffer ebo; + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo); + glBufferData(GL_ELEMENT_ARRAY_BUFFER, count, nullptr, GL_STATIC_DRAW); + ASSERT_GL_NO_ERROR(); + + glDrawElements(GL_LINE_LOOP, count, GL_UNSIGNED_BYTE, 0); + EXPECT_GL_ERROR(GL_OUT_OF_MEMORY); +} + +// Test an edge case in the D3D11 backend where a large index array of GL_UNSIGNED_BYTE +// drawn as a GL_TRIANGLE_FAN is widened to 32-bits internally and overflows. +// Disabled because it slow and triggers an internal error. +TEST_P(LineLoopTestES3, DISABLED_LargeFanUnsignedByte) +{ + size_t count = 805306368; + + glLinkProgram(mProgram); + glUseProgram(mProgram); + ASSERT_GL_NO_ERROR(); + + GLBuffer ebo; + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo); + glBufferData(GL_ELEMENT_ARRAY_BUFFER, count, nullptr, GL_STATIC_DRAW); + ASSERT_GL_NO_ERROR(); + + glDrawElements(GL_TRIANGLE_FAN, count, GL_UNSIGNED_BYTE, 0); + EXPECT_GL_ERROR(GL_OUT_OF_MEMORY); +} + // Tests an edge case with a very large line loop element count. // Disabled because it is slow and triggers an internal error. TEST_P(LineLoopTest, DISABLED_DrawArraysWithLargeCount)
Regression Test / PoC
diff --git a/src/tests/gl_tests/LineLoopTest.cpp b/src/tests/gl_tests/LineLoopTest.cpp
index 696dde3..2dc6551 100644
--- a/src/tests/gl_tests/LineLoopTest.cpp
+++ b/src/tests/gl_tests/LineLoopTest.cpp
@@ -546,6 +546,46 @@
EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::green);
}
+// Test an edge case in the D3D11 backend where a large index array of GL_UNSIGNED_BYTE
+// drawn as a GL_LINE_LOOP is widened to 32-bits internally and overflows.
+// Disabled because it slow and triggers an internal error.
+TEST_P(LineLoopTestES3, DISABLED_LargeLoopUnsignedByte)
+{
+ size_t count = 805306368;
+
+ glLinkProgram(mProgram);
+ glUseProgram(mProgram);
+ ASSERT_GL_NO_ERROR();
+
+ GLBuffer ebo;
+ glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
+ glBufferData(GL_ELEMENT_ARRAY_BUFFER, count, nullptr, GL_STATIC_DRAW);
+ ASSERT_GL_NO_ERROR();
+
+ glDrawElements(GL_LINE_LOOP, count, GL_UNSIGNED_BYTE, 0);
+ EXPECT_GL_ERROR(GL_OUT_OF_MEMORY);
+}
+
+// Test an edge case in the D3D11 backend where a large index array of GL_UNSIGNED_BYTE
+// drawn as a GL_TRIANGLE_FAN is widened to 32-bits internally and overflows.
+// Disabled because it slow and triggers an internal error.
+TEST_P(LineLoopTestES3, DISABLED_LargeFanUnsignedByte)
+{
+ size_t count = 805306368;
+
+ glLinkProgram(mProgram);
+ glUseProgram(mProgram);
+ ASSERT_GL_NO_ERROR();
+
+ GLBuffer ebo;
+ glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
+ glBufferData(GL_ELEMENT_ARRAY_BUFFER, count, nullptr, GL_STATIC_DRAW);
+ ASSERT_GL_NO_ERROR();
+
+ glDrawElements(GL_TRIANGLE_FAN, count, GL_UNSIGNED_BYTE, 0);
+ EXPECT_GL_ERROR(GL_OUT_OF_MEMORY);
+}
+
// Tests an edge case with a very large line loop element count.
// Disabled because it is slow and triggers an internal error.
TEST_P(LineLoopTest, DISABLED_DrawArraysWithLargeCount)
Original Bug Report
ANGLE D3D11 drawLineLoop Integer Overflow causes GPU OOB Write
Report description
ANGLE D3D11 drawLineLoop Integer Overflow causes GPU OOB Write
Bug location
Where do you want to report your vulnerability?
Chrome VRP – Report security issues affecting the Chrome browser. See program rules
Which URL (or repository) have you found the vulnerability in?
The problem
Please describe the technical details of the vulnerability
Renderer11::drawLineLoop has an integer overflow when computing the size of a mapped index buffer. The overflow check at line 2073 only validates the non-primitive-restart case (count+1 elements). When primitive restart is enabled, GetLineLoopIndices calls GetLineLoopWithRestartIndexCount which can return a significantly larger count due to index expansion at each restart boundary. The expanded output count multiplied by sizeof(GLuint) overflows unsigned int at line 2086-2087, truncating spaceNeeded to zero. The subsequent memcpy at line 2096 uses the full untruncated size, writing approximately 4GB of attacker-influenced data into a 16KB streaming index buffer.
Affected Code
Renderer11.cpp, drawLineLoop():
// Line 2073-2078: overflow check only validates (count+1), not primitive restart expansion
bool indexCheck = static_cast<unsigned int>(count) + 1 >
(std::numeric_limits<unsigned int>::max() / sizeof(unsigned int));
// Line 2080-2081: GetLineLoopIndices expands indices beyond what the check allows
GetLineLoopIndices(indices, type, static_cast<GLuint>(count),
glState.isPrimitiveRestartEnabled(), &mScratchIndexDataBuffer);
// Line 2086-2087: unsigned int truncation (e.g. 1073741824 * 4 = 0x100000000 -> 0)
unsigned int spaceNeeded =
static_cast<unsigned int>(sizeof(GLuint) * mScratchIndexDataBuffer.size());
// Line 2096-2097: memcpy uses full untruncated size, not spaceNeeded
memcpy(mappedMemory, &mScratchIndexDataBuffer[0],
sizeof(GLuint) * mScratchIndexDataBuffer.size());
Steps to Reproduce
- Open Chrome on Windows (D3D11 is the default ANGLE backend, no flags needed)
- Navigate to the attached poc_lineloop_d3d11.html
- The page auto-fires: creates a WebGL2 context, allocates an element array buffer with 805306368 UNSIGNED_BYTE indices in the pattern [0, 1, 0xFF] repeating (0xFF = primitive restart), then calls gl.drawElements(gl.LINE_LOOP, 805306368, gl.UNSIGNED_BYTE, 0)
- GPU process crashes with access violation (WRITE of 4GB)
Note: the PoC allocates ~805MB for the element array buffer. Systems with less than ~2GB of available GPU process memory may fail the allocation before reaching the overflow.
Tested on:
- Chrome 145.0.7632.110 (stable, Windows 11) – GPU process crash
- Chromium 146.0.7678.0 ASan (Windows 11, –in-process-gpu) – heap-buffer-overflow WRITE of size 4294967296 at Renderer11.cpp:2096
With –in-process-gpu, the crash occurs in the browser process address space.
ASan Output
==21376==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x12722fe50000 at pc 0x7ffcdf71b4fc bp 0x00ed077fdb70 sp 0x00ed077fdbb8
WRITE of size 4294967296 at 0x12722fe50000 thread T23
#0 0x7ffcdf71b4fb (clang_rt.asan_dynamic-x86_64.dll+0x18004b4fb)
#1 0x7ffcdb3fe543 in rx::Renderer11::drawLineLoop Renderer11.cpp:2096:5
#2 0x7ffcdb401b6d in rx::Renderer11::drawElements Renderer11.cpp:1970:16
#3 0x7ffcdb3b0a49 in rx::Context11::drawElements Context11.cpp:361:12
#4 0x7ffcdabbacfb in GL_DrawElements entry_points_gles_2_0_autogen.cpp:1862:22
#5 0x7ffc7d294f7b in gl::RealGLApi::glDrawElementsFn gl_gl_api_implementation.cc:401:16
#6 0x7ffc7dae946b in gpu::gles2::GLES2DecoderPassthroughImpl::DoDrawElements gles2_cmd_decoder_passthrough_doers.cc:1163:10
#7 0x7ffc7db338ea in gpu::gles2::GLES2DecoderPassthroughImpl::HandleDrawElements gles2_cmd_decoder_passthrough_handlers.cc:138:10
#8 0x7ffc7db51cbc in gpu::gles2::GLES2DecoderPassthroughImpl::DoCommandsImpl gles2_cmd_decoder_passthrough.cc:742:20
#9 0x7ffc67ce5e7b in gpu::CommandBufferService::Flush command_buffer_service.cc:267:35
#10 0x7ffc7dcfd071 in gpu::CommandBufferStub::OnAsyncFlush command_buffer_stub.cc:504:22
0x12722fe50000 is located 6144 bytes before 359576-byte region [0x12722fe51800,0x12722fea9498)
SUMMARY: AddressSanitizer: heap-buffer-overflow Renderer11.cpp:2096:5 in rx::Renderer11::drawLineLoop
Full unabridged ASan output is attached as asan_output.txt.
Fix
Replace the unsigned int spaceNeeded with size_t, or validate mScratchIndexDataBuffer.size() after GetLineLoopIndices returns (accounting for primitive restart expansion) rather than only validating count+1 before the call:
size_t spaceNeeded = sizeof(GLuint) * mScratchIndexDataBuffer.size();
ANGLE_CHECK(GetImplAs<Context11>(context),
spaceNeeded <= std::numeric_limits<unsigned int>::max(),
"Line loop index buffer too large", GL_OUT_OF_MEMORY);
The same fix should be applied to drawTriangleFan (line 2168).
Bisect
The vulnerable code was introduced in ANGLE commit 0d3537c224c7df5b90a2cf77678eb0987b6b7a30 (“D3D11: Implement ES3 primitive restart with line loops”, 2015-11-06). This commit introduced unsigned int spaceNeeded = static_cast<unsigned int>(sizeof(GLuint) * mScratchIndexDataBuffer.size()) (truncation), and added a memcpy using the full untruncated sizeof(GLuint) * mScratchIndexDataBuffer.size() — but did not update the pre-existing overflow check to account for the expanded index count.
Impact analysis
Heap buffer overflow in the GPU process, reachable from JavaScript via WebGL2 on any webpage. No flags or special configuration required. D3D11 is the default ANGLE backend on all Windows Chrome installations. The overflow writes approximately 4GB of partially attacker-controlled data (vertex index values from the element array buffer) into a 16KB D3D11 streaming index buffer. With –in-process-gpu the corruption occurs in the browser process address space.
The cause
What version of Chrome have you found the security issue in?
145.0.7632.110 Stable
Is the security issue related to a crash?
Yes, it is related to a crash.
Choose the type of vulnerability
Memory Corruption (in a sandboxed process)
How would you like to be publicly acknowledged for your report?
cinzinga