Chrome · Media › Media
CVE-2026-113074
UAF in Media (Media)
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcontent/browser/media/media_interface_proxy.cc |
modified | |
MediaInterfaceProxycontent/browser/media/media_interface_proxy.h |
modified | |
CONTENT_EXPORTcontent/browser/media/media_interface_proxy.h |
modified |
Files Changed
content/browser/media/media_interface_proxy_unittest.cccontent/browser/media/media_interface_proxy.cccontent/browser/media/media_interface_proxy.h
Patch
From 4ef5d279d5a7404c7f796611038bcfc3d85d3efe Mon Sep 17 00:00:00 2001
From: Sangbaek Park <sangbaekpark@chromium.org>
Date: Mon, 21 Sep 2026 16:29:17 -0700
Subject: [PATCH] media: Restrict AudibilityBypassTracker grants to valid MF context
Previously in CL 7932763, audibility bypass grants were tied to the
lifetime of the MediaFoundationRenderer extension Mojo pipe to prevent
accumulating leaked grants from dropped pipes.
However, on Windows x86_64, MediaInterfaceProxy unconditionally minted
an AudibilityBypassTracker grant upon CreateMediaFoundationRenderer(),
requiring only media::SupportMediaFoundationPlayback() (which is enabled
by default). A compromised renderer could call
CreateMediaFoundationRenderer(), keep the Mojo pipe connected so the
grant is not revoked, and claim the grant for a forged player. This
allowed spoofing tab audibility and bypassing the WasRecentlyAudible
Auto-PiP gate with no real media playback.
This CL ensures an AudibilityBypassTracker grant is only minted when the
document has established a legitimate MediaFoundation playback context:
either clear playback is enabled, or a hardware-secure CDM was
initialized via CreateCdm().
Unit tests added in content_unittests:
MediaInterfaceProxyTest:
- CreateMediaFoundationRenderer_NoGrantWithoutContext
- CreateMediaFoundationRenderer_GrantWithProtectedContext
- CreateMediaFoundationRenderer_GrantWithClearPlayback
Bug: 563003907
Change-Id: I0152949c6cc4a2028147e6b5df417c816223a881
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8421176
Reviewed-by: Dale Curtis <dalecurtis@chromium.org>
Commit-Queue: Sangbaek Park <sangbaekpark@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1702189}
---
diff --git a/content/browser/media/media_interface_proxy.cc b/content/browser/media/media_interface_proxy.cc
index 23904da..a5e155fc 100644
--- a/content/browser/media/media_interface_proxy.cc
+++ b/content/browser/media/media_interface_proxy.cc
@@ -494,9 +494,17 @@
auto* factory = GetMediaFoundationServiceInterfaceFactory(base::FilePath());
if (factory) {
// `MediaFoundationRenderer` bypasses the browser's audio service.
- // Authorize the frame for audibility bypass claims.
- AudibilityBypassTracker::ScopedGrant grant =
- AudibilityBypassTracker::AddGrant(&render_frame_host());
+ // Authorize the frame for audibility bypass claims — but only when the
+ // document is in a legitimate MediaFoundation playback context (clear
+ // playback enabled, or a hardware-secure CDM created via CreateCdm()).
+ // Otherwise any renderer could self-mint the authorization and spoof
+ // tab audibility (WasRecentlyAudible / speaker indicator / Auto-PiP
+ // gates) with no real playback.
+ AudibilityBypassTracker::ScopedGrant grant;
+ if (media::SupportMediaFoundationClearPlayback() ||
+ mf_protected_playback_context_) {
+ grant = AudibilityBypassTracker::AddGrant(&render_frame_host());
+ }
mojo::PendingRemote<media::mojom::MediaFoundationRendererExtension>
utility_extension_remote;
@@ -582,6 +590,10 @@
bool is_cached_factory = mf_interface_factory_remote_.is_bound();
auto* factory = GetMediaFoundationServiceInterfaceFactory(cdm_info->path);
if (factory) {
+ // A hardware-secure CDM is being created for this document, so
+ // subsequent MediaFoundationRenderer creation is part of a real
+ // protected-playback setup and may claim audibility bypass.
+ mf_protected_playback_context_ = true;
factory->CreateCdm(
cdm_config,
WrapCreateCdmCallback(base::BindOnce(
diff --git a/content/browser/media/media_interface_proxy.h b/content/browser/media/media_interface_proxy.h
index ec766d1..fb8250fd 100644
--- a/content/browser/media/media_interface_proxy.h
+++ b/content/browser/media/media_interface_proxy.h
@@ -14,6 +14,7 @@
#include "base/unguessable_token.h"
#include "build/build_config.h"
#include "content/browser/media/media_interface_factory_holder.h"
+#include "content/common/content_export.h"
#include "content/public/browser/document_user_data.h"
#include "content/public/common/cdm_info.h"
#include "media/cdm/cdm_type.h"
@@ -42,8 +43,9 @@
// This implements the media::mojom::InterfaceFactory interface for a
// RenderFrameHostImpl to help create remote media components in different
// processes.
-class MediaInterfaceProxy final : public DocumentUserData<MediaInterfaceProxy>,
- public media::mojom::InterfaceFactory {
+class CONTENT_EXPORT MediaInterfaceProxy final
+ : public DocumentUserData<MediaInterfaceProxy>,
+ public media::mojom::InterfaceFactory {
public:
~MediaInterfaceProxy() final;
@@ -89,6 +91,7 @@
private:
friend class DocumentUserData<MediaInterfaceProxy>;
+ friend class MediaInterfaceProxyTest;
explicit MediaInterfaceProxy(RenderFrameHost* rfh);
DOCUMENT_USER_DATA_KEY_DECL();
@@ -148,6 +151,15 @@
const media::CdmConfig& cdm_config);
mojo::Remote<media::mojom::InterfaceFactory> mf_interface_factory_remote_;
+
+ // Only authorize audibility-bypass grants when this document actually
+ // established a MediaFoundation playback context: either MediaFoundation
+ // clear playback is enabled, or a hardware-secure CDM was set up through
+ // CreateCdm(). This stops a (compromised) renderer from self-minting
+ // AudibilityBypassTracker grants by merely calling
+ // CreateMediaFoundationRenderer(), which requires no CDM, no content setting
+ // and no user interaction.
+ bool mf_protected_playback_context_ = false;
#endif // BUILDFLAG(IS_WIN)
mojo::UniqueReceiverSet<media::mojom::FrameInterfaceFactory> frame_factories_;
diff --git a/content/browser/media/media_interface_proxy_unittest.cc b/content/browser/media/media_interface_proxy_unittest.cc
index f70e58d..7ea2fd3 100644
--- a/content/browser/media/media_interface_proxy_unittest.cc
+++ b/content/browser/media/media_interface_proxy_unittest.cc
@@ -2,25 +2,35 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
+#include "content/browser/media/media_interface_proxy.h"
+
#include <memory>
#include <optional>
#include <string>
#include "base/command_line.h"
#include "base/files/file_path.h"
+#include "base/functional/callback_helpers.h"
#include "base/run_loop.h"
#include "base/test/mock_callback.h"
+#include "base/test/scoped_feature_list.h"
#include "build/build_config.h"
#include "content/browser/media/cdm_registry_impl.h"
+#include "content/browser/media/media_web_contents_observer.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/common/pseudonymization_salt.h"
+#include "content/public/browser/media_player_id.h"
#include "content/public/common/cdm_info.h"
#include "content/public/test/test_browser_context.h"
#include "content/public/test/test_renderer_host.h"
#include "media/base/cdm_capability.h"
#include "media/base/cdm_config.h"
+#include "media/base/media_switches.h"
#include "media/mojo/mojom/content_decryption_module.mojom.h"
#include "media/mojo/mojom/interface_factory.mojom.h"
+#include "media/mojo/mojom/media_log.mojom.h"
+#include "media/mojo/mojom/renderer.mojom.h"
+#include "media/mojo/mojom/renderer_extensions.mojom.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "sandbox/policy/switches.h"
@@ -55,6 +65,15 @@
RenderViewHostTestHarness::TearDown();
}
+#if BUILDFLAG(IS_WIN)
+ void SetMediaFoundationInterfaceFactoryRemote(
+ mojo::PendingRemote<media::mojom::InterfaceFactory> factory_remote) {
+ auto* proxy =
+ MediaInterfaceProxy::GetOrCreateForCurrentDocument(main_rfh());
+ proxy->mf_interface_factory_remote_.Bind(std::move(factory_remote));
+ }
+#endif
+
mojo::Remote<media::mojom::InterfaceFactory> GetMediaInterfaceFactory() {
mojo::Remote<media::mojom::InterfaceFactory> factory;
static_cast<RenderFrameHostImpl*>(main_rfh())
@@ -135,6 +154,95 @@
ASSERT_TRUE(created_status.has_value());
EXPECT_EQ(created_status.value(), media::CreateCdmStatus::kInvalidCdmConfig);
}
+
+TEST_F(MediaInterfaceProxyTest,
+ CreateMediaFoundationRenderer_NoGrantWithoutContext) {
+ mojo::PendingRemote<media::mojom::InterfaceFactory> dummy_factory_remote;
+ auto dummy_factory_receiver =
+ dummy_factory_remote.InitWithNewPipeAndPassReceiver();
+ SetMediaFoundationInterfaceFactoryRemote(std::move(dummy_factory_remote));
+
+ auto factory = GetMediaInterfaceFactory();
+ ASSERT_TRUE(factory.is_bound());
+
+ mojo::PendingRemote<media::mojom::MediaLog> media_log_remote;
+ auto media_log_receiver = media_log_remote.InitWithNewPipeAndPassReceiver();
+ mojo::PendingRemote<media::mojom::Renderer> renderer_remote;
+ auto renderer_receiver = renderer_remote.InitWithNewPipeAndPassReceiver();
+ mojo::PendingRemote<media::mojom::MediaFoundationRendererExtension>
+ extension_remote;
+
+ // A renderer should NOT be granted audibility bypass authorization when
+ // calling CreateMediaFoundationRenderer without an established
+ // MediaFoundation playback context (no CDM created, clear playback disabled).
+ factory->CreateMediaFoundationRenderer(
+ std::move(media_log_remote), std::move(renderer_receiver),
+ extension_remote.InitWithNewPipeAndPassReceiver());
+ base::RunLoop().RunUntilIdle();
+
+ MediaPlayerId player_id(main_rfh()->GetGlobalId(), 1);
+ EXPECT_FALSE(AudibilityBypassTracker::ClaimGrant(player_id));
+}
+
+TEST_F(MediaInterfaceProxyTest,
+ CreateMediaFoundationRenderer_GrantWithProtectedContext) {
+ mojo::PendingRemote<media::mojom::InterfaceFactory> dummy_factory_remote;
+ auto dummy_factory_receiver =
+ dummy_factory_remote.InitWithNewPipeAndPassReceiver();
+ SetMediaFoundationInterfaceFactoryRemote(std::move(dummy_factory_remote));
+
+ auto factory = GetMediaInterfaceFactory();
+ ASSERT_TRUE(factory.is_bound());
+
+ // Establish a hardware-secure CDM context first.
+ factory->CreateCdm(CreateHwSecureCdmConfig(), base::DoNothing());
+ base::RunLoop().RunUntilIdle();
+
+ mojo::PendingRemote<media::mojom::MediaLog> media_log_remote;
+ auto media_log_receiver = media_log_remote.InitWithNewPipeAndPassReceiver();
+ mojo::PendingRemote<media::mojom::Renderer> renderer_remote;
+ auto renderer_receiver = renderer_remote.InitWithNewPipeAndPassReceiver();
+ mojo::PendingRemote<media::mojom::MediaFoundationRendererExtension>
+ extension_remote;
+
+ factory->CreateMediaFoundationRenderer(
+ std::move(media_log_remote), std::move(renderer_receiver),
+ extension_remote.InitWithNewPipeAndPassReceiver());
+ base::RunLoop().RunUntilIdle();
+
+ MediaPlayerId player_id(main_rfh()->GetGlobalId(), 1);
+ EXPECT_TRUE(AudibilityBypassTracker::ClaimGrant(player_id));
+}
+
+TEST_F(MediaInterfaceProxyTest,
+ CreateMediaFoundationRenderer_GrantWithClearPlayback) {
+ base::test::ScopedFeatureList scoped_feature_list;
+ scoped_feature_list.InitAndEnableFeature(
+ media::kMediaFoundationClearPlayback);
+
+ mojo::PendingRemote<media::mojom::InterfaceFactory> dummy_factory_remote;
+ auto dummy_factory_receiver =
+ dummy_factory_remote.InitWithNewPipeAndPassReceiver();
+ SetMediaFoundationInterfaceFactoryRemote(std::move(dummy_factory_remote));
+
+ auto factory = GetMediaInterfaceFactory();
+ ASSERT_TRUE(factory.is_bound());
+
+ mojo::PendingRemote<media::mojom::MediaLog> media_log_remote;
+ auto media_log_receiver = media_log_remote.InitWithNewPipeAndPassReceiver();
+ mojo::PendingRemote<media::mojom::Renderer> renderer_remote;
+ auto renderer_receiver = renderer_remote.InitWithNewPipeAndPassReceiver();
+ mojo::PendingRemote<media::mojom::MediaFoundationRendererExtension>
+ extension_remote;
+
+ factory->CreateMediaFoundationRenderer(
+ std::move(media_log_remote), std::move(renderer_receiver),
+ extension_remote.InitWithNewPipeAndPassReceiver());
+ base::RunLoop().RunUntilIdle();
+
+ MediaPlayerId player_id(main_rfh()->GetGlobalId(), 1);
+ EXPECT_TRUE(AudibilityBypassTracker::ClaimGrant(player_id));
+}
#endif // BUILDFLAG(IS_WIN)
} // namespace content
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/content/browser/media/media_interface_proxy_unittest.cc b/content/browser/media/media_interface_proxy_unittest.cc
index f70e58d..7ea2fd3 100644
--- a/content/browser/media/media_interface_proxy_unittest.cc
+++ b/content/browser/media/media_interface_proxy_unittest.cc
@@ -2,25 +2,35 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
+#include "content/browser/media/media_interface_proxy.h"
+
#include <memory>
#include <optional>
#include <string>
#include "base/command_line.h"
#include "base/files/file_path.h"
+#include "base/functional/callback_helpers.h"
#include "base/run_loop.h"
#include "base/test/mock_callback.h"
+#include "base/test/scoped_feature_list.h"
#include "build/build_config.h"
#include "content/browser/media/cdm_registry_impl.h"
+#include "content/browser/media/media_web_contents_observer.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/common/pseudonymization_salt.h"
+#include "content/public/browser/media_player_id.h"
#include "content/public/common/cdm_info.h"
#include "content/public/test/test_browser_context.h"
#include "content/public/test/test_renderer_host.h"
#include "media/base/cdm_capability.h"
#include "media/base/cdm_config.h"
+#include "media/base/media_switches.h"
#include "media/mojo/mojom/content_decryption_module.mojom.h"
#include "media/mojo/mojom/interface_factory.mojom.h"
+#include "media/mojo/mojom/media_log.mojom.h"
+#include "media/mojo/mojom/renderer.mojom.h"
+#include "media/mojo/mojom/renderer_extensions.mojom.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "sandbox/policy/switches.h"
@@ -55,6 +65,15 @@
RenderViewHostTestHarness::TearDown();
}
+#if BUILDFLAG(IS_WIN)
+ void SetMediaFoundationInterfaceFactoryRemote(
+ mojo::PendingRemote<media::mojom::InterfaceFactory> factory_remote) {
+ auto* proxy =
+ MediaInterfaceProxy::GetOrCreateForCurrentDocument(main_rfh());
+ proxy->mf_interface_factory_remote_.Bind(std::move(factory_remote));
+ }
+#endif
+
mojo::Remote<media::mojom::InterfaceFactory> GetMediaInterfaceFactory() {
mojo::Remote<media::mojom::InterfaceFactory> factory;
static_cast<RenderFrameHostImpl*>(main_rfh())
@@ -135,6 +154,95 @@
ASSERT_TRUE(created_status.has_value());
EXPECT_EQ(created_status.value(), media::CreateCdmStatus::kInvalidCdmConfig);
}
+
+TEST_F(MediaInterfaceProxyTest,
+ CreateMediaFoundationRenderer_NoGrantWithoutContext) {
+ mojo::PendingRemote<media::mojom::InterfaceFactory> dummy_factory_remote;
+ auto dummy_factory_receiver =
+ dummy_factory_remote.InitWithNewPipeAndPassReceiver();
+ SetMediaFoundationInterfaceFactoryRemote(std::move(dummy_factory_remote));
+
+ auto factory = GetMediaInterfaceFactory();
+ ASSERT_TRUE(factory.is_bound());
+
+ mojo::PendingRemote<media::mojom::MediaLog> media_log_remote;
+ auto media_log_receiver = media_log_remote.InitWithNewPipeAndPassReceiver();
+ mojo::PendingRemote<media::mojom::Renderer> renderer_remote;
+ auto renderer_receiver = renderer_remote.InitWithNewPipeAndPassReceiver();
+ mojo::PendingRemote<media::mojom::MediaFoundationRendererExtension>
+ extension_remote;
+
+ // A renderer should NOT be granted audibility bypass authorization when
+ // calling CreateMediaFoundationRenderer without an established
+ // MediaFoundation playback context (no CDM created, clear playback disabled).
+ factory->CreateMediaFoundationRenderer(
+ std::move(media_log_remote), std::move(renderer_receiver),
+ extension_remote.InitWithNewPipeAndPassReceiver());
+ base::RunLoop().RunUntilIdle();
+
+ MediaPlayerId player_id(main_rfh()->GetGlobalId(), 1);
+ EXPECT_FALSE(AudibilityBypassTracker::ClaimGrant(player_id));
+}
+
+TEST_F(MediaInterfaceProxyTest,
+ CreateMediaFoundationRenderer_GrantWithProtectedContext) {
+ mojo::PendingRemote<media::mojom::InterfaceFactory> dummy_factory_remote;
+ auto dummy_factory_receiver =
+ dummy_factory_remote.InitWithNewPipeAndPassReceiver();
+ SetMediaFoundationInterfaceFactoryRemote(std::move(dummy_factory_remote));
+
+ auto factory = GetMediaInterfaceFactory();
+ ASSERT_TRUE(factory.is_bound());
+
+ // Establish a hardware-secure CDM context first.
+ factory->CreateCdm(CreateHwSecureCdmConfig(), base::DoNothing());
+ base::RunLoop().RunUntilIdle();
+
+ mojo::PendingRemote<media::mojom::MediaLog> media_log_remote;
+ auto media_log_receiver = media_log_remote.InitWithNewPipeAndPassReceiver();
+ mojo::PendingRemote<media::mojom::Renderer> renderer_remote;
+ auto renderer_receiver = renderer_remote.InitWithNewPipeAndPassReceiver();
+ mojo::PendingRemote<media::mojom::MediaFoundationRendererExtension>
+ extension_remote;
+
+ factory->CreateMediaFoundationRenderer(
+ std::move(media_log_remote), std::move(renderer_receiver),
+ extension_remote.InitWithNewPipeAndPassReceiver());
+ base::RunLoop().RunUntilIdle();
+
+ MediaPlayerId player_id(main_rfh()->GetGlobalId(), 1);
+ EXPECT_TRUE(AudibilityBypassTracker::ClaimGrant(player_id));
+}
+
+TEST_F(MediaInterfaceProxyTest,
+ CreateMediaFoundationRenderer_GrantWithClearPlayback) {
+ base::test::ScopedFeatureList scoped_feature_list;
+ scoped_feature_list.InitAndEnableFeature(
+ media::kMediaFoundationClearPlayback);
+
+ mojo::PendingRemote<media::mojom::InterfaceFactory> dummy_factory_remote;
+ auto dummy_factory_receiver =
+ dummy_factory_remote.InitWithNewPipeAndPassReceiver();
+ SetMediaFoundationInterfaceFactoryRemote(std::move(dummy_factory_remote));
+
+ auto factory = GetMediaInterfaceFactory();
+ ASSERT_TRUE(factory.is_bound());
+
+ mojo::PendingRemote<media::mojom::MediaLog> media_log_remote;
+ auto media_log_receiver = media_log_remote.InitWithNewPipeAndPassReceiver();
+ mojo::PendingRemote<media::mojom::Renderer> renderer_remote;
+ auto renderer_receiver = renderer_remote.InitWithNewPipeAndPassReceiver();
+ mojo::PendingRemote<media::mojom::MediaFoundationRendererExtension>
+ extension_remote;
+
+ factory->CreateMediaFoundationRenderer(
+ std::move(media_log_remote), std::move(renderer_receiver),
+ extension_remote.InitWithNewPipeAndPassReceiver());
+ base::RunLoop().RunUntilIdle();
+
+ MediaPlayerId player_id(main_rfh()->GetGlobalId(), 1);
+ EXPECT_TRUE(AudibilityBypassTracker::ClaimGrant(player_id));
+}
#endif // BUILDFLAG(IS_WIN)
} // 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