Low chrome Logic Error 🔧 Commit mapped

Overview

Low
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncorrect authorization in Media
DescriptionIncorrect authorization in Media
ComponentMedia
Bug ClassLogic Error
Tracker520002854
Fix commit9a158bed51d4 (chromium/src) +161/-22
CISA KEVNot listed
CreditedGoogle
Disclosed2026-08-25

Changed Functions

FunctionChangeNotes
MediaFoundationRendererExtensionProxy
content/browser/media/media_interface_proxy.cc
modified
if
content/browser/media/media_interface_proxy.cc
modified
if
content/browser/media/media_web_contents_observer.cc
modified

Files Changed

  • content/browser/media/media_interface_proxy.cc
  • content/browser/media/media_web_contents_observer.cc
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.