Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOut of bounds read in Media
DescriptionOut of bounds read in Media
ComponentMedia
Bug ClassOOB
Tracker497412658
Fix commit41bfbc009df8 (chromium/src) +12/-3
CISA KEVNot listed
CreditedGoogle
Disclosed2026-04-15

Files Changed

  • content/browser/media/session/media_session_impl.cc
From 41bfbc009df8336d2c813cd3963e75a28a022e43 Mon Sep 17 00:00:00 2001
From: Tommy Steimel <steimel@chromium.org>
Date: Tue, 31 Mar 2026 16:11:55 -0700
Subject: [PATCH] [Media Session] Don't assume there is still 1 normal player

There are some actions in MediaSessionImpl that are only available when
there is exactly 1 normal player, so when they're called, there's a
DCHECK that we do in fact have 1 normal player. However, since Mojo
calls are asynchronous, it's possible for one of these actions to be
legitimately called with 1 normal player, but by the time it runs there
are either 0 or 2+ normal players.

This CL changes these instances to no longer DCHECK that there is 1
normal player and instead just early return if there isn't.

Bug: 497412658
Change-Id: I0fdf3c6779c224db996091b2fd463bc3cb9464f3
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7719021
Reviewed-by: Benjamin Keen <bkeen@google.com>
Commit-Queue: Tommy Steimel <steimel@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1608166}
---

diff --git a/content/browser/media/session/media_session_impl.cc b/content/browser/media/session/media_session_impl.cc
index 542bd7e..24115ab 100644
--- a/content/browser/media/session/media_session_impl.cc
+++ b/content/browser/media/session/media_session_impl.cc
@@ -1290,7 +1290,6 @@
     return;
   }
 
-  DCHECK_EQ(normal_players_.size(), 1u);
   if (normal_players_.size() != 1u) {
     // There should be one and only one player when we enter picture-in-picture.
     return;
@@ -1355,13 +1354,23 @@
 }
 
 void MediaSessionImpl::SetMute(bool mute) {
-  DCHECK_EQ(normal_players_.size(), 1u);
+  // The SetMute action should only be available when there is one normal
+  // player, though due to the asynchronous nature of mojo, we may no longer
+  // have 1 normal player. In that case, just return.
+  if (normal_players_.size() != 1u) {
+    return;
+  }
   normal_players_.begin()->first.observer->OnSetMute(
       normal_players_.begin()->first.player_id, mute);
 }
 
 void MediaSessionImpl::RequestMediaRemoting() {
-  DCHECK_EQ(normal_players_.size(), 1u);
+  // The RequestMediaRemoting action should only be available when there is one
+  // normal player, though due to the asynchronous nature of mojo, we may no
+  // longer have 1 normal player. In that case, just return.
+  if (normal_players_.size() != 1u) {
+    return;
+  }
   normal_players_.begin()->first.observer->OnRequestMediaRemoting(
       normal_players_.begin()->first.player_id);
 }
Loading diff…

Original Bug Report

reported by rj...@google.com

Potential RCE in Browser Process via out-of-bounds read in MediaSessionImpl

Project Fortify, an experimental security project, has identified the following potential security issue.

Overview: MediaSessionImpl::SetMute and RequestMediaRemoting lack bounds checking on the normal_players_ map in release builds. Dereferencing an empty map iterator causes an out-of-bounds read that perfectly aligns with the capacity pointer of the adjacent one_shot_players_ vector. This allows an attacker to hijack a virtual function call on an unprotected raw pointer, potentially achieving remote code execution in the browser process.

Affected files:

  • content/browser/media/session/media_session_impl.cc
  • content/browser/media/session/media_session_impl.h

Estimated timestamp from git blame: 2022-11-18

Vulnerability Summary

A potential out-of-bounds read exists in MediaSessionImpl::SetMute and MediaSessionImpl::RequestMediaRemoting within the browser process. Both methods rely on a DCHECK to ensure the normal_players_ map is not empty before dereferencing its begin() iterator. In release builds where DCHECK is compiled out, an empty map dereference causes an out-of-bounds memory read into the adjacent class member. This read perfectly aligns with a vector’s capacity pointer, allowing an attacker to supply a fake object and hijack a virtual function call to achieve a full sandbox escape (Browser RCE).

