Critical chrome Integer Overflow 📄 Reporter bug report 🔧 Commit mapped

Overview

Critical
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInteger overflow in ANGLE
DescriptionInteger overflow in ANGLE
ComponentANGLE
Bug ClassInteger Overflow
Tracker506375217
Fix commitff1b91d5f69e (angle/angle) +39/-17
CISA KEVNot listed
CreditedGoogle
Disclosed2026-05-27

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
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