[1] RemoteBuffer allows to write non-writable buffers, leading to shader's OOB accesses
Severity: High | Component: WebGPU RemoteBuffer (GPU process) | 3aed028
Rated High because the observable effect is a GPU-process-side state validation bypass reachable from a compromised renderer via IPC, and the projected primitive (CPU-side write to GPU buffer contents during a pending-map window) is consistent with the eager m_isMapped assignment shown in the diff, though the downstream GPU memory corruption path depends on unverified getBufferContents() behavior.
From the JS level, certain buffer operations are prevented when a buffer is mapped, but this was not enforced in the GPU process — allowing a compromised web content process to write to buffers in unexpected states. This would give the ability to perform a CPU-side write after index validation occurs, leading to OOB reads from vertex buffers on the shader cores.
Source/WebKit/GPUProcess/graphics/WebGPU/RemoteBuffer.cpp
Source/WebKit/GPUProcess/graphics/WebGPU/RemoteBuffer.h
Patch Details
The patch makes three changes to RemoteBuffer. First, mapAsync() no longer sets m_isMapped = true and m_mapModeFlags eagerly before the async map completes. Instead, a new m_pendingMap flag is set immediately, and the state transitions to mapped only inside the success callback. Second, copyWithCopy() gains an m_pendingMap rejection check. Third, copy() is refactored: the old code combined the !m_isMapped || !m_mapModeFlags.contains(Write) check with the data.size() check in a single if-statement that ran after SharedMemory::map had already processed the renderer-supplied handle. The fix separates these into an early guard (adding the m_pendingMap check) that rejects invalid states before any SharedMemory work.
Before: After:
mapAsync() mapAsync()
├─ m_isMapped = true (EAGER) ├─ m_pendingMap = true
├─ m_mapModeFlags = flags (EAGER) └─ backing->mapAsync(callback)
└─ backing->mapAsync(callback) └─ on success:
└─ on success: callback(true) m_isMapped = true
m_mapModeFlags = flags
copy() callback(true)
├─ SharedMemory::map(handle) ← FIRST copy()
└─ if (!m_isMapped || ...) reject ├─ if (m_pendingMap || !m_isMapped || ...) reject ← FIRST
└─ SharedMemory::map(handle)
Missing GPU-process-side enforcement of buffer state invariants across an IPC trust boundary, allowing a compromised renderer to bypass JS-level write guards during an async map window.
Background
WebGPU buffers must be explicitly mapped via mapAsync() before their contents can be read or written from the CPU side. The MapMode::Write flag must be set for write access, and while a map is pending (async operation in flight), no CPU-side writes should be permitted. WebKit's multi-process architecture places GPU operations in a separate GPU process, with the WebContent (renderer) process sending commands via IPC. The GPU process is expected to independently validate all operations because the renderer is treated as potentially compromised — any JS-level guards in the renderer are defense-in-depth, not trust boundaries.
RemoteBuffer is the GPU-process-side IPC receiver that wraps a WebCore::WebGPU::Buffer and handles mapAsync, copy, copyWithCopy, and unmap messages from the renderer. It maintains shadow state (m_isMapped, m_mapModeFlags) to enforce that write operations only proceed when the buffer is validly mapped with write access. The correctness of these shadow state transitions is the sole enforcement point for buffer access control in the GPU process.
Analysis
The root cause is a classic "validate at the JS layer but not at the IPC layer" anti-pattern. Before the fix, RemoteBuffer::mapAsync() set m_isMapped = true and m_mapModeFlags eagerly — before the underlying async map operation completed. Because existing state checks in copyWithCopy() and copy() tested m_isMapped, these checks would pass incorrectly during the pending-map window: the buffer appeared validly mapped when the backing map was still in flight. A compromised renderer could call mapAsync and then immediately send copy/copyWithCopy via IPC, and the write-state guards (!m_isMapped || !m_mapModeFlags.contains(Write)) would not reject the operation.
Additionally, in copy(), SharedMemory::map was called on the renderer-supplied handle before state validation. While no memcpy occurred before the check, processing attacker-supplied data before validating the operation's legality is a defense-in-depth failure — the fix eliminates this by validating state first.
The bug was likely found through pattern auditing of GPU process IPC handlers for missing state validation, or through IPC fuzzing targeting RemoteBuffer message sequences with out-of-order map/copy/unmap operations.