CVE-2024-23213
Overview
Background
- Encrypted Media Extensions (EME)
- The DRM API (navigator.requestMediaKeySystemAccess) that negotiates a media key system, gated by an asynchronous user/UI-process permission grant.
- ScriptExecutionContext / Document lifetime
- A frame’s Document/context can be destroyed by navigation or detach; async continuations must not dereference it after teardown.
- WeakPtr vs raw capture
- Capturing a Document by WeakPtr lets a callback detect destruction and bail; a raw reference dereferences freed memory.
- queueTaskKeepingObjectAlive / std::exchange
- Keeps the request object alive across the queued task and ensures the completion handler runs at most once, preventing use-after-free and double-invoke.
Root Cause Analysis
This fixes a document/execution-context lifetime bug (a use-after-free, rated critical / arbitrary code execution) in the Encrypted Media Extensions requestMediaKeySystemAccess permission flow. The EME grant is asynchronous and user-gated: NavigatorEME::requestMediaKeySystemAccess creates a MediaKeySystemRequest and installs an ‘allow’ callback; when the user/UI process approves, the WebProcess receives the MediaKeySystemWasGranted IPC and calls MediaKeySystemRequest::allow(), which resolves the JS promise and continues the algorithm on the document.
Pre-patch, two lifetime assumptions were unsafe. First, NavigatorEME’s setAllowCallback lambda captured the Document by RAW reference (&document) and later did document.postTask(…); between the async request and the grant, the document/frame can be navigated away or detached and destroyed, so the callback dereferenced a freed Document. Second, MediaKeySystemRequest::allow() captured raw ’this’ and unconditionally moved and invoked m_allowCompletionHandler (and a second handler), with no check that the ScriptExecutionContext still existed and no guard against the handler being run twice/after teardown.
The result is a use-after-free of the Document/ScriptExecutionContext (or the request state) when the EME permission grant is processed after the associated context has gone away — a strong, attacker-timable primitive consistent with the advisory’s arbitrary-code-execution rating.
The fix makes the callback capture weakDocument = WeakPtr { document } and, on invocation, upgrade to RefPtr and reject with InvalidStateError if the document is gone (instead of touching a raw &document); the posted task now derives the document from the passed ScriptExecutionContext via downcast. allow() now early-returns if !scriptExecutionContext(), uses queueTaskKeepingObjectAlive to keep the request alive, and runs the completion handler at most once via std::exchange(m_allowCompletionHandler, {}). Supporting this, the MediaKeySystemWasGranted IPC is changed from a sync/async-reply message (-> ()) to a one-way send, and the CompletionHandler plumbing is removed from WebPage / MediaKeySystemPermissionRequestManager, eliminating the reply-handler lifetime chain that tied a UI-process reply to WebProcess object lifetime.
The restored invariant is that the EME grant continuation never dereferences a Document/context/request that may have been destroyed between the (async, user-gated) permission decision and the continuation running.
Attack Path
- Request media key system access Web content calls navigator.requestMediaKeySystemAccess(), creating a MediaKeySystemRequest and prompting for permission (async, user-gated).
- Tear down the context Before the grant is processed, the page navigates away or detaches the frame so the Document/ScriptExecutionContext is destroyed.
- Deliver the grant The UI process approves and sends MediaKeySystemWasGranted; the WebProcess runs allow()/the allow-callback capturing the now-freed document by raw reference.
- Use-after-free The continuation dereferences the freed Document/context (and completion handler), a controllable UAF.
- Escalate to code execution Reclaim the freed allocation with controlled data to turn the UAF into arbitrary read/write and code execution in WebContent.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
MediaKeySystemRequest::allowSource/WebCore/Modules/encryptedmedia/MediaKeySystemRequest.cpp |
modified | Drops the CompletionHandler parameter; early-returns if !scriptExecutionContext(); uses queueTaskKeepingObjectAlive and runs the completion handler at most once via std::exchange(m_allowCompletionHandler, {}). |
NavigatorEME::requestMediaKeySystemAccess (allow callback)Source/WebCore/Modules/encryptedmedia/NavigatorEME.cpp |
modified | Captures the document as a WeakPtr; on invocation upgrades to RefPtr and rejects with InvalidStateError if gone, and derives the Document from the posted task's ScriptExecutionContext instead of a raw &document capture. |
MediaKeySystemPermissionRequestManager::mediaKeySystemWasGrantedSource/WebKit/WebProcess/EncryptedMedia/MediaKeySystemPermissionRequestManager.cpp |
modified | Drops the CompletionHandler; simply takes the pending request and calls request->allow(). |
MediaKeySystemPermissionRequestManagerProxy::grantRequestSource/WebKit/UIProcess/MediaKeySystemPermissionRequestManagerProxy.cpp |
modified | Sends MediaKeySystemWasGranted as a one-way message (m_page.send) instead of sendWithAsyncReply, removing the reply-handler lifetime chain. |
WebPage::mediaKeySystemWasGranted + WebPage.messages.inSource/WebKit/WebProcess/WebPage/WebPage.cpp |
modified | Removes the CompletionHandler and changes MediaKeySystemWasGranted from a reply message (-> ()) to a plain asynchronous message. |
WebMediaKeySystemClient::requestMediaKeySystemSource/WebKitLegacy/mac/WebCoreSupport/WebMediaKeySystemClient.mm |
modified | Updates the legacy caller to the no-argument allow(). |
Files Changed
Source/WebCore/Modules/encryptedmedia/MediaKeySystemRequest.cppSource/WebCore/Modules/encryptedmedia/MediaKeySystemRequest.hSource/WebCore/Modules/encryptedmedia/NavigatorEME.cppSource/WebKit/UIProcess/MediaKeySystemPermissionRequestManagerProxy.cppSource/WebKit/WebProcess/EncryptedMedia/MediaKeySystemPermissionRequestManager.cppSource/WebKit/WebProcess/EncryptedMedia/MediaKeySystemPermissionRequestManager.hSource/WebKit/WebProcess/WebPage/WebPage.cppSource/WebKit/WebProcess/WebPage/WebPage.hSource/WebKit/WebProcess/WebPage/WebPage.messages.inSource/WebKitLegacy/mac/WebCoreSupport/WebMediaKeySystemClient.mm
Audit Directions
- Same module: EME callbacksAudit MediaKeySystemRequest/NavigatorEME and MediaKeySystemPermissionRequestManager for other raw Document/context captures or completion handlers invoked without a scriptExecutionContext() / WeakPtr guard.
- Async permission grantsReview other user-gated async grant flows (getUserMedia, geolocation, notifications, WebXR) for callbacks capturing Document/frame by raw pointer/reference across the UI-process round trip.
- IPC reply-handler lifetimesGrep for sendWithAsyncReply grant/permission messages whose reply handler assumes WebProcess objects survive; prefer one-way sends plus keep-alive tasks.
Patch
diff --git a/Source/WebKit/CMakeLists.txt b/Source/WebKit/CMakeLists.txt
index 3b782a4b3d6f..bd39f0753235 100644
--- a/Source/WebKit/CMakeLists.txt
+++ b/Source/WebKit/CMakeLists.txt
@@ -40,6 +40,7 @@ set(WebKit_PRIVATE_INCLUDE_DIRECTORIES
"${WEBKIT_DIR}/Shared/CoreIPCSupport"
"${WEBKIT_DIR}/Shared/Databases"
"${WEBKIT_DIR}/Shared/Databases/IndexedDB"
+ "${WEBKIT_DIR}/Shared/Extensions"
"${WEBKIT_DIR}/Shared/FileAPI"
"${WEBKIT_DIR}/Shared/Gamepad"
"${WEBKIT_DIR}/Shared/Notifications"