Medium chrome Integer Overflow 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInteger overflow in ANGLE
DescriptionInteger overflow in ANGLE
ComponentANGLE
Bug ClassInteger Overflow
Tracker505056913
Fix commitff1b91d5f69e (angle/angle) +39/-17
CISA KEVNot listed
CreditedMufeed VH from Winfunc Research (winfunc.com)
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 <[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;
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 [email protected]

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 vec4 vertex in an ARRAY_BUFFER
  • one UNSIGNED_INT element index equal to 0x80000000
  • 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.html
  • chrome_parent_metal_index_oob_asan_latest.log
  • chrome_parent_metal_index_oob_ramp16.log
  • chrome_parent_metal_index_oob.log (active-release Chrome confirmation)

Numbered repro steps:

  1. Serve the attached PoC locally:
python3 -m http.server 8001
  1. 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)

View on issue tracker
Links in the report