← WebKit Silent-Fix Report — 2026-W25

446481a1be  Media Containment Bypass: GPU Process Still Registers Contained Media Engines

severity medium class SandboxEscape confidence 0.70 WebKit GPU RemoteMediaPlayerManagerProxy exploitable-grade
Jean-Yves Avenard Wed Jun 17 11:30:19 2026 -0700 full: 446481a1be0908cc9c7273846e63f840efe02215 bug report ↗ view on GitHub ↗
Primitive: instantiate blocked media engine in GPU process
Triage note: A media-containment/isolation bypass letting a compromised WebContent register disallowed media engines in the GPU process.
Contents

The bug at a glance

This is a media-containment/isolation bypass reachable only from an already-compromised or IPC-fuzzing WebContent process: it lets a rogue renderer ask the GPU process to instantiate media engines that containment policy is supposed to forbid, widening the GPU-process attack surface. The fix responds with a MESSAGE_CHECK that terminates the offending WebContent, so the demonstrated outcome is process termination (defense-in-depth) rather than proven GPU-process code execution, warranting medium severity.

RemoteMediaPlayerManagerProxy::createMediaPlayer in the GPU process trusted the engineIdentifier supplied over IPC and never re-checked it against the MediaContainmentEnabled policy. A compromised WebContent could therefore register ‘contained’ engines (AVFoundationMSE, CocoaWebM) in the GPU process. The fix adds a MESSAGE_CHECK that, when containment is enabled, only permits AVFoundation and WirelessPlayback and otherwise kills the sender.

Root cause

createMediaPlayer is the GPU-process handler for the RemoteMediaPlayerManagerProxy_CreateMediaPlayer IPC message. WebContent sends a MediaPlayerIdentifier and a MediaPlayerEnums::MediaEngineIdentifier telling the GPU process which platform media engine to construct (via RemoteMediaPlayerProxy::create). When the MediaContainmentEnabled preference is on, only a restricted set of engines is supposed to be usable, AVFoundation (id 0) and WirelessPlayback (id 10), so that engines such as AVFoundationMSE (id 1, MediaPlayerPrivateMediaSourceAVFObjC) and CocoaWebM (id 9, MediaPlayerPrivateWebM) are contained/disallowed.

Before the patch, createMediaPlayer performed only ASSERT(RunLoop::isMain()) and ASSERT(!m_proxies.contains(identifier)) and then constructed the proxy with whatever engineIdentifier arrived. ASSERTs are release-build no-ops and, crucially, there was no check tying engineIdentifier to the containment policy. A WebContent process that is compromised (or an IPC fuzzer via IPCTestingAPI) could therefore send CreateMediaPlayer with a blocked engine id and have the GPU process instantiate that engine’s private player, defeating containment and exposing the disallowed engine’s parsing/decoding surface inside the GPU process.

The patch defines MESSAGE_CHECK as MESSAGE_CHECK_BASE(assertion, connection) and adds, under PLATFORM(COCOA), a check that passes only if mediaContainmentEnabled is false OR the engineIdentifier is AVFoundation OR WirelessPlayback. MESSAGE_CHECK_BASE treats a failed assertion as a malformed/hostile IPC message: it asks the UI process to terminate the sending WebContent rather than continuing. sharedPreferencesForWebProcessValue().mediaContainmentEnabled reads the per-connection policy, so the enforcement is authoritative in the GPU process rather than relying on WebContent to self-restrict.

The two WebCore engine files named in the commit (MediaPlayerPrivateMediaSourceAVFObjC.mm and MediaPlayerPrivateWebM.mm) correspond to the contained engines the check now blocks (they inform the id-to-engine mapping asserted by the test), though the security-relevant enforcement is the single MESSAGE_CHECK in the GPU process. The added layout test drives the bypass through IPCTestingAPI by intercepting the legitimate CreateMediaPlayer, rewriting the engine id to a blocked engine (1 and 9) plus a fresh MediaPlayerIdentifier to avoid an id collision, and resending; the test expects the WebContent to be terminated, and any survival is reported as FAIL.

Key code

GPU-process MESSAGE_CHECK enforcing the media-containment engine allow-list

#define MESSAGE_CHECK(assertion) MESSAGE_CHECK_BASE(assertion, m_gpuConnectionToWebProcess.get()->connection())

