Overview

Critical
Severity
6.5
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInteger overflow in ANGLE
DescriptionInteger overflow in ANGLE
ComponentANGLE
Bug ClassInteger Overflow
Action tierwatch — information disclosure — security-relevant, below the RCE-class bar — revised by independent review by AI
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.
Independent review by AIConfirmed (tier revised from patch-now)
Reviewer notes: Primitive overclaimed: diff/advisory and the analysis's own text show a bounds-check bypass enabling an out-of-bounds vertex fetch (OOB read), not a demonstrated write/memory corruption; should be info-leak.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker506375217
Fix commitff1b91d5f69e (angle/angle) +39/-17
CWECWE-472, CWE-190 (Integer overflow)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-04-25 (per vendor advisory)
Fix landed2026-04-29 (public git)
Disclosed2026-05-27
Patch gap28 days — fix public in source before the advisory

Background

`gl::IndexRange`
ANGLE helper storing the span of element indices touched by a draw; historically held mStart and mCount as uint32_t and derived end() as mStart + mCount - 1.
Primitive restart
a GL feature that reserves UINT_MAX as a restart marker; when it is disabled, UINT_MAX becomes a legal element index that a draw may actually dereference.
`maxElementIndex`
the reported cap (GL_MAX_ELEMENT_INDEX) on the largest usable element index, feeding validation of index-buffer contents.
`gl::ComputeVertexBindingElementCount`
computes how many vertices a binding must supply from a draw’s index range, used by buffer-size/bounds validation.

Root Cause Analysis

gl::IndexRange computed mCount = end - start + 1 in 32-bit arithmetic, so when primitive restart is disabled and the maximum index end = UINT_MAX is valid, end - start + 1 wraps to 0 (for start = 0) instead of the true count UINT_MAX + 1. Downstream, gl::ComputeVertexBindingElementCount returned that wrapped drawCount unchecked and vertexCount() was a uint32_t, so buffer-size and range validation saw a tiny required vertex count rather than the enormous real one. Consequently bounds checks that should reject an under-sized vertex buffer passed, while the actual draw fetched a vertex range up to UINT_MAX + 1, producing an out-of-bounds vertex fetch. A second defect compounded this: ValidateDrawElementsCommon cast IndexRange::end to a signed int32, further corrupting the comparison.

The fix widens mCount to uint64_t, stores mEnd explicitly so end() no longer re-derives via 32-bit math, returns 64-bit counts, CheckedNumeric-casts drawCount, and caps maxElementIndex at UINT_MAX - 1 so a valid draw’s element count can never reach the 32-bit overflow boundary.

Key insight
Treating UINT_MAX as a valid element index means the count (end - start + 1) needs one more bit than the index itself, so any 32-bit count computation silently wraps at exactly the largest legal draw and turns a bounds check into a green light.

Attack Path

  1. Disable primitive restart Issue GL calls so UINT_MAX is treated as a real index rather than a restart marker.
  2. Craft index data Supply an index buffer whose maximum element index is UINT_MAX, yielding end = UINT_MAX.
  3. Overflow the count gl::IndexRange computes mCount = end - start + 1 in 32-bit, wrapping to 0/tiny value.
  4. Pass validation gl::ComputeVertexBindingElementCount and buffer-range checks see the wrapped small count and accept an under-sized vertex buffer.
  5. Trigger OOB fetch The GPU/backend draw reads the true UINT_MAX + 1-wide vertex range, reading out of bounds beyond the bound buffer.

Impact Assessment

An attacker-controlled WebGL/ANGLE draw call can bypass vertex-buffer bounds validation and induce an out-of-bounds vertex fetch spanning up to UINT_MAX + 1 vertices. This yields out-of-bounds reads (and potential downstream memory corruption or information disclosure) within the GPU process from renderer-reachable content, all without any memory-safety error being surfaced to validation.

Changed Functions

FunctionChangeNotes
IndexRange
src/common/mathutil.h
modified

Files Changed

  • src/common/mathutil.h
  • src/libANGLE/Context.cpp
  • src/libANGLE/VertexAttribute.cpp
  • src/libANGLE/VertexAttribute.h
  • src/libANGLE/renderer/d3d/d3d9/Renderer9.cpp
  • src/libANGLE/renderer/renderer_utils.cpp
  • src/libANGLE/validationES.h
  • src/tests/gl_tests/WebGLCompatibilityTest.cpp

Audit Directions

  • Count vs. index width
    Wherever an inclusive end - start + 1 (or max - min + 1) count is computed, verify the count type has one more bit of range than the index type, since a maximal valid index overflows a same-width count.
  • Truncating casts in validation
    Flag casts of IndexRange::end/counts to signed or narrower integer types (int32, uint32_t) inside draw/bounds validation; these silently corrupt comparisons at boundary values.
  • Feature-gated valid ranges
    Audit code that assumes sentinel values (like UINT_MAX primitive-restart) are never real data; when the gating feature is disabled the full range becomes reachable and arithmetic must tolerate the extreme.
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;
Loading diff…

Regression Test / PoC

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

Original Bug Report

reported by vm...@google.com

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.h
  • third_party/angle/src/libANGLE/validationES.h
  • third_party/angle/src/common/utilities.cpp
  • third_party/angle/src/libANGLE/Context.cpp
  • third_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.

  1. On macOS Chrome (Metal backend), create a WebGL 1 context and enable OES_element_index_uint.
  2. Bind a small ARRAY_BUFFER to a vertex attribute.
  3. Bind an ELEMENT_ARRAY_BUFFER populated with a Uint32Array([0, K, 0xFFFFFFFF]) where K is a very large out-of-bounds index.
  4. Call gl.drawElements(gl.POINTS, 3, gl.UNSIGNED_INT, 0).
  5. Observe that ANGLE validation passes due to the IndexRange overflow, 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.

View on issue tracker