Released May 11, 2026

CVE-2026-28917

CVSS 4.3 severity medium CWE-20 vector CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:L

Overview

Impact:

Description: Processing maliciously crafted web content may lead to an unexpected process crash

Researchers: Vitaly Simonovich

Key insight. Two independent hand-rolled index<->primitive count conversions could disagree or overflow, so the Metal index-rewrite buffer was sized inconsistently with how it was indexed; deriving all counts together in one function removes the mismatch.

Patch Details

diff --git a/Source/ThirdParty/ANGLE/src/libANGLE/renderer/metal/ProvokingVertexHelper.mm b/Source/ThirdParty/ANGLE/src/libANGLE/renderer/metal/ProvokingVertexHelper.mm
index ac00ae604cf9..0faee6c05875 100644
--- a/Source/ThirdParty/ANGLE/src/libANGLE/renderer/metal/ProvokingVertexHelper.mm
+++ b/Source/ThirdParty/ANGLE/src/libANGLE/renderer/metal/ProvokingVertexHelper.mm
@@ -20,84 +20,40 @@ namespace rx
 namespace
 {
 constexpr size_t kInitialIndexBufferSize = 0xFFFF;  // Initial 64k pool.
-}
-static inline uint primCountForIndexCount(const uint fixIndexBufferKey, const uint indexCount)
-{
-    const uint fixIndexBufferMode =
-        (fixIndexBufferKey >> MtlFixIndexBufferKeyModeShift) & MtlFixIndexBufferKeyModeMask;
 
-    switch (fixIndexBufferMode)
-    {
-        case MtlFixIndexBufferKeyPoints:
-            return indexCount;
-        case MtlFixIndexBufferKeyLines:
-            return indexCount / 2;
-        case MtlFixIndexBufferKeyLineStrip:
-            return (uint)MAX(0, (int)indexCount - 1);
-        case MtlFixIndexBufferKeyLineLoop:
-            return (uint)MAX(0, (int)indexCount);
-        case MtlFixIndexBufferKeyTriangles:
-            return indexCount / 3;
-        case MtlFixIndexBufferKeyTriangleStrip:
-            return (uint)MAX(0, (int)indexCount - 2);
-        case MtlFixIndexBufferKeyTriangleFan:
-            return (uint)MAX(0, (int)indexCount - 2);
-        default:
-            ASSERT(false);
-            return 0;
-    }
-}
+struct IndexedDrawRewriteInfoMtl {
+    uint32_t primitiveCount;
+    size_t newIndexCount;
+    gl::PrimitiveMode newPrimitiveMode;
+};
 
-static inline uint indexCountForPrimCount(const uint fixIndexBufferKey, const uint primCount)
+// Returns counts for Metal indexed draw for GL indexed draw parameters.
+inline IndexedDrawRewriteInfoMtl resolveIndexedDrawRewriteInfo(gl::PrimitiveMode mode, GLsizei count)
 {
-    const uint fixIndexBufferMode =
-        (fixIndexBufferKey >> MtlFixIndexBufferKeyModeShift) & MtlFixIndexBufferKeyModeMask;
-    switch (fixIndexBufferMode)
+    uint32_t indexCount = static_cast<uint32_t>(count);
+    switch (mode)
     {
-        case MtlFixIndexBufferKeyPoints:
-            return primCount;
-        case MtlFixIndexBufferKeyLines:
-            return primCount * 2;
-        case MtlFixIndexBufferKeyLineStrip:
-            return primCount * 2;
-        case MtlFixIndexBufferKeyLineLoop:
-            return primCount * 2;
-        case MtlFixIndexBufferKeyTriangles:
-            return primCount * 3;
-        case MtlFixIndexBufferKeyTriangleStrip:
-            return primCount * 3;
-        case MtlFixIndexBufferKeyTriangleFan:
-            return primCount * 3;
+        case gl::PrimitiveMode::Points:
+            return {indexCount, indexCount, gl::PrimitiveMode::Points};
+        case gl::PrimitiveMode::Lines:
+            return {indexCount / 2, indexCount - (indexCount % 2), gl::PrimitiveMode::Lines};
+        case gl::PrimitiveMode::LineLoop:
+            return {indexCount, static_cast<size_t>(indexCount) * 2, gl::PrimitiveMode::Lines};
+        case gl::PrimitiveMode::LineStrip:
+            indexCount = indexCount < 1 ? 0 : indexCount - 1;
+            return {indexCount, static_cast<size_t>(indexCount) * 2, gl::PrimitiveMode::Lines};
+        case gl::PrimitiveMode::Triangles:
+            return {indexCount / 3, indexCount - (indexCount % 3), gl::PrimitiveMode::Triangles};
+        case gl::PrimitiveMode::TriangleStrip:
+        case gl::PrimitiveMode::TriangleFan:
+            indexCount = indexCount < 2 ? 0 : indexCount - 2;
+            return {indexCount, static_cast<size_t>(indexCount) * 3, gl::PrimitiveMode::Triangles};
         default:
-            ASSERT(false);
-            return 0;
+            UNREACHABLE();
+            return {0, 0, gl::PrimitiveMode::InvalidEnum};
     }
 }
 
