OOB 3 CVEs 3 mapped CWE-787CWE-131CWE-125 2026-06 → 2026-08

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.

Invariant
A converted/expanded index buffer must be sized from the maximum actual end offset across every clipped range (not the total index count), each converted range’s destination offset must be derived from the original buffer’s element origin, and any count driving buffer sizing must be rounded down to whole-primitive granularity.

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

Defer the output allocation until it has taken the 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
CVEWhatComponentVendorSeverityMonth
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.

grep / ripgrep
# 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