High chrome UAF 🔧 Commit mapped

Overview

High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactMemory corruption in Media
DescriptionMemory corruption in Media
ComponentMedia
Sub-componentMedia
Bug ClassUAF
Tracker573938968
Fix commit8b782807c92e (chromium/src) +20/-0
CISA KEVNot listed
Creditedsecbuddy(Ben)
Disclosed2026-10-27

Background

MojoCdm
The renderer-side media::ContentDecryptionModule implementation (media/mojo/clients/mojo_cdm.cc) that proxies EME/CDM operations over Mojo to a remote CDM service. It is a scoped_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 of MojoCdm) to notify clients each session is closed.
session_closed_cb_
A MojoCdm member callback passed by reference into CloseRemainingSessions(); 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 to MojoCdm.

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.

Key insight
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

  1. 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.
  2. 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 begin CloseRemainingSessions() over the multiple open sessions. (reachability of the crash trigger is (inferred); the diff proves only the re-entrant-destruction path.)
  3. Drop last reference mid-loop During the first session’s close notification, JS-observable teardown (MediaKeySession::OnSessionClosed() → disposing the session → releasing CdmSessionAdapter) drops the final scoped_refptr<MojoCdm>, freeing MojoCdm.
  4. Re-enter freed object The loop proceeds to the next session and dereferences the freed MojoCdm::session_closed_cb_, operating on reclaimed heap memory.
  5. Primitive Use-after-free of a MojoCdm instance 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

This is a renderer-process use-after-free reachable from untrusted web content via the EME/CDM API surface, requiring no special privilege beyond scripting a page that opens multiple CDM sessions. The guaranteed primitive from the diff is a UAF read/use of a freed 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

FunctionChangeNotes
TEST_F
media/mojo/clients/mojo_cdm_unittest.cc
modified

Files Changed

  • media/mojo/clients/mojo_cdm.cc
  • media/mojo/clients/mojo_cdm_unittest.cc

Audit Directions

  • Self-referential cleanup loops
    Audit other Mojo disconnect/OnConnectionError handlers in media/mojo/clients/ (and elsewhere) that iterate over promises/sessions while invoking client callbacks; any that lack a stack scoped_refptr<> self(this) guard are suspect.
  • CdmSessionTracker callers
    Review 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 coupling
    Trace MediaKeySession::OnSessionClosed() / CdmSessionAdapter reference drops to find other code paths where closing a session can synchronously destroy the MojoCdm while it is still on the stack.
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();
Loading diff…

Regression Test / PoC

shipped with the fix
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();
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.