CVE-2026-28917
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