Overview

Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactRace condition in WebAudio
DescriptionRace condition in WebAudio
ComponentWebAudio
Sub-componentWebAudio
Bug ClassRace
Tracker520153320
Fix commit629d412d8e0b (chromium/src) +215/-46
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-06

Changed Functions

FunctionChangeNotes
MediaElementAudioSourceHandlerLocker
third_party/blink/renderer/modules/webaudio/media_element_audio_source_handler.cc
modified
if
third_party/blink/renderer/modules/webaudio/media_element_audio_source_handler.cc
modified
AudioContext
third_party/blink/renderer/modules/webaudio/media_element_audio_source_handler.h
modified
HTMLMediaElement
third_party/blink/renderer/modules/webaudio/media_element_audio_source_handler.h
modified
KURL
third_party/blink/renderer/modules/webaudio/media_element_audio_source_handler.h
modified
MediaElementAudioSourceOptions
third_party/blink/renderer/modules/webaudio/media_element_audio_source_handler.h
modified
MediaElementAudioSourceHandler
third_party/blink/renderer/modules/webaudio/media_element_audio_source_handler.h
modified
MODULES_EXPORT
third_party/blink/renderer/modules/webaudio/media_element_audio_source_handler.h
modified
HTMLMediaElement
third_party/blink/renderer/modules/webaudio/media_element_audio_source_node.h
modified
MediaElementAudioSourceOptions
third_party/blink/renderer/modules/webaudio/media_element_audio_source_node.h
modified
MediaElementAudioSourceNode
third_party/blink/renderer/modules/webaudio/media_element_audio_source_node.h
modified
MODULES_EXPORT
third_party/blink/renderer/modules/webaudio/media_element_audio_source_node.h
modified
AudioContextTestPlatform
third_party/blink/renderer/modules/webaudio/media_element_audio_source_node_test.cc
modified
MediaElementAudioSourceNodeTest
third_party/blink/renderer/modules/webaudio/media_element_audio_source_node_test.cc
modified
TEST_F
third_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.cc
  • third_party/blink/renderer/modules/webaudio/media_element_audio_source_handler.cc
  • third_party/blink/renderer/modules/webaudio/media_element_audio_source_node.h
  • third_party/blink/renderer/modules/webaudio/media_element_audio_source_node.cc
  • third_party/blink/renderer/modules/BUILD.gn
  • third_party/blink/renderer/modules/webaudio/media_element_audio_source_handler.h
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)
Loading diff…

Regression Test / PoC

shipped with the fix
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
Loading diff…

Original Bug Report

reported by vm...@google.com

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.cc
  • third_party/blink/renderer/modules/webaudio/media_element_audio_source_node.h
  • third_party/blink/renderer/platform/media/web_audio_source_provider_impl.cc
  • third_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:

  1. The existing MediaElementAudioSourceHandler is kept alive and reused for the lifetime of the WebAudio node.
  2. During the source change, HTMLMediaElement invokes the provider client’s OnCurrentSrcChanged() callback. However, OnCurrentSrcChanged in MediaElementAudioSourceNode is an empty stub, meaning is_origin_tainted_ is not reset and remains stale (false).
  3. A new WebAudioSourceProviderImpl is created for the new resource. Its format update callback (set_format_cb_) is bound to the main thread via BindPostTaskToCurrentDefault.
  4. When the background media thread initializes and starts decoding the new resource, it calls WebAudioSourceProviderImpl::Initialize(), which posts an OnSetFormat task to the main thread. It then immediately starts playing.
  5. During this window, if the real-time audio thread executes MediaElementAudioSourceHandler::Process(), it pulls raw PCM frames from the new player. Since is_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

  1. An attacker plays a same-origin audio file to prime the WebAudio graph so that is_origin_tainted_ is cached as false.
  2. The attacker sets the src of the <audio> element to a sensitive cross-origin target.
  3. Upon the 'loadstart' event, the attacker blocks the renderer’s main thread with a busy loop (e.g., 200ms).
  4. 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.
  5. 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.

View on issue tracker