← All issues

[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.

Severity: High | Component: ANGLE (WebGL) | 56c55dd

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

- IndexRange(uint32_t start, uint32_t end)
- : mStart(start), mEnd(end), mCount(static_cast<uint64_t>(end - start) + 1)
- { ASSERT(start <= end); }
- bool isEmpty() const { return mCount == 0; }
+ IndexRange(uint32_t start, uint32_t end) : mStart(start), mEnd(end) { ASSERT(mStart <= mEnd); }
+ bool isEmpty() const { return mStart > mEnd; }
...
- uint64_t vertexCount() const { return mCount; }
+ // Range: [0, 0xFFFFFFFF] == 0x100000000 (needs size_t).
+ size_t vertexCount() const
+ {
+ // Note: unsigned underflow ok on isEmpty() == true.
+ return static_cast<size_t>(mEnd) - mStart + 1u;
+ }
private:
- uint32_t mStart{0};
+ uint32_t mStart{1};
uint32_t mEnd{0};
- uint64_t mCount{0};
+ friend bool operator==(const IndexRange &a, const IndexRange &b) noexcept = default;

Source/ThirdParty/ANGLE/src/tests/gl_tests/WebGLCompatibilityTest.cpp

+ constexpr GLuint kIndexData2[] = { 0, std::numeric_limits<GLuint>::max() };
+ glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(kIndexData2), kIndexData2, GL_DYNAMIC_DRAW);
+ glDrawElements(GL_LINES, 2, GL_UNSIGNED_INT, 0);
+ EXPECT_GL_ERROR(GL_INVALID_OPERATION); // 0xFFFFFFFF index now rejected

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.

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.

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.