CVE-2026-106213
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
MediaElementAudioSourceHandlerLockerthird_party/blink/renderer/modules/webaudio/media_element_audio_source_handler.cc |
modified | |
ifthird_party/blink/renderer/modules/webaudio/media_element_audio_source_handler.cc |
modified | |
AudioContextthird_party/blink/renderer/modules/webaudio/media_element_audio_source_handler.h |
modified | |
HTMLMediaElementthird_party/blink/renderer/modules/webaudio/media_element_audio_source_handler.h |
modified | |
KURLthird_party/blink/renderer/modules/webaudio/media_element_audio_source_handler.h |
modified | |
MediaElementAudioSourceOptionsthird_party/blink/renderer/modules/webaudio/media_element_audio_source_handler.h |
modified | |
MediaElementAudioSourceHandlerthird_party/blink/renderer/modules/webaudio/media_element_audio_source_handler.h |
modified | |
MODULES_EXPORTthird_party/blink/renderer/modules/webaudio/media_element_audio_source_handler.h |
modified | |
HTMLMediaElementthird_party/blink/renderer/modules/webaudio/media_element_audio_source_node.h |
modified | |
MediaElementAudioSourceOptionsthird_party/blink/renderer/modules/webaudio/media_element_audio_source_node.h |
modified | |
MediaElementAudioSourceNodethird_party/blink/renderer/modules/webaudio/media_element_audio_source_node.h |
modified | |
MODULES_EXPORTthird_party/blink/renderer/modules/webaudio/media_element_audio_source_node.h |
modified | |
AudioContextTestPlatformthird_party/blink/renderer/modules/webaudio/media_element_audio_source_node_test.cc |
modified | |
MediaElementAudioSourceNodeTestthird_party/blink/renderer/modules/webaudio/media_element_audio_source_node_test.cc |
modified | |
TEST_Fthird_party/blink/renderer/modules/webaudio/media_element_audio_source_node_test.cc |
modified |
Files Changed
third_party/blink/renderer/modules/webaudio/media_element_audio_source_node_test.ccthird_party/blink/renderer/modules/webaudio/media_element_audio_source_handler.ccthird_party/blink/renderer/modules/webaudio/media_element_audio_source_node.hthird_party/blink/renderer/modules/webaudio/media_element_audio_source_node.ccthird_party/blink/renderer/modules/BUILD.gnthird_party/blink/renderer/modules/webaudio/media_element_audio_source_handler.h
Patch
From 629d412d8e0b42ab5a5999f3a6946d1ad628a9e2 Mon Sep 17 00:00:00 2001
From: Hongchan Choi <hongchan@google.com>
Date: Wed, 09 Sep 2026 11:07:25 -0700
Subject: [PATCH] [WebAudio] Clear MediaElementAudioSource cached format on src change
MediaElementAudioSourceHandler caches the channel count, sample rate,
and origin-taint state of the current HTMLMediaElement source. These
are only refreshed when SetFormat() is delivered for a new
WebAudioSourceProviderImpl, which is posted to the main thread
asynchronously after the new player initializes.
HTMLMediaElement::LoadResource() already notifies the attached
AudioSourceProviderClient via OnCurrentSrcChanged(), but
MediaElementAudioSourceNode left that override empty. Consequently, the
handler retained the previous source's state during the transitional
window between the new player starting and SetFormat() arriving.
This CL forwards OnCurrentSrcChanged() to the handler and resets the
cached channel count and sample rate under the process lock. With
source_sample_rate_ == 0, Process() outputs silence until SetFormat()
runs for the new source and establishes the new format and taint
status.
Bug: 520153320
Test: blink_unittests --gtest_filter="MediaElementAudioSourceNodeTest.*"
Change-Id: I1d71f82895fbc91842bcbd1374119432dbce3493
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8351244
Reviewed-by: Thomas Guilbert <tguilbert@chromium.org>
Commit-Queue: Hongchan Choi <hongchan@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1694737}
---
diff --git a/third_party/blink/renderer/modules/webaudio/media_element_audio_source_handler.cc b/third_party/blink/renderer/modules/webaudio/media_element_audio_source_handler.cc
index d53adc8..1e03a9e 100644
--- a/third_party/blink/renderer/modules/webaudio/media_element_audio_source_handler.cc
+++ b/third_party/blink/renderer/modules/webaudio/media_element_audio_source_handler.cc
@@ -34,26 +34,7 @@
} // namespace
-class MediaElementAudioSourceHandlerLocker final {
- STACK_ALLOCATED();
- public:
- explicit MediaElementAudioSourceHandlerLocker(
- MediaElementAudioSourceHandler& lockable)
- : lockable_(lockable) {
- lockable_.lock();
- }
-
- MediaElementAudioSourceHandlerLocker(
- const MediaElementAudioSourceHandlerLocker&) = delete;
- MediaElementAudioSourceHandlerLocker& operator=(
- const MediaElementAudioSourceHandlerLocker&) = delete;
-
- ~MediaElementAudioSourceHandlerLocker() { lockable_.unlock(); }
-
- private:
- MediaElementAudioSourceHandler& lockable_;
-};
MediaElementAudioSourceHandler::MediaElementAudioSourceHandler(
AudioNode& node,
@@ -103,49 +84,48 @@
PrintCorsMessage(MediaElement()->currentSrc().GetString());
}
+ bool format_changed = false;
{
- // Make sure `is_origin_tainted_` matches `is_tainted`. But need to
- // synchronize with `Process()` to set this.
- MediaElementAudioSourceHandlerLocker locker(*this);
+ base::AutoLock locker(process_lock_);
is_origin_tainted_ = is_tainted;
+ if (number_of_channels != source_number_of_channels_ ||
+ source_sample_rate != source_sample_rate_) {
+ format_changed = true;
+ }
}
- if (number_of_channels != source_number_of_channels_ ||
- source_sample_rate != source_sample_rate_) {
+ if (format_changed) {
if (!number_of_channels ||
number_of_channels > BaseAudioContext::MaxNumberOfChannels() ||
!audio_utilities::IsValidAudioBufferSampleRate(source_sample_rate)) {
// `Process()` will generate silence for these uninitialized values.
DLOG(ERROR) << "setFormat(" << number_of_channels << ", "
<< source_sample_rate << ") - unhandled format change";
- // Synchronize with `Process()`.
- MediaElementAudioSourceHandlerLocker locker(*this);
+ base::AutoLock locker(process_lock_);
source_number_of_channels_ = 0;
source_sample_rate_ = 0;
return;
}
- // Synchronize with `Process()` to protect `source_number_of_channels_`,
- // `source_sample_rate_`, `multi_channel_resampler_`.
- MediaElementAudioSourceHandlerLocker locker(*this);
-
- source_number_of_channels_ = number_of_channels;
- source_sample_rate_ = source_sample_rate;
-
+ std::unique_ptr<MediaMultiChannelResampler> new_resampler;
if (source_sample_rate != Context()->sampleRate()) {
double scale_factor = source_sample_rate / Context()->sampleRate();
const size_t resampler_request_frames =
audio_utilities::RoundUpToMultiple(
media::SincResampler::kMinRequestSize,
GetDeferredTaskHandler().RenderQuantumFrames());
- multi_channel_resampler_ = std::make_unique<MediaMultiChannelResampler>(
+ new_resampler = std::make_unique<MediaMultiChannelResampler>(
number_of_channels, scale_factor, resampler_request_frames,
CrossThreadBindRepeating(
&MediaElementAudioSourceHandler::ProvideResamplerInput,
CrossThreadUnretained(this)));
- } else {
- // Bypass resampling.
- multi_channel_resampler_.reset();
+ }
+
+ {
+ base::AutoLock locker(process_lock_);
+ source_number_of_channels_ = number_of_channels;
+ source_sample_rate_ = source_sample_rate;
+ multi_channel_resampler_ = std::move(new_resampler);
}
{
@@ -153,7 +133,7 @@
DeferredTaskHandler::GraphAutoLocker context_locker(
Context()->GetDeferredTaskHandler());
- // Do any necesssary re-configuration to the output's number of channels.
+ // Do any necessary re-configuration to the output's number of channels.
Output(0).SetNumberOfChannels(number_of_channels);
}
}
@@ -161,7 +141,8 @@
bool MediaElementAudioSourceHandler::WouldTaintOrigin() {
DCHECK(MediaElement());
- return MediaElement()->GetWebMediaPlayer()->WouldTaintOrigin();
+ WebMediaPlayer* player = MediaElement()->GetWebMediaPlayer();
+ return player ? player->WouldTaintOrigin() : false;
}
void MediaElementAudioSourceHandler::PrintCorsMessage(const String& message) {
@@ -239,4 +220,32 @@
process_lock_.Release();
}
+void MediaElementAudioSourceHandler::OnCurrentSrcChanged(const KURL&) {
+ DCHECK(IsMainThread());
+ base::AutoLock locker(process_lock_);
+ source_number_of_channels_ = 0;
+ source_sample_rate_ = 0;
+ is_origin_tainted_ = true;
+ multi_channel_resampler_.reset();
+}
+
+unsigned MediaElementAudioSourceHandler::SourceNumberOfChannelsForTesting() {
+ base::AutoLock locker(process_lock_);
+ return source_number_of_channels_;
+}
+
+double MediaElementAudioSourceHandler::SourceSampleRateForTesting() {
+ base::AutoLock locker(process_lock_);
+ return source_sample_rate_;
+}
+
+bool MediaElementAudioSourceHandler::IsOriginTaintedForTesting() {
+ base::AutoLock locker(process_lock_);
+ return is_origin_tainted_;
+}
+
+bool MediaElementAudioSourceHandler::WouldTaintOriginForTesting() {
+ return WouldTaintOrigin();
+}
+
} // namespace blink
diff --git a/third_party/blink/renderer/modules/webaudio/media_element_audio_source_handler.h b/third_party/blink/renderer/modules/webaudio/media_element_audio_source_handler.h
index dda12cb..4ee7df1b 100644
--- a/third_party/blink/renderer/modules/webaudio/media_element_audio_source_handler.h
+++ b/third_party/blink/renderer/modules/webaudio/media_element_audio_source_handler.h
@@ -12,6 +12,7 @@
#include "base/task/single_thread_task_runner.h"
#include "base/thread_annotations.h"
#include "third_party/blink/renderer/bindings/core/v8/active_script_wrappable.h"
+#include "third_party/blink/renderer/modules/modules_export.h"
#include "third_party/blink/renderer/modules/webaudio/audio_node.h"
#include "third_party/blink/renderer/platform/audio/audio_source_provider_client.h"
#include "third_party/blink/renderer/platform/audio/media_multi_channel_resampler.h"
@@ -21,9 +22,11 @@
class AudioContext;
class HTMLMediaElement;
+class KURL;
class MediaElementAudioSourceOptions;
-class MediaElementAudioSourceHandler final : public AudioHandler {
+class MODULES_EXPORT MediaElementAudioSourceHandler final
+ : public AudioHandler {
public:
static scoped_refptr<MediaElementAudioSourceHandler> Create(
AudioNode&,
@@ -45,6 +48,12 @@
void SetFormat(uint32_t number_of_channels, float sample_rate);
void lock() EXCLUSIVE_LOCK_FUNCTION(GetProcessLock());
void unlock() UNLOCK_FUNCTION(GetProcessLock());
+ void OnCurrentSrcChanged(const KURL& current_src);
+
+ unsigned SourceNumberOfChannelsForTesting();
+ double SourceSampleRateForTesting();
+ bool IsOriginTaintedForTesting();
+ bool WouldTaintOriginForTesting();
// For thread safety analysis only. Does not actually return mu.
base::Lock* GetProcessLock() LOCK_RETURNED(process_lock_) { NOTREACHED(); }
@@ -77,10 +86,11 @@
CrossThreadWeakPersistent<HTMLMediaElement> media_element_;
base::Lock process_lock_;
- unsigned source_number_of_channels_ = 0;
- double source_sample_rate_ = 0;
+ unsigned source_number_of_channels_ GUARDED_BY(process_lock_) = 0;
+ double source_sample_rate_ GUARDED_BY(process_lock_) = 0;
- std::unique_ptr<MediaMultiChannelResampler> multi_channel_resampler_;
+ std::unique_ptr<MediaMultiChannelResampler> multi_channel_resampler_
+ GUARDED_BY(process_lock_);
scoped_refptr<base::SingleThreadTaskRunner> task_runner_;
@@ -88,7 +98,7 @@
// this node outputs silence. This can happen if the media element source is
// a cross-origin source which we're not allowed to access due to CORS
// restrictions.
- bool is_origin_tainted_ = false;
+ bool is_origin_tainted_ GUARDED_BY(process_lock_) = false;
};
} // namespace blink
diff --git a/third_party/blink/renderer/modules/webaudio/media_element_audio_source_node.cc b/third_party/blink/renderer/modules/webaudio/media_element_audio_source_node.cc
index f233555..d591d705 100644
--- a/third_party/blink/renderer/modules/webaudio/media_element_audio_source_node.cc
+++ b/third_party/blink/renderer/modules/webaudio/media_element_audio_source_node.cc
@@ -112,6 +112,10 @@
GetMediaElementAudioSourceHandler().unlock();
}
+void MediaElementAudioSourceNode::OnCurrentSrcChanged(const KURL& current_src) {
+ GetMediaElementAudioSourceHandler().OnCurrentSrcChanged(current_src);
+}
+
void MediaElementAudioSourceNode::ReportDidCreate() {
GraphTracer().DidCreateAudioNode(this);
}
diff --git a/third_party/blink/renderer/modules/webaudio/media_element_audio_source_node.h b/third_party/blink/renderer/modules/webaudio/media_element_audio_source_node.h
index eabf9f1..e80b4c1 100644
--- a/third_party/blink/renderer/modules/webaudio/media_element_audio_source_node.h
+++ b/third_party/blink/renderer/modules/webaudio/media_element_audio_source_node.h
@@ -30,6 +30,7 @@
#include "base/task/single_thread_task_runner.h"
#include "base/thread_annotations.h"
#include "third_party/blink/renderer/bindings/core/v8/active_script_wrappable.h"
+#include "third_party/blink/renderer/modules/modules_export.h"
#include "third_party/blink/renderer/modules/webaudio/audio_node.h"
#include "third_party/blink/renderer/modules/webaudio/media_element_audio_source_handler.h"
#include "third_party/blink/renderer/platform/audio/audio_source_provider_client.h"
@@ -42,7 +43,7 @@
class HTMLMediaElement;
class MediaElementAudioSourceOptions;
-class MediaElementAudioSourceNode final
+class MODULES_EXPORT MediaElementAudioSourceNode final
: public AudioNode,
public AudioSourceProviderClient,
public ActiveScriptWrappable<MediaElementAudioSourceNode> {
@@ -67,7 +68,7 @@
GetMediaElementAudioSourceHandler().GetProcessLock());
void unlock() override
UNLOCK_FUNCTION(GetMediaElementAudioSourceHandler().GetProcessLock());
- void OnCurrentSrcChanged(const KURL& current_src) override {}
+ void OnCurrentSrcChanged(const KURL& current_src) override;
// InspectorHelperMixin
void ReportDidCreate() final;
diff --git a/third_party/blink/renderer/modules/webaudio/media_element_audio_source_node_test.cc b/third_party/blink/renderer/modules/webaudio/media_element_audio_source_node_test.cc
new file mode 100644
index 0000000..78aa1a7
--- /dev/null
+++ b/third_party/blink/renderer/modules/webaudio/media_element_audio_source_node_test.cc
@@ -0,0 +1,144 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/modules/webaudio/media_element_audio_source_node.h"
+
+#include <algorithm>
+#include <memory>
+
+#include "base/containers/span.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/public/platform/web_audio_device.h"
+#include "third_party/blink/public/platform/web_audio_latency_hint.h"
+#include "third_party/blink/public/platform/web_audio_sink_descriptor.h"
+#include "third_party/blink/renderer/bindings/modules/v8/v8_audio_context_options.h"
+#include "third_party/blink/renderer/core/frame/local_dom_window.h"
+#include "third_party/blink/renderer/core/html/media/html_audio_element.h"
+#include "third_party/blink/renderer/core/testing/dummy_page_holder.h"
+#include "third_party/blink/renderer/modules/webaudio/audio_context.h"
+#include "third_party/blink/renderer/modules/webaudio/audio_node_output.h"
+#include "third_party/blink/renderer/modules/webaudio/media_element_audio_source_handler.h"
+#include "third_party/blink/renderer/modules/webaudio/testing/fake_audio_thread.h"
+#include "third_party/blink/renderer/modules/webaudio/testing/mock_web_audio_device.h"
+#include "third_party/blink/renderer/platform/audio/audio_bus.h"
+#include "third_party/blink/renderer/platform/bindings/exception_state.h"
+#include "third_party/blink/renderer/platform/heap/garbage_collected.h"
+#include "third_party/blink/renderer/platform/testing/task_environment.h"
+#include "third_party/blink/renderer/platform/testing/testing_platform_support.h"
+#include "third_party/blink/renderer/platform/weborigin/kurl.h"
+#include "third_party/blink/renderer/platform/wtf/cross_thread_functional.h"
+
+namespace blink {
+
+namespace {
+
+class AudioContextTestPlatform : public TestingPlatformSupport {
+ public:
+ std::unique_ptr<WebAudioDevice> CreateAudioDevice(
+ const WebAudioSinkDescriptor& sink_descriptor,
+ unsigned number_of_output_channels,
+ const WebAudioLatencyHint& latency_hint,
+ std::optional<float> context_sample_rate,
+ media::AudioRendererSink::RenderCallback*) override {
+ return std::make_unique<MockWebAudioDevice>(AudioHardwareSampleRate(),
+ AudioHardwareBufferSize());
+ }
+
+ double AudioHardwareSampleRate() override { return 44100; }
+ size_t AudioHardwareBufferSize() override { return 128; }
+};
+
+constexpr unsigned kInitialNumberOfChannels = 2;
+constexpr float kInitialSampleRate = 44100.0f;
+constexpr unsigned kNewNumberOfChannels = 1;
+constexpr float kNewSampleRate = 48000.0f;
+
+} // namespace
+
+class MediaElementAudioSourceNodeTest : public testing::Test {
+ protected:
+ void SetUp() override {
+ page_ = std::make_unique<DummyPageHolder>();
+ AudioContextOptions* options = AudioContextOptions::Create();
+ context_ = AudioContext::Create(page_->GetFrame().DomWindow(), options,
+ ASSERT_NO_EXCEPTION);
+ media_element_ =
+ MakeGarbageCollected<HTMLAudioElement>(page_->GetDocument());
+ node_ = MediaElementAudioSourceNode::Create(*context_, *media_element_,
+ ASSERT_NO_EXCEPTION);
+ }
+
+ test::TaskEnvironment task_environment_;
+ ScopedTestingPlatformSupport<AudioContextTestPlatform> platform_;
+ std::unique_ptr<DummyPageHolder> page_;
+ Persistent<AudioContext> context_;
+ Persistent<HTMLAudioElement> media_element_;
+ Persistent<MediaElementAudioSourceNode> node_;
+};
+
+TEST_F(MediaElementAudioSourceNodeTest, OnCurrentSrcChangedClearsCachedFormat) {
+ MediaElementAudioSourceHandler& handler =
+ node_->GetMediaElementAudioSourceHandler();
+
+ EXPECT_DOUBLE_EQ(handler.SourceSampleRateForTesting(), 0.0);
+ EXPECT_EQ(handler.SourceNumberOfChannelsForTesting(), 0u);
+ EXPECT_FALSE(handler.IsOriginTaintedForTesting());
+
+ // Set initial format as if a 2-channel 44.1kHz audio source was loaded.
+ node_->SetFormat(kInitialNumberOfChannels, kInitialSampleRate);
+ EXPECT_DOUBLE_EQ(handler.SourceSampleRateForTesting(), kInitialSampleRate);
+ EXPECT_EQ(handler.SourceNumberOfChannelsForTesting(),
+ kInitialNumberOfChannels);
+ EXPECT_FALSE(handler.IsOriginTaintedForTesting());
+
+ // Triggering source change should clear cached format and default to tainted
+ // until SetFormat() validates the new source.
+ node_->OnCurrentSrcChanged(KURL("https://example.com/new-source.wav"));
+ EXPECT_DOUBLE_EQ(handler.SourceSampleRateForTesting(), 0.0);
+ EXPECT_EQ(handler.SourceNumberOfChannelsForTesting(), 0u);
+ EXPECT_TRUE(handler.IsOriginTaintedForTesting());
+
+ // Verify that Process() outputs silence on the audio thread when format is
+ // cleared.
+ FakeAudioThread audio_thread(ThreadType::kRealtimeAudioWorkletThread);
+ audio_thread.RunOnAudioThreadWithContext(
+ context_.Get(),
... (file diff truncated)
Regression Test / PoC
diff --git a/third_party/blink/renderer/modules/webaudio/media_element_audio_source_node_test.cc b/third_party/blink/renderer/modules/webaudio/media_element_audio_source_node_test.cc
new file mode 100644
index 0000000..78aa1a7
--- /dev/null
+++ b/third_party/blink/renderer/modules/webaudio/media_element_audio_source_node_test.cc
@@ -0,0 +1,144 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/modules/webaudio/media_element_audio_source_node.h"
+
+#include <algorithm>
+#include <memory>
+
+#include "base/containers/span.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/public/platform/web_audio_device.h"
+#include "third_party/blink/public/platform/web_audio_latency_hint.h"
+#include "third_party/blink/public/platform/web_audio_sink_descriptor.h"
+#include "third_party/blink/renderer/bindings/modules/v8/v8_audio_context_options.h"
+#include "third_party/blink/renderer/core/frame/local_dom_window.h"
+#include "third_party/blink/renderer/core/html/media/html_audio_element.h"
+#include "third_party/blink/renderer/core/testing/dummy_page_holder.h"
+#include "third_party/blink/renderer/modules/webaudio/audio_context.h"
+#include "third_party/blink/renderer/modules/webaudio/audio_node_output.h"
+#include "third_party/blink/renderer/modules/webaudio/media_element_audio_source_handler.h"
+#include "third_party/blink/renderer/modules/webaudio/testing/fake_audio_thread.h"
+#include "third_party/blink/renderer/modules/webaudio/testing/mock_web_audio_device.h"
+#include "third_party/blink/renderer/platform/audio/audio_bus.h"
+#include "third_party/blink/renderer/platform/bindings/exception_state.h"
+#include "third_party/blink/renderer/platform/heap/garbage_collected.h"
+#include "third_party/blink/renderer/platform/testing/task_environment.h"
+#include "third_party/blink/renderer/platform/testing/testing_platform_support.h"
+#include "third_party/blink/renderer/platform/weborigin/kurl.h"
+#include "third_party/blink/renderer/platform/wtf/cross_thread_functional.h"
+
+namespace blink {
+
+namespace {
+
+class AudioContextTestPlatform : public TestingPlatformSupport {
+ public:
+ std::unique_ptr<WebAudioDevice> CreateAudioDevice(
+ const WebAudioSinkDescriptor& sink_descriptor,
+ unsigned number_of_output_channels,
+ const WebAudioLatencyHint& latency_hint,
+ std::optional<float> context_sample_rate,
+ media::AudioRendererSink::RenderCallback*) override {
+ return std::make_unique<MockWebAudioDevice>(AudioHardwareSampleRate(),
+ AudioHardwareBufferSize());
+ }
+
+ double AudioHardwareSampleRate() override { return 44100; }
+ size_t AudioHardwareBufferSize() override { return 128; }
+};
+
+constexpr unsigned kInitialNumberOfChannels = 2;
+constexpr float kInitialSampleRate = 44100.0f;
+constexpr unsigned kNewNumberOfChannels = 1;
+constexpr float kNewSampleRate = 48000.0f;
+
+} // namespace
+
+class MediaElementAudioSourceNodeTest : public testing::Test {
+ protected:
+ void SetUp() override {
+ page_ = std::make_unique<DummyPageHolder>();
+ AudioContextOptions* options = AudioContextOptions::Create();
+ context_ = AudioContext::Create(page_->GetFrame().DomWindow(), options,
+ ASSERT_NO_EXCEPTION);
+ media_element_ =
+ MakeGarbageCollected<HTMLAudioElement>(page_->GetDocument());
+ node_ = MediaElementAudioSourceNode::Create(*context_, *media_element_,
+ ASSERT_NO_EXCEPTION);
+ }
+
+ test::TaskEnvironment task_environment_;
+ ScopedTestingPlatformSupport<AudioContextTestPlatform> platform_;
+ std::unique_ptr<DummyPageHolder> page_;
+ Persistent<AudioContext> context_;
+ Persistent<HTMLAudioElement> media_element_;
+ Persistent<MediaElementAudioSourceNode> node_;
+};
+
+TEST_F(MediaElementAudioSourceNodeTest, OnCurrentSrcChangedClearsCachedFormat) {
+ MediaElementAudioSourceHandler& handler =
+ node_->GetMediaElementAudioSourceHandler();
+
+ EXPECT_DOUBLE_EQ(handler.SourceSampleRateForTesting(), 0.0);
+ EXPECT_EQ(handler.SourceNumberOfChannelsForTesting(), 0u);
+ EXPECT_FALSE(handler.IsOriginTaintedForTesting());
+
+ // Set initial format as if a 2-channel 44.1kHz audio source was loaded.
+ node_->SetFormat(kInitialNumberOfChannels, kInitialSampleRate);
+ EXPECT_DOUBLE_EQ(handler.SourceSampleRateForTesting(), kInitialSampleRate);
+ EXPECT_EQ(handler.SourceNumberOfChannelsForTesting(),
+ kInitialNumberOfChannels);
+ EXPECT_FALSE(handler.IsOriginTaintedForTesting());
+
+ // Triggering source change should clear cached format and default to tainted
+ // until SetFormat() validates the new source.
+ node_->OnCurrentSrcChanged(KURL("https://example.com/new-source.wav"));
+ EXPECT_DOUBLE_EQ(handler.SourceSampleRateForTesting(), 0.0);
+ EXPECT_EQ(handler.SourceNumberOfChannelsForTesting(), 0u);
+ EXPECT_TRUE(handler.IsOriginTaintedForTesting());
+
+ // Verify that Process() outputs silence on the audio thread when format is
+ // cleared.
+ FakeAudioThread audio_thread(ThreadType::kRealtimeAudioWorkletThread);
+ audio_thread.RunOnAudioThreadWithContext(
+ context_.Get(),
+ CrossThreadBindOnce(
+ [](MediaElementAudioSourceHandler* handler) {
+ AudioBus* output_bus = handler->Output(0).Bus();
+ ASSERT_TRUE(output_bus);
+ output_bus->Zero();
+ for (unsigned ch = 0; ch < output_bus->NumberOfChannels(); ++ch) {
+ base::span<float> channel_span =
+ output_bus->Channel(ch)->MutableSpan();
+ std::ranges::fill(channel_span, 1.0f);
+ }
+ handler->Process(128);
+ for (unsigned ch = 0; ch < output_bus->NumberOfChannels(); ++ch) {
+ base::span<const float> channel_span =
+ output_bus->Channel(ch)->Span();
+ for (float sample : channel_span) {
+ EXPECT_EQ(sample, 0.0f);
+ }
+ }
+ },
+ CrossThreadUnretained(&handler)));
+
+ // Set new format for the new source.
+ node_->SetFormat(kNewNumberOfChannels, kNewSampleRate);
+ EXPECT_DOUBLE_EQ(handler.SourceSampleRateForTesting(), kNewSampleRate);
+ EXPECT_EQ(handler.SourceNumberOfChannelsForTesting(), kNewNumberOfChannels);
+ EXPECT_FALSE(handler.IsOriginTaintedForTesting());
+}
+
+TEST_F(MediaElementAudioSourceNodeTest, WouldTaintOriginWithoutPlayer) {
+ MediaElementAudioSourceHandler& handler =
+ node_->GetMediaElementAudioSourceHandler();
+ // When there is no WebMediaPlayer (e.g. before media load initializes a
+ // player), the element does not taint origin by default. This aligns with
+ // HTMLMediaElement::IsMediaDataCorsSameOrigin().
+ EXPECT_FALSE(handler.WouldTaintOriginForTesting());
+}
+
+} // namespace blink
Original Bug Report
Potential cross-origin audio leak via stale is_origin_tainted_ cache in WebAudio during src change
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A potential race condition in WebAudio’s MediaElementAudioSourceHandler may allow the initial decoded PCM samples of a cross-origin audio stream to be exfiltrated. Because the handler’s security state is not reset when the HTMLMediaElement source changes, a stale origin-taint flag can be used to render audio from a new cross-origin resource. An attacker could potentially exploit this by stalling the main thread during media loading to capture the raw audio frames.
Affected files:
third_party/blink/renderer/modules/webaudio/media_element_audio_source_handler.ccthird_party/blink/renderer/modules/webaudio/media_element_audio_source_node.hthird_party/blink/renderer/platform/media/web_audio_source_provider_impl.ccthird_party/blink/renderer/core/html/media/html_media_element.cc
Estimated timestamp from git blame: 2018-06-05
Potential Cross-Origin Audio Leak via Stale is_origin_tainted_ Cache in WebAudio
Analysis
In WebAudio, the MediaElementAudioSourceHandler (owned by MediaElementAudioSourceNode) caches the origin-taint status in is_origin_tainted_ (default false). This flag is only updated inside SetFormat() on the main thread.
When an HTMLMediaElement’s source changes from same-origin to cross-origin:
- The existing
MediaElementAudioSourceHandleris kept alive and reused for the lifetime of the WebAudio node. - During the source change,
HTMLMediaElementinvokes the provider client’sOnCurrentSrcChanged()callback. However,OnCurrentSrcChangedinMediaElementAudioSourceNodeis an empty stub, meaningis_origin_tainted_is not reset and remains stale (false). - A new
WebAudioSourceProviderImplis created for the new resource. Its format update callback (set_format_cb_) is bound to the main thread viaBindPostTaskToCurrentDefault. - When the background media thread initializes and starts decoding the new resource, it calls
WebAudioSourceProviderImpl::Initialize(), which posts anOnSetFormattask to the main thread. It then immediately starts playing. - During this window, if the real-time audio thread executes
MediaElementAudioSourceHandler::Process(), it pulls raw PCM frames from the new player. Sinceis_origin_tainted_is still stale (false) and the background taint check (tee_filter_->is_tainted()) has not yet been updated on the stalled main thread, the cross-origin PCM frames are processed normally and can be captured.
Note: Because our analysis is static and our tooling does not yet have the ability to run code, these are potential steps and vulnerabilities that have not been dynamically executed or confirmed via a working proof-of-concept.
Potential Attack Scenario
- An attacker plays a same-origin audio file to prime the WebAudio graph so that
is_origin_tainted_is cached asfalse. - The attacker sets the
srcof the<audio>element to a sensitive cross-origin target. - Upon the
'loadstart'event, the attacker blocks the renderer’s main thread with a busy loop (e.g., 200ms). - The background network and media threads load and decode the cross-origin audio in parallel. Because the main thread is stalled, the posted tasks to update the format and mark the source as tainted are delayed.
- The real-time WebAudio thread continues to run, pulling the raw cross-origin PCM frames and rendering them into the attacker’s downstream WebAudio nodes (such as an
AudioWorkletNode) due to the stale security flags.
Suggested Fix
To prevent the leak, the handler’s security and format state should be reset to a safe default whenever the media element’s source changes. This can be achieved by implementing OnCurrentSrcChanged in MediaElementAudioSourceNode:
void MediaElementAudioSourceNode::OnCurrentSrcChanged(const KURL& current_src) {
// Reset format variables and default to tainted/silent state to avoid leaks
GetMediaElementAudioSourceHandler().ResetCurrentSourceState();
}
Inside MediaElementAudioSourceHandler, we should define ResetCurrentSourceState to safely reset the cached parameters under a lock:
void MediaElementAudioSourceHandler::ResetCurrentSourceState() {
MediaElementAudioSourceHandlerLocker locker(*this);
source_sample_rate_ = 0;
source_number_of_channels_ = 0;
is_origin_tainted_ = true; // Safe default until proven same-origin
}
Evaluated with Chrome root at commit: d8b226a3be7c9c1ac9240c09e14698866c82e4ac
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.