CVE-2026-6308
Overview
Files Changed
content/browser/media/session/media_session_impl.cc
Patch
From 41bfbc009df8336d2c813cd3963e75a28a022e43 Mon Sep 17 00:00:00 2001 From: Tommy Steimel <[email protected]> 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 <[email protected]> Commit-Queue: Tommy Steimel <[email protected]> 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); }
Original Bug Report
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.cccontent/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.
- Heap Grooming: An attacker creates a malicious page that adds exactly enough
MediaStreamplayers (via WebRTC/getUserMedia) to populate theone_shot_players_vector such that its backing buffer sits adjacent to an attacker-controlled ArrayBuffer on the PartitionAlloc heap. The attacker places a fakeMediaSessionPlayerObservervtable at the very beginning of this ArrayBuffer. - State Setup: The page plays standard
<video>media, populatingnormal_players_and activating the Global Media Controls (GMC) UI in the browser toolbar. - 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).
- Execution:
- The UI click asynchronously dispatches a
SetMuteMojo 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 clearsnormal_players_on the UI thread. - The
SetMuteMojo message is processed shortly after, invokingMediaSessionImpl::SetMutewith 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.
- The UI click asynchronously dispatches a
Recommended Fix
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.