Root Cause Analysis

In content/browser/media/session/media_session_impl.cc, SetMute and RequestMediaRemoting are implemented as follows:

void MediaSessionImpl::SetMute(bool mute) {
  DCHECK_EQ(normal_players_.size(), 1u);
  normal_players_.begin()->first.observer->OnSetMute(
      normal_players_.begin()->first.player_id, mute);
}

Unlike EnterPictureInPicture which has an explicit runtime check (if (normal_players_.size() != 1u) return;), these methods do not check the container size in release builds.

normal_players_ is a std::map<PlayerIdentifier, AudioFocusType>. In libc++, std::map (via __tree) has a size of 24 bytes on 64-bit systems. When the map is empty, begin() returns an iterator pointing to the internal sentinel node (__end_node_), which sits at offset 8 within the std::map object.

Dereferencing the iterator (operator->) to access first expects a full __tree_node, where the value payload is stored 32 bytes past the node pointer. Thus, the read occurs at 8 + 32 = 40 bytes relative to the start of normal_players_.

Since normal_players_ is 24 bytes, this reads exactly 16 bytes past the end of the map. In MediaSessionImpl, the very next member is one_shot_players_ (a base::flat_set, backed by std::vector). A std::vector has its __cap_ (capacity) pointer at offset 16. Therefore, the out-of-bounds read fetches the one_shot_players_ capacity pointer.

The payload’s first member is MediaSessionPlayerObserver* observer inside the PlayerIdentifier struct. Crucially, this pointer is annotated with RAW_PTR_EXCLUSION, meaning it is completely unprotected by MiraclePtr/BackupRefPtr. The code then uses this out-of-bounds pointer for a virtual function call (observer->OnSetMute(...)).

Potential Exploitation Steps

Note: These are suggested steps based on static analysis, as our tooling agent cannot yet run live exploit code.

  1. Heap Grooming: An attacker creates a malicious page that adds exactly enough MediaStream players (via WebRTC/getUserMedia) to populate the one_shot_players_ vector such that its backing buffer sits adjacent to an attacker-controlled ArrayBuffer on the PartitionAlloc heap. The attacker places a fake MediaSessionPlayerObserver vtable at the very beginning of this ArrayBuffer.
  2. State Setup: The page plays standard <video> media, populating normal_players_ and activating the Global Media Controls (GMC) UI in the browser toolbar.
  3. Race Condition Trigger: The attacker tricks the user into clicking the “Mute” button in the GMC UI (e.g., by playing an annoying looping sound and highlighting the browser UI).
  4. Execution:
    • The UI click asynchronously dispatches a SetMute Mojo message to the browser’s UI thread.
    • Exactly at the same time, the attacker’s JavaScript synchronously removes the <video> element, sending an IPC that clears normal_players_ on the UI thread.
    • The SetMute Mojo message is processed shortly after, invoking MediaSessionImpl::SetMute with an empty map.
    • The empty map dereference reads the one_shot_players_ capacity pointer (__cap_).
    • __cap_ points exactly to the end of the vector’s heap buffer—which is the start of the attacker’s groomed ArrayBuffer containing the fake vtable.
    • observer->OnSetMute() uses the fake vtable, hijacking control flow and resulting in arbitrary Remote Code Execution in the browser process.

Add the same runtime bounds check used in MediaSessionImpl::EnterPictureInPicture to SetMute and RequestMediaRemoting:

void MediaSessionImpl::SetMute(bool mute) {
  if (normal_players_.size() != 1u)
    return;
  
  normal_players_.begin()->first.observer->OnSetMute(
      normal_players_.begin()->first.player_id, mute);
}

void MediaSessionImpl::RequestMediaRemoting() {
  if (normal_players_.size() != 1u)
    return;

  normal_players_.begin()->first.observer->OnRequestMediaRemoting(
      normal_players_.begin()->first.player_id);
}

Evaluated with Chrome root at commit: 876d480da1f794d87813cfa2e6ff4fcf9771e939


Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.

View on issue tracker