Index-buffer conversion sizes or positions writes without accounting for restart offsets
When a GPU backend rewrites an index buffer — for primitive-restart emulation, format conversion, or line-loop expansion — it sizes the destination or computes a write offset from a raw index count, ignoring the per-range begin offsets and whole-primitive rounding that restart and expansion introduce, so a per-range write lands past the allocation or an offset is taken against the wrong origin.
Mechanism
ANGLE’s Metal backend rewrites index buffers to emulate primitive restart, convert index formats, and expand line loops. Correct sizing and positioning of the rewritten data is subtle: primitive restart splits a draw into clipped ranges whose clippedRange.begin pushes each range’s destination writes further into the output buffer, per-primitive expansion enlarges each range, and the byte offset where converted indices live in the device buffer is distinct from the element index of the first vertex within the original GL buffer.
The bug is arithmetic that collapses these distinctions: the output buffer is sized from the total index count while ignoring restart-induced begin offsets and expansion, a converted-pool byte offset is conflated with the absolute origin of restart indices in the original buffer, or a raw application-supplied vertex count is fed into transform-feedback/output sizing without rounding to whole primitives. The result is a destination write past the allocation or an index read against the wrong origin — an OOB write/read in the GPU process, reachable from WebGL content that chooses the index data and draw mode.
Fix shape
Max of every clipped range’s actual end offset; distinguish the device-buffer byte offset (offsetInBytes) from the original-buffer element origin (startIndex from originalOffsetOrClientPtr); and round any count that drives sizing down to whole-primitive granularity (gl::GetVerticesNeededForDraw) before it bounds a write.CVEs in this pattern
1 component · ANGLE| CVE | What | Component | Vendor | Severity | Month | |
|---|---|---|---|---|---|---|
| CVE-2026-79131 | OOB in ANGLE | ANGLE | chrome | High | 2026-08 | 🔧 |
| CVE-2026-14396 | OOB in ANGLE | ANGLE | chrome | High | 2026-06 | 🔧 |
| CVE-2026-14400 | OOB in ANGLE | ANGLE | chrome | High | 2026-06 | 🔧 |
Hunt for siblings
In the index-buffer conversion/expansion paths, check that the destination is sized from the max realized end offset (not the total count), and that byte offsets are never used where an element origin is required.
# Index-buffer conversion sizing and offset math:
rg -n 'clippedRange|primitiveRestart|GetVerticesNeededForDraw|offsetInBytes|convertIndexBuffer|LineLoop' \
src/libANGLE/renderer/metal
# Then: confirm allocation uses Max(range end), and byte-offset vs element-origin are kept distinct.
Mitigations that would catch this class
- Size any converted/expanded buffer from the realized maximum end offset, never from a raw input count
- Keep device-buffer byte offsets and original-buffer element origins in separate, named variables through the conversion
- Fuzz index-buffer conversion with primitive restart enabled, line-loop mode, and counts that are not whole multiples of the primitive