CVE-2026-9882
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
IndexRangesrc/common/mathutil.h |
modified |
Files Changed
src/common/mathutil.hsrc/libANGLE/Context.cppsrc/libANGLE/VertexAttribute.cppsrc/libANGLE/VertexAttribute.hsrc/libANGLE/renderer/d3d/d3d9/Renderer9.cppsrc/libANGLE/renderer/renderer_utils.cppsrc/libANGLE/validationES.hsrc/tests/gl_tests/WebGLCompatibilityTest.cpp
Patch
From ff1b91d5f69e8253a5f8d7075a1253b287ebe9e2 Mon Sep 17 00:00:00 2001 From: Geoff Lang <[email protected]> Date: Mon, 27 Apr 2026 11:33:19 -0400 Subject: [PATCH] Fix overflows in IndexRange storage. IndexRange stores mStart and mCount (instead of mEnd) as uint32_t. mCount will overflow when the end index is UINT_MAX, this can happen when primitive restart is disabled making UINT_MAX a valid index. Also fix an invalid cast of IndexRange::end to a signed 32-bit integer in ValidateDrawElementsCommon. The test for this behaviour, WebGLCompatibilityTest.LargeIndexRange, had a bug and did not call glVertexAttribPointer causing validation to fail earlier due to buffer being bound to the attribute. Also universally limit the max element index to UINT_MAX - 1 to protect against incorrect math assuming draw count can fit in a 32-bit integer. Fixed: chromium:504175501 Fixed: chromium:505056913 Fixed: chromium:506375217 Change-Id: I20ebd619e65801833862846a70d31138b2e576b5 Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7797469 Reviewed-by: Shahbaz Youssefi <[email protected]> Commit-Queue: Geoff Lang <[email protected]> --- diff --git a/src/common/mathutil.h b/src/common/mathutil.h index 4f5b5c5..5cb2d3e 100644 --- a/src/common/mathutil.h +++ b/src/common/mathutil.h @@ -872,9 +872,10 @@ {}; IndexRange(Undefined) {} IndexRange() = default; - IndexRange(uint32_t start_, uint32_t end_) : mStart(start_), mCount(end_ - start_ + 1) + IndexRange(uint32_t start, uint32_t end) + : mStart(start), mEnd(end), mCount(static_cast<uint64_t>(end - start) + 1) { - ASSERT(start_ <= end_); + ASSERT(start <= end); } bool isEmpty() const { return mCount == 0; } uint32_t start() const @@ -885,15 +886,18 @@ uint32_t end() const { ASSERT(!isEmpty()); - return mStart + mCount - 1; + return mEnd; } // Number of vertices in the range. - uint32_t vertexCount() const { return mCount; } + uint64_t vertexCount() const { return mCount; } private: uint32_t mStart{0}; - uint32_t mCount{0}; + uint32_t mEnd{0}; + + // Since the range is inclusive, mCount == 0 indicates an empty range + uint64_t mCount{0}; }; inline bool operator==(const IndexRange &a, const IndexRange &b) diff --git a/src/libANGLE/Context.cpp b/src/libANGLE/Context.cpp index 5db0dda..20a1ea4 100644 --- a/src/libANGLE/Context.cpp +++ b/src/libANGLE/Context.cpp @@ -4429,6 +4429,11 @@ ANGLE_LIMIT_CAP(caps->maxDualSourceDrawBuffers, IMPLEMENTATION_MAX_DUAL_SOURCE_DRAW_BUFFERS); + // Disallow using UINT_MAX as an index. This would allow for a draw count of UINT_MAX + 1, + // overflowing a 32-bit integer. + constexpr GLint64 kMaxElementIndex = std::numeric_limits<GLuint>::max() - 1; + ANGLE_LIMIT_CAP(caps->maxElementIndex, kMaxElementIndex); + // WebGL compatibility extensions->webglCompatibilityANGLE = mWebGLContext; for (const auto &extensionInfo : GetExtensionInfoMap()) diff --git a/src/libANGLE/VertexAttribute.cpp b/src/libANGLE/VertexAttribute.cpp index bd1b692..d88956c 100644 --- a/src/libANGLE/VertexAttribute.cpp +++ b/src/libANGLE/VertexAttribute.cpp @@ -139,7 +139,7 @@ return attrib.relativeOffset + binding.getOffset(); } -size_t ComputeVertexBindingElementCount(GLuint divisor, size_t drawCount, size_t instanceCount) +size_t ComputeVertexBindingElementCount(GLuint divisor, uint64_t drawCount, size_t instanceCount) { // For instanced rendering, we draw "instanceDrawCount" sets of "vertexDrawCount" vertices. // @@ -154,7 +154,9 @@ return (instanceCount + divisor - 1u) / divisor; } - return drawCount; + // Ensure that drawCount can always fit into a size_t. This should also be validated by + // maxElementIndex. + return angle::CheckedNumeric<size_t>(drawCount).ValueOrDie(); } } // namespace gl diff --git a/src/libANGLE/VertexAttribute.h b/src/libANGLE/VertexAttribute.h index 40f2bb2..1b31b9b 100644 --- a/src/libANGLE/VertexAttribute.h +++ b/src/libANGLE/VertexAttribute.h @@ -100,7 +100,7 @@ // Warning: you should ensure binding really matches attrib.bindingIndex before using this function. GLintptr ComputeVertexAttributeOffset(const VertexAttribute &attrib, const VertexBinding &binding); -size_t ComputeVertexBindingElementCount(GLuint divisor, size_t drawCount, size_t instanceCount); +size_t ComputeVertexBindingElementCount(GLuint divisor, uint64_t drawCount, size_t instanceCount); struct VertexAttribCurrentValueData { diff --git a/src/libANGLE/renderer/d3d/d3d9/Renderer9.cpp b/src/libANGLE/renderer/d3d/d3d9/Renderer9.cpp index 773ac17..cabd5ad 100644 --- a/src/libANGLE/renderer/d3d/d3d9/Renderer9.cpp +++ b/src/libANGLE/renderer/d3d/d3d9/Renderer9.cpp @@ -1508,7 +1508,7 @@ context, type, count, indices, context->getState().isPrimitiveRestartEnabled(), &indexRange)); - size_t vertexCount = indexRange.vertexCount(); + uint64_t vertexCount = indexRange.vertexCount(); ANGLE_TRY(applyVertexBuffer(context, mode, static_cast<GLsizei>(indexRange.start()), static_cast<GLsizei>(vertexCount), instances, &indexInfo)); diff --git a/src/libANGLE/renderer/renderer_utils.cpp b/src/libANGLE/renderer/renderer_utils.cpp index 096732d..cd0e52d 100644 --- a/src/libANGLE/renderer/renderer_utils.cpp +++ b/src/libANGLE/renderer/renderer_utils.cpp @@ -1613,7 +1613,15 @@ context->getState().isPrimitiveRestartEnabled(), &indexRange)); ANGLE_TRY(ComputeStartVertex(context->getImplementation(), indexRange, baseVertex, startVertexOut)); - *vertexCountOut = indexRange.vertexCount(); + + // Protect against requiring 64-bits to store a draw count. Most math is done in size_t and + // not safe on 32-bit systems. This would require a UINT_MAX index when primitive restart is + // disabled. + uint64_t vertexCount = indexRange.vertexCount(); + ANGLE_CHECK_GL_MATH(context->getImplementation(), + vertexCount <= std::numeric_limits<GLuint>::max()); + + *vertexCountOut = static_cast<size_t>(vertexCount); } else { diff --git a/src/libANGLE/validationES.h b/src/libANGLE/validationES.h index 155d5f5..44c6d10 100644 --- a/src/libANGLE/validationES.h +++ b/src/libANGLE/validationES.h @@ -1134,7 +1134,7 @@ return false; } - if (!ValidateDrawAttribs(context, entryPoint, static_cast<GLint>(indexRange.end()))) + if (!ValidateDrawAttribs(context, entryPoint, static_cast<GLint64>(indexRange.end()))) { return false; } diff --git a/src/tests/gl_tests/WebGLCompatibilityTest.cpp b/src/tests/gl_tests/WebGLCompatibilityTest.cpp index 784ce9b5..030cb82 100644 --- a/src/tests/gl_tests/WebGLCompatibilityTest.cpp +++ b/src/tests/gl_tests/WebGLCompatibilityTest.cpp @@ -1812,8 +1812,6 @@ ANGLE_GL_PROGRAM(program, kVS, essl1_shaders::fs::Red()); glUseProgram(program); - glEnableVertexAttribArray(glGetAttribLocation(program, "a_Position")); - constexpr float kVertexData[] = { 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, }; @@ -1822,12 +1820,17 @@ glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer); glBufferData(GL_ARRAY_BUFFER, sizeof(kVertexData), kVertexData, GL_STREAM_DRAW); + GLuint positionLocation = glGetAttribLocation(program, "a_Position"); + glEnableVertexAttribArray(positionLocation); + glVertexAttribPointer(positionLocation, 4, GL_FLOAT, GL_FALSE, 0, nullptr); + constexpr GLuint kMaxIntAsGLuint = static_cast<GLuint>(std::numeric_limits<GLint>::max()); + constexpr GLuint kMaxGLuint = std::numeric_limits<GLuint>::max(); constexpr GLuint kIndexData[] = { + 0, kMaxIntAsGLuint, kMaxIntAsGLuint + 1, - kMaxIntAsGLuint + 2, - kMaxIntAsGLuint + 3, + kMaxGLuint, }; GLBuffer indexBuffer;
Regression Test / PoC
diff --git a/src/tests/gl_tests/WebGLCompatibilityTest.cpp b/src/tests/gl_tests/WebGLCompatibilityTest.cpp
index 784ce9b5..030cb82 100644
--- a/src/tests/gl_tests/WebGLCompatibilityTest.cpp
+++ b/src/tests/gl_tests/WebGLCompatibilityTest.cpp
@@ -1812,8 +1812,6 @@
ANGLE_GL_PROGRAM(program, kVS, essl1_shaders::fs::Red());
glUseProgram(program);
- glEnableVertexAttribArray(glGetAttribLocation(program, "a_Position"));
-
constexpr float kVertexData[] = {
1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f,
};
@@ -1822,12 +1820,17 @@
glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer);
glBufferData(GL_ARRAY_BUFFER, sizeof(kVertexData), kVertexData, GL_STREAM_DRAW);
+ GLuint positionLocation = glGetAttribLocation(program, "a_Position");
+ glEnableVertexAttribArray(positionLocation);
+ glVertexAttribPointer(positionLocation, 4, GL_FLOAT, GL_FALSE, 0, nullptr);
+
constexpr GLuint kMaxIntAsGLuint = static_cast<GLuint>(std::numeric_limits<GLint>::max());
+ constexpr GLuint kMaxGLuint = std::numeric_limits<GLuint>::max();
constexpr GLuint kIndexData[] = {
+ 0,
kMaxIntAsGLuint,
kMaxIntAsGLuint + 1,
- kMaxIntAsGLuint + 2,
- kMaxIntAsGLuint + 3,
+ kMaxGLuint,
};
GLBuffer indexBuffer;
@@ -1837,11 +1840,11 @@
EXPECT_GL_NO_ERROR();
// First index is representable as 32-bit int but second is not
- glDrawElements(GL_LINES, 2, GL_UNSIGNED_INT, 0);
+ glDrawElements(GL_POINTS, 4, GL_UNSIGNED_INT, 0);
EXPECT_GL_ERROR(GL_INVALID_OPERATION);
// Neither index is representable as 32-bit int
- glDrawElements(GL_LINES, 2, GL_UNSIGNED_INT, reinterpret_cast<void *>(sizeof(GLuint) * 2));
+ glDrawElements(GL_POINTS, 4, GL_UNSIGNED_INT, reinterpret_cast<void *>(sizeof(GLuint) * 2));
EXPECT_GL_ERROR(GL_INVALID_OPERATION);
}
Original Bug Report
Potential integer overflow in ANGLE IndexRange bypasses software robust-buffer-access validation
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: An integer overflow in ANGLE’s IndexRange constructor can potentially cause 32-bit index ranges to bypass software robust-buffer-access validation. This could enable out-of-bounds GPU memory reads on backends lacking hardware robust access support, such as Metal on macOS.
Affected files:
third_party/angle/src/common/mathutil.hthird_party/angle/src/libANGLE/validationES.hthird_party/angle/src/common/utilities.cppthird_party/angle/src/libANGLE/Context.cppthird_party/angle/src/libANGLE/renderer/metal/DisplayMtl.mm
Estimated timestamp from git blame: 2025-04-11
Root Cause
A potential integer overflow vulnerability exists in the IndexRange constructor within third_party/angle/src/common/mathutil.h. The constructor computes the number of elements in a range using 32-bit unsigned arithmetic without overflow protection:
// third_party/angle/src/common/mathutil.h
IndexRange(uint32_t start_, uint32_t end_) : mStart(start_), mCount(end_ - start_ + 1)
{
ASSERT(start_ <= end_);
}
bool isEmpty() const { return mCount == 0; }
If the constructor is invoked with a range where start_ = 0 and end_ = 0xFFFFFFFF, the ASSERT passes, but the calculation 0xFFFFFFFF - 0 + 1 overflows the uint32_t capacity and wraps around to 0. This causes isEmpty() to return true for a range that actually encompasses all possible 32-bit indices.
Validation Bypass
This poisoned IndexRange can be produced when ANGLE scans an index buffer containing both 0 and 0xFFFFFFFF to determine bounds (via gl::ComputeIndexRange).
In the draw validation path, ANGLE implements software-based robust-access checking for backends that lack hardware support (such as Metal, where robustBufferAccessBehaviorKHR is false). This check is gated on the index range not being empty:
// third_party/angle/src/libANGLE/validationES.h:1115
// No op if there are no real indices in the index data (all are primitive restart).
if (!indexRange.isEmpty())
{
// ...
if (!ValidateDrawAttribs(context, entryPoint, static_cast<GLint>(indexRange.end())))
{
return false;
}
}
Because isEmpty() incorrectly returns true due to the overflow, the entire validation block—including index clamping and attribute bounds checking in ValidateDrawAttribs—is skipped. ValidateDrawElementsCommon subsequently returns true, permitting the draw call to proceed with unvalidated indices.
Applicability and Impact
In WebGL 1, primitive restart is disabled by default. If the OES_element_index_uint extension is enabled, an attacker can provide a GL_UNSIGNED_INT element buffer containing indices like [0, K, 0xFFFFFFFF]. This triggers the overflow, bypasses validation, and results in the GPU executing a draw call with an attacker-controlled out-of-bounds index K relative to the vertex buffer.
The fetched out-of-bounds data can be processed by an attacker-controlled vertex shader and exfiltrated via gl.readPixels(). Because the GPU process is shared across origins, this enables the potential disclosure of cross-origin GPU memory, such as data from other tabs or WebGL resources.
Potential Reproduction Steps (Theoretical)
Note: These are suggested steps; we have not yet verified them with a working Proof of Concept.
- On macOS Chrome (Metal backend), create a WebGL 1 context and enable
OES_element_index_uint. - Bind a small
ARRAY_BUFFERto a vertex attribute. - Bind an
ELEMENT_ARRAY_BUFFERpopulated with aUint32Array([0, K, 0xFFFFFFFF])whereKis a very large out-of-bounds index. - Call
gl.drawElements(gl.POINTS, 3, gl.UNSIGNED_INT, 0). - Observe that ANGLE validation passes due to the
IndexRangeoverflow, submitting the draw call to the GPU with OOB indices.
Suggested Fix
Modify the IndexRange constructor in third_party/angle/src/common/mathutil.h to use 64-bit arithmetic or a safe math wrapper (like base::CheckedNumeric) when calculating mCount to prevent the overflow. If the element count exceeds the 32-bit limit, the logic should safely handle the maximum range without wrapping to 0.
Evaluated with Chrome root at commit: a1e33f5848218e21d4a16ae2c1bc94e815c30c7f
Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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.