// ... in createMediaPlayer():
#if PLATFORM(COCOA)
    MESSAGE_CHECK(!connection->sharedPreferencesForWebProcessValue().mediaContainmentEnabled
        || engineIdentifier == MediaPlayerEnums::MediaEngineIdentifier::AVFoundation
        || engineIdentifier == MediaPlayerEnums::MediaEngineIdentifier::WirelessPlayback);
#endif

Patch walkthrough

  • Source/WebKit/GPUProcess/media/RemoteMediaPlayerManagerProxy.cpp — Defines MESSAGE_CHECK(assertion) as MESSAGE_CHECK_BASE(assertion, m_gpuConnectionToWebProcess.get()->connection()) and adds, in createMediaPlayer under PLATFORM(COCOA), a check that the request is allowed only when mediaContainmentEnabled is false, or engineIdentifier is AVFoundation, or WirelessPlayback. A blocked engine under containment now terminates the sending WebContent instead of constructing the proxy.
  • Source/WebCore/platform/graphics/avfoundation/objc/MediaPlayerPrivateMediaSourceAVFObjC.mm — Referenced by the commit as the AVFoundationMSE (engine id 1) engine implementation that containment must block; supports the id-to-engine mapping the fix relies on.
  • Source/WebCore/platform/graphics/cocoa/MediaPlayerPrivateWebM.mm — Referenced by the commit as the CocoaWebM (engine id 9) engine implementation that containment must block.
  • LayoutTests/ipc/media-containment-bypass-create-player.html — Added IPCTestingAPI test that intercepts RemoteMediaPlayerManagerProxy_CreateMediaPlayer, rewrites the engine id to blocked engines (1 AVFoundationMSE, 9 CocoaWebM) with a fresh MediaPlayerIdentifier, resends, and expects the WebContent to be terminated by the GPU MESSAGE_CHECK.
  • LayoutTests/platform/wk2/TestExpectations — Marks the new test [ Debug ] … [ Crash ], encoding that the expected outcome is a (deliberate) process termination.

Background

Media containment (MediaContainmentEnabled) — A policy that restricts which media engines a page/process may use, intended to shrink the decoding attack surface reachable in the GPU process. Under containment only AVFoundation and WirelessPlayback are meant to be usable; engines like AVFoundationMSE and CocoaWebM are ‘contained’ (disallowed).

RemoteMediaPlayerManagerProxy (GPU process) — The GPU-process endpoint that services media-player IPC from WebContent, including CreateMediaPlayer. It constructs RemoteMediaPlayerProxy objects that wrap the actual platform media engines, so it is the trust boundary where an engine choice made by WebContent becomes real work in the GPU process.

MediaEngineIdentifier — An enum identifying the platform media engine to instantiate. The test and fix reference AVFoundation (0), AVFoundationMSE (1), CocoaWebM (9), WirelessPlayback (10). WebContent supplies this over IPC, so it is untrusted input that must be validated against policy in the GPU process.

MESSAGE_CHECK / MESSAGE_CHECK_BASE — WebKit’s IPC hardening macro: if the assertion fails, the message is treated as malformed/hostile and the sending process is terminated rather than the handler proceeding. It converts a silent policy violation into an authoritative, sender-killing check inside the receiving (GPU) process.

IPCTestingAPI — A test-only facility that lets a page craft and send raw IPC messages, used here to simulate a compromised WebContent. The test rewrites the engine id in an otherwise legitimate CreateMediaPlayer payload to exercise the bypass without a real renderer exploit.

Vulnerability window

  1. Pre-patch gap — createMediaPlayer validated only RunLoop and identifier uniqueness (via ASSERTs) and trusted the WebContent-supplied engineIdentifier, with no containment re-check in the GPU process.
  2. Bypass — A compromised WebContent (or IPCTestingAPI) sends CreateMediaPlayer with a contained engine id (1 or 9), and the GPU process instantiates the disallowed engine.
  3. Surface exposure — The disallowed engine’s media-parsing/decoding code runs in the GPU process, defeating the containment policy’s attack-surface reduction.
  4. Fix — A PLATFORM(COCOA) MESSAGE_CHECK re-validates engineIdentifier against the allow-list when mediaContainmentEnabled, terminating the sender on violation.
  5. Regression lock — The IPCTestingAPI layout test resends rogue CreateMediaPlayer for engines 1 and 9 and expects WebContent termination (Crash) rather than survival.

