CVE-2026-10018
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 <geofflang@chromium.org>
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 <syoussefi@chromium.org>
Commit-Queue: Geoff Lang <geofflang@chromium.org>
---
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
Integer wrap in ANGLE IndexRange allows WebGL OOB vertex fetch
VULNERABILITY DETAILS
The IndexRange(uint32_t start, uint32_t end) constructor at third_party/angle/src/common/mathutil.h:875 computes mCount = end - start + 1. For IndexRange(0, 0xffffffff) this wraps to 0 and isEmpty() returns true.
ValidateDrawElementsCommon at validationES.h:1116 uses isEmpty() to gate both maxElementIndex and ValidateDrawAttribs. A WebGL1 drawElements(GL_POINTS, 2, GL_UNSIGNED_INT, 0) with OES_element_index_uint enabled and element-array buffer {0, 0xffffffff} is therefore accepted even though the second index is out of range for the bound vertex-attribute buffer. Primitive restart is off in WebGL1, so 0xffffffff reaches ComputeTypedIndexRange as a real index.
The OOB vertex fetch reaches the GPU. On ANGLE Vulkan backends whose physical device does not enable robustBufferAccess (the ES2_Vulkan_SwiftShader configuration Chromium ships for conformance testing is one such), this causes a GPU process SIGSEGV at the address (0xffffffff * stride) mod 2^32 past the bound vertex buffer. With WebGL side spray to cover the 4 GiB region past the bound VBO, readPixels returns 16 bytes of attacker placed GPU process memory.
VERSION
Chrome Version: 147.0.7727.102 stable Operating System: Multi-platform, ANGLE code. Directly verified on: macOS 26.4, Apple M1 Windows 11, NVIDIA GeForce RTX 5070 Ti Laptop GPU Ubuntu 24.04 x86_64
REPRODUCTION CASE
To see the bypass directly at JS level, open poc.html in Chrome on a backend where ANGLE runs its manual buffer-access validation.
On macOS (Apple Silicon), the default backend is already such a backend. Just open the file:
open /Applications/Google\ Chrome.app poc.html
On Windows, the default backend is D3D11 which masks the OOB. Use –use-angle=d3d9 to get an observable backend:
chrome.exe –use-angle=d3d9 –user-data-dir=C:\tmp\chrome-d3d9 path\to\poc.html
Expected output on an observable backend:
all_max_control=INVALID_OPERATION indices=[0xffffffff] min_max_wrap_probe=NO_ERROR indices=[0x0,0xffffffff] RESULT=SUSPICIOUS: mixed 0/UINT_MAX bypassed where all-UINT_MAX was rejected
On Windows D3D11/GL/Vulkan and on in-browser SwiftShader, both calls return NO_ERROR. The bypass still fires inside ANGLE; the resulting OOB is masked by the driver’s robust buffer access.
To verify the memory safety consequence on the non-robust path that Chrome ships for its own conformance tests, apply poc.patch and run:
autoninja -C out/Test angle_unittests angle_end2end_tests
./out/Test/angle_unittests –gtest_filter=Utilities.IndexRanges
xvfb-run -a ./out/Test/angle_end2end_tests
–gtest_filter=WebGLDrawElementsTest.MixedUintMaxIndexRangeIsNotEmpty/ES2_Vulkan_SwiftShader
The unit test fails because isEmpty() returns true for the full uint range. The end to end test under an is_asan=true build produces an AddressSanitizer DEADLYSIGNAL whose register state has rdx = r8 = r9 = 0xfffffff4 = (0xffffffff * 12) mod 2^32. The attacker supplied index becomes the wrapped vertex fetch byte offset.
CREDIT INFORMATION Reporter credit: Rahul Raj