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 <sangbaekpark@google.com>
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 <sangbaekpark@chromium.org>
Reviewed-by: Dale Curtis <dalecurtis@chromium.org>
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