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
Tracker484986863
Fix commit52ba614db7e9 (angle/angle) +104/-50
CISA KEVNot listed
Creditedcinzinga
Disclosed2026-05-12

Files Changed

  • src/libANGLE/renderer/metal/ContextMtl.mm
  • src/libANGLE/renderer/metal/ProvokingVertexHelper.h
  • src/libANGLE/renderer/metal/ProvokingVertexHelper.mm
From 52ba614db7e9df373eb33f2c431e47e657b173a9 Mon Sep 17 00:00:00 2001
From: Geoff Lang <geofflang@chromium.org>
Date: Mon, 23 Mar 2026 17:10:53 -0400
Subject: [PATCH] Metal: Protect against overflow in provoking vertex index count

Update the ProvokingVertexHelper methods to pass in index counts as
GLsizei which matches what the API gives us and return index counts in
uint32_t which is what is passed to Metal.

Do internal math in 64 bits and then validate the results fit in 32
bits.

Bug: chromium:484986863
Change-Id: I56553a3deddc98834645c0fab4129dbc65a830d6
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7695152
Reviewed-by: Shahbaz Youssefi <syoussefi@chromium.org>
Reviewed-by: Kimmo Kinnunen <kkinnunen@apple.com>
Commit-Queue: Geoff Lang <geofflang@chromium.org>
---

