Chrome · WebAudio
CVE-2026-106299
Input Validation in WebAudio
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Fcontent/browser/media/webaudio/audio_context_manager_impl_unittest.cc |
modified |
Files Changed
content/browser/media/webaudio/audio_context_manager_impl_unittest.cccontent/browser/media/webaudio/audio_context_manager_impl.cc
Patch
From 08625f7ba49b76f2d093197c9077145514686342 Mon Sep 17 00:00:00 2001 From: Michael Wilson <[email protected]> Date: Wed, 09 Sep 2026 14:03:40 -0700 Subject: [PATCH] Validate audio stream, context ID in AudioContextAudiblePlaybackStarted This CL adds browser-side validation: 1. Rejects AudioContextAudiblePlaybackStarted calls if audio_context_id is not present in concurrent_audio_context_ids_. 2. Rejects AudioContextAudiblePlaybackStarted calls if the associated RenderFrameHostImpl has no active kPlayingAudioStream media stream. 3. Hardens AudioContextAudiblePlaybackStopped and AudioContextClosed against unstarted context IDs, and flushes pending audible durations and playback state upon context closure. 4. Adds unit tests verifying that audibility notifications for unregistered context IDs or frames without active streams are ignored. Bug: 519211890 Change-Id: Icd0e2d4cf4287a06155b58f6f69861e23fd47136 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8372615 Reviewed-by: Hongchan Choi <[email protected]> Commit-Queue: Michael Wilson <[email protected]> Cr-Commit-Position: refs/heads/main@{#1694857} --- diff --git a/content/browser/media/webaudio/audio_context_manager_impl.cc b/content/browser/media/webaudio/audio_context_manager_impl.cc index 003f81d1..6afa1bc 100644 --- a/content/browser/media/webaudio/audio_context_manager_impl.cc +++ b/content/browser/media/webaudio/audio_context_manager_impl.cc @@ -67,6 +67,21 @@ void AudioContextManagerImpl::AudioContextAudiblePlaybackStarted( uint32_t audio_context_id) { + // Reject audibility assertions for contexts that were not created by this + // frame (or have already been closed). + if (!concurrent_audio_context_ids_.contains(audio_context_id)) { + return; + } + + // Reject audibility assertions if the frame has no active audio output + // streams. + auto& render_frame_host_impl = + static_cast<RenderFrameHostImpl&>(render_frame_host()); + if (!render_frame_host_impl.HasMediaStreams( + RenderFrameHostImpl::MediaStreamType::kPlayingAudioStream)) { + return; + } + if (!pending_audible_durations_[audio_context_id].is_null()) { mojo::ReportBadMessage( "AudioContextAudiblePlaybackStarted() called more than once with the " @@ -77,26 +92,30 @@ // Keeps track of the start audible time for this context. pending_audible_durations_[audio_context_id] = clock_->NowTicks(); - static_cast<RenderFrameHostImpl&>(render_frame_host()) - .AudioContextPlaybackStarted(audio_context_id); + render_frame_host_impl.AudioContextPlaybackStarted(audio_context_id); } void AudioContextManagerImpl::AudioContextAudiblePlaybackStopped( uint32_t audio_context_id) { - base::TimeTicks then = pending_audible_durations_[audio_context_id]; - DCHECK(!then.is_null()); + auto it = pending_audible_durations_.find(audio_context_id); + if (it == pending_audible_durations_.end() || it->second.is_null()) { + return; + } + base::TimeTicks then = it->second; RecordAudibleTime(clock_->NowTicks() - then); // Resets the context slot because the context is not audible. - pending_audible_durations_[audio_context_id] = base::TimeTicks(); + it->second = base::TimeTicks(); static_cast<RenderFrameHostImpl&>(render_frame_host()) .AudioContextPlaybackStopped(audio_context_id); } void AudioContextManagerImpl::RecordAudibleTime(base::TimeDelta audible_time) { - DCHECK(!audible_time.is_zero()); + if (!audible_time.is_positive()) { + return; + } ukm::UkmRecorder* ukm_recorder = ukm::UkmRecorder::Get(); DCHECK(ukm_recorder); @@ -123,6 +142,15 @@ void AudioContextManagerImpl::AudioContextClosed(uint32_t audio_context_id) { concurrent_audio_context_ids_.erase(audio_context_id); + auto it = pending_audible_durations_.find(audio_context_id); + if (it != pending_audible_durations_.end()) { + if (!it->second.is_null()) { + RecordAudibleTime(clock_->NowTicks() - it->second); + static_cast<RenderFrameHostImpl&>(render_frame_host()) + .AudioContextPlaybackStopped(audio_context_id); + } + pending_audible_durations_.erase(it); + } } } // namespace content diff --git a/content/browser/media/webaudio/audio_context_manager_impl_unittest.cc b/content/browser/media/webaudio/audio_context_manager_impl_unittest.cc index b7ac5c2..61c3923 100644 --- a/content/browser/media/webaudio/audio_context_manager_impl_unittest.cc +++ b/content/browser/media/webaudio/audio_context_manager_impl_unittest.cc @@ -10,6 +10,7 @@ #include "base/test/metrics/histogram_tester.h" #include "base/test/simple_test_tick_clock.h" #include "components/ukm/test_ukm_recorder.h" +#include "content/browser/renderer_host/render_frame_host_impl.h" #include "content/public/test/test_renderer_host.h" #include "mojo/public/cpp/bindings/remote.h" #include "services/metrics/public/cpp/ukm_builders.h" @@ -25,6 +26,8 @@ clock_.SetNowTicks(base::TimeTicks::Now()); + SimulateAudioStreamStarted(); + mojo::Remote<blink::mojom::AudioContextManager> service_remote; audio_context_manager_ = &AudioContextManagerImpl::CreateForTesting( *main_rfh(), service_remote.BindNewPipeAndPassReceiver()); @@ -32,10 +35,28 @@ } void TearDown() override { + if (web_contents() && main_rfh() && + static_cast<RenderFrameHostImpl*>(main_rfh()) + ->HasMediaStreams( + RenderFrameHostImpl::MediaStreamType::kPlayingAudioStream)) { + SimulateAudioStreamStopped(); + } audio_context_manager_ = nullptr; RenderViewHostTestHarness::TearDown(); } + void SimulateAudioStreamStarted() { + static_cast<RenderFrameHostImpl*>(main_rfh()) + ->OnMediaStreamAdded( + RenderFrameHostImpl::MediaStreamType::kPlayingAudioStream); + } + + void SimulateAudioStreamStopped() { + static_cast<RenderFrameHostImpl*>(main_rfh()) + ->OnMediaStreamRemoved( + RenderFrameHostImpl::MediaStreamType::kPlayingAudioStream); + } + AudioContextManagerImpl* audio_context_manager() { return audio_context_manager_; } @@ -53,6 +74,7 @@ }; TEST_F(AudioContextManagerImplTest, TimeBelow10SecondsIsRaw) { + audio_context_manager()->AudioContextCreated(0); // Entry for 42 milliseconds. audio_context_manager()->AudioContextAudiblePlaybackStarted(0); clock().Advance(base::Milliseconds(42)); @@ -79,6 +101,7 @@ } TEST_F(AudioContextManagerImplTest, TimeGreater10SecondsIsRoundedDown) { + audio_context_manager()->AudioContextCreated(0); // Entry for 42 seconds. audio_context_manager()->AudioContextAudiblePlaybackStarted(0); clock().Advance(base::Seconds(42)); @@ -165,4 +188,64 @@ /*expected_bucket_count=*/1); } +TEST_F(AudioContextManagerImplTest, + AudioContextAudiblePlaybackStoppedWithoutStartedDoesNotCrash) { + // Calling stopped for an unstarted context ID should not crash. + audio_context_manager()->AudioContextAudiblePlaybackStopped(42); + + auto ukm_entries = test_ukm_recorder().GetEntriesByName(UkmEntry::kEntryName); + EXPECT_EQ(0u, ukm_entries.size()); +} + +TEST_F(AudioContextManagerImplTest, + AudioContextClosedWithPendingAudiblePlayback) { + audio_context_manager()->AudioContextCreated(1); + audio_context_manager()->AudioContextAudiblePlaybackStarted(1); + clock().Advance(base::Seconds(5)); + + // Closing context should flush the pending audible duration to UKM. + audio_context_manager()->AudioContextClosed(1); + + auto ukm_entries = test_ukm_recorder().GetEntriesByName(UkmEntry::kEntryName); + EXPECT_EQ(1u, ukm_entries.size()); + EXPECT_EQ(5000, *test_ukm_recorder().GetEntryMetric( + ukm_entries[0], UkmEntry::kAudibleTimeName)); +} + +TEST_F(AudioContextManagerImplTest, AudioContextAudiblePlaybackZeroDuration) { + audio_context_manager()->AudioContextCreated(1); + audio_context_manager()->AudioContextAudiblePlaybackStarted(1); + // Stopped in the same clock tick (duration == 0). + audio_context_manager()->AudioContextAudiblePlaybackStopped(1); + + // Zero duration should not record to UKM. + auto ukm_entries = test_ukm_recorder().GetEntriesByName(UkmEntry::kEntryName); + EXPECT_EQ(0u, ukm_entries.size()); +} + +TEST_F(AudioContextManagerImplTest, + AudioContextAudiblePlaybackStartedIgnoredIfContextNotCreated) { + // Started called for a context ID that was never created should be ignored. + audio_context_manager()->AudioContextAudiblePlaybackStarted(42); + clock().Advance(base::Seconds(5)); + audio_context_manager()->AudioContextAudiblePlaybackStopped(42); + + auto ukm_entries = test_ukm_recorder().GetEntriesByName(UkmEntry::kEntryName); + EXPECT_EQ(0u, ukm_entries.size()); +} + +TEST_F(AudioContextManagerImplTest, + AudioContextAudiblePlaybackStartedIgnoredIfNoAudioStream) { + // Context is created, but the frame has no active audio output stream. + SimulateAudioStreamStopped(); + audio_context_manager()->AudioContextCreated(1); + + audio_context_manager()->AudioContextAudiblePlaybackStarted(1); + clock().Advance(base::Seconds(5)); + audio_context_manager()->AudioContextAudiblePlaybackStopped(1); + + auto ukm_entries = test_ukm_recorder().GetEntriesByName(UkmEntry::kEntryName); + EXPECT_EQ(0u, ukm_entries.size()); +} + } // namespace content
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/content/browser/media/webaudio/audio_context_manager_impl_unittest.cc b/content/browser/media/webaudio/audio_context_manager_impl_unittest.cc
index b7ac5c2..61c3923 100644
--- a/content/browser/media/webaudio/audio_context_manager_impl_unittest.cc
+++ b/content/browser/media/webaudio/audio_context_manager_impl_unittest.cc
@@ -10,6 +10,7 @@
#include "base/test/metrics/histogram_tester.h"
#include "base/test/simple_test_tick_clock.h"
#include "components/ukm/test_ukm_recorder.h"
+#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/public/test/test_renderer_host.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "services/metrics/public/cpp/ukm_builders.h"
@@ -25,6 +26,8 @@
clock_.SetNowTicks(base::TimeTicks::Now());
+ SimulateAudioStreamStarted();
+
mojo::Remote<blink::mojom::AudioContextManager> service_remote;
audio_context_manager_ = &AudioContextManagerImpl::CreateForTesting(
*main_rfh(), service_remote.BindNewPipeAndPassReceiver());
@@ -32,10 +35,28 @@
}
void TearDown() override {
+ if (web_contents() && main_rfh() &&
+ static_cast<RenderFrameHostImpl*>(main_rfh())
+ ->HasMediaStreams(
+ RenderFrameHostImpl::MediaStreamType::kPlayingAudioStream)) {
+ SimulateAudioStreamStopped();
+ }
audio_context_manager_ = nullptr;
RenderViewHostTestHarness::TearDown();
}
+ void SimulateAudioStreamStarted() {
+ static_cast<RenderFrameHostImpl*>(main_rfh())
+ ->OnMediaStreamAdded(
+ RenderFrameHostImpl::MediaStreamType::kPlayingAudioStream);
+ }
+
+ void SimulateAudioStreamStopped() {
+ static_cast<RenderFrameHostImpl*>(main_rfh())
+ ->OnMediaStreamRemoved(
+ RenderFrameHostImpl::MediaStreamType::kPlayingAudioStream);
+ }
+
AudioContextManagerImpl* audio_context_manager() {
return audio_context_manager_;
}
@@ -53,6 +74,7 @@
};
TEST_F(AudioContextManagerImplTest, TimeBelow10SecondsIsRaw) {
+ audio_context_manager()->AudioContextCreated(0);
// Entry for 42 milliseconds.
audio_context_manager()->AudioContextAudiblePlaybackStarted(0);
clock().Advance(base::Milliseconds(42));
@@ -79,6 +101,7 @@
}
TEST_F(AudioContextManagerImplTest, TimeGreater10SecondsIsRoundedDown) {
+ audio_context_manager()->AudioContextCreated(0);
// Entry for 42 seconds.
audio_context_manager()->AudioContextAudiblePlaybackStarted(0);
clock().Advance(base::Seconds(42));
@@ -165,4 +188,64 @@
/*expected_bucket_count=*/1);
}
+TEST_F(AudioContextManagerImplTest,
+ AudioContextAudiblePlaybackStoppedWithoutStartedDoesNotCrash) {
+ // Calling stopped for an unstarted context ID should not crash.
+ audio_context_manager()->AudioContextAudiblePlaybackStopped(42);
+
+ auto ukm_entries = test_ukm_recorder().GetEntriesByName(UkmEntry::kEntryName);
+ EXPECT_EQ(0u, ukm_entries.size());
+}
+
+TEST_F(AudioContextManagerImplTest,
+ AudioContextClosedWithPendingAudiblePlayback) {
+ audio_context_manager()->AudioContextCreated(1);
+ audio_context_manager()->AudioContextAudiblePlaybackStarted(1);
+ clock().Advance(base::Seconds(5));
+
+ // Closing context should flush the pending audible duration to UKM.
+ audio_context_manager()->AudioContextClosed(1);
+
+ auto ukm_entries = test_ukm_recorder().GetEntriesByName(UkmEntry::kEntryName);
+ EXPECT_EQ(1u, ukm_entries.size());
+ EXPECT_EQ(5000, *test_ukm_recorder().GetEntryMetric(
+ ukm_entries[0], UkmEntry::kAudibleTimeName));
+}
+
+TEST_F(AudioContextManagerImplTest, AudioContextAudiblePlaybackZeroDuration) {
+ audio_context_manager()->AudioContextCreated(1);
+ audio_context_manager()->AudioContextAudiblePlaybackStarted(1);
+ // Stopped in the same clock tick (duration == 0).
+ audio_context_manager()->AudioContextAudiblePlaybackStopped(1);
+
+ // Zero duration should not record to UKM.
+ auto ukm_entries = test_ukm_recorder().GetEntriesByName(UkmEntry::kEntryName);
+ EXPECT_EQ(0u, ukm_entries.size());
+}
+
+TEST_F(AudioContextManagerImplTest,
+ AudioContextAudiblePlaybackStartedIgnoredIfContextNotCreated) {
+ // Started called for a context ID that was never created should be ignored.
+ audio_context_manager()->AudioContextAudiblePlaybackStarted(42);
+ clock().Advance(base::Seconds(5));
+ audio_context_manager()->AudioContextAudiblePlaybackStopped(42);
+
+ auto ukm_entries = test_ukm_recorder().GetEntriesByName(UkmEntry::kEntryName);
+ EXPECT_EQ(0u, ukm_entries.size());
+}
+
+TEST_F(AudioContextManagerImplTest,
+ AudioContextAudiblePlaybackStartedIgnoredIfNoAudioStream) {
+ // Context is created, but the frame has no active audio output stream.
+ SimulateAudioStreamStopped();
+ audio_context_manager()->AudioContextCreated(1);
+
+ audio_context_manager()->AudioContextAudiblePlaybackStarted(1);
+ clock().Advance(base::Seconds(5));
+ audio_context_manager()->AudioContextAudiblePlaybackStopped(1);
+
+ auto ukm_entries = test_ukm_recorder().GetEntriesByName(UkmEntry::kEntryName);
+ EXPECT_EQ(0u, ukm_entries.size());
+}
+
} // 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