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 |
|---|---|---|
ifSource/WebCore/Modules/encryptedmedia/NavigatorEME.cpp |
modified | |
ifSource/WebKit/WebProcess/EncryptedMedia/MediaKeySystemPermissionRequestManager.cpp |
modified |
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/WebCore/Modules/encryptedmedia/MediaKeySystemRequest.cpp b/Source/WebCore/Modules/encryptedmedia/MediaKeySystemRequest.cpp
index 61d2706b1d88..a3d0de621971 100644
--- a/Source/WebCore/Modules/encryptedmedia/MediaKeySystemRequest.cpp
+++ b/Source/WebCore/Modules/encryptedmedia/MediaKeySystemRequest.cpp
@@ -86,12 +86,14 @@ void MediaKeySystemRequest::start()
controller->requestMediaKeySystem(*this);
}
-void MediaKeySystemRequest::allow(CompletionHandler<void()>&& completionHandler)
+void MediaKeySystemRequest::allow()
{
- queueTaskKeepingObjectAlive(*this, TaskSource::UserInteraction, [this, handler = WTFMove(completionHandler)]() mutable {
- auto completionHandler = WTFMove(m_allowCompletionHandler);
- completionHandler(WTFMove(m_promise));
- handler();
+ if (!scriptExecutionContext())
+ return;
+
+ queueTaskKeepingObjectAlive(*this, TaskSource::UserInteraction, [this] {
+ if (auto allowCompletionHandler = std::exchange(m_allowCompletionHandler, { }))
+ allowCompletionHandler(WTFMove(m_promise));
});
}
diff --git a/Source/WebCore/Modules/encryptedmedia/MediaKeySystemRequest.h b/Source/WebCore/Modules/encryptedmedia/MediaKeySystemRequest.h
index ca43f60542c3..a3247ff6a5ca 100644
--- a/Source/WebCore/Modules/encryptedmedia/MediaKeySystemRequest.h
+++ b/Source/WebCore/Modules/encryptedmedia/MediaKeySystemRequest.h
@@ -51,7 +51,7 @@ public:
MediaKeySystemRequestIdentifier identifier() const { return m_identifier; }
WEBCORE_EXPORT void start();
- WEBCORE_EXPORT void allow(CompletionHandler<void()>&&);
+ WEBCORE_EXPORT void allow();
WEBCORE_EXPORT void deny(const String& errorMessage = emptyString());
WEBCORE_EXPORT SecurityOrigin* topLevelDocumentOrigin() const;
diff --git a/Source/WebCore/Modules/encryptedmedia/NavigatorEME.cpp b/Source/WebCore/Modules/encryptedmedia/NavigatorEME.cpp
index 01d686e304a7..719bbf6c6517 100644
--- a/Source/WebCore/Modules/encryptedmedia/NavigatorEME.cpp
+++ b/Source/WebCore/Modules/encryptedmedia/NavigatorEME.cpp
@@ -104,8 +104,14 @@ void NavigatorEME::requestMediaKeySystemAccess(Navigator& navigator, Document& d
}
auto request = MediaKeySystemRequest::create(document, keySystem, WTFMove(promise));
- request->setAllowCallback([keySystem, supportedConfigurations = WTFMove(supportedConfigurations), &document, logger = WTFMove(logger), identifier = WTFMove(identifier)](Ref<DeferredPromise>&& promise) mutable {
- document.postTask([promise = WTFMove(promise), &document, keySystem, logger = WTFMove(logger), identifier = WTFMove(identifier), supportedConfigurations = WTFMove(supportedConfigurations)] (ScriptExecutionContext&) mutable {
+ request->setAllowCallback([keySystem, supportedConfigurations = WTFMove(supportedConfigurations), weakDocument = WeakPtr { document }, logger = WTFMove(logger), identifier = WTFMove(identifier)](Ref<DeferredPromise>&& promise) mutable {
+ RefPtr document = weakDocument.get();
+ if (!document) {
+ promise->reject(ExceptionCode::InvalidStateError);
+ return;
+ }
+
+ document->postTask([promise = WTFMove(promise), keySystem, logger = WTFMove(logger), identifier = WTFMove(identifier), supportedConfigurations = WTFMove(supportedConfigurations)] (ScriptExecutionContext& context) mutable {
// 3. Let document be the calling context's Document.
// 4. Let origin be the origin of document.
// 5. Let promise be a new promise.
@@ -119,6 +125,7 @@ void NavigatorEME::requestMediaKeySystemAccess(Navigator& navigator, Document& d
}
// 6.2. Let implementation be the implementation of keySystem.
+ auto& document = downcast<Document>(context);
auto implementation = CDM::create(document, keySystem);
tryNextSupportedConfiguration(document, WTFMove(implementation), WTFMove(supportedConfigurations), WTFMove(promise), WTFMove(logger), WTFMove(identifier));
});
diff --git a/Source/WebKit/UIProcess/MediaKeySystemPermissionRequestManagerProxy.cpp b/Source/WebKit/UIProcess/MediaKeySystemPermissionRequestManagerProxy.cpp
index f38b57167305..fbb78b40d412 100644
--- a/Source/WebKit/UIProcess/MediaKeySystemPermissionRequestManagerProxy.cpp
+++ b/Source/WebKit/UIProcess/MediaKeySystemPermissionRequestManagerProxy.cpp
@@ -105,7 +105,7 @@ void MediaKeySystemPermissionRequestManagerProxy::grantRequest(MediaKeySystemPer
#if ENABLE(ENCRYPTED_MEDIA)
ALWAYS_LOG(LOGIDENTIFIER, request.mediaKeySystemID().toUInt64(), ", keySystem: ", request.keySystem());
- m_page.sendWithAsyncReply(Messages::WebPage::MediaKeySystemWasGranted { request.mediaKeySystemID() }, [] { });
+ m_page.send(Messages::WebPage::MediaKeySystemWasGranted { request.mediaKeySystemID() });
#else
UNUSED_PARAM(request);
#endif
diff --git a/Source/WebKit/WebProcess/EncryptedMedia/MediaKeySystemPermissionRequestManager.cpp b/Source/WebKit/WebProcess/EncryptedMedia/MediaKeySystemPermissionRequestManager.cpp
index 7512ba9b4835..1af0b7bd97f0 100644
--- a/Source/WebKit/WebProcess/EncryptedMedia/MediaKeySystemPermissionRequestManager.cpp
+++ b/Source/WebKit/WebProcess/EncryptedMedia/MediaKeySystemPermissionRequestManager.cpp
@@ -122,15 +122,13 @@ void MediaKeySystemPermissionRequestManager::mediaCanStart(Document& document)
sendMediaKeySystemRequest(pendingRequest);
}
-void MediaKeySystemPermissionRequestManager::mediaKeySystemWasGranted(MediaKeySystemRequestIdentifier requestID, CompletionHandler<void()>&& completionHandler)
+void MediaKeySystemPermissionRequestManager::mediaKeySystemWasGranted(MediaKeySystemRequestIdentifier requestID)
{
auto request = m_ongoingMediaKeySystemRequests.take(requestID);
- if (!request) {
- completionHandler();
+ if (!request)
return;
- }
- request->allow(WTFMove(completionHandler));
+ request->allow();
}
void MediaKeySystemPermissionRequestManager::mediaKeySystemWasDenied(MediaKeySystemRequestIdentifier requestID, String&& message)
diff --git a/Source/WebKit/WebProcess/EncryptedMedia/MediaKeySystemPermissionRequestManager.h b/Source/WebKit/WebProcess/EncryptedMedia/MediaKeySystemPermissionRequestManager.h
index c4f42d8a9e36..ebfc17f5051a 100644
--- a/Source/WebKit/WebProcess/EncryptedMedia/MediaKeySystemPermissionRequestManager.h
+++ b/Source/WebKit/WebProcess/EncryptedMedia/MediaKeySystemPermissionRequestManager.h
@@ -47,7 +47,7 @@ public:
void startMediaKeySystemRequest(WebCore::MediaKeySystemRequest&);
void cancelMediaKeySystemRequest(WebCore::MediaKeySystemRequest&);
- void mediaKeySystemWasGranted(WebCore::MediaKeySystemRequestIdentifier, CompletionHandler<void()>&&);
+ void mediaKeySystemWasGranted(WebCore::MediaKeySystemRequestIdentifier);
void mediaKeySystemWasDenied(WebCore::MediaKeySystemRequestIdentifier, String&&);
private:
diff --git a/Source/WebKit/WebProcess/WebPage/WebPage.cpp b/Source/WebKit/WebProcess/WebPage/WebPage.cpp
index db36d63a7b92..dea1e2362020 100644
--- a/Source/WebKit/WebProcess/WebPage/WebPage.cpp
+++ b/Source/WebKit/WebProcess/WebPage/WebPage.cpp
@@ -5560,9 +5560,9 @@ void WebPage::setMockCaptureDevicesInterrupted(bool isCameraInterrupted, bool is
#endif // ENABLE(MEDIA_STREAM)
#if ENABLE(ENCRYPTED_MEDIA)
-void WebPage::mediaKeySystemWasGranted(MediaKeySystemRequestIdentifier mediaKeySystemID, CompletionHandler<void()>&& completionHandler)
+void WebPage::mediaKeySystemWasGranted(MediaKeySystemRequestIdentifier mediaKeySystemID)
{
- m_mediaKeySystemPermissionRequestManager->mediaKeySystemWasGranted(mediaKeySystemID, WTFMove(completionHandler));
+ m_mediaKeySystemPermissionRequestManager->mediaKeySystemWasGranted(mediaKeySystemID);
}
void WebPage::mediaKeySystemWasDenied(MediaKeySystemRequestIdentifier mediaKeySystemID, String&& message)
diff --git a/Source/WebKit/WebProcess/WebPage/WebPage.h b/Source/WebKit/WebProcess/WebPage/WebPage.h
index f9a2937a4609..9088069682f4 100644
--- a/Source/WebKit/WebProcess/WebPage/WebPage.h
+++ b/Source/WebKit/WebProcess/WebPage/WebPage.h
@@ -2005,7 +2005,7 @@ private:
#endif
#if ENABLE(ENCRYPTED_MEDIA)
- void mediaKeySystemWasGranted(WebCore::MediaKeySystemRequestIdentifier, CompletionHandler<void()>&&);
+ void mediaKeySystemWasGranted(WebCore::MediaKeySystemRequestIdentifier);
void mediaKeySystemWasDenied(WebCore::MediaKeySystemRequestIdentifier, String&& message);
#endif
diff --git a/Source/WebKit/WebProcess/WebPage/WebPage.messages.in b/Source/WebKit/WebProcess/WebPage/WebPage.messages.in
index 09ba66333885..98027f0b171e 100644
--- a/Source/WebKit/WebProcess/WebPage/WebPage.messages.in
+++ b/Source/WebKit/WebProcess/WebPage/WebPage.messages.in
@@ -430,7 +430,7 @@ GenerateSyntheticEditingCommand(enum:uint8_t WebKit::SyntheticEditingCommandType
#endif
#if ENABLE(ENCRYPTED_MEDIA)
- MediaKeySystemWasGranted(WebCore::MediaKeySystemRequestIdentifier mediaKeySystemID) -> ()
+ MediaKeySystemWasGranted(WebCore::MediaKeySystemRequestIdentifier mediaKeySystemID)
MediaKeySystemWasDenied(WebCore::MediaKeySystemRequestIdentifier mediaKeySystemID, String message)
#endif
diff --git a/Source/WebKitLegacy/mac/WebCoreSupport/WebMediaKeySystemClient.mm b/Source/WebKitLegacy/mac/WebCoreSupport/WebMediaKeySystemClient.mm
index cb611e9e8f15..95203caf1c6c 100644
--- a/Source/WebKitLegacy/mac/WebCoreSupport/WebMediaKeySystemClient.mm
+++ b/Source/WebKitLegacy/mac/WebCoreSupport/WebMediaKeySystemClient.mm
@@ -43,7 +43,7 @@ void WebMediaKeySystemClient::requestMediaKeySystem(MediaKeySystemRequest& reque
{
BEGIN_BLOCK_OBJC_EXCEPTIONS
- request.allow([] { });
+ request.allow();
END_BLOCK_OBJC_EXCEPTIONS
}