[5] Fix mapAsync/unmap race in RemoteBuffer
Severity: Medium | Component: WebGPU RemoteBuffer (GPU process) | 9c6bfd6
Medium으로 평가한 근거는 다음과 같습니다. 관측 가능한 영향은 WebGPU API를 통해 unmap 이후에도 buffer에 접근할 수 있는 상태 비동기화에 한정됩니다. stale GPU buffer 내용에 대한 read/write까지 확대될 가능성은 backing 구현의 미검증 동작, 즉 unmapped buffer에 대한 getBufferContents() 호출 결과에 달려 있습니다. trigger 경로는 단순하지만, 이 불확실성으로 인해 corruption primitive의 신뢰도는 낮아집니다.
mapAsync()와 unmap() 사이에 race condition이 존재합니다. 이는 위의 Security Fix #1(3aed028)에 대한 후속 수정 사항입니다. mapAsync가 pending 상태일 때 unmap()이 호출되면, completion callback이 여전히 m_isMapped = true를 설정합니다. 그 결과 RemoteBuffer는 명시적인 unmap 이후에도 유효하게 mapped된 것처럼 보이는 상태로 남게 됩니다.
Source/WebKit/GPUProcess/graphics/WebGPU/RemoteBuffer.cpp
Patch Details
이 수정에서는 두 가지 변경이 race condition을 해결합니다. 먼저 mapAsync의 completion callback에서는 m_pendingMap을 초기화하기 전에 로컬 변수 mapWasPending에 복사합니다. 이후 m_isMapped와 m_mapModeFlags 전환은 callback 시점에 map이 여전히 pending 상태였을 때만 수행됩니다. 만약 unmap()이 그 사이에 m_pendingMap을 초기화한 경우, callback은 아무런 동작도 수행하지 않습니다. 한편 unmap()에서는 protect(m_backing)->unmap() 호출 조건이 m_isMapped에서 m_isMapped || m_pendingMap으로 확장되었고, m_pendingMap도 명시적으로 초기화됩니다. 이로써 Security Fix #1에서 지적된 m_pendingMap이 false로 재설정되지 않던 문제도 함께 해결되었습니다.
Before (race): After (fixed):
mapAsync() → m_pendingMap = true mapAsync() → m_pendingMap = true
unmap() unmap()
m_isMapped? no → skip backing unmap m_isMapped || m_pendingMap? yes
m_isMapped = false → backing->unmap()
(m_pendingMap still true!) m_isMapped = false
m_pendingMap = false
callback fires (success):
m_isMapped = true ← STALE! callback fires (success):
m_mapModeFlags = flags mapWasPending = m_pendingMap (false)
m_pendingMap = false
Result: m_isMapped=true after unmap() mapWasPending? no → skip transition
→ copy/copyWithCopy guards pass
→ writes to logically unmapped buffer Result: m_isMapped stays false ✓
비동기 작업의 시작 플래그와 completion callback 사이의 TOCTOU 상태 비동기화. 중간에 발생한 취소 작업이 pending 작업을 무효화하지 못하는 구조.
Background
WebGPU의 buffer mapping lifecycle에서 mapAsync()는 GPU 측의 비동기 mapping 작업을 시작하고, unmap()은 pending 상태의 map을 취소한 뒤 unmapped 상태로 전환합니다. RemoteBuffer는 GPU process에 위치하며, WebContent process와의 IPC를 위해 mapping lifecycle을 추적하는 shadow state(m_isMapped, m_pendingMap, m_mapModeFlags)를 관리합니다. 이 boolean 플래그들은 실제 backing buffer 상태를 대리하는 유일한 수단이자, GPU process에서 buffer 접근을 제어하는 유일한 enforcement point입니다.
Analysis
이 버그는 Security Fix #1에서 도입된 m_pendingMap 플래그의 직접적인 결과입니다. 첫 번째 수정은 m_isMapped 전환을 async callback으로 올바르게 지연시켰지만, 새로운 문제를 만들어냈습니다. unmap()이 m_isMapped만 확인하는 구조였는데, pending 단계에서 이 값은 false이므로 protect(m_backing)->unmap() 호출도, m_pendingMap 초기화도 발생하지 않았습니다. 이후 async callback이 success=true로 발화하면 m_isMapped = true가 무조건 설정되었고, 이는 unmap()이 이미 호출된 이후에도 마찬가지였습니다.
web content에서 이 race를 유발하는 방법은 단순합니다. buffer.mapAsync() 직후 buffer.unmap()을 호출하면 됩니다. 명세에 따르면 unmap()은 pending 상태의 map을 취소해야 하지만, 수정 이전의 RemoteBuffer는 이 취소를 상태 추적에 반영하지 않았습니다.