← 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

fix 이전에 크기가 부족한 buffer에 대해 최대값인 0xFFFFFFFF index가 허용되었음이 regression test로 확인되었습니다. GPU process에서 out-of-bounds vertex fetch가 발생했다는 점에서 High로 평가합니다. 다만 write primitive가 드러나지 않고, truncation이 발생하는 consumer 산술 연산이 이 diff에 포함되어 있지 않기 때문에, 정보 유출을 넘어선 확장은 제한적입니다.

IndexRange는 index 범위를 uint32_t startuint32_t count로 표현했는데, 이 방식으로는 [0, 0xFFFFFFFF] 범위를 올바르게 나타내기 어렵습니다. start/end 방식으로 전환하고, start > end인 경우를 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; }
+ // 범위: [0, 0xFFFFFFFF] == 0x100000000 (size_t 필요).
+ size_t vertexCount() const
+ {
+ // 참고: isEmpty() == true일 때 unsigned underflow는 허용됨.
+ 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 이제 거부됨

gl::IndexRange(mStart, mEnd, mCount) 표현에서 순수한 (mStart, mEnd) 쌍으로 재작성되었습니다. 생성자에서는 더 이상 mCount = end - start + 1을 미리 계산하지 않으며, isEmpty()mStart > mEnd로 변경되었습니다. 기본 초기화 값은 mStart{1}, mEnd{0}으로 설정되어 기본 range가 empty 상태가 됩니다. vertexCount()uint64_t에서 size_t로 변경되어 요청 시 계산되며, [0, 0xFFFFFFFF]0x100000000을 산출한다는 명시적인 주석이 추가되었습니다. 직접 작성된 operator==는 삭제되고 기본 멤버별 비교로 대체되었습니다. 새로운 단위 테스트가 full-range 값을 검증합니다. 또한 WebGLCompatibilityTest.cpp에서는 소형 buffer에 대해 0xFFFFFFFF index로 glDrawElements를 호출했을 때 GL_INVALID_OPERATION이 반환됨을 확인합니다.

downstream draw-bounds 산술 연산에서 inclusive full-width integer range의 cardinality가 truncation되는 패턴. 이 값은 33비트가 필요하지만, truncation으로 인해 최대 범위가 apparently empty 또는 in-bounds로 축소되어 vertex bounds check가 무력화됩니다.

WebGL의 drawElements는 indexed draw를 수행합니다. element-array buffer에는 bound된 vertex attribute array에서 vertex를 선택하는 정수 index가 담겨 있습니다. draw를 실행하기 전, ANGLE은 실제로 참조되는 index의 최솟값과 최댓값을 계산합니다. 이때 최댓값이 bound된 buffer가 제공하는 vertex 수를 초과하면 GL_INVALID_OPERATION이 반환되어야 합니다.

gl::IndexRange는 이 inclusive [min, max] 범위를 담습니다. inclusive range의 cardinality는 max - min + 1이므로, [0, 0xFFFFFFFF] 전체 범위에는 0x100000000개의 vertex가 존재합니다. 이 값은 33비트가 필요하기 때문에 32비트 정수에는 들어가지 않습니다. ComputeTypedIndexRange는 index buffer를 스캔하여 range를 생성합니다. primitive restart가 비활성화된 경우 0xFFFFFFFF는 실제 index로 처리됩니다. WebKit에서 ANGLE은 샌드박스 처리된 GPU process 내부에서 실행됩니다.

이 문제는 integer truncation/overflow로 인해 bounds check가 우회되고 out-of-bounds read가 발생하는 취약점입니다. Inclusive full-width range [0, 0xFFFFFFFF]의 cardinality는 0x100000000으로, 33비트가 필요합니다. 주목할 점은, 이 diff에서 제거된 IndexRange 코드 자체에서는 wrap이 발생하지 않았다는 것입니다. 기존의 mCountuint64_t0x100000000을 정확히 보관하고 있었습니다. 따라서 0으로의 truncation은 이 cardinality를 가져와(또는 upper bound를 재구성하여) uint32_tGLsizei처럼 더 좁은 타입에 저장하는 consumer 산술 연산에서 발생합니다.

GL_UNSIGNED_INT buffer에 0xFFFFFFFF가 포함된 경우, ComputeTypedIndexRangeIndexRange(0, 0xFFFFFFFF)를 생성합니다. downstream validator는 참조된 최대 index가 bound된 attribute array를 뒷받침하는 vertex 수 이내임을 확인해야 합니다. 이 과정에서 range의 cardinality를 32비트 폭으로 표현하면, 0x1000000000으로 truncation됩니다. 그 결과 최대 범위가 empty 또는 in-bounds처럼 보이게 되어 bound 검사가 수행되지 않고 draw가 진행됩니다. truncation이 발생하는 정확한 코드는 ANGLE draw-validation 경로(validationES*/Context draw handling)에 위치하는데, 이 부분은 이번 diff에 포함되어 있지 않습니다. 따라서 해당 산술 연산은 테스트 및 표현 방식 변경으로부터 추론됩니다. regression test에서는 외부에서 관찰 가능한 결과를 확인할 수 있습니다. 소형 buffer에 대해 {0, 0xFFFFFFFF}glDrawElements(GL_LINES, 2, GL_UNSIGNED_INT, 0)을 호출하면 이제 GL_INVALID_OPERATION이 반환됩니다.

이 취약점은 WebGL drawElements를 통해 web content에서 도달 가능한 경로로, GPU process 내 vertex attribute 메모리에 대한 controlled out-of-bounds read로 악용될 가능성이 있습니다. out-of-bounds 거리는 공격자가 선택한 index/stride 값에 따라 달라지며, write primitive는 드러나지 않습니다. 유출된 데이터가 shader 연산 및 렌더링된 픽셀에 나타날 수 있다는 commit message의 주장은 WebGL vertex-fetch pipeline과 일관되지만, 이는 이번 diff에 드러나지 않는 downstream 동작입니다.

이 취약점은 WebGL indexed draw의 메모리 안전성 bound 검사를 약화시킵니다. ANGLE이 enabled attribute storage 범위를 벗어난 vertex를 참조하는 draw를 거부해야 한다는 불변식이 있습니다. fix 이전에는 최대 32비트 index에서 이 불변식이 위반되었습니다. ANGLE이 GPU process에서 실행되는 만큼, OOB read는 GPU-process sandbox 내의 정보 유출에 해당합니다. 이를 더 광범위한 침해로 확장하려면 별도의 GPU-process escape가 필요합니다.

Note: 0x1000000000으로 truncation하는 정확한 consumer 산술 연산은 표현 방식 변경과 regression test로부터 추론된 것으로, diff에서 직접 확인되지는 않습니다. bounds check 우회와 그 fix는 테스트를 통해 확인됩니다.