CVE-2026-10019
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
WebGL indexed-draw validation truncation on ANGLE Metal allows out-of-bounds vertex fetches
Security Bug
VULNERABILITY DETAILS
ANGLE’s indexed-draw validator narrows the computed unsigned maximum element index to signed GLint before enforcing vertex-buffer bounds:
if (!ValidateDrawAttribs(context, entryPoint, static_cast<GLint>(indexRange.end())))
{
return false;
}
That cast appears in third_party/angle/src/libANGLE/validationES.h in the source tree and is reachable after the correct 64-bit maxElementIndex comparison. On backends where maxElementIndex can exceed INT_MAX and WebGL relies on ANGLE’s manual buffer-access validation, indices above INT_MAX can wrap negative at this point and bypass the attribute-limit rejection.
The attached browser PoC creates:
- one
vec4vertex in anARRAY_BUFFER - one
UNSIGNED_INTelement index equal to0x80000000 - a normal
drawElements(gl.POINTS, 1, gl.UNSIGNED_INT, 0)call
On the Metal-backed browser configuration below, the draw is accepted and returns gl.getError() = 0x0, even though the page references a vertex index far beyond the end of the one-vertex buffer and WebGL should reject the call with GL_INVALID_OPERATION.
This run did not produce a visible non-black pixel in the simple point-draw shader, but that does not weaken the validation result: the security bug here is that ANGLE accepts the out-of-bounds indexed draw at all.
VERSION
Chrome Version: Chromium 149.0.7805.0 + dev (local ASan build)
Source Revision: 9b1af1c3bfa10271c6f92691e32659acea5f941c
Operating System: macOS 26.1 arm64
GPU / renderer: Apple M3 Pro / ANGLE (Apple, ANGLE Metal Renderer: Apple M3 Pro, Unspecified Version)
REPRODUCTION CASE
Attachments:
poc.htmlchrome_parent_metal_index_oob_asan_latest.logchrome_parent_metal_index_oob_ramp16.logchrome_parent_metal_index_oob.log(active-release Chrome confirmation)
Numbered repro steps:
- Serve the attached PoC locally:
python3 -m http.server 8001
- Run Chromium directly with ANGLE Metal forced:
/Volumes/BOX/winfunc/winfunc_artifacts/TARGETS/chromium-index-oob-latest/out/asan_index_oob/Chromium.app/Contents/MacOS/Chromium \
--user-data-dir=/tmp/chrome-index-oob \
--no-first-run \
--disable-background-networking \
--disable-default-apps \
--disable-sync \
--metrics-recording-only \
--enable-logging=stderr \
--ignore-gpu-blocklist \
--use-gl=angle \
--use-angle=metal \
http://127.0.0.1:8001/poc.html
Observed result:
[poc] UNMASKED_RENDERER_WEBGL=ANGLE (Apple, ANGLE Metal Renderer: Apple M3 Pro, Unspecified Version)
[poc] uploaded index=0x80000000 count=16 mode=ramp
[poc] gl.getError()=0x0
[poc] sum(rgb)=0 first16=0,0,0,255,...
The accepted draw is the proof. A correct WebGL implementation should reject this call before draw submission because the element index references vertex data far outside the bound attribute buffer.
This latest-build ASan browser run did not itself produce a sanitizer report under
ASAN_OPTIONS=detect_leaks=0:abort_on_error=1:symbolize=1:log_path=...; no
index-oob-asan-run* log files were emitted. The bug still manifests as invalid draw acceptance on
the current ASan build, which is consistent with the out-of-bounds fetch occurring in backend GPU
execution rather than in a CPU-side memory access path directly observable to ASan.
I also retained an active-release browser proof and the stronger 16-index variant:
[poc] uploaded index=0x80000000 count=16 mode=ramp
[poc] gl.getError()=0x0
[poc] sum(rgb)=0 ...
So on this Metal configuration the bug is not just a one-off acceptance of a single invalid index; the draw remains accepted under a denser invalid index pattern as well.
Type of crash: N/A (out-of-bounds indexed draw acceptance; no crash required for proof)
Crash State:
N/A
Client ID (if relevant): N/A
CREDIT INFORMATION
Reporter credit: Mufeed VH from Winfunc Research (winfunc.com)