Chrome · Media
CVE-2026-79177
Logic Error in Media
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
MediaFoundationRendererExtensionProxycontent/browser/media/media_interface_proxy.cc |
modified | |
ifcontent/browser/media/media_interface_proxy.cc |
modified | |
ifcontent/browser/media/media_web_contents_observer.cc |
modified |
Files Changed
content/browser/media/media_interface_proxy.cccontent/browser/media/media_web_contents_observer.cc
Patch
From 9a158bed51d4160609cbdc6242c338bfcc97e8f3 Mon Sep 17 00:00:00 2001 From: Sangbaek Park <[email protected]> Date: Mon, 06 Jul 2026 17:02:04 -0700 Subject: [PATCH] media: Fix AudibilityBypassTracker authorization spoofing vulnerability This patch addresses a security vulnerability where a compromised renderer could mint an arbitrary number of audibility bypass grants by repeatedly invoking `CreateMediaFoundationRenderer` and dropping the pipes. These grants could then be used by unauthorized players to spoof the tab audio indicator and acquire power wake-locks. The fix resolves this by tightly coupling each audibility bypass grant to the Mojo pipeline lifetime of the specific `MediaFoundationRenderer`. A `MediaFoundationRendererExtensionProxy` now intercepts the extension Mojo pipe and owns a move-only `ScopedGrant`. If either the renderer or the utility process disconnects the pipe, the `ScopedGrant` is destroyed and the bypass authorization is explicitly revoked. By moving away from a fungible document-wide integer pool to a strict RAII lifecycle tied directly to the extension pipe, spoofing attempts are thwarted. Unit tests added: { MediaWebContentsObserverTest.GrantRevokedOnDisconnect } Bug: 520002854 Change-Id: I34d8a8e41f183d5e1a5e148c55a8b8ae0380836a Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7932763 Commit-Queue: Sangbaek Park <[email protected]> Reviewed-by: Dale Curtis <[email protected]> Cr-Commit-Position: refs/heads/main@{#1657586} --- diff --git a/content/browser/media/media_interface_proxy.cc b/content/browser/media/media_interface_proxy.cc index b7d670b..7557b2b 100644 --- a/content/browser/media/media_interface_proxy.cc +++ b/content/browser/media/media_interface_proxy.cc @@ -38,6 +38,7 @@ #include "media/mojo/buildflags.h" #include "media/mojo/mojom/frame_interface_factory.mojom.h" #include "media/mojo/mojom/media_service.mojom.h" +#include "media/mojo/mojom/renderer_extensions.mojom.h" #include "mojo/public/cpp/bindings/callback_helpers.h" #include "mojo/public/cpp/bindings/remote_set.h" @@ -294,6 +295,50 @@ #endif // BUILDFLAG(IS_WIN) }; +#if BUILDFLAG(IS_WIN) +// Proxies the MediaFoundationRendererExtension to observe the lifetime of the +// MediaFoundationRenderer in the utility process. When either the renderer +// or the utility process disconnects, the audibility bypass grant is revoked. +class MediaFoundationRendererExtensionProxy + : public media::mojom::MediaFoundationRendererExtension { + public: + MediaFoundationRendererExtensionProxy( + mojo::PendingRemote<media::mojom::MediaFoundationRendererExtension> + target_remote, + AudibilityBypassTracker::ScopedGrant grant) + : target_remote_(std::move(target_remote)), grant_(std::move(grant)) { + target_remote_.set_disconnect_handler(base::BindOnce( + &MediaFoundationRendererExtensionProxy::OnTargetDisconnect, + base::Unretained(this))); + } + + ~MediaFoundationRendererExtensionProxy() override = default; + + // media::mojom::MediaFoundationRendererExtension implementation. + void GetDCOMPSurface(GetDCOMPSurfaceCallback callback) override { + target_remote_->GetDCOMPSurface(std::move(callback)); + } + void SetVideoStreamEnabled(bool enabled) override { + target_remote_->SetVideoStreamEnabled(enabled); + } + void SetOutputRect(const gfx::Rect& rect, + SetOutputRectCallback callback) override { + target_remote_->SetOutputRect(rect, std::move(callback)); + } + + private: + void OnTargetDisconnect() { + // The utility process disconnected (MediaFoundationRenderer destroyed). + // Revoke the grant by resetting it. + grant_.RunAndReset(); + target_remote_.reset(); + } + + mojo::Remote<media::mojom::MediaFoundationRendererExtension> target_remote_; + AudibilityBypassTracker::ScopedGrant grant_; +}; +#endif // BUILDFLAG(IS_WIN) + } // namespace MediaInterfaceProxy::MediaInterfaceProxy(RenderFrameHost* render_frame_host) @@ -439,13 +484,24 @@ // with a CDM path in CreateCdm(). auto* factory = GetMediaFoundationServiceInterfaceFactory(base::FilePath()); if (factory) { - factory->CreateMediaFoundationRenderer( - std::move(media_log_remote), std::move(receiver), - std::move(renderer_extension_receiver)); - // `MediaFoundationRenderer` bypasses the browser's audio service. // Authorize the frame for audibility bypass claims. - AudibilityBypassTracker::AddGrant(&render_frame_host()); + AudibilityBypassTracker::ScopedGrant grant = + AudibilityBypassTracker::AddGrant(&render_frame_host()); + + mojo::PendingRemote<media::mojom::MediaFoundationRendererExtension> + utility_extension_remote; + auto utility_extension_receiver = + utility_extension_remote.InitWithNewPipeAndPassReceiver(); + + mojo::MakeSelfOwnedReceiver( + std::make_unique<MediaFoundationRendererExtensionProxy>( + std::move(utility_extension_remote), std::move(grant)), + std::move(renderer_extension_receiver)); + + factory->CreateMediaFoundationRenderer( + std::move(media_log_remote), std::move(receiver), + std::move(utility_extension_receiver)); } } #endif // BUILDFLAG(IS_WIN) diff --git a/content/browser/media/media_web_contents_observer.cc b/content/browser/media/media_web_contents_observer.cc index 2b7eba1..124085e 100644 --- a/content/browser/media/media_web_contents_observer.cc +++ b/content/browser/media/media_web_contents_observer.cc @@ -6,7 +6,6 @@ #include <algorithm> #include <memory> -#include <set> #include <tuple> #include "base/debug/crash_logging.h" @@ -655,6 +654,14 @@ has_played_before_ = true; } +void MediaWebContentsObserver::OnAudibilityBypassRevoked( + const MediaPlayerId& id) { + auto it = media_player_observer_hosts_.find(id); + if (it != media_player_observer_hosts_.end()) { + it->second->NotifyAudioStreamMonitorIfNeeded(); + } +} + void MediaWebContentsObserver::OnAudioOutputSinkChangedWithRawDeviceId( const MediaPlayerId& player_id, const std::string& raw_device_id) { @@ -821,10 +828,16 @@ AudibilityBypassTracker::~AudibilityBypassTracker() = default; // static -void AudibilityBypassTracker::AddGrant(RenderFrameHost* rfh) { +AudibilityBypassTracker::ScopedGrant AudibilityBypassTracker::AddGrant( + RenderFrameHost* rfh) { if (rfh) { - GetOrCreateForCurrentDocument(rfh)->pending_grants_++; + auto* tracker = GetOrCreateForCurrentDocument(rfh); + int grant_id = ++tracker->next_grant_id_; + tracker->pending_grants_.insert(grant_id); + return ScopedGrant(base::BindOnce(&AudibilityBypassTracker::RevokeGrant, + rfh->GetGlobalId(), grant_id)); } + return ScopedGrant(); } // static @@ -834,12 +847,13 @@ return false; } auto* tracker = GetForCurrentDocument(rfh); - if (tracker && tracker->authorized_players_.contains(id)) { + if (tracker && tracker->active_grants_.contains(id)) { return true; } - if (tracker && tracker->pending_grants_ > 0) { - tracker->pending_grants_--; - tracker->authorized_players_.insert(id); + if (tracker && !tracker->pending_grants_.empty()) { + int grant_id = *tracker->pending_grants_.begin(); + tracker->pending_grants_.erase(tracker->pending_grants_.begin()); + tracker->active_grants_[id] = grant_id; return true; } return false; @@ -853,7 +867,37 @@ } auto* tracker = GetForCurrentDocument(rfh); if (tracker) { - tracker->authorized_players_.erase(id); + tracker->active_grants_.erase(id); + } +} + +// static +void AudibilityBypassTracker::RevokeGrant(GlobalRenderFrameHostId rfh_id,
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/content/browser/media/media_web_contents_observer_unittest.cc b/content/browser/media/media_web_contents_observer_unittest.cc
index 80f57f7f..b0cb6f01 100644
--- a/content/browser/media/media_web_contents_observer_unittest.cc
+++ b/content/browser/media/media_web_contents_observer_unittest.cc
@@ -363,7 +363,8 @@
auto player = CreateAndAddPlayer(player_host);
// Simulate audibility bypass authorization for the document.
- AudibilityBypassTracker::AddGrant(contents()->GetPrimaryMainFrame());
+ auto grant =
+ AudibilityBypassTracker::AddGrant(contents()->GetPrimaryMainFrame());
SetMediaMetadata(player.observer, /*has_audio=*/true, /*has_video=*/false);
@@ -395,7 +396,7 @@
auto child_player = CreateAndAddPlayer(child_player_host);
// Authorize only the main frame.
- AudibilityBypassTracker::AddGrant(main_rfh);
+ auto grant = AudibilityBypassTracker::AddGrant(main_rfh);
// Child frame attempts bypass.
SetMediaMetadata(child_player.observer, /*has_audio=*/true,
@@ -422,7 +423,8 @@
auto original_player = CreateAndAddPlayer(player_host);
// Simulate MediaFoundationRenderer creation grant.
- AudibilityBypassTracker::AddGrant(contents()->GetPrimaryMainFrame());
+ auto grant =
+ AudibilityBypassTracker::AddGrant(contents()->GetPrimaryMainFrame());
// Original player claims the grant successfully.
SetMediaMetadata(original_player.observer, true, false);
@@ -456,8 +458,10 @@
auto player_host = SetupPlayerHost();
// Simulate two MediaFoundationRenderer creation grants.
- AudibilityBypassTracker::AddGrant(contents()->GetPrimaryMainFrame());
- AudibilityBypassTracker::AddGrant(contents()->GetPrimaryMainFrame());
+ auto grant1 =
+ AudibilityBypassTracker::AddGrant(contents()->GetPrimaryMainFrame());
+ auto grant2 =
+ AudibilityBypassTracker::AddGrant(contents()->GetPrimaryMainFrame());
auto player1 = CreateAndAddPlayer(player_host);
auto player2 = CreateAndAddPlayer(player_host);
@@ -489,5 +493,30 @@
EXPECT_FALSE(IsWebContentsAudible());
}
+// This test reproduces the issue where an attacker can mint bypass grants.
+// By creating grants and then disconnecting the pipe, the grant should be
+// automatically revoked, preventing the bypass from being exploited.
+TEST_F(MediaWebContentsObserverTest, GrantRevokedOnDisconnect) {
+ RenderFrameHost* rfh = contents()->GetPrimaryMainFrame();
+ MediaPlayerId player_id(rfh->GetGlobalId(), 1);
+
+ // Simulate a compromised renderer requesting MediaFoundationRenderer
+ // creation, which mints a new bypass grant.
+ auto grant = AudibilityBypassTracker::AddGrant(rfh);
+
+ // The player can successfully claim the grant.
+ EXPECT_TRUE(AudibilityBypassTracker::ClaimGrant(player_id));
+
+ // In the exploit scenario, the attacker disconnects the dummy Mojo pipe
+ // but attempts to continue using the grant.
+ // With the fix, the disconnection automatically calls RevokeGrant.
+ grant.RunAndReset();
+
+ // After revocation, the player should be unregistered from the bypass
+ // authorization. Next time it tries to claim, it should fail.
+ AudibilityBypassTracker::ReleaseGrant(player_id);
+ EXPECT_FALSE(AudibilityBypassTracker::ClaimGrant(player_id));
+}
+
} // namespace
} // namespace content
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.
References
On This Page