CVE-2026-118311
Overview
Background
- MojoCdm
- The renderer-side
media::ContentDecryptionModuleimplementation (media/mojo/clients/mojo_cdm.cc) that proxies EME/CDM operations over Mojo to a remote CDM service. It is ascoped_refptr-managed (reference-counted) object co-owned by the sessions/adapters built on top of it. - MojoCdm::OnConnectionError()
- The disconnect handler invoked when the Mojo pipe to the remote CDM breaks. It tears down state by rejecting outstanding promises and closing all remaining sessions.
- CdmSessionTracker::CloseRemainingSessions()
- A helper that iterates every tracked session and runs a per-session
session_closed_cb_(a member ofMojoCdm) to notify clients each session is closed. - session_closed_cb_
- A
MojoCdmmember callback passed by reference intoCloseRemainingSessions(); it is dereferenced once per remaining session inside the loop. - CdmSessionAdapter / MediaKeySession
- Higher-level objects that hold references to
MojoCdm. When a session is closed (MediaKeySession::OnSessionClosed()), the adapter can drop what may be the last reference toMojoCdm.
Root Cause Analysis
The patch adds a single stack-held scoped_refptr<MojoCdm> self(this); at the top of MojoCdm::OnConnectionError() in media/mojo/clients/mojo_cdm.cc, immediately after remote_cdm_.reset() and before the cleanup sequence (cdm_promise_adapter_.Clear(...) and the call into CdmSessionTracker::CloseRemainingSessions()). The invariant being violated was lifetime ownership of this across a re-entrant cleanup loop. OnConnectionError() runs on an object it does not itself own a reference to; it drives teardown by (a) rejecting every pending CDM promise and (b) closing every remaining session, and each of those operations can synchronously run application/JS-visible callbacks. In particular, closing a session calls back through MediaKeySession::OnSessionClosed(), which can dispose the session and drop the last reference to CdmSessionAdapter, which in turn releases the last scoped_refptr<MojoCdm>. Because CloseRemainingSessions() holds session_closed_cb_ by reference into the destroyed MojoCdm, and continues looping over additional sessions, the next iteration dereferences the now-freed MojoCdm::session_closed_cb_ — a classic “object deletes itself in the middle of its own method” use-after-free.
The fix is the standard scoped_refptr<> self(this) guard: by taking a reference on the stack, MojoCdm is pinned alive until OnConnectionError() returns, so even if every external reference is dropped mid-loop, the member callback and the loop state remain valid. The added regression test (MojoCdmTest.ConnectionError_DestroyCdmInSessionClosedCB) reproduces the exact pattern: two sessions, and the first OnSessionClosed mock resets mojo_cdm_ (dropping the owner’s reference) before the second session is processed.
OnConnectionError() ran a multi-iteration teardown loop that invoked client callbacks capable of releasing the last reference to the very MojoCdm instance executing the loop, so a later iteration touched freed memory; nothing kept this alive for the duration of its own method.Attack Path
- Setup
A page uses Encrypted Media Extensions to create a
MediaKeys/CDM backed by a Mojo remote CDM service and opens two or more sessions on it. - Trigger disconnect
The attacker induces a remote CDM connection error (e.g. the CDM/utility process crashes or the pipe is closed), causing
MojoCdm::OnConnectionError()to run and beginCloseRemainingSessions()over the multiple open sessions. (reachability of the crash trigger is (inferred); the diff proves only the re-entrant-destruction path.) - Drop last reference mid-loop
During the first session’s close notification, JS-observable teardown (
MediaKeySession::OnSessionClosed()→ disposing the session → releasingCdmSessionAdapter) drops the finalscoped_refptr<MojoCdm>, freeingMojoCdm. - Re-enter freed object
The loop proceeds to the next session and dereferences the freed
MojoCdm::session_closed_cb_, operating on reclaimed heap memory. - Primitive
Use-after-free of a
MojoCdminstance in the renderer; with heap grooming of the freed slot, this yields a controllable UAF suitable for corruption/control-flow hijack (escalation beyond the freed-memory access is inferred, not shown by the diff).
Impact Assessment
scoped_refptr-managed MojoCdm and its session_closed_cb_ member while the cleanup loop continues. Gating preconditions: at least two remaining sessions (so the loop iterates after the free) and the ability to make session-close notifications drop the last owning reference. The advisory’s generic “Memory corruption in Media” is consistent with the diff, which specifically substantiates a UAF; promotion to RCE depends on heap-layout control and is inferred.Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Fmedia/mojo/clients/mojo_cdm_unittest.cc |
modified |
Files Changed
media/mojo/clients/mojo_cdm.ccmedia/mojo/clients/mojo_cdm_unittest.cc
Audit Directions
- Self-referential cleanup loopsAudit other Mojo disconnect/
OnConnectionErrorhandlers inmedia/mojo/clients/(and elsewhere) that iterate over promises/sessions while invoking client callbacks; any that lack a stackscoped_refptr<> self(this)guard are suspect. - CdmSessionTracker callersReview every caller of
CdmSessionTracker::CloseRemainingSessions()and anything passing a member callback by reference into a loop, since callback execution can free the owner. - EME lifetime couplingTrace
MediaKeySession::OnSessionClosed()/CdmSessionAdapterreference drops to find other code paths where closing a session can synchronously destroy theMojoCdmwhile it is still on the stack.
Patch
From 8b782807c92e9c9ef01d869adac351db43dd1612 Mon Sep 17 00:00:00 2001 From: Sangbaek Park <[email protected]> Date: Mon, 19 Oct 2026 13:58:29 -0700 Subject: [PATCH] media: Fix UAF when MojoCdm is destroyed during OnConnectionError When a remote CDM connection error occurs, MojoCdm::OnConnectionError() rejects all pending promises and calls CdmSessionTracker::CloseRemainingSessions() by passing a reference to its `session_closed_cb_` member. Running `session_closed_cb_` (or rejecting a pending promise) can synchronously release the last reference to `MojoCdm` (for example, when `MediaKeySession::OnSessionClosed()` disposes its session and drops the last reference to `CdmSessionAdapter`). If `MojoCdm` is destroyed mid-loop while multiple sessions are being closed, subsequent iterations in `CdmSessionTracker::CloseRemainingSessions()` dereference the freed `MojoCdm::session_closed_cb_`. Fix this by holding a `scoped_refptr<MojoCdm>` on the stack in `MojoCdm::OnConnectionError()`, so `MojoCdm` stays alive until the method returns. Bug: 573938968 Change-Id: I57a976035d2818ac621ee9bb7f7ab77a950f5ece Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8593547 Reviewed-by: Colin Blundell <[email protected]> Commit-Queue: Sangbaek Park <[email protected]> Cr-Commit-Position: refs/heads/main@{#1722311} --- diff --git a/media/mojo/clients/mojo_cdm.cc b/media/mojo/clients/mojo_cdm.cc index b454815..3bfbf574 100644 --- a/media/mojo/clients/mojo_cdm.cc +++ b/media/mojo/clients/mojo_cdm.cc @@ -123,6 +123,11 @@ remote_cdm_.reset(); + // Keep `this` alive in case rejecting a promise or closing a session drops + // what would otherwise be the last reference to `this` while in the middle + // of looping through the remaining promises/sessions. + scoped_refptr<MojoCdm> self(this); + // As communication with the remote CDM is broken, reject any outstanding // promises and close all the existing sessions. cdm_promise_adapter_.Clear(CdmPromiseAdapter::ClearReason::kConnectionError); diff --git a/media/mojo/clients/mojo_cdm_unittest.cc b/media/mojo/clients/mojo_cdm_unittest.cc index d3b898d..efae1b14 100644 --- a/media/mojo/clients/mojo_cdm_unittest.cc +++ b/media/mojo/clients/mojo_cdm_unittest.cc @@ -633,6 +633,21 @@ base::RunLoop().RunUntilIdle(); } +TEST_F(MojoCdmTest, ConnectionError_DestroyCdmInSessionClosedCB) { + const std::string session_id_1 = "session1"; + const std::string session_id_2 = "session2"; + Initialize(SUCCESS); + CreateSessionAndExpect(session_id_1, SUCCESS); + CreateSessionAndExpect(session_id_2, SUCCESS); + + EXPECT_CALL(cdm_client_, + OnSessionClosed(_, CdmSessionClosedReason::kInternalError)) + .WillOnce(WithoutArgs([this]() { mojo_cdm_.reset(); })) + .WillOnce(Return()); + + ForceConnectionError(); +} + TEST_F(MojoCdmTest, NoDecryptor) { Initialize(SUCCESS); auto* cdm_context = mojo_cdm_->GetCdmContext();
Regression Test / PoC
diff --git a/media/mojo/clients/mojo_cdm_unittest.cc b/media/mojo/clients/mojo_cdm_unittest.cc
index d3b898d..efae1b14 100644
--- a/media/mojo/clients/mojo_cdm_unittest.cc
+++ b/media/mojo/clients/mojo_cdm_unittest.cc
@@ -633,6 +633,21 @@
base::RunLoop().RunUntilIdle();
}
+TEST_F(MojoCdmTest, ConnectionError_DestroyCdmInSessionClosedCB) {
+ const std::string session_id_1 = "session1";
+ const std::string session_id_2 = "session2";
+ Initialize(SUCCESS);
+ CreateSessionAndExpect(session_id_1, SUCCESS);
+ CreateSessionAndExpect(session_id_2, SUCCESS);
+
+ EXPECT_CALL(cdm_client_,
+ OnSessionClosed(_, CdmSessionClosedReason::kInternalError))
+ .WillOnce(WithoutArgs([this]() { mojo_cdm_.reset(); }))
+ .WillOnce(Return());
+
+ ForceConnectionError();
+}
+
TEST_F(MojoCdmTest, NoDecryptor) {
Initialize(SUCCESS);
auto* cdm_context = mojo_cdm_->GetCdmContext();