Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Media Stream
DescriptionUse after free in Media Stream
ComponentMedia Stream
Bug ClassUAF
Tracker426054987
Fix commit90d99dcd83af (chromium/src) +79/-18
CISA KEVNot listed
CreditedAnonymous
Disclosed2025-07-29

Changed Functions

FunctionChangeNotes
for
third_party/blink/renderer/modules/mediastream/media_stream_track_impl.cc
modified
MODULES_EXPORT
third_party/blink/renderer/modules/mediastream/media_stream_track_impl.h
modified
MakeLocalMediaStreamAudioSource
third_party/blink/renderer/modules/mediastream/media_stream_track_impl_test.cc
modified
TEST_F
third_party/blink/renderer/modules/mediastream/media_stream_track_impl_test.cc
modified

Files Changed

  • third_party/blink/renderer/modules/mediastream/media_stream_track_impl.cc
  • third_party/blink/renderer/modules/mediastream/media_stream_track_impl.h
  • third_party/blink/renderer/modules/mediastream/media_stream_track_impl_test.cc
From 90d99dcd83af2f13e0f917b5a9dc59eeee0204d0 Mon Sep 17 00:00:00 2001
From: Evan Liu <evliu@google.com>
Date: Mon, 14 Jul 2025 12:21:45 -0700
Subject: [PATCH] Fix potential UAF in MediaStreamTrackImpl

This CL fixes a potential UAF vulnerability in MediaStreamTrackImpl
where pointers to the SpeechRecognitionMediaStreamAudioSinks that are
owned by the MediaStreamTrackImpl could potentially be accessed after
the sinks are destroyed.

Fixed: 426054987
Change-Id: I453160a8eed7926e2cc3500260de04d2722c98e1
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6712212
Commit-Queue: Evan Liu <evliu@google.com>
Reviewed-by: Mark Foltz <mfoltz@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1486476}
---

diff --git a/third_party/blink/renderer/modules/mediastream/media_stream_track_impl.cc b/third_party/blink/renderer/modules/mediastream/media_stream_track_impl.cc
index 2131a1b1..320d73e80 100644
--- a/third_party/blink/renderer/modules/mediastream/media_stream_track_impl.cc
+++ b/third_party/blink/renderer/modules/mediastream/media_stream_track_impl.cc
@@ -810,6 +810,19 @@
   return capture_handle;
 }
 