-static inline gl::PrimitiveMode getNewPrimitiveMode(const uint fixIndexBufferKey)
-{
-    const uint fixIndexBufferMode =
-        (fixIndexBufferKey >> MtlFixIndexBufferKeyModeShift) & MtlFixIndexBufferKeyModeMask;
-    switch (fixIndexBufferMode)
-    {
-        case MtlFixIndexBufferKeyPoints:
-            return gl::PrimitiveMode::Points;
-        case MtlFixIndexBufferKeyLines:
-            return gl::PrimitiveMode::Lines;
-        case MtlFixIndexBufferKeyLineStrip:
-            return gl::PrimitiveMode::Lines;
-        case MtlFixIndexBufferKeyLineLoop:
-            return gl::PrimitiveMode::Lines;
-        case MtlFixIndexBufferKeyTriangles:
-            return gl::PrimitiveMode::Triangles;
-        case MtlFixIndexBufferKeyTriangleStrip:
-            return gl::PrimitiveMode::Triangles;
-        case MtlFixIndexBufferKeyTriangleFan:
-            return gl::PrimitiveMode::Triangles;
-        default:
-            ASSERT(false);
-            return gl::PrimitiveMode::InvalidEnum;
-    }
 }
 
 ProvokingVertexHelper::ProvokingVertexHelper(ContextMtl *context) : mIndexBuffers(false)
