CVE-2026-28917
Overview
Background
- 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
- Use WebGL on Metal Run WebGL content on an Apple/Metal ANGLE backend that uses ProvokingVertexHelper to rewrite index buffers.
- 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.
- Mis-size the rewrite buffer [inference] The rewritten Metal index buffer is allocated/indexed from an inconsistent count, so writes/reads fall outside it.
- Out-of-bounds / crash The index-buffer fixup accesses out of bounds, corrupting memory or crashing the GPU/WebContent process.
Impact Assessment
Files Changed
Source/ThirdParty/ANGLE/src/libANGLE/renderer/metal/ProvokingVertexHelper.mm
Audit Directions
- Other count arithmetic in the Metal backendgrep 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 indexingAudit 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 validationReview where GL draw count/mode reach the Metal rewrite to confirm bounds/mode validation before the conversion.
Patch
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;
}