← All issues

[2] WebGPU vertex buffer OOB read via flipped comparison in index validation

Severity: High | Component: WebGPU Buffer | 15ddef0

WebGPU API를 통해 임의의 웹 페이지에서 접근 가능한 vertex buffer 메모리에 대해 GPU 측 OOB read가 직접 관찰되므로 High로 평가됩니다. Bypass 메커니즘(보안에 중요한 validation 함수 내 비교 연산자 방향 오류)은 단일 라인 diff에서 confidence 0.95로 확인됩니다. 다만 shader output을 통한 GPU memory exfiltration과 cross-context data leakage는 플랫폼별 GPU memory model에 의존하므로, 이 부분은 확인되지 않은 사항입니다.

Buffer::needsIndexValidation에서 m_maxUshortIndex > maxUshortIndex라는 비교 조건의 피연산자 순서가 뒤집혀 있었습니다. 올바른 방향은 maxUshortIndex > m_maxUshortIndex입니다. 수정 후에는 두 if 블록을 단일 needsUpdate 표현식으로 대체했습니다. 두 비교 모두 올바른 방향의 || 조합으로 구성되었으며, cached 값은 std::max를 통해 무조건 갱신됩니다.

Source/WebGPU/WebGPU/Buffer.mm

bool Buffer::needsIndexValidation(uint32_t maxUnsignedIndex, uint16_t maxUshortIndex)
{
- bool needsUpdate = false;
- if (maxUnsignedIndex > m_maxUnsignedIndex) {
- m_maxUnsignedIndex = maxUnsignedIndex;
- needsUpdate = true;
- }
- if (m_maxUshortIndex > maxUshortIndex) {
- m_maxUshortIndex = maxUshortIndex;
- needsUpdate = true;
- }
+ const bool needsUpdate = maxUnsignedIndex > m_maxUnsignedIndex || maxUshortIndex > m_maxUshortIndex;
+ m_maxUnsignedIndex = std::max(m_maxUnsignedIndex, maxUnsignedIndex);
+ m_maxUshortIndex = std::max(m_maxUshortIndex, maxUshortIndex);
 
return needsUpdate;
}

기존 코드는 두 개의 if 블록으로 구성된 update-and-check 패턴이었습니다. 첫 번째 블록은 maxUnsignedIndex를 처리하며 비교 방향이 올바랐습니다(maxUnsignedIndex > m_maxUnsignedIndex). 두 번째 블록은 maxUshortIndex를 처리했는데, 피연산자 순서가 뒤집혀 있었습니다(m_maxUshortIndex > maxUshortIndex).

이 뒤집힌 조건은 두 가지 문제를 동시에 유발했습니다. 먼저 cached max가 새 max보다 클 때만 조건이 성립하는데, 이는 필요한 방향과 정반대입니다. 또한 조건이 성립하면 더 작은 새 값이 m_maxUshortIndex하향 할당됩니다.

수정 후에는 단일 const bool needsUpdate로 두 비교를 올바른 방향으로 결합하고, 두 cached 값을 std::max로 무조건 갱신합니다.

WebGPU의 drawIndexed는 index buffer를 통해 처리할 vertex를 선택합니다. Index buffer에는 vertex buffer에 대한 정수 인덱스가 담겨 있습니다. Draw를 실행하기 전, 구현은 index buffer의 최대 인덱스가 vertex buffer의 범위를 초과하지 않는지 검증해야 합니다. 이 검증이 없으면 GPU가 vertex data 영역 밖을 읽게 됩니다.

Buffer::needsIndexValidation은 최적화를 위한 함수입니다. 이전에 검증된 최대 인덱스를 cached 값으로 보관하며, uint32 인덱스는 m_maxUnsignedIndex에, uint16 인덱스는 m_maxUshortIndex에 저장됩니다. 함수는 현재 draw의 최대 인덱스가 이전에 검증된 최댓값을 초과하는지, 즉 새로운 validation 수행이 필요한지 여부를 반환합니다. true이면 호출자가 재검증을 수행해야 하고, false이면 기존에 검증된 범위가 현재 draw를 이미 포함하는 것으로 간주됩니다.

Index validation guard에서 비교 피연산자 순서가 뒤집혀, 닫혀야 할 때 열리고 열려야 할 때 닫히는 역전 현상이 발생한 패턴.

m_maxUshortIndex > maxUshortIndex 조건은 cached max가 max를 초과할 때만 성립합니다. 보안상 중요한 방향과 정반대입니다. draw call이 이전보다 ushort 인덱스를 제시하는 경우(위험한 상황)에는 조건이 false가 되어 needsUpdate가 false로 유지되고, m_maxUshortIndex도 갱신되지 않습니다. 결과적으로 재검증이 반드시 필요한 상황에서 함수는 호출자에게 불필요하다고 알립니다.

반대로 더 작은 인덱스(이미 검증된)가 제시될 때는 조건이 true가 됩니다. 이때 cached max가 더 작은 값으로 하향 갱신되고, 함수는 불필요하게 재검증이 필요하다고 보고합니다. uint32 경로는 올바른 반면 uint16 경로만 뒤집혀 있었는데, 두 번째 블록을 작성할 때 피연산자 순서가 역전된 전형적인 copy-paste 오류입니다.

🔒

Explores the exploitation mechanics of bypassing GPU index validation and what GPU memory could be disclosed

더 확인하려면 구독해 주세요

🔒

Multiple audit patterns identified for WebGPU validation caching and index-type handling, with concrete search targets

더 확인하려면 구독해 주세요