Proof of concept

Verbatim excerpt of the added IPCTestingAPI layout test. It hooks the outgoing CreateMediaPlayer IPC, clones the legitimate payload, overwrites the MediaPlayerIdentifier (offset 0, set to 990001+id to avoid collision) and the engine identifier byte (offset 16) with a blocked engine (AVFoundationMSE=1, CocoaWebM=9), and resends to the GPU process. The test then waits 5s and reports FAIL if WebContent is still alive; with the fix, the GPU MESSAGE_CHECK terminates WebContent (expected Crash). This exercises the bypass but demonstrates only sender termination, not GPU code execution.

$F.GPUOutgoingHandler[IPC.messages.RemoteMediaPlayerManagerProxy_CreateMediaPlayer.name] = function(msg) {
        // Use the legitimate CreateMediaPlayer payload as a template, modify
        // the engine identifier (and a unique MediaPlayerIdentifier so GPU does
        // not reject for ID collision), then resend.
        var origBody = msg.buffer.slice(16);
        for (var i = 0; i < blockedEngines.length; ++i) {
            var engine = blockedEngines[i];
            var body = origBody.slice(0);
            var bodyArr = new Uint8Array(body);
            var bodyView = new DataView(body);
            bodyView.setUint32(0, 990001 + engine.id, true);
            bodyView.setUint32(4, 0, true);
            bodyArr[16] = engine.id;
            try {
                $F.enableListener = false;
                IPC.sendMessage("GPU", msg.destinationID, IPC.messages.RemoteMediaPlayerManagerProxy_CreateMediaPlayer.name, bodyArr);
                sent.push(engine.name);
            } catch (e) {
            } finally {
                $F.enableListener = true;
            }
        }
    };

Exploitation

  1. Precondition — Requires an already-compromised WebContent (arbitrary IPC send) or IPCTestingAPI; this is a sandbox/containment escape aid, not a first-stage bug reachable from ordinary web content.
  2. Bypass — Sending CreateMediaPlayer with a contained engine id causes the GPU process to instantiate a disallowed engine, exposing that engine’s decode/parse surface inside the GPU process.
  3. Escalation (inferred) — Any further compromise would depend on a separate bug in the newly-reachable engine; the patch turns the violation into a deterministic WebContent termination, so as shipped the observable effect is denial-of-service/self-kill, not proven GPU RCE.

Detection & hunting

For defenders and SOC / detection engineers:

  • CreateMediaPlayer with contained engine under containment — Log engineIdentifier on RemoteMediaPlayerManagerProxy_CreateMediaPlayer when mediaContainmentEnabled; any value other than AVFoundation/WirelessPlayback indicates a bypass attempt (and now triggers MESSAGE_CHECK).
  • WebContent terminations from GPU MESSAGE_CHECK — Monitor for GPU-process-initiated WebContent kills originating in RemoteMediaPlayerManagerProxy::createMediaPlayer, which post-patch signal rogue engine requests.
  • IPCTestingAPI usage in the wild — IPCTestingAPI should never be enabled in production; alert on its presence, as this class of bypass depends on raw IPC crafting.

Audit directions

  • Other GPU media IPC handlers — Audit sibling RemoteMediaPlayerManagerProxy and RemoteMediaPlayerProxy handlers for engine/format parameters that are trusted from WebContent without re-validating against MediaContainmentEnabled or other policy.
  • ASSERT-only validation in GPU handlers — Grep GPUProcess handlers for ASSERT(…) used where MESSAGE_CHECK is required; ASSERTs are release no-ops and leave the trust boundary unguarded, exactly the gap here.
  • Containment policy coverage — Enumerate every place a MediaEngineIdentifier or comparable capability crosses into the GPU process and confirm the containment allow-list is applied uniformly, not just in createMediaPlayer.
  • Non-COCOA platforms — The new check is guarded by PLATFORM(COCOA); verify whether other platforms have an equivalent containment concept and whether they need analogous enforcement.

Before / after

Loading diff…