[5] ANGLE IndexRange integer truncation bypasses vertex bounds check
An inclusive [0, 0xFFFFFFFF] index range holds 0x100000000 elements — one bit too many; truncated to zero, the maximal WebGL index sails past the bounds check.
Rated High because the regression test confirms a maximal 0xFFFFFFFF index was accepted against an undersized buffer before the fix, producing an out-of-bounds vertex fetch in the GPU process; escalation past information disclosure is limited because no write primitive is evident and the truncating consumer arithmetic is not in this diff.
IndexRange marked up an index range with uint32_t start, uint32_t count, which cannot easily represent range [0, 0xFFFFFFFF]. Switch to start/end markup, with start > end marking an empty range.
Source/ThirdParty/ANGLE/src/common/mathutil.h
Source/ThirdParty/ANGLE/src/tests/gl_tests/WebGLCompatibilityTest.cpp
Patch Details
gl::IndexRange is rewritten from a (mStart, mEnd, mCount) representation to a pure (mStart, mEnd) pair. The constructor no longer precomputes mCount = end - start + 1; isEmpty() becomes mStart > mEnd; default initializers become mStart{1}, mEnd{0} so a default range is empty. vertexCount() changes from uint64_t to a size_t computed on demand, with an explicit comment that [0, 0xFFFFFFFF] yields 0x100000000. The hand-written operator== is deleted in favour of a defaulted member-wise one. New unit tests exercise full-range values, and WebGLCompatibilityTest.cpp asserts that glDrawElements with 0xFFFFFFFF against a small buffer returns GL_INVALID_OPERATION.
Truncation of an inclusive full-width integer range's cardinality (which needs one extra bit) in downstream draw-bounds arithmetic, collapsing the maximal range to an apparently-empty/in-bounds value and defeating the vertex bounds check.
Background
WebGL drawElements issues an indexed draw: an element-array buffer holds integer indices that select vertices from the bound vertex attribute arrays. Before dispatching, ANGLE computes the minimum and maximum index actually referenced and checks the maximum is within the number of vertices the bound buffers supply; an out-of-range maximum must produce GL_INVALID_OPERATION. gl::IndexRange carries that inclusive [min, max] span; its cardinality for an inclusive range is max - min + 1, so the full [0, 0xFFFFFFFF] range has 0x100000000 vertices — a value needing 33 bits that does not fit in a 32-bit integer. ComputeTypedIndexRange scans the index buffer to produce the range; with primitive restart disabled, 0xFFFFFFFF is a real index. In WebKit, ANGLE runs inside the sandboxed GPU process.
Analysis
This is integer truncation/overflow leading to a bounds-check bypass and out-of-bounds read. The inclusive full-width range [0, 0xFFFFFFFF] has cardinality 0x100000000, requiring 33 bits. Notably, the IndexRange code removed by this diff did not itself wrap — the old mCount was a uint64_t that correctly held 0x100000000. The truncation-to-zero therefore occurs in consumer arithmetic that takes this cardinality (or reconstructs an upper bound) and stores it in a narrower type such as uint32_t/GLsizei.
ComputeTypedIndexRange produces IndexRange(0, 0xFFFFFFFF) for a GL_UNSIGNED_INT buffer containing 0xFFFFFFFF. A downstream validator must confirm the highest referenced index fits within the vertices backing the bound attribute arrays; if that check is expressed via the range's cardinality at 32-bit width, 0x100000000 truncates to 0, making the maximal range appear empty or trivially in-bounds, so the bound is not enforced and the draw proceeds. The exact truncating line lives in the ANGLE draw-validation path (validationES*/Context draw handling), which is not part of this diff, so that specific arithmetic is inferred from the test plus the representation change. The regression test confirms the externally observable consequence: glDrawElements(GL_LINES, 2, GL_UNSIGNED_INT, 0) over {0, 0xFFFFFFFF} against a tiny buffer now yields GL_INVALID_OPERATION.
This is exploitable as a controlled out-of-bounds read of vertex attribute memory in the GPU process, reachable from web content through WebGL drawElements, with the out-of-bounds distance influenced by attacker-chosen index/stride values; no write primitive is evident. The commit message's claim that leaked data could surface in shader computation and rendered pixels is consistent with the WebGL vertex-fetch pipeline but is downstream behaviour not visible in this diff.
This vulnerability weakens the memory-safety bounds enforcement of WebGL indexed draws. The model assumes ANGLE rejects any draw whose index buffer references a vertex outside the enabled attribute storage; before the fix that invariant was violated for the maximal 32-bit index. Since ANGLE runs in the GPU process, the OOB read is information disclosure within the GPU-process sandbox, and converting it to broader compromise would require a separate GPU-process escape.
Note: The precise consumer arithmetic that truncates 0x100000000 to 0 is inferred from the representation change and the regression test rather than directly visible; the observable bounds-check bypass and its fix are confirmed by the test.
Audit directions
- Wide counts truncated at a bounds-check boundary. A full-width count computed correctly in a wide type then truncated into a narrower type at a bounds check. Audit ANGLE draw-validation consumers of
gl::IndexRangefor places assigningvertexCount()orend - start + 1intouint32_t/GLsizei/int. Start withsrc/libANGLE/validationES*.cppand Context draw handling, and confirm the upper-bound check now usesend()or asize_t/CheckedNumericcount rather than a 32-bit length. - Inclusive full-domain ranges summarized by a same-width count. Grep ANGLE and WebKit GPU-process WebGL/WebGPU command handling for
+ 1,- 1, andstart + countnear buffer-size comparisons; verify the arithmetic uses a strictly wider type. Examinecommon/mathutil.handanglebase/numerics/safe_math.hfor where checked arithmetic is and is not applied. - Cache-key/consumer correctness after the representation change.
IndexRangeCachekeys on(type, offset, count, primitiveRestartEnabled)and stores start/end ranges; traceIndexRangeCache::findRange/addRangeconsumers to confirm none reconstructs a maximum index from a stored length or narrows the cardinality before the bounds comparison.