Medium webkit Race 🔧 Commit mapped

Overview

Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactProcessing maliciously crafted web content may lead to an unexpected Safari crash
DescriptionA race condition was addressed with improved state handling.
ComponentWebKit
Bug ClassRace
Tracker323173
Fix commit5ec5cc91d50a (webkit) +3/-1
CISA KEVNot listed
Creditedremanone (@remanone_)
Disclosed2026-10-26

Background

GPU process / `RemoteImageDecoderAVFProxy`
In WebKit’s multi-process model, image decoding (AVFoundation-backed) runs in the GPU process on behalf of a web content process. RemoteImageDecoderAVFProxy is the GPU-process endpoint receiving IPC from the web process.
`setData(ImageDecoderIdentifier, ...)`
IPC handler that feeds encoded image bytes to a decoder identified by identifier.
`IPC::SharedBufferReference` / `unsafeBuffer()`
The encoded bytes arrive referencing memory the sender (web process) controls. unsafeBuffer() returns a SharedBuffer backed by that sender-controlled (shared) memory; its name signals the backing store is not owned/trusted by the receiver.
`SharedBuffer::create(span)`
Constructs a new SharedBuffer by copying the given bytes into GPU-process-owned memory.
WebKit lifetime model
Ref/RefPtr/WeakPtr; here the hazard is not a local free but concurrent mutation of memory shared across a process boundary.

Root Cause Analysis

RemoteImageDecoderAVFProxy::setData (Source/WebKit/GPUProcess/media/RemoteImageDecoderAVFProxy.cpp) handed the decoder a buffer taken directly from the IPC message: imageDecoder->setData(data.isNull() ? SharedBuffer::create() : data.unsafeBuffer().releaseNonNull(), allDataReceived). data.unsafeBuffer() wraps memory that is shared with and controlled by the web content process. Once setData stores and the decoder reads from that shared buffer, the web process can concurrently mutate or invalidate the underlying memory. The decoder immediately proceeds to use the data — imageDecoder->setData(...) is followed by imageDecoder->frameCount() and further decoding — reading backing store that another process can change out from under it. This is a time-of-check/time-of-use data race across the process boundary (and, if the shared mapping is torn down, a read of invalid memory), which the advisory classifies as a race condition leading to a crash.

The patch copies the bytes into GPU-process-owned memory before use: the non-null branch becomes SharedBuffer::create(data.span()) instead of data.unsafeBuffer().releaseNonNull(). By making an independent copy, the decoder no longer reads from memory the web process can concurrently modify, eliminating the race/TOCTOU on the shared backing store. (The null branch is unchanged: SharedBuffer::create().) The advisory’s “race condition … improved state handling” matches the diff; the fix is the standard “copy untrusted shared memory before consuming it” remedy.

Key insight
The GPU-process decoder consumed a SharedBuffer still backed by web-process-controlled shared memory (unsafeBuffer()), so the sending process could mutate the bytes concurrently; copying them into GPU-owned memory (SharedBuffer::create(data.span())) removes the cross-process race.

Attack Path

  1. Compromise/driver in the web process A malicious page drives image decoding so the web process sends setData IPC with encoded bytes backed by shared memory it still controls.
  2. Reference, don't copy The GPU process wraps that shared memory via unsafeBuffer() and passes it straight to the decoder.
  3. Concurrent mutation From the web process, the attacker mutates (or unmaps/shrinks) the shared buffer’s backing store while the GPU-process decoder is reading it.
  4. TOCTOU / torn read The decoder observes bytes that change mid-decode, or reads memory whose contents/size no longer match what was validated.
  5. Crash primitive Inconsistent/invalid buffer state leads to an unexpected GPU-process termination (the advisory’s stated impact); any stronger corruption primitive is not shown by the diff (inferred).

Impact Assessment

This is a GPU-process vulnerability driven across the IPC boundary from a web content process. The primitive is a cross-process race / TOCTOU on shared-memory-backed encoded image data: the GPU process consumes a buffer the sender can concurrently mutate or invalidate, yielding at least an unexpected GPU-process crash (DoS). Apple’s advisory labels it a race condition leading to a Safari crash, which the diff substantiates; escalation beyond a crash (e.g. to GPU-process memory corruption) is not demonstrated by the one-line change (inferred). Preconditions: the attacker must be able to drive image decoding and control the shared-memory contents from the web process — standard for a compromised or malicious renderer.

Files Changed

  • Source/WebKit/GPUProcess/media/RemoteImageDecoderAVFProxy.cpp

Audit Directions

  • `unsafeBuffer()` consumers
    Grep the GPU/utility processes for other unsafeBuffer() / SharedBufferReference call sites that pass IPC-supplied buffers directly to consumers without copying, especially media/codec paths.
  • IPC shared-memory TOCTOU
    Audit GPU-process handlers that validate a shared buffer’s size/contents and then use it, where the web process could mutate it between check and use.
  • Other `RemoteImageDecoderAVFProxy` entry points
    Review sibling handlers (frame fetches, metadata) for lingering references to sender-controlled memory.
diff --git a/Source/WebKit/GPUProcess/media/RemoteImageDecoderAVFProxy.cpp b/Source/WebKit/GPUProcess/media/RemoteImageDecoderAVFProxy.cpp
index c29a0151587d..f2561f5191d0 100644
--- a/Source/WebKit/GPUProcess/media/RemoteImageDecoderAVFProxy.cpp
+++ b/Source/WebKit/GPUProcess/media/RemoteImageDecoderAVFProxy.cpp
@@ -129,7 +129,9 @@ void RemoteImageDecoderAVFProxy::setData(ImageDecoderIdentifier identifier, cons
     }
 
     RefPtr imageDecoder = m_imageDecoders.get(identifier);
-    imageDecoder->setData(data.isNull() ? SharedBuffer::create() : data.unsafeBuffer().releaseNonNull(), allDataReceived);
+    imageDecoder->setData(
+        data.isNull() ? SharedBuffer::create() : SharedBuffer::create(data.span()),
+        allDataReceived);
 
     auto frameCount = imageDecoder->frameCount();
 
Loading diff…