62b56128ec4b8c85f3dac45e4342ca1419255953 Unvalidated MeshDescriptor bufferStride in RemoteMesh::Update drives out-of-bounds AGX vertex-fetch read in the GPUP
Triage note: RC_REMOVE_SEC_ACCEL_3-bracketed fix adds isValidDescriptor/isValidGeometry with CheckedInt64 overflow checks and MESSAGE_CHECK_COMPLETION in RemoteMesh::update before calling m_backing->update(); previously attacker-controlled MeshDescriptor (vertexCapacity*bufferStride, indexOffset+indexCount vs indexCapacity, bufferIndex bounds) reached the backing with no validation, a GPU-process OOB/integer-overflow primitive.
The bug at a glance
Attacker-controlled WebGPU/Model mesh geometry (WebModel::MeshDescriptor / UpdateMeshDescriptor) arrives at RemoteMesh::update in the GPU process over IPC and, pre-patch, was handed straight to m_backing->update() with no bounds or overflow checking. Fields such as vertexCapacity*bufferStride, layout.bufferIndex, attribute.layoutIndex, and part.indexOffset+indexCount govern how large geometry buffers are sized and where data is written, so an over-large product or negative/out-of-range index yields an undersized allocation or an out-of-bounds buffer index in the GPU process. This is a genuine GPU-process out-of-bounds-write / integer-overflow primitive (high severity is supported); it is memory corruption in a privileged process, though escalation past a controlled heap write to full RCE is (inferred) and not shown by the diff.
A web-reachable IPC entry point (RemoteMesh::update) that fed entirely unvalidated geometry-sizing arithmetic to the GPU-process backing, so the first validation the descriptor ever met was the allocator itself.
Root cause
The violated invariant is that every size and index in a WebModel::MeshDescriptor must be non-negative and must fit the buffers the backing will allocate before RemoteMesh (Source/WebKit/GPUProcess/graphics/Model/RemoteMesh.cpp) forwards it to m_backing->update(). Pre-patch RemoteMesh::update did nothing but m_backing->update(WTF::move(descriptor)); completionHandler(true);. The sequence an attacker controlled from a compromised or scripted WebContent process was: 1. craft an UpdateMeshDescriptor with, e.g., a huge vertexCapacity and bufferStride so vertexCapacity*bufferStride overflows int64, or a layout.bufferIndex >= vertexBufferCount, or an attribute.layoutIndex outside vertexLayouts, or a part.indexOffset+indexCount exceeding indexCapacity; 2. send it over the RemoteMesh IPC; 3. RemoteMesh::update forwards it verbatim to the backing; 4. the backing sizes/addresses GPU geometry buffers from those products and indices, under-allocating or indexing out of range, giving an OOB write in the GPU process.
The patch reconstructs the missing invariant in isValidDescriptor() and isValidGeometry(). isValidDescriptor rejects vertexBufferCount<=0 / negative capacities, clamps every layout.bufferIndex to [0,vertexBufferCount) and every attribute.layoutIndex to [0,vertexLayouts.size()), requires attributeByteSize<=bufferStride, and performs the key overflow guard with CheckedInt64: vertexBufferByteCount = bufferOffset + attribute.offset + vertexCapacitybufferStride, bailing on hasOverflowed(). isValidGeometry adds that update.vertexData.size() must not exceed declaredBufferCount(descriptor) (one buffer is created per declared buffer index, so data for an undeclared buffer has nowhere to go), that indexCapacityindexByteSize and each part’s indexOffset+indexCount*indexByteSize do not overflow and stay within indexBufferByteCount.
A second invariant the patch adds is descriptor stability: m_meshDescriptors (a HashMap<int64_t, MeshDescriptor> keyed by identifier) caches the descriptor first validated for a resource, and a later update whose updateType is the initial type must re-declare an isSameDescriptor() descriptor, while any redeclaration must re-pass isValidDescriptor. This closes a TOCTOU where buffers are allocated for one descriptor and then a follow-up update supplies geometry sized for a different (larger) descriptor. All checks use MESSAGE_CHECK_COMPLETION, which tears down the IPC connection and invokes completionHandler(false) on violation rather than proceeding.
Key code
CheckedInt64 overflow guard on per-attribute vertex-buffer sizing (isValidDescriptor) and the gating call in RemoteMesh::update
CheckedInt64 vertexBufferByteCount = CheckedInt64(layout.bufferOffset) + attribute.offset + CheckedInt64(descriptor.vertexCapacity) * layout.bufferStride;
if (vertexBufferByteCount.hasOverflowed())
return false;
...
// in RemoteMesh::update, new-resource path:
MESSAGE_CHECK_COMPLETION(declaresDescriptor, completionHandler(false));
MESSAGE_CHECK_COMPLETION(isValidGeometry(update.descriptor, update), completionHandler(false));
Fix approach
The fix inserts a full validation gate in RemoteMesh::update (bracketed by #ifndef RC_REMOVE_SEC_ACCEL_3) before m_backing->update() is ever reached. Each descriptor is validated with isValidDescriptor/isValidGeometry using CheckedInt64 so all size arithmetic that the backing relies on cannot overflow, and all bufferIndex/layoutIndex values are range-checked against their declared counts. It also introduces a per-resource descriptor cache (m_meshDescriptors) so that non-initial updates must present a descriptor identical to the one already validated (isSameDescriptor) and processRemovals evicts cached descriptors on successful removal via a protectedThis = Ref{*this} completion lambda. After the fix the hot path collapses to: validate-all-then-forward, so m_backing->update() only ever sees arithmetic-safe, index-bounded, stability-checked geometry; any bad field terminates the connection via MESSAGE_CHECK_COMPLETION.
Patch walkthrough
Source/WebKit/GPUProcess/graphics/Model/RemoteMesh.cpp— Adds vertexFormatByteSize/deformedAttributeByteSize/indexByteSize helpers, isSameDescriptor, declaredBufferCount, isValidDescriptor and isValidGeometry, and rewrites RemoteMesh::update to validate every descriptor (new vs. cached, initial vs. non-initial) with MESSAGE_CHECK_COMPLETION before forwarding to m_backing->update(). processRemovals is wrapped to drop the cached descriptor for each removed identifier on success.Source/WebKit/GPUProcess/graphics/Model/RemoteMesh.h— Includes ModelTypes.h and wtf/HashMap.h and adds the m_meshDescriptors HashMap member (keyed by int64 identifier) that backs the cross-update descriptor-stability check.
Background
GPU Process (GPU_PROCESS_MODEL) — A sandboxed helper process that performs graphics/Model work on behalf of WebContent. RemoteMesh lives here and receives mesh geometry over IPC, so a bug here is memory corruption in a separate, GPU-privileged process driven by web content.
WebModel::MeshDescriptor / UpdateMeshDescriptor — IPC-serialized structs describing vertex buffers (vertexBufferCount, vertexCapacity, per-layout bufferIndex/bufferOffset/bufferStride) and index data (indexCapacity, indexType, parts with indexOffset/indexCount). All fields are attacker-controlled once in the WebContent process.
CheckedInt64 / wtf/CheckedArithmetic.h — WTF’s overflow-detecting integer wrapper; arithmetic sets an overflow flag instead of wrapping, and hasOverflowed() must be checked before value() is trusted. Used here to make vertexCapacitybufferStride and indexCountindexByteSize safe.
MESSAGE_CHECK_COMPLETION — A WebKit IPC macro (MESSAGE_CHECK_COMPLETION_BASE) that, on a failed assertion, treats the message as malformed - killing/flagging the connection - and runs the supplied completion handler (here completionHandler(false)) instead of continuing. The standard way to reject hostile IPC.
VertexFormat / VertexSemantic byte sizing — The helpers map each GPU vertex format (e.g. Float32x4 -> 16 bytes) and, for deformed meshes, each semantic (Position/Normal -> 12 bytes) to a byte size, so the validator can require attributeByteSize <= bufferStride and reject attributes that would read past a vertex stride.
Vulnerability window
- Setup — WebContent (scripted via the Model/WebGPU surface, or already compromised) builds a WebModel::UpdateMeshDescriptor with hostile sizing: overflowing vertexCapacity*bufferStride, out-of-range bufferIndex/layoutIndex, or parts whose indexOffset+indexCount exceed indexCapacity.
- Trigger — The descriptor is sent to the GPU process over the RemoteMesh IPC, invoking RemoteMesh::update.
- Pre-patch flaw — update forwards the Vector straight to m_backing->update() with no validation; the backing sizes/addresses geometry buffers from the overflowed products and raw indices.
- Corruption — The backing under-allocates (integer overflow) or writes/reads using an out-of-range buffer/layout index, producing an OOB write in GPU-process memory.
- Post-patch — isValidGeometry/isValidDescriptor reject the descriptor via MESSAGE_CHECK_COMPLETION before the backing is called, and the descriptor cache blocks a follow-up update from changing geometry sizing after allocation.
Proof of concept
No regression test ships in the diff. Conceptually, from a WebContent context that can issue RemoteMesh updates (the Model/WebGPU mesh path), send an UpdateMeshDescriptor for a new identifier with vertexBufferCount=1, a layout with a huge bufferStride and a vertexCapacity chosen so vertexCapacity*bufferStride overflows int64 (or a part with indexOffset+indexCount > indexCapacity); pre-patch this reaches m_backing->update() and corrupts GPU-process geometry buffers. The exact web/JS entry point and the backing allocation code are (inferred) - not in the diff.
Exploitation
- Reach — Drive the RemoteMesh IPC from WebContent via the Model/WebGPU mesh-update API (or directly from a compromised WebContent process), reaching RemoteMesh::update in the GPU process.
- Trigger — Supply a MeshDescriptor whose vertexCapacity*bufferStride overflows int64 so the backing under-allocates, or an out-of-range bufferIndex/layoutIndex, or index parts exceeding indexCapacity.
- Primitive — Primary primitive is a GPU-process OOB write/read against an undersized or wrongly-indexed geometry buffer, with attacker influence over both the size and (via vertexData/parts) the written bytes.
- Escalation (inferred) — Open questions gating RCE: whether the under-allocated geometry buffer is adjacent to steerable GPU-process metadata, whether the write length/content is controllable enough to clobber a pointer or size field, and whether the overflow can be repeated to groom the heap. None of this is provable from the diff.
Detection & hunting
For defenders and detection engineers:
- RemoteMesh IPC rejections / connection terminations — MESSAGE_CHECK_COMPLETION failures in RemoteMesh::update indicate a WebContent process sending descriptors with bad sizing/indices - a strong malformed-IPC signal post-patch.
- GPU-process crashes in Model geometry paths — Pre-patch, look for GPU-process memory-corruption crashes (ASan heap-buffer-overflow) originating under m_backing->update() with large/odd vertexCapacity or bufferStride values in the descriptor.
- Anomalous mesh descriptor values — Telemetry on vertexCapacity, bufferStride, bufferIndex, indexOffset/indexCount at the IPC boundary; values near INT64 limits or indices >= declared counts are suspicious.
Remaining risk in the patch
- Backing update body not in diff — m_backing->update() and the actual geometry buffer allocation/copy are not in the supplied diff, so that the validator’s constraints exactly match the backing’s assumptions (e.g. that attributeByteSize<=bufferStride is the only per-attribute bound the backing needs) is unverified from here.
- Non-initial update coverage — The isSameDescriptor/updateType logic is intricate; a non-initial updateType with declaresDescriptor=false relies on the cached descriptor still matching the backing’s live allocation - correctness depends on the backing never resizing independently, which the diff does not show.
Audit directions
- Other RemoteGPU/Model IPC receivers — Audit sibling StreamMessageReceiver entry points in Source/WebKit/GPUProcess/graphics/Model and /webgpu for descriptors forwarded to a backing without CheckedInt64 size validation.
- Backing geometry allocation — Review the m_backing->update() implementation (not in this diff) to confirm it still assumes validated sizes, and that no other caller bypasses RemoteMesh validation.
- Cross-update stability elsewhere — Look for other resources whose initial descriptor sizes an allocation that later updates can mutate - the same TOCTOU the m_meshDescriptors cache fixes here.
- Overflow in product arithmetic — Grep GPU-process code for
capacity * stride/count * elementSizepatterns on IPC-derived integers lacking CheckedInt64/hasOverflowed() guards.