diff --git a/src/libANGLE/renderer/metal/ContextMtl.mm b/src/libANGLE/renderer/metal/ContextMtl.mm
index 4a01efb..3506996 100644
--- a/src/libANGLE/renderer/metal/ContextMtl.mm
+++ b/src/libANGLE/renderer/metal/ContextMtl.mm
@@ -666,7 +666,7 @@
                                                         GLuint baseInstance)
 {
 
-    size_t outIndexCount               = 0;
+    uint32_t outIndexCount             = 0;
     size_t outIndexOffset              = 0;
     gl::DrawElementsType convertedType = gl::DrawElementsType::UnsignedInt;
     gl::PrimitiveMode outIndexMode     = gl::PrimitiveMode::InvalidEnum;
@@ -675,7 +675,6 @@
     ANGLE_TRY(mProvokingVertexHelper.generateIndexBuffer(
         mtl::GetImpl(context), first, count, mode, convertedType, outIndexCount, outIndexOffset,
         outIndexMode, drawIdxBuffer));
-    GLsizei outIndexCounti32 = static_cast<GLsizei>(outIndexCount);
 
     // Note: we don't need to pass the generated index buffer to ContextMtl::setupDraw.
     // Because setupDraw only needs to operate on the original vertex buffers & PrimitiveMode.
@@ -722,20 +721,20 @@
             MTLIndexType mtlIdxType  = mtl::GetIndexType(convertedType);                           \
             if (instances == 0)                                                                    \
             {                                                                                      \
-                mRenderEncoder.drawIndexed(mtlType, outIndexCounti32, mtlIdxType, drawIdxBuffer,   \
+                mRenderEncoder.drawIndexed(mtlType, outIndexCount, mtlIdxType, drawIdxBuffer,      \
                                            outIndexOffset);                                        \
             }                                                                                      \
             else                                                                                   \
             {                                                                                      \
                 if (baseInstance == 0)                                                             \
                 {                                                                                  \
-                    mRenderEncoder.drawIndexedInstanced(mtlType, outIndexCounti32, mtlIdxType,     \
+                    mRenderEncoder.drawIndexedInstanced(mtlType, outIndexCount, mtlIdxType,        \
                                                         drawIdxBuffer, outIndexOffset, instances); \
                 }                                                                                  \
                 else                                                                               \
                 {                                                                                  \
                     mRenderEncoder.drawIndexedInstancedBaseVertexBaseInstance(                     \
-                        mtlType, outIndexCounti32, mtlIdxType, drawIdxBuffer, outIndexOffset,      \
+                        mtlType, outIndexCount, mtlIdxType, drawIdxBuffer, outIndexOffset,         \
                         instances, 0, baseInstance);                                               \
                 }                                                                                  \
             }                                                                                      \
@@ -792,15 +791,11 @@
 
     if (requiresIndexRewrite(context->getState(), mode))
     {
-        size_t outIndexCount      = 0;
-        gl::PrimitiveMode newMode = gl::PrimitiveMode::InvalidEnum;
+        // Line strips and triangle strips are rewritten to flat line arrays and tri arrays.
         ANGLE_TRY(mProvokingVertexHelper.preconditionIndexBuffer(
             mtl::GetImpl(context), idxBuffer, count, convertedOffset,
-            mState.isPrimitiveRestartEnabled(), mode, convertedType, outIndexCount,
-            provokingVertexAdditionalOffset, newMode, drawIdxBuffer));
-        // Line strips and triangle strips are rewritten to flat line arrays and tri arrays.
-        convertedCounti32 = (uint32_t)outIndexCount;
-        mode              = newMode;
+            mState.isPrimitiveRestartEnabled(), mode, convertedType, convertedCounti32,
+            provokingVertexAdditionalOffset, mode, drawIdxBuffer));
     }
     else
     {
diff --git a/src/libANGLE/renderer/metal/ProvokingVertexHelper.h b/src/libANGLE/renderer/metal/ProvokingVertexHelper.h
index 5389753..b25f1da 100644
--- a/src/libANGLE/renderer/metal/ProvokingVertexHelper.h
+++ b/src/libANGLE/renderer/metal/ProvokingVertexHelper.h
@@ -28,22 +28,22 @@
     ProvokingVertexHelper(ContextMtl *context);
     angle::Result preconditionIndexBuffer(ContextMtl *context,
                                           mtl::BufferRef indexBuffer,
-                                          size_t indexCount,
+                                          GLsizei indexCount,
                                           size_t indexOffset,
                                           bool primitiveRestartEnabled,
                                           gl::PrimitiveMode primitiveMode,
                                           gl::DrawElementsType elementsType,
-                                          size_t &outIndexCount,
+                                          uint32_t &outIndexCount,
                                           size_t &outIndexOffset,
                                           gl::PrimitiveMode &outPrimitiveMode,
                                           mtl::BufferRef &outNewBuffer);
 
     angle::Result generateIndexBuffer(ContextMtl *context,
                                       size_t first,
-                                      size_t indexCount,
+                                      GLsizei indexCount,
                                       gl::PrimitiveMode primitiveMode,
                                       gl::DrawElementsType elementsType,
-                                      size_t &outIndexCount,
+                                      uint32_t &outIndexCount,
                                       size_t &outIndexOffset,
                                       gl::PrimitiveMode &outPrimitiveMode,
                                       mtl::BufferRef &outNewBuffer);
diff --git a/src/libANGLE/renderer/metal/ProvokingVertexHelper.mm b/src/libANGLE/renderer/metal/ProvokingVertexHelper.mm
index 407117b..eeda9bc 100644
--- a/src/libANGLE/renderer/metal/ProvokingVertexHelper.mm
+++ b/src/libANGLE/renderer/metal/ProvokingVertexHelper.mm
@@ -9,11 +9,13 @@
 
 #include "libANGLE/renderer/metal/ProvokingVertexHelper.h"
 #import <Foundation/Foundation.h>
+#include "common/base/anglebase/numerics/checked_math.h"
 #include "libANGLE/Display.h"
 #include "libANGLE/renderer/metal/ContextMtl.h"
 #include "libANGLE/renderer/metal/DisplayMtl.h"
 #include "libANGLE/renderer/metal/mtl_common.h"
 #include "libANGLE/renderer/metal/shaders/rewrite_indices_shared.h"
+
 namespace rx
 {
 
@@ -21,7 +23,8 @@
 {
 constexpr size_t kInitialIndexBufferSize = 0xFFFF;  // Initial 64k pool.
 }
-static inline uint primCountForIndexCount(const uint fixIndexBufferKey, const uint indexCount)
+static inline uint32_t primCountForIndexCount(const uint fixIndexBufferKey,
+                                              const GLsizei indexCount)
 {
     const uint fixIndexBufferMode =
         (fixIndexBufferKey >> MtlFixIndexBufferKeyModeShift) & MtlFixIndexBufferKeyModeMask;
@@ -33,45 +36,57 @@
         case MtlFixIndexBufferKeyLines:
             return indexCount / 2;
         case MtlFixIndexBufferKeyLineStrip:
-            return (uint)MAX(0, (int)indexCount - 1);
+            // Prevent underflow with subtraction and avoid casting to a signed type
+            return std::max(indexCount - 1, 0);
         case MtlFixIndexBufferKeyLineLoop:
-            return (uint)MAX(0, (int)indexCount);
+            return indexCount;
         case MtlFixIndexBufferKeyTriangles:
             return indexCount / 3;
         case MtlFixIndexBufferKeyTriangleStrip:
-            return (uint)MAX(0, (int)indexCount - 2);
+            // Prevent underflow with subtraction and avoid casting to a signed type
+            return std::max(indexCount - 2, 0);
         case MtlFixIndexBufferKeyTriangleFan:
-            return (uint)MAX(0, (int)indexCount - 2);
+            // Prevent underflow with subtraction and avoid casting to a signed type
+            return std::max(indexCount - 2, 0);
         default:
             ASSERT(false);
             return 0;
     }
 }
 
-static inline uint indexCountForPrimCount(const uint fixIndexBufferKey, const uint primCount)
+static inline bool indexCountForPrimCount(const uint fixIndexBufferKey,
+                                          const uint32_t primCount,
+                                          uint32_t *outIndexCount)
 {
+
     const uint fixIndexBufferMode =
         (fixIndexBufferKey >> MtlFixIndexBufferKeyModeShift) & MtlFixIndexBufferKeyModeMask;
+
+    uint32_t indicesPerPrimitive = 0;
     switch (fixIndexBufferMode)
     {
         case MtlFixIndexBufferKeyPoints:
-            return primCount;
+            indicesPerPrimitive = 1;
+            break;
         case MtlFixIndexBufferKeyLines:
-            return primCount * 2;
         case MtlFixIndexBufferKeyLineStrip:
-            return primCount * 2;
         case MtlFixIndexBufferKeyLineLoop:
-            return primCount * 2;
+            indicesPerPrimitive = 2;
+            break;
         case MtlFixIndexBufferKeyTriangles:
-            return primCount * 3;
         case MtlFixIndexBufferKeyTriangleStrip:
-            return primCount * 3;
         case MtlFixIndexBufferKeyTriangleFan:
-            return primCount * 3;
+            indicesPerPrimitive = 3;
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/src/tests/gl_tests/ProvokingVertexTest.cpp b/src/tests/gl_tests/ProvokingVertexTest.cpp
index 59b9128..fc5fb4a 100644
--- a/src/tests/gl_tests/ProvokingVertexTest.cpp
+++ b/src/tests/gl_tests/ProvokingVertexTest.cpp
@@ -745,9 +745,35 @@
     checkFlatQuadColors(kWidth, kHeight, GLColor::red, GLColor::green);
 }
 
+// Only run these tests on Metal. Other backends tend to time out the test suite but not crash.
+class ProvokingVertexTestMetal : public ProvokingVertexTest
+{};
+
+// Test that a very large draw call with flat shading doesn't cause an integer overflow in the Metal
+// backend.
+TEST_P(ProvokingVertexTestMetal, LargeDrawTriangleFan)
+{
+    GLsizei count = 1431655768;
+    glUseProgram(mProgram);
+    glDrawArrays(GL_TRIANGLE_FAN, 0, count);
+    EXPECT_GL_ERROR(GL_INVALID_OPERATION);
+}
+
+// Test that a very large draw call with flat shading doesn't cause an integer overflow in the Metal
+// backend.
+TEST_P(ProvokingVertexTestMetal, LargeDrawTriangleStrip)
+{
+    GLsizei count = 1431655768;
+    glUseProgram(mProgram);
+    glDrawArrays(GL_TRIANGLE_STRIP, 0, count);
+}
+
 GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(ProvokingVertexTest);
 ANGLE_INSTANTIATE_TEST_ES3(ProvokingVertexTest);
 
+GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(ProvokingVertexTestMetal);
+ANGLE_INSTANTIATE_TEST(ProvokingVertexTestMetal, ES3_METAL());
+
 GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(ProvokingVertexBufferUpdateTest);
 ANGLE_INSTANTIATE_TEST_ES3(ProvokingVertexBufferUpdateTest);
Loading diff…

Original Bug Report

reported by ci...@gmail.com

ANGLE Metal ProvokingVertexHelper uint32 Overflow causes GPU OOB Write


Report description

ANGLE Metal ProvokingVertexHelper uint32 Overflow causes GPU OOB Write


Bug location

Where do you want to report your vulnerability?

Chrome VRP – Report security issues affecting the Chrome browser. See program rules

Which URL (or repository) have you found the vulnerability in?

https://chromium.googlesource.com/angle/angle/+/refs/heads/main/src/libANGLE/renderer/metal/ProvokingVertexHelper.mm


The problem

Please describe the technical details of the vulnerability

A uint32 integer overflow in ANGLE’s Metal backend ProvokingVertexHelper::generateIndexBuffer() allows any WebGL 2 webpage to trigger a ~17GB GPU heap buffer overflow. The function indexCountForPrimCount() computes primCount * 3 in 32-bit unsigned arithmetic. With count=1431655768, primCount = count - 2 = 1431655766, then primCount * 3 = 0x100000002 wraps to 2, allocating 8 bytes while the GPU compute shader dispatches 1.4 billion primitives writing ~17GB past the allocation.

This produces a controlled write primitive: the attacker-chosen firstVertex value is written to every 3rd uint32 across ~17GB of GPU memory, corrupting other WebGL buffers. The corrupted data is readable from JavaScript via getBufferSubData().

The shader must use a flat interpolation qualifier to trigger the ProvokingVertexHelper code path (ANGLE’s provoking vertex convention rewrite). Without flat, ANGLE routes through the safe TriangleFanBoundCheck in mtl_utils.mm which correctly rejects the oversized draw. With flat, ANGLE routes through the vulnerable indexCountForPrimCount in ProvokingVertexHelper.mm. No user interaction, no permissions, no flags required. Runs in the GPU process and affects all macOS (Intel + Apple Silicon confirmed), Metal backend only.

Affected Code

Primary overflow in ProvokingVertexHelper.mm:51-75:

static inline uint indexCountForPrimCount(const uint fixIndexBufferKey, const uint primCount)
{
    switch (fixIndexBufferMode) {
        case MtlFixIndexBufferKeyTriangleStrip:
            return primCount * 3;  // uint32 OVERFLOW when primCount > UINT32_MAX/3
        case MtlFixIndexBufferKeyTriangleFan:
            return primCount * 3;  // same overflow
    }
}

Undersized allocation in ProvokingVertexHelper.mm:238-244:

uint primCount     = primCountForIndexCount(indexBufferKey, (uint32_t)indexCount);
uint newIndexCount = indexCountForPrimCount(indexBufferKey, primCount);   // wraps to 2
ANGLE_TRY(mIndexBuffers.allocate(context, newIndexCount * indexSize, ...)); // 8 bytes!
// GPU dispatch: 22.4M thread groups writing ~17GB to the 8-byte buffer

Silent error handling in ContextMtl.mm:2956-2961: checkCommandBufferError() only checks MTLCommandBufferErrorOutOfMemory. All other errors (including GPU timeout/internal error from the 17GB write) are silently ignored. Corrupted buffers remain accessible.

Steps to Reproduce

Prerequisites: Chrome on macOS (any version using ANGLE Metal backend, Chrome 96+).

Minimal overflow (overflow_minimal.html) - no flags required:

/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome \
  "file:///path/to/overflow_minimal.html"
  1. Open overflow_minimal.html in Chrome on macOS
  2. The overflow triggers automatically on page load
  3. Expected: drawArrays accepted with GL error 0x0000, then after 30s: “Context survived. ANGLE silently handled the GPU timeout.” Chrome’s stderr will show: mtl_command_buffer.mm:693 (onCommandBufferCompleted): Completed MTLCommandBuffer failed, and error is Caused GPU Timeout Error (00000002:kIOAccelCommandBufferCallbackErrorTimeout).

Write primitive (write_primitive.html) - requires --in-process-gpu:

/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --in-process-gpu \
  "file:///path/to/write_primitive.html"
  1. Open write_primitive.html in Chrome on macOS with --in-process-gpu
  2. Click “Run Write Primitive Test”
  3. Wait ~30 seconds (15s GPU write + readback)
  4. Expected result: 200/600 victim buffers show corruption. Attacker-chosen firstVertex value 0x2AAAAAA0 appears every 3rd uint32 in victim buffers, readable via getBufferSubData(). The output will show CONFIRMED: Attacker value 0x2AAAAAA0 in 200 victim buffers.
  5. If batch 0 shows 0 corrupted, click again without refreshing (race timing).

The --in-process-gpu flag runs the GPU process in-process, which changes GPU error recovery timing and allows readback of corrupted buffer data before the driver’s error recovery cleans it up. On Chrome stable without this flag, the overflow fires and corrupts cross-buffer memory (visible as 0xDEADBEEF markers overwritten to zeros in 297/600 buffers), but the GPU error recovery zeros corrupted regions before JavaScript can read the overflow’s index pattern. The --in-process-gpu flag is NOT required for the bug to trigger - the overflow is reachable from any renderer via normal IPC to the GPU process.

Tested on:

  • Chrome 144.0.7559.133 (Official Build) (x86_64), macOS 15.7.2
  • Chrome 146.0.7676.0 (Developer Build) (x86_64), macOS 15.7.2

Note on hardware variability: The write primitive PoC requires winning a race condition to place victim buffers in the GPU overflow write path. On my test hardware (Intel/AMD MacBook Pro 15,3), this succeeds on the first attempt with 200/600 buffers corrupted (batch 0: 200/200). On Apple Silicon (M1), the GPU error recovery mechanism (kIOGPUCommandBufferCallbackErrorInnocentVictim) kills in-flight command buffers as collateral damage, preventing readback of corrupted data in our testing - though the overflow itself still fires (visible as GPU reset in kernel logs). The overflow_minimal.html PoC is the simplest way to confirm the overflow triggers on any hardware.

System DoS (no flags required)

Opening any of the PoCs in default Chrome (no flags) may trigger kernel-level GPU resets or “visual snow”. The macOS kernel GPU restart report directly names genIndexBuffer as the hung shader. This was followed by WindowServer watchdog timeout and full system freeze requiring forced reboot.

Fix

Promote primCount * 3 to 64-bit and validate before allocation:

static inline uint64_t indexCountForPrimCount(const uint fixIndexBufferKey, const uint primCount)
{
    switch (fixIndexBufferMode) {
        case MtlFixIndexBufferKeyTriangleStrip:
        case MtlFixIndexBufferKeyTriangleFan:
            return static_cast<uint64_t>(primCount) * 3;
    }
}

// In generateIndexBuffer():
uint64_t newIndexCount = indexCountForPrimCount(indexBufferKey, primCount);
ANGLE_CHECK(context, newIndexCount <= std::numeric_limits<uint32_t>::max(),
            "Index count overflow in provoking vertex rewrite", GL_OUT_OF_MEMORY);

Note: A safe pattern already exists in ANGLE at mtl_utils.mm:1577-1598 (TriangleFanBoundCheck + GetTriangleFanIndicesCount) which uses size_t arithmetic with explicit overflow checks. The vulnerable indexCountForPrimCount deviates from this existing safe pattern.

Additionally, checkCommandBufferError() at ContextMtl.mm:2956 should handle MTLCommandBufferErrorTimeout and other error codes instead of silently ignoring them.

Bisect

Introducing commit: da3db87ec4a491a650d86d3d2776466a48135972 (“Upstream latest changes to Metal backend from Apple to 7/1/2021”), committed October 1, 2021. Review: https://chromium-review.googlesource.com/c/angle/angle/+/3167010. Bug: angleproject:6395. The primCount * 3 overflow has been present since this initial commit of ProvokingVertexHelper.mm. The preconditionIndexBuffer path (line 193-199) has the same overflow.

Earliest affected stable release: Chrome 96 (stable November 16, 2021). Chrome 95 branched September 9, 2021, before this commit landed. Chrome 96 branched October 7, 2021, after.

The rewrite_indices.metal compute shader lacks ANGLE_KERNEL_GUARD bounds checking. While it has a manual if(prim < primCount) guard, this checks against the correct (non-overflowed) primitive count, not the output buffer size. The safe TriangleFanBoundCheck pattern at mtl_utils.mm:1577 predates this code (Copyright 2019).

Impact analysis

GPU process controlled write primitive from any webpage.

Primitive PoC Evidence
Write write_primitive.html 193/200 victim buffers contain attacker-chosen firstVertex value 0x2AAAAAA0 every 3rd uint32, readable via getBufferSubData().
System DoS Any PoC, no flags 10 kernel GPU resets + 5 WindowServer crashes in one session. Kernel report names genIndexBuffer as hung shader. Full system freeze requiring forced reboot.

Write characterization: the overflow writes ~17GB sequentially through GPU VA space. The TRIANGLE_FAN index rewrite pattern {firstVertex+prim+2, firstVertex, firstVertex+prim+1} means every 3rd uint32 = the attacker-chosen firstVertex value exactly. The other 2/3 are sequential counters offset by firstVertex. Data is readable from JavaScript via getBufferSubData(). The write is sequential and the attacker controls 1/3 of the written values.


The cause

What version of Chrome have you found the security issue in?

Chrome 144.0.7559.133 Stable

Yes, it is related to a crash.

Choose the type of vulnerability

Memory Corruption (in a sandboxed process)

How would you like to be publicly acknowledged for your report?

cinzinga

View on issue tracker