@@ -191,14 +147,13 @@ angle::Result ProvokingVertexHelper::preconditionIndexBuffer(ContextMtl *context
     // Upload index buffer
     // dispatch per-primitive?
     uint indexBufferKey = buildIndexBufferKey(elementsType, primitiveRestartEnabled, primitiveMode);
-    uint primCount      = primCountForIndexCount(indexBufferKey, (uint32_t)count);
-    uint newIndexCount = indexCountForPrimCount(indexBufferKey, primCount);
+    auto [primCount, newIndexCount, newPrimitiveMode] = resolveIndexedDrawRewriteInfo(primitiveMode, count);
     // We do not support large buffers at the moment.
     ANGLE_CHECK_GL_MATH(context, newIndexCount <= std::numeric_limits<uint32_t>::max());
-    size_t indexSize   = gl::GetDrawElementsTypeSize(elementsType);
-    size_t newOffset   = 0;
+    size_t newIndexBufferSize = newIndexCount << gl::GetDrawElementsTypeShift(elementsType);
+    size_t newOffset          = 0;
     mtl::BufferRef newBuffer;
-    ANGLE_TRY(mIndexBuffers.allocate(context, newIndexCount * indexSize + indexOffset, nullptr,
+    ANGLE_TRY(mIndexBuffers.allocate(context, newIndexBufferSize + indexOffset, nullptr,
                                      &newBuffer, &newOffset));
     auto threadsPerThreadgroup = MTLSizeMake(MIN(primCount, 64u), 1, 1);
 
@@ -217,7 +172,7 @@ angle::Result ProvokingVertexHelper::preconditionIndexBuffer(ContextMtl *context
         threadsPerThreadgroup);
     outIndexCount    = static_cast<uint32_t>(newIndexCount);
     outIndexOffset   = newOffset;
-    outPrimitiveMode = getNewPrimitiveMode(indexBufferKey);
+    outPrimitiveMode = newPrimitiveMode;
     outNewBuffer     = newBuffer;
     return angle::Result::Continue;
 }
@@ -237,14 +192,13 @@ angle::Result ProvokingVertexHelper::generateIndexBuffer(ContextMtl *context,
     // dispatch per-primitive?
     const bool primitiveRestartEnabled = false;
     uint indexBufferKey = buildIndexBufferKey(elementsType, primitiveRestartEnabled, primitiveMode);
-    uint primCount      = primCountForIndexCount(indexBufferKey, (uint32_t)count);
-    uint newIndexCount    = indexCountForPrimCount(indexBufferKey, primCount);
+    auto [primCount, newIndexCount, newPrimitiveMode] = resolveIndexedDrawRewriteInfo(primitiveMode, count);
     // We do not support large buffers at the moment.
     ANGLE_CHECK_GL_MATH(context, newIndexCount <= std::numeric_limits<uint32_t>::max());
-    size_t indexSize      = gl::GetDrawElementsTypeSize(elementsType);
+    size_t newIndexBufferSize = newIndexCount << gl::GetDrawElementsTypeShift(elementsType);
     size_t newIndexOffset = 0;
     mtl::BufferRef newBuffer;
-    ANGLE_TRY(mIndexBuffers.allocate(context, newIndexCount * indexSize, nullptr, &newBuffer,
+    ANGLE_TRY(mIndexBuffers.allocate(context, newIndexBufferSize, nullptr, &newBuffer,
                                      &newIndexOffset));
     auto threadsPerThreadgroup = MTLSizeMake(MIN(primCount, 64u), 1, 1);
 
@@ -263,7 +217,7 @@ angle::Result ProvokingVertexHelper::generateIndexBuffer(ContextMtl *context,
         threadsPerThreadgroup);
     outIndexCount    = static_cast<uint32_t>(newIndexCount);
     outIndexOffset   = newIndexOffset;
-    outPrimitiveMode = getNewPrimitiveMode(indexBufferKey);
+    outPrimitiveMode = newPrimitiveMode;
     outNewBuffer     = newBuffer;
     return angle::Result::Continue;
 }

Background

Advisory: Apple Security Advisory

Attribute Value
CVE CVE-2026-28917
Bugzilla 310527
Component WebKit
Bug Class OOB
Severity medium
Commit cf2e67ecb9131bdf…
Advisory Apple Advisory

Terminology

  • Provoking vertex — The vertex whose attributes are used for flat-shaded primitives; ANGLE rewrites index buffers on Metal to emulate GL provoking-vertex semantics.
  • Index vs primitive count — For each primitive mode (points/lines/strips/triangles/fans) there is a fixed relation between index count and primitive count used to size buffers.
  • Index-buffer rewrite — ANGLE builds a new Metal index buffer; its size must match how it is later indexed, or accesses go out of bounds.
  • ANGLE Metal backend — The translation layer that implements WebGL/GLES on Apple’s Metal API.

Root Cause Analysis

ANGLE’s Metal backend rewrites index buffers for provoking-vertex handling (ProvokingVertexHelper). The pre-patch code converted between index counts and primitive counts with two hand-rolled per-mode helpers, primCountForIndexCount() and indexCountForPrimCount(), that switched on a packed fixIndexBufferKey mode and did unchecked integer arithmetic (indexCount/2, indexCount-1, primCount*3, casts through int/uint). Those computations size and index the rewritten Metal index buffer, so an inconsistent or wrapped count (from a crafted draw with an unusual mode/count, or the two directions disagreeing) yields a buffer sized differently from how it is later indexed – an out-of-bounds access during the index-buffer fixup, surfacing as a GPU/WebContent process crash. The fix replaces both helpers with a single resolveIndexedDrawRewriteInfo(gl::PrimitiveMode mode, GLsizei count) that returns a coherent {primitiveCount, newIndexCount, newPrimitiveMode} struct computed once from the GL draw parameters, so the primitive count, the rewritten index count, and the mode are derived together and cannot disagree. [Inference] The exact overflow/mismatch input is not spelled out by the diff; what the patch establishes is that the fragile dual-direction count arithmetic is consolidated into one bounded computation, removing the count-mismatch that led to the OOB. This is reached through WebGL draw calls that hit the Metal provoking-vertex index-rewrite path.

Attack Path

1. Use WebGL on Metal

Run WebGL content on an Apple/Metal ANGLE backend that uses ProvokingVertexHelper to rewrite index buffers.

2. Issue a crafted indexed draw

Call an indexed draw with a primitive mode and index count chosen so the per-mode index<->primitive count arithmetic miscomputes or the two directions disagree.

3. Mis-size the rewrite buffer

[inference] The rewritten Metal index buffer is allocated/indexed from an inconsistent count, so writes/reads fall outside it.

4. Out-of-bounds / crash

The index-buffer fixup accesses out of bounds, corrupting memory or crashing the GPU/WebContent process.

Impact Assessment

An out-of-bounds access in ANGLE’s Metal index-buffer rewrite, reachable from crafted WebGL indexed draws, in the GPU/WebContent process. The demonstrated effect is a process crash; whether it yields a controlled corruption primitive depends on the specific count mismatch, which the diff does not fully expose (inference). Confined to the GPU/WebContent process. Rated medium.

Audit Directions

Other count arithmetic in the Metal backend

grep ANGLE metal/ for per-primitive-mode count math (count/2, count-1, *3) that sizes buffers without a single coherent computation or overflow check.

Index-buffer sizing vs indexing

Audit index-rewrite/line-loop/triangle-fan expansion paths to ensure the allocated size and the write/read loop use the same derived count.

Draw-parameter validation

Review where GL draw count/mode reach the Metal rewrite to confirm bounds/mode validation before the conversion.

Changed Functions

Function File Change Note
resolveIndexedDrawRewriteInfo (replaces primCountForIndexCount / indexCountForPrimCount) Source/ThirdParty/ANGLE/src/libANGLE/renderer/metal/ProvokingVertexHelper.mm modified Consolidates the index<->primitive count conversions into one function returning a coherent {primitiveCount, newIndexCount, newPrimitiveMode}, so the counts used to size and index the rewritten Metal index buffer are derived together and cannot disagree/overflow.

Files Changed

Source Files

  • Source/ThirdParty/ANGLE/src/libANGLE/renderer/metal/ProvokingVertexHelper.mm