+void MediaStreamTrackImpl::Dispose() {
+  // `MediaStreamTrackImpl` and the `SpeechRecognitionMediaStreamAudioSink`
+  // which it owns may be destroyed before the `MediaStreamAudioTrack`. Remove
+  // the sinks before destroying them to prevent `MediaStreamAudioTrack` from
+  // using them after destruction.
+  if (MediaStreamAudioTrack* audio_track =
+          MediaStreamAudioTrack::From(Component())) {
+    for (SpeechRecognitionMediaStreamAudioSink* sink : registered_sinks_) {
+      audio_track->RemoveSink(sink);
+    }
+  }
+}
+
 ScriptPromise<IDLUndefined> MediaStreamTrackImpl::applyConstraints(
     ScriptState* script_state,
     const MediaTrackConstraints* constraints) {
diff --git a/third_party/blink/renderer/modules/mediastream/media_stream_track_impl.h b/third_party/blink/renderer/modules/mediastream/media_stream_track_impl.h
index a7ec455..aa392255 100644
--- a/third_party/blink/renderer/modules/mediastream/media_stream_track_impl.h
+++ b/third_party/blink/renderer/modules/mediastream/media_stream_track_impl.h
@@ -57,6 +57,8 @@
 // Primary implementation of the MediaStreamTrack interface and idl type.
 class MODULES_EXPORT MediaStreamTrackImpl : public MediaStreamTrack,
                                             public MediaStreamSource::Observer {
+  USING_PRE_FINALIZER(MediaStreamTrackImpl, Dispose);
+
  public:
   // Create a MediaStreamTrackImpl of the appropriate type for the display
   // surface type.
@@ -169,6 +171,8 @@
   friend class CanvasCaptureMediaStreamTrack;
   friend class InternalsMediaStream;
 
+  void Dispose();
+
   // MediaStreamTrack
   void applyConstraints(ScriptPromiseResolver<IDLUndefined>*,
                         const MediaTrackConstraints*) override;
diff --git a/third_party/blink/renderer/modules/mediastream/media_stream_track_impl_test.cc b/third_party/blink/renderer/modules/mediastream/media_stream_track_impl_test.cc
index 55c7b9c0..f9675051 100644
--- a/third_party/blink/renderer/modules/mediastream/media_stream_track_impl_test.cc
+++ b/third_party/blink/renderer/modules/mediastream/media_stream_track_impl_test.cc
@@ -20,6 +20,7 @@
 #include "third_party/blink/renderer/bindings/core/v8/script_promise_tester.h"
 #include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_testing.h"
 #include "third_party/blink/renderer/bindings/modules/v8/v8_constrain_long_range.h"
+#include "third_party/blink/renderer/bindings/modules/v8/v8_media_stream_track_state.h"
 #include "third_party/blink/renderer/bindings/modules/v8/v8_media_track_constraints.h"
 #include "third_party/blink/renderer/bindings/modules/v8/v8_union_constrainlongrange_long.h"
 #include "third_party/blink/renderer/core/frame/frame_test_helpers.h"
@@ -28,14 +29,15 @@
 #include "third_party/blink/renderer/core/streams/readable_stream.h"
 #include "third_party/blink/renderer/core/streams/readable_stream_default_reader.h"
 #include "third_party/blink/renderer/modules/mediastream/apply_constraints_processor.h"
-#include "third_party/blink/renderer/modules/mediastream/local_media_stream_audio_source.h"
 #include "third_party/blink/renderer/modules/mediastream/media_constraints.h"
 #include "third_party/blink/renderer/modules/mediastream/media_constraints_impl.h"
 #include "third_party/blink/renderer/modules/mediastream/media_stream.h"
 #include "third_party/blink/renderer/modules/mediastream/media_stream_constraints_util_video_content.h"
+#include "third_party/blink/renderer/modules/mediastream/media_stream_track.h"
 #include "third_party/blink/renderer/modules/mediastream/media_stream_video_track.h"
 #include "third_party/blink/renderer/modules/mediastream/mock_media_stream_video_sink.h"
 #include "third_party/blink/renderer/modules/mediastream/mock_media_stream_video_source.h"
+#include "third_party/blink/renderer/modules/mediastream/speech_recognition_media_stream_audio_sink.h"
 #include "third_party/blink/renderer/modules/peerconnection/mock_peer_connection_dependency_factory.h"
 #include "third_party/blink/renderer/platform/bindings/exception_state.h"
 #include "third_party/blink/renderer/platform/mediastream/media_stream_audio_source.h"
@@ -90,20 +92,6 @@
   MOCK_METHOD(void, EnabledStateChangedForWebRtcAudio, (bool));
 };
 
-std::unique_ptr<blink::LocalMediaStreamAudioSource>
-MakeLocalMediaStreamAudioSource() {
-  blink::MediaStreamDevice device;
-  device.type = blink::mojom::MediaStreamType::DEVICE_AUDIO_CAPTURE;
-  return std::make_unique<blink::LocalMediaStreamAudioSource>(
-      /*blink::WebLocalFrame=*/nullptr, device,
-      /*requested_buffer_size=*/nullptr,
-      /*disable_local_echo=*/false,
-      MediaStreamAudioProcessingLayout::MakeForUnprocessedLocalSourceForTests(
-          /*platform_aec=*/false, /*available_platform_effects=*/0),
-      blink::WebPlatformMediaStreamSource::ConstraintsRepeatingCallback(),
-      blink::scheduler::GetSingleThreadTaskRunnerForTesting());
-}
-
 MediaStreamComponent* MakeMockVideoComponent() {
   std::unique_ptr<MockMediaStreamVideoSource> platform_source =
       MakeMockMediaStreamVideoSource();
@@ -121,7 +109,10 @@
 MediaStreamComponent* MakeMockAudioComponent() {
   MediaStreamSource* source = MakeGarbageCollected<MediaStreamSource>(
       "id", MediaStreamSource::StreamType::kTypeAudio, "name",
-      /*remote=*/false, MakeLocalMediaStreamAudioSource());
+      /*remote=*/false,
+      std::make_unique<MediaStreamAudioSource>(
+          scheduler::GetSingleThreadTaskRunnerForTesting(),
+          true /* is_local_source */));
   auto platform_track =
       std::make_unique<MediaStreamAudioTrack>(true /* is_local_track */);
   return MakeGarbageCollected<MediaStreamComponentImpl>(
@@ -284,7 +275,10 @@
   MediaStreamComponent* MakeMockWebRtcAudioComponent() {
     auto* source = MakeGarbageCollected<MediaStreamSource>(
         "id", MediaStreamSource::StreamType::kTypeAudio, "name",
-        /*remote=*/true, MakeLocalMediaStreamAudioSource());
+        /*remote=*/true,
+        std::make_unique<MediaStreamAudioSource>(
+            scheduler::GetSingleThreadTaskRunnerForTesting(),
+            false /* is_local_source */));
 
     scoped_refptr<webrtc::AudioTrackInterface> remote_track(
         blink::MockWebRtcAudioTrack::Create("track_id").get());
@@ -298,7 +292,10 @@
   MediaStreamComponent* MakeMockAudioComponent() {
     MediaStreamSource* source = MakeGarbageCollected<MediaStreamSource>(
         "id", MediaStreamSource::StreamType::kTypeAudio, "name",
-        /*remote=*/false, MakeLocalMediaStreamAudioSource());
+        /*remote=*/false,
+        std::make_unique<MediaStreamAudioSource>(
+            scheduler::GetSingleThreadTaskRunnerForTesting(),
+            true /* is_local_source */));
     auto platform_track =
         std::make_unique<MediaStreamAudioTrack>(true /* is_local_track */);
     return MakeGarbageCollected<MediaStreamComponentImpl>(
@@ -861,4 +858,51 @@
   EXPECT_EQ(video_track->min_frame_rate(), kMinFrameRate);
 }
 
+TEST_F(MediaStreamTrackImplTest, StopAudioTrackAfterSinkDestroyed) {
+  V8TestingScope v8_scope;
+
+  // 1. Create the underlying platform track and its component.
+  // Keep this component alive with a Persistent handle to control its
+  // lifetime, ensuring it outlives the temporary track and sink created below.
+  Persistent<MediaStreamComponent> component = MakeMockAudioComponent();
+  MediaStreamAudioSource* source =
+      MediaStreamAudioSource::From(component->Source());
+
+  MediaStreamAudioTrack* platform_track =
+      MediaStreamAudioTrack::From(component.Get());
+  ASSERT_TRUE(source);
+  ASSERT_TRUE(platform_track);
+
+  // 2. Start the platform track by connecting it to the source. After this, the
+  // track is "live" and can accept sinks.
+  source->ConnectToInitializedTrack(component.Get());
+
+  // 3. Create a temporary MediaStreamTrackImpl wrapper and a sink in a
+  // separate scope. This wrapper will "own" the sink via a strong GC ref.
+  {
+    MediaStreamTrack* track1 = MakeGarbageCollected<MediaStreamTrackImpl>(
+        v8_scope.GetExecutionContext(), component.Get());
+    auto* sink = MakeGarbageCollected<SpeechRecognitionMediaStreamAudioSink>(
+        v8_scope.GetExecutionContext(), base::DoNothing());
+
+    // 4. Register the sink with the wrapper and add its raw pointer to the
+    // platform track's sink list.
+    track1->RegisterSink(sink);
+    platform_track->AddSink(sink);
+  }
+
+  // 5. Force garbage collection. This destroys `track_with_sink` and `sink`.
+  // If the fix is present, `track_with_sink->Dispose()` is called, removing
+  // the sink from `platform_track`.
+  WebHeap::CollectAllGarbageForTesting();
+
+  // 5. Now, destroy the component that owns the platform track by clearing the
+  // persistent handle and running GC again. The component's pre-finalizer,
+  // Dispose(), will call `platform_track->StopAndNotify()`.
+  component.Clear();
+  WebHeap::CollectAllGarbageForTesting();
+
+  // The test passes if it doesn't crash.
+}
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/third_party/blink/renderer/modules/mediastream/media_stream_track_impl_test.cc b/third_party/blink/renderer/modules/mediastream/media_stream_track_impl_test.cc
index 55c7b9c0..f9675051 100644
--- a/third_party/blink/renderer/modules/mediastream/media_stream_track_impl_test.cc
+++ b/third_party/blink/renderer/modules/mediastream/media_stream_track_impl_test.cc
@@ -20,6 +20,7 @@
 #include "third_party/blink/renderer/bindings/core/v8/script_promise_tester.h"
 #include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_testing.h"
 #include "third_party/blink/renderer/bindings/modules/v8/v8_constrain_long_range.h"
+#include "third_party/blink/renderer/bindings/modules/v8/v8_media_stream_track_state.h"
 #include "third_party/blink/renderer/bindings/modules/v8/v8_media_track_constraints.h"
 #include "third_party/blink/renderer/bindings/modules/v8/v8_union_constrainlongrange_long.h"
 #include "third_party/blink/renderer/core/frame/frame_test_helpers.h"
@@ -28,14 +29,15 @@
 #include "third_party/blink/renderer/core/streams/readable_stream.h"
 #include "third_party/blink/renderer/core/streams/readable_stream_default_reader.h"
 #include "third_party/blink/renderer/modules/mediastream/apply_constraints_processor.h"
-#include "third_party/blink/renderer/modules/mediastream/local_media_stream_audio_source.h"
 #include "third_party/blink/renderer/modules/mediastream/media_constraints.h"
 #include "third_party/blink/renderer/modules/mediastream/media_constraints_impl.h"
 #include "third_party/blink/renderer/modules/mediastream/media_stream.h"
 #include "third_party/blink/renderer/modules/mediastream/media_stream_constraints_util_video_content.h"
+#include "third_party/blink/renderer/modules/mediastream/media_stream_track.h"
 #include "third_party/blink/renderer/modules/mediastream/media_stream_video_track.h"
 #include "third_party/blink/renderer/modules/mediastream/mock_media_stream_video_sink.h"
 #include "third_party/blink/renderer/modules/mediastream/mock_media_stream_video_source.h"
+#include "third_party/blink/renderer/modules/mediastream/speech_recognition_media_stream_audio_sink.h"
 #include "third_party/blink/renderer/modules/peerconnection/mock_peer_connection_dependency_factory.h"
 #include "third_party/blink/renderer/platform/bindings/exception_state.h"
 #include "third_party/blink/renderer/platform/mediastream/media_stream_audio_source.h"
@@ -90,20 +92,6 @@
   MOCK_METHOD(void, EnabledStateChangedForWebRtcAudio, (bool));
 };
 
-std::unique_ptr<blink::LocalMediaStreamAudioSource>
-MakeLocalMediaStreamAudioSource() {
-  blink::MediaStreamDevice device;
-  device.type = blink::mojom::MediaStreamType::DEVICE_AUDIO_CAPTURE;
-  return std::make_unique<blink::LocalMediaStreamAudioSource>(
-      /*blink::WebLocalFrame=*/nullptr, device,
-      /*requested_buffer_size=*/nullptr,
-      /*disable_local_echo=*/false,
-      MediaStreamAudioProcessingLayout::MakeForUnprocessedLocalSourceForTests(
-          /*platform_aec=*/false, /*available_platform_effects=*/0),
-      blink::WebPlatformMediaStreamSource::ConstraintsRepeatingCallback(),
-      blink::scheduler::GetSingleThreadTaskRunnerForTesting());
-}
-
 MediaStreamComponent* MakeMockVideoComponent() {
   std::unique_ptr<MockMediaStreamVideoSource> platform_source =
       MakeMockMediaStreamVideoSource();
@@ -121,7 +109,10 @@
 MediaStreamComponent* MakeMockAudioComponent() {
   MediaStreamSource* source = MakeGarbageCollected<MediaStreamSource>(
       "id", MediaStreamSource::StreamType::kTypeAudio, "name",
-      /*remote=*/false, MakeLocalMediaStreamAudioSource());
+      /*remote=*/false,
+      std::make_unique<MediaStreamAudioSource>(
+          scheduler::GetSingleThreadTaskRunnerForTesting(),
+          true /* is_local_source */));
   auto platform_track =
       std::make_unique<MediaStreamAudioTrack>(true /* is_local_track */);
   return MakeGarbageCollected<MediaStreamComponentImpl>(
@@ -284,7 +275,10 @@
   MediaStreamComponent* MakeMockWebRtcAudioComponent() {
     auto* source = MakeGarbageCollected<MediaStreamSource>(
         "id", MediaStreamSource::StreamType::kTypeAudio, "name",
-        /*remote=*/true, MakeLocalMediaStreamAudioSource());
+        /*remote=*/true,
+        std::make_unique<MediaStreamAudioSource>(
+            scheduler::GetSingleThreadTaskRunnerForTesting(),
+            false /* is_local_source */));
 
     scoped_refptr<webrtc::AudioTrackInterface> remote_track(
         blink::MockWebRtcAudioTrack::Create("track_id").get());
@@ -298,7 +292,10 @@
   MediaStreamComponent* MakeMockAudioComponent() {
     MediaStreamSource* source = MakeGarbageCollected<MediaStreamSource>(
         "id", MediaStreamSource::StreamType::kTypeAudio, "name",
-        /*remote=*/false, MakeLocalMediaStreamAudioSource());
+        /*remote=*/false,
+        std::make_unique<MediaStreamAudioSource>(
+            scheduler::GetSingleThreadTaskRunnerForTesting(),
+            true /* is_local_source */));
     auto platform_track =
         std::make_unique<MediaStreamAudioTrack>(true /* is_local_track */);
     return MakeGarbageCollected<MediaStreamComponentImpl>(
@@ -861,4 +858,51 @@
   EXPECT_EQ(video_track->min_frame_rate(), kMinFrameRate);
 }
 
+TEST_F(MediaStreamTrackImplTest, StopAudioTrackAfterSinkDestroyed) {
+  V8TestingScope v8_scope;
+
+  // 1. Create the underlying platform track and its component.
+  // Keep this component alive with a Persistent handle to control its
+  // lifetime, ensuring it outlives the temporary track and sink created below.
+  Persistent<MediaStreamComponent> component = MakeMockAudioComponent();
+  MediaStreamAudioSource* source =
+      MediaStreamAudioSource::From(component->Source());
+
+  MediaStreamAudioTrack* platform_track =
+      MediaStreamAudioTrack::From(component.Get());
+  ASSERT_TRUE(source);
+  ASSERT_TRUE(platform_track);
+
+  // 2. Start the platform track by connecting it to the source. After this, the
+  // track is "live" and can accept sinks.
+  source->ConnectToInitializedTrack(component.Get());
+
+  // 3. Create a temporary MediaStreamTrackImpl wrapper and a sink in a
+  // separate scope. This wrapper will "own" the sink via a strong GC ref.
+  {
+    MediaStreamTrack* track1 = MakeGarbageCollected<MediaStreamTrackImpl>(
+        v8_scope.GetExecutionContext(), component.Get());
+    auto* sink = MakeGarbageCollected<SpeechRecognitionMediaStreamAudioSink>(
+        v8_scope.GetExecutionContext(), base::DoNothing());
+
+    // 4. Register the sink with the wrapper and add its raw pointer to the
+    // platform track's sink list.
+    track1->RegisterSink(sink);
+    platform_track->AddSink(sink);
+  }
+
+  // 5. Force garbage collection. This destroys `track_with_sink` and `sink`.
+  // If the fix is present, `track_with_sink->Dispose()` is called, removing
+  // the sink from `platform_track`.
+  WebHeap::CollectAllGarbageForTesting();
+
+  // 5. Now, destroy the component that owns the platform track by clearing the
+  // persistent handle and running GC again. The component's pre-finalizer,
+  // Dispose(), will call `platform_track->StopAndNotify()`.
+  component.Clear();
+  WebHeap::CollectAllGarbageForTesting();
+
+  // The test passes if it doesn't crash.
+}
+
 }  // namespace blink
Loading diff…

Original Bug Report

reported by m....@gmail.com

use-after-poison in blink::MediaStreamAudioTrack::StopAndNotify(class base::OnceCallback<(void)>)

Security Bug

Important: Please do not change the component of this bug manually.

Please READ THIS FAQ before filing a bug: https://chromium.googlesource.com/chromium/src/+/HEAD/docs/security/faq.md

Please see the following link for instructions on filing security bugs: https://www.chromium.org/Home/chromium-security/reporting-security-bugs

Reports may be eligible for reward payments under the Chrome VRP: https://g.co/chrome/vrp

NOTE: Security bugs are normally made public once a fix has been widely deployed.


VULNERABILITY DETAILS AddressSanitizer: use-after-poison in blink::MediaStreamAudioTrack::StopAndNotify(class base::OnceCallback<(void)>)

VERSION

asan-win32-release_x64-1476007

Reproduction Steps

  • python -m http.server 8000
  • Run: chrome --autoplay-policy=no-user-gesture-required --js-flags="-expose-gc" --no-sandbox --user-data-dir=test --enable-logging=stderr poc.html

Type of crash

Tab

Root Cause Analysis (RCA)

Coming soon.

View on issue tracker