Critical chrome UAF 🔧 Commit mapped

Overview

Critical
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactMemory corruption in MediaStream
DescriptionMemory corruption in MediaStream
ComponentMediaStream
Sub-componentMediaStream
Bug ClassUAF
Tracker576215540
Fix commit29268f92bbf3 (chromium/src) +291/-4
CISA KEVNot listed
CreditedXinyang Ge
Disclosed2026-10-29

Background

`MediaStreamTrack` (Audio/Video)
Blink platform tracks (MediaStreamAudioTrack, MediaStreamVideoTrack) that deliver media from a MediaStreamSource to sinks. Multiple tracks can share one source.
`MediaStreamComponentImpl`
The Oilpan (GarbageCollected) object that owns the platform track (held as a std::unique_ptr). Its pre-finalizer MediaStreamComponentImpl::Dispose deletes the platform track when the component is garbage-collected.
`StopAndNotify(callback)`
Stops a track. If this is the source’s last track, it stops the source, which synchronously dispatches ended on the other MediaStreamTrackImpls sharing the source. That dispatch runs script, which can trigger an Oilpan GC.
`base::WeakPtr` / `weak_factory_`
Each platform track has a WeakFactory; the destructor path invalidates weak pointers, so a WeakPtr taken before stopping can be used to detect whether this was destroyed mid-call.

Root Cause Analysis

The bug is a re-entrancy use-after-free in MediaStreamVideoTrack::StopAndNotify and MediaStreamAudioTrack::StopAndNotify. The old code stopped the source while this was still on the stack and then kept using this. In the video case, source_->RemoveTrack(this, std::move(callback)); source_ = nullptr; — RemoveTrack stops the source when this is the last track, synchronously firing ended on sibling MediaStreamTrackImpls; that script can run a GC, and if nothing else references the MediaStreamComponentImpl owning this, the component’s pre-finalizer Dispose() deletes the platform track. Control then returns into StopAndNotify and the source_ = nullptr write (and in general any further member access) lands on freed memory. The audio case is analogous: running stop_callback_ (MediaStreamAudioSource::StopAudioDeliveryTo) stops the source and fires ended, after which the code continued to touch deliverer_, sinks, and weak_factory_ on a possibly-freed this.

The patch takes a base::WeakPtr to this before the operation that can re-enter and destroy it, then returns early if the weak pointer is cleared. For video: base::WeakPtr<MediaStreamVideoTrack> weak_this = weak_factory_.GetWeakPtr(); and source_ is cleared with std::exchange(source_, nullptr) before calling source->RemoveTrack(this, std::move(callback)) (so the re-entrant Stop() from ~MediaStreamVideoTrack() does not double-remove); then if (!weak_this) return;. For audio: base::WeakPtr<MediaStreamAudioTrack> weak_this = GetWeakPtr(); is captured before running stop_callback_, and if !weak_this afterward, it only runs callback (which the request queue relies on for progress) and returns, skipping the now-freed deliverer_/sink access. The destructor already runs Stop() (ending sinks, invalidating weak pointers), so nothing is lost by returning early. This matches the advisory class (UAF, labeled “Memory corruption in MediaStream”).

Key insight
StopAndNotify stopped the source — which synchronously fires ended, runs script, and can GC the MediaStreamComponentImpl whose pre-finalizer deletes the very track StopAndNotify is running on — and then kept dereferencing this, so the continuation ran on freed memory.

Attack Path

  1. Share a source Attacker page creates a MediaStream with two tracks backed by the same source; one sibling track has an ended event listener. (web-reachable via getUserMedia/capture APIs; reachability inferred for the exact GC timing)
  2. Drop references Arrange that the MediaStreamComponentImpl owning the target platform track is referenced only transiently, so it becomes collectable once stop begins.
  3. Stop the last track Call track.stop() on the track that is the source’s last track, entering StopAndNotify.
  4. Re-enter via `ended` Stopping the source synchronously fires ended on the sibling; the listener’s script drops the last reference to the owning component and provokes a GC.
  5. Free during call Oilpan runs the component’s pre-finalizer Dispose(), deleting the platform track while StopAndNotify is still on the stack.
  6. Primitive The old code’s post-stop member writes/reads (source_ = nullptr, sink/deliverer_ access) execute on the freed track object: a use-after-free in the renderer, exploitable for memory corruption (controlled reclamation of the freed Oilpan object inferred).

Impact Assessment

Renderer process, no elevated privilege — reachable from untrusted web content using standard MediaStream APIs (getUserMedia / display / canvas capture) that let a page create sibling tracks on one source, attach an ended listener, and control references so GC reclaims the owning component during stop. The primitive is a heap use-after-free of a Blink/Oilpan platform-track object with an attacker-timed free (via ended-driven script + forced GC); rated critical by the advisory, consistent with a strong UAF primitive, though a full RCE requires heap grooming of the reclaimed object (inferred). Precondition: the stopped track must be the source’s last track so stopping it stops the source and fires ended on a sibling.

Changed Functions

FunctionChangeNotes
if
third_party/blink/renderer/modules/mediastream/media_stream_video_track.cc
modified
if
third_party/blink/renderer/platform/mediastream/media_stream_audio_track.cc
modified
TEST_F
third_party/blink/renderer/modules/mediastream/media_stream_track_impl_test.cc
modified
BindOnce
third_party/blink/renderer/modules/mediastream/media_stream_track_impl_test.cc
modified
GarbageCollectingSourceObserver
third_party/blink/renderer/modules/mediastream/media_stream_video_track_test.cc
modified
if
third_party/blink/renderer/modules/mediastream/media_stream_video_track_test.cc
modified
BindLambdaForTesting
third_party/blink/renderer/modules/mediastream/media_stream_video_track_test.cc
modified
TEST_F
third_party/blink/renderer/modules/mediastream/media_stream_video_track_test.cc
modified
GarbageCollectingSourceObserver
third_party/blink/renderer/platform/mediastream/media_stream_audio_test.cc
modified
if
third_party/blink/renderer/platform/mediastream/media_stream_audio_test.cc
modified

Files Changed

  • third_party/blink/renderer/platform/mediastream/media_stream_audio_test.cc
  • third_party/blink/renderer/modules/mediastream/media_stream_video_track.h
  • third_party/blink/renderer/modules/mediastream/media_stream_video_track_test.cc
  • third_party/blink/renderer/modules/mediastream/media_stream_video_track.cc
  • third_party/blink/renderer/modules/mediastream/media_stream_track_impl_test.cc
  • third_party/blink/renderer/platform/mediastream/media_stream_audio_track.cc

Audit Directions

  • Post-stop member access
    Audit other Stop/StopAndNotify/RemoveTrack/RemoveSink paths in mediastream for member access after a call that can synchronously fire events (ended, state-change observers) and thus GC this.
  • Pre-finalizer-deletable ownership
    Review objects owned via unique_ptr by an Oilpan GarbageCollected holder with a pre-finalizer (MediaStreamComponentImpl::Dispose); any method that re-enters script must guard its continuation with a WeakPtr.
  • Synchronous `ended`/event dispatch
    Trace all synchronous source-stop → sibling-track ended dispatch sites; verify callers tolerate self-destruction during dispatch.
From 29268f92bbf3b2a2c5566bab2eb4d16b0e5bee49 Mon Sep 17 00:00:00 2001
From: Guido Urdaneta <[email protected]>
Date: Thu, 22 Oct 2026 03:56:15 -0700
Subject: [PATCH] [MediaStream] Fix UAF in MediaStream(Audio|Video)Track::StopAndNotify

Stopping a track stops the corresponding source if the track was the
last one connected to the source. Stopping the source synchronously
dispatches the `ended` event on other MediaStreamTrackImpls sharing
that source, which runs script and can trigger an Oilpan GC.
If nothing else references the MediaStreamComponentImpl owning
the platform track, the compoenent's pre-finalizer
(MediaStreamComponentImpl::Dispose) deletes the platform track while
StopAndNotify() is still on the stack.

In that case, the platform track destructor has already called Stop()
(ending sinks and invalidating weak pointers), so check a `WeakPtr`
acquired before stopping the track and return early if the platform
track has been deleted.

Bug: 576215540
Change-Id: I06e79a8afc1f5b23b2b903ca364b27c6c689ce0e
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8626348
Commit-Queue: Guido Urdaneta <[email protected]>
Reviewed-by: Henrik Boström <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1724620}
---
diff --git a/third_party/blink/renderer/modules/mediastream/media_stream_video_track.cc b/third_party/blink/renderer/modules/mediastream/media_stream_video_track.cc
index 235dbf3c..53f11849 100644
--- a/third_party/blink/renderer/modules/mediastream/media_stream_video_track.cc
+++ b/third_party/blink/renderer/modules/mediastream/media_stream_video_track.cc
@@ -1113,8 +1113,25 @@
   TRACE_EVENT("media", "MediaStreamVideoTrack::StopAndNotify");
   DCHECK_CALLED_ON_VALID_THREAD(main_render_thread_checker_);
   if (source_) {
-    source_->RemoveTrack(this, std::move(callback));
-    source_ = nullptr;
+    // MediaStreamVideoSource::RemoveTrack() stops the source if this was its
+    // last track. That synchronously fires 'ended' on other
+    // MediaStreamTrackImpls sharing the source, which runs script, which can
+    // trigger a GC. If nothing else references the MediaStreamComponentImpl
+    // owning |this|, its pre-finalizer deletes |this| while this frame is still
+    // on the stack. See https://crbug.com/576215540.
+    //
+    // Clear |source_| before calling RemoveTrack() so that the re-entrant
+    // Stop() call from ~MediaStreamVideoTrack() does not try to remove |this|
+    // from the source a second time. The destructor's Stop() call takes care
+    // of ending the sinks and stopping |refresh_timer_|, and |callback| is
+    // owned by the source once RemoveTrack() has been called, so there is
+    // nothing left to do here if |this| is gone.
+    base::WeakPtr<MediaStreamVideoTrack> weak_this = weak_factory_.GetWeakPtr();
+    MediaStreamVideoSource* source = std::exchange(source_, nullptr).get();
+    source->RemoveTrack(this, std::move(callback));
+    if (!weak_this) {
+      return;
+    }
   } else if (callback) {
     std::move(callback).Run();
   }
diff --git a/third_party/blink/renderer/modules/mediastream/media_stream_video_track.h b/third_party/blink/renderer/modules/mediastream/media_stream_video_track.h
index 40f92b9..e7f553b 100644
--- a/third_party/blink/renderer/modules/mediastream/media_stream_video_track.h
+++ b/third_party/blink/renderer/modules/mediastream/media_stream_video_track.h
@@ -199,6 +199,10 @@
     is_screencast_ = is_screencast;
   }
 
+  base::WeakPtr<MediaStreamVideoTrack> GetWeakPtrForTesting() {
+    return weak_factory_.GetWeakPtr();
+  }
+
   MediaStreamTrackPlatform::StreamType Type() const override {
     return MediaStreamTrackPlatform::StreamType::kVideo;
   }
diff --git a/third_party/blink/renderer/platform/mediastream/media_stream_audio_track.cc b/third_party/blink/renderer/platform/mediastream/media_stream_audio_track.cc
index ac47c0dd..0626c0c 100644
--- a/third_party/blink/renderer/platform/mediastream/media_stream_audio_track.cc
+++ b/third_party/blink/renderer/platform/mediastream/media_stream_audio_track.cc
@@ -144,8 +144,26 @@
   DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
   WebRtcLog(kTag, this, "%s()", __func__);
 
-  if (!stop_callback_.is_null())
+  // Running |stop_callback_| (MediaStreamAudioSource::StopAudioDeliveryTo)
+  // stops the source if this was its last track. That synchronously fires
+  // 'ended' on other MediaStreamTrackImpls sharing the source, which runs
+  // script, which can trigger a GC. If nothing else references the
+  // MediaStreamComponentImpl owning |this|, its pre-finalizer deletes |this|
+  // while this frame is still on the stack. In that case
+  // ~MediaStreamAudioTrack() has already run Stop() (ending the sinks and
+  // invalidating weak pointers), so the only thing left to do is run
+  // |callback|, which UserMediaClient's request queue relies on to make
+  // progress. See https://crbug.com/576215540.
+  base::WeakPtr<MediaStreamAudioTrack> weak_this = GetWeakPtr();
+  if (!stop_callback_.is_null()) {
     std::move(stop_callback_).Run();
+  }
+  if (!weak_this) {
+    if (callback) {
+      std::move(callback).Run();
+    }
+    return;
+  }
 
   Vector<WebMediaStreamAudioSink*> sinks_to_end;
   deliverer_.GetConsumerList(&sinks_to_end);
@@ -154,8 +172,9 @@
     sink->OnReadyStateChanged(WebMediaStreamSource::kReadyStateEnded);
   }
 
-  if (callback)
+  if (callback) {
     std::move(callback).Run();
+  }
   weak_factory_.InvalidateWeakPtrs();
 }
 
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 fec1479a..4460fea 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
@@ -24,6 +24,7 @@
 #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/dom/events/event.h"
 #include "third_party/blink/renderer/core/dom/events/native_event_listener.h"
 #include "third_party/blink/renderer/core/frame/frame_test_helpers.h"
 #include "third_party/blink/renderer/core/html/media/html_media_element.h"
@@ -957,4 +958,107 @@
   // The test passes if it doesn't crash.
 }
 
+TEST_F(MediaStreamTrackImplTest, StopAudioTrackWhenEndedEventTriggersGC) {
+  V8TestingScope v8_scope;
+
+  Persistent<MediaStreamComponent> component = MakeMockAudioComponent();
+  MediaStreamAudioSource* source =
+      MediaStreamAudioSource::From(component->Source());
+  MediaStreamAudioTrack* platform_track =
+      MediaStreamAudioTrack::From(component.Get());
+  ASSERT_TRUE(source);
+  ASSERT_TRUE(platform_track);
+  EXPECT_TRUE(source->ConnectToInitializedTrack(component.Get()));
+
+  MediaStreamComponent* other_component =
+      MakeGarbageCollected<MediaStreamComponentImpl>(
+          component->Source(),
+          std::make_unique<MediaStreamAudioTrack>(/*is_local_track=*/true));
+  Persistent<MediaStreamTrackImpl> other_track =
+      MakeGarbageCollected<MediaStreamTrackImpl>(v8_scope.GetExecutionContext(),
+                                                 other_component);
+
+  Persistent<testing::StrictMock<MockEventListener>> event_listener =
+      MakeGarbageCollected<testing::StrictMock<MockEventListener>>();
+  other_track->addEventListener(event_type_names::kEnded, event_listener.Get());
+
+  base::WeakPtr<MediaStreamAudioTrack> weak_platform_track =
+      platform_track->GetWeakPtr();
+  // Precise GC (WebHeap::CollectAllGarbageForTesting) does not scan the C++
+  // stack and would otherwise collect the in-flight Event, which
+  // EventDispatcher holds only on the stack.
+  Persistent<Event> keep_event_alive;
+  EXPECT_CALL(*event_listener, Invoke(_, _))
+      .WillOnce([&](ExecutionContext*, Event* event) {
+        keep_event_alive = event;
+        component.Clear();
+        WebHeap::CollectAllGarbageForTesting();
+        EXPECT_FALSE(weak_platform_track);
+      });
+
+  bool callback_called = false;
+  platform_track->StopAndNotify(
+      blink::BindOnce([](bool* called) { *called = true; },
+                      base::Unretained(&callback_called)));
+  EXPECT_TRUE(callback_called);
+  EXPECT_FALSE(weak_platform_track);
+}
+
+TEST_F(MediaStreamTrackImplTest, StopVideoTrackWhenEndedEventTriggersGC) {
+  V8TestingScope v8_scope;
+
+  Persistent<MediaStreamComponent> component = MakeMockVideoComponent();
+  MediaStreamVideoSource* source =
+      MediaStreamVideoSource::GetVideoSource(component->Source());
+  MediaStreamVideoTrack* platform_track =
+      MediaStreamVideoTrack::From(component.Get());
+  ASSERT_TRUE(source);
+  ASSERT_TRUE(platform_track);
+
+  // A second track on the same source, with an 'ended' listener. Its platform
+  // track is stopped right away so that |platform_track| is the source's last
+  // track and stopping it stops the source.
+  MediaStreamComponent* other_component =
+      MakeGarbageCollected<MediaStreamComponentImpl>(
+          component->Source(),
+          std::make_unique<MediaStreamVideoTrack>(
+              source, MediaStreamVideoSource::ConstraintsOnceCallback(),
+              /*enabled=*/true));
+  MediaStreamVideoTrack::From(other_component)->Stop();
+  Persistent<MediaStreamTrackImpl> other_track =
+      MakeGarbageCollected<MediaStreamTrackImpl>(v8_scope.GetExecutionContext(),
+                                                 other_component);
+
+  Persistent<testing::StrictMock<MockEventListener>> event_listener =
+      MakeGarbageCollected<testing::StrictMock<MockEventListener>>();
+  other_track->addEventListener(event_type_names::kEnded, event_listener.Get());
+
+  base::WeakPtr<MediaStreamVideoTrack> weak_platform_track =
+      platform_track->GetWeakPtrForTesting();
+  // Precise GC (WebHeap::CollectAllGarbageForTesting) does not scan the C++
+  // stack and would otherwise collect the in-flight Event, which
+  // EventDispatcher holds only on the stack.
+  Persistent<Event> keep_event_alive;
+  EXPECT_CALL(*event_listener, Invoke(_, _))
+      .WillOnce([&](ExecutionContext*, Event* event) {
+        keep_event_alive = event;
+        component.Clear();
+        WebHeap::CollectAllGarbageForTesting();
+        EXPECT_FALSE(weak_platform_track);
+      });
+
+  bool callback_called = false;
+  platform_track->StopAndNotify(
+      blink::BindOnce([](bool* called) { *called = true; },
+                      base::Unretained(&callback_called)));
+  EXPECT_FALSE(weak_platform_track);
+  EXPECT_EQ(MediaStreamSource::kReadyStateEnded,
+            other_component->Source()->GetReadyState());
+
+  // MediaStreamVideoSource runs the callback once the source has actually
+  // stopped, which happens on a separate task.
+  test::RunPendingTasks();
+  EXPECT_TRUE(callback_called);
+}
+
 }  // namespace blink
diff --git a/third_party/blink/renderer/modules/mediastream/media_stream_video_track_test.cc b/third_party/blink/renderer/modules/mediastream/media_stream_video_track_test.cc
index 2d5a72c..1b178807 100644
--- a/third_party/blink/renderer/modules/mediastream/media_stream_video_track_test.cc
+++ b/third_party/blink/renderer/modules/mediastream/media_stream_video_track_test.cc
@@ -15,6 +15,7 @@
 #include "base/functional/callback_helpers.h"
 #include "base/memory/ptr_util.h"
 #include "base/memory/raw_ptr.h"
+#include "base/memory/weak_ptr.h"
 #include "base/metrics/histogram_macros.h"
 #include "base/run_loop.h"
 #include "base/strings/utf_string_conversions.h"
@@ -24,6 +25,7 @@
 #include "base/test/metrics/histogram_tester.h"
 #include "base/test/test_future.h"
 #include "base/threading/thread_checker.h"
+#include "build/build_config.h"
 #include "media/base/video_frame.h"
 #include "testing/gmock/include/gmock/gmock.h"
 #include "testing/gtest/include/gtest/gtest.h"
@@ -379,6 +381,77 @@
   sink2.DisconnectFromTrack();
 }
 
+class GarbageCollectingSourceObserver final
+    : public GarbageCollected<GarbageCollectingSourceObserver>,
+      public MediaStreamSource::Observer {
+ public:
+  explicit GarbageCollectingSourceObserver(base::OnceClosure on_state_changed)
+      : on_state_changed_(std::move(on_state_changed)) {}
+
+  void SourceChangedState() override {
+    if (on_state_changed_) {
+      std::move(on_state_changed_).Run();
+    }
+  }
+  void SourceChangedCaptureConfiguration() override {}
+  void SourceChangedCaptureHandle() override {}
+#if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS)
+  void SourceChangedZoomLevel(int) override {}
+#endif
+
+  void Trace(Visitor* visitor) const override {
+    MediaStreamSource::Observer::Trace(visitor);
+  }
+
+ private:
+  base::OnceClosure on_state_changed_;
+};
+
+// Regression test for https://crbug.com/576215540: verifies that
+// MediaStreamVideoTrack::StopAndNotify() does not UAF if stopping the source
+// synchronously triggers garbage collection of the owning
+// MediaStreamComponentImpl and destroys the MediaStreamVideoTrack.
+TEST_F(MediaStreamVideoTrackTest,
+       StopAndNotifyComponentGarbageCollectedOnSourceStop) {
+  InitializeSource();
+  WebMediaStreamTrack track = CreateTrack();
+  MediaStreamVideoTrack* raw_track = MediaStreamVideoTrack::From(track);
+  ASSERT_TRUE(raw_track);
+  base::WeakPtr<MediaStreamVideoTrack> weak_track =
+      raw_track->GetWeakPtrForTesting();
+  ASSERT_TRUE(weak_track);
+  EXPECT_EQ(MediaStreamSource::kReadyStateLive,
+            stream_source()->GetReadyState());
+
+  bool observer_called = false;
+  Persistent<GarbageCollectingSourceObserver> observer =
+      MakeGarbageCollected<GarbageCollectingSourceObserver>(
+          base::BindLambdaForTesting([&]() {
+            observer_called = true;
+            // Drop the last reference to the MediaStreamComponentImpl owning
+            // |raw_track| and collect it, which destroys |raw_track| while
+            // StopAndNotify() is still on the stack.
+            track.Reset();
+            WebHeap::CollectAllGarbageForTesting();
+            EXPECT_FALSE(weak_track);
+          }));
+  stream_source()->AddObserver(observer.Get());
+
+  bool notify_callback_called = false;
+  raw_track->StopAndNotify(
+      base::BindLambdaForTesting([&]() { notify_callback_called = true; }));
+
+  EXPECT_TRUE(observer_called);
+  EXPECT_FALSE(weak_track);
+  EXPECT_EQ(MediaStreamSource::kReadyStateEnded,
+            stream_source()->GetReadyState());
+
+  // The source runs the callback once it has actually stopped, which happens
+  // on a separate task.
+  base::RunLoop().RunUntilIdle();
+  EXPECT_TRUE(notify_callback_called);
+}
+
 TEST_F(MediaStreamVideoTrackTest, CheckTrackRequestsFrame) {
   InitializeSource();
   UpdateVideoSourceToRespondToRequestRefreshFrame();
diff --git a/third_party/blink/renderer/platform/mediastream/media_stream_audio_test.cc b/third_party/blink/renderer/platform/mediastream/media_stream_audio_test.cc
index 609e99a..0a875d0 100644
--- a/third_party/blink/renderer/platform/mediastream/media_stream_audio_test.cc
+++ b/third_party/blink/renderer/platform/mediastream/media_stream_audio_test.cc
@@ -15,11 +15,13 @@
 #include "base/numerics/safe_conversions.h"
 #include "base/synchronization/lock.h"
 #include "base/synchronization/waitable_event.h"
+#include "base/test/bind.h"
 #include "base/test/task_environment.h"
 #include "base/test/test_timeouts.h"
 #include "base/threading/platform_thread.h"
 #include "base/threading/thread_checker.h"
 #include "base/time/time.h"
+#include "build/build_config.h"
 #include "build/buildflag.h"
 #include "media/base/audio_bus.h"
 #include "media/base/audio_glitch_info.h"
@@ -632,4 +634,72 @@
 #endif
 }
 
+namespace {
+
+class GarbageCollectingSourceObserver final
+    : public GarbageCollected<GarbageCollectingSourceObserver>,
+      public MediaStreamSource::Observer {
+ public:
+  explicit GarbageCollectingSourceObserver(base::OnceClosure on_state_changed)
+      : on_state_changed_(std::move(on_state_changed)) {}
+
+  void SourceChangedState() override {
+    if (on_state_changed_) {
+      std::move(on_state_changed_).Run();
+    }
+  }
+  void SourceChangedCaptureConfiguration() override {}
+  void SourceChangedCaptureHandle() override {}
+#if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS)
+  void SourceChangedZoomLevel(int) override {}
+#endif
+
+  void Trace(Visitor* visitor) const override {
+    MediaStreamSource::Observer::Trace(visitor);
+  }
+
+ private:
+  base::OnceClosure on_state_changed_;
+};
+
+}  // namespace
+
+// Regression test for https://crbug.com/576215540: verifies that
+// MediaStreamAudioTrack::StopAndNotify() does not UAF if stopping the source
+// synchronously triggers garbage collection of the owning
+// MediaStreamComponentImpl and destroys the MediaStreamAudioTrack.
+TEST_F(MediaStreamAudioTest,
+       StopAndNotifyComponentGarbageCollectedOnSourceStop) {
+  ASSERT_TRUE(source());
+  EXPECT_TRUE(source()->ConnectToInitializedTrack(audio_component_));
+  MediaStreamAudioTrack* raw_track = track();
+  ASSERT_TRUE(raw_track);
+  base::WeakPtr<MediaStreamAudioTrack> weak_track = raw_track->GetWeakPtr();
+  ASSERT_TRUE(weak_track);
+
+  FakeMediaStreamAudioSink sink;
... (file diff truncated)
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 fec1479a..4460fea 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
@@ -24,6 +24,7 @@
 #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/dom/events/event.h"
 #include "third_party/blink/renderer/core/dom/events/native_event_listener.h"
 #include "third_party/blink/renderer/core/frame/frame_test_helpers.h"
 #include "third_party/blink/renderer/core/html/media/html_media_element.h"
@@ -957,4 +958,107 @@
   // The test passes if it doesn't crash.
 }
 
+TEST_F(MediaStreamTrackImplTest, StopAudioTrackWhenEndedEventTriggersGC) {
+  V8TestingScope v8_scope;
+
+  Persistent<MediaStreamComponent> component = MakeMockAudioComponent();
+  MediaStreamAudioSource* source =
+      MediaStreamAudioSource::From(component->Source());
+  MediaStreamAudioTrack* platform_track =
+      MediaStreamAudioTrack::From(component.Get());
+  ASSERT_TRUE(source);
+  ASSERT_TRUE(platform_track);
+  EXPECT_TRUE(source->ConnectToInitializedTrack(component.Get()));
+
+  MediaStreamComponent* other_component =
+      MakeGarbageCollected<MediaStreamComponentImpl>(
+          component->Source(),
+          std::make_unique<MediaStreamAudioTrack>(/*is_local_track=*/true));
+  Persistent<MediaStreamTrackImpl> other_track =
+      MakeGarbageCollected<MediaStreamTrackImpl>(v8_scope.GetExecutionContext(),
+                                                 other_component);
+
+  Persistent<testing::StrictMock<MockEventListener>> event_listener =
+      MakeGarbageCollected<testing::StrictMock<MockEventListener>>();
+  other_track->addEventListener(event_type_names::kEnded, event_listener.Get());
+
+  base::WeakPtr<MediaStreamAudioTrack> weak_platform_track =
+      platform_track->GetWeakPtr();
+  // Precise GC (WebHeap::CollectAllGarbageForTesting) does not scan the C++
+  // stack and would otherwise collect the in-flight Event, which
+  // EventDispatcher holds only on the stack.
+  Persistent<Event> keep_event_alive;
+  EXPECT_CALL(*event_listener, Invoke(_, _))
+      .WillOnce([&](ExecutionContext*, Event* event) {
+        keep_event_alive = event;
+        component.Clear();
+        WebHeap::CollectAllGarbageForTesting();
+        EXPECT_FALSE(weak_platform_track);
+      });
+
+  bool callback_called = false;
+  platform_track->StopAndNotify(
+      blink::BindOnce([](bool* called) { *called = true; },
+                      base::Unretained(&callback_called)));
+  EXPECT_TRUE(callback_called);
+  EXPECT_FALSE(weak_platform_track);
+}
+
+TEST_F(MediaStreamTrackImplTest, StopVideoTrackWhenEndedEventTriggersGC) {
+  V8TestingScope v8_scope;
+
+  Persistent<MediaStreamComponent> component = MakeMockVideoComponent();
+  MediaStreamVideoSource* source =
+      MediaStreamVideoSource::GetVideoSource(component->Source());
+  MediaStreamVideoTrack* platform_track =
+      MediaStreamVideoTrack::From(component.Get());
+  ASSERT_TRUE(source);
+  ASSERT_TRUE(platform_track);
+
+  // A second track on the same source, with an 'ended' listener. Its platform
+  // track is stopped right away so that |platform_track| is the source's last
+  // track and stopping it stops the source.
+  MediaStreamComponent* other_component =
+      MakeGarbageCollected<MediaStreamComponentImpl>(
+          component->Source(),
+          std::make_unique<MediaStreamVideoTrack>(
+              source, MediaStreamVideoSource::ConstraintsOnceCallback(),
+              /*enabled=*/true));
+  MediaStreamVideoTrack::From(other_component)->Stop();
+  Persistent<MediaStreamTrackImpl> other_track =
+      MakeGarbageCollected<MediaStreamTrackImpl>(v8_scope.GetExecutionContext(),
+                                                 other_component);
+
+  Persistent<testing::StrictMock<MockEventListener>> event_listener =
+      MakeGarbageCollected<testing::StrictMock<MockEventListener>>();
+  other_track->addEventListener(event_type_names::kEnded, event_listener.Get());
+
+  base::WeakPtr<MediaStreamVideoTrack> weak_platform_track =
+      platform_track->GetWeakPtrForTesting();
+  // Precise GC (WebHeap::CollectAllGarbageForTesting) does not scan the C++
+  // stack and would otherwise collect the in-flight Event, which
+  // EventDispatcher holds only on the stack.
+  Persistent<Event> keep_event_alive;
+  EXPECT_CALL(*event_listener, Invoke(_, _))
+      .WillOnce([&](ExecutionContext*, Event* event) {
+        keep_event_alive = event;
+        component.Clear();
+        WebHeap::CollectAllGarbageForTesting();
+        EXPECT_FALSE(weak_platform_track);
+      });
+
+  bool callback_called = false;
+  platform_track->StopAndNotify(
+      blink::BindOnce([](bool* called) { *called = true; },
+                      base::Unretained(&callback_called)));
+  EXPECT_FALSE(weak_platform_track);
+  EXPECT_EQ(MediaStreamSource::kReadyStateEnded,
+            other_component->Source()->GetReadyState());
+
+  // MediaStreamVideoSource runs the callback once the source has actually
+  // stopped, which happens on a separate task.
+  test::RunPendingTasks();
+  EXPECT_TRUE(callback_called);
+}
+
 }  // namespace blink
diff --git a/third_party/blink/renderer/modules/mediastream/media_stream_video_track_test.cc b/third_party/blink/renderer/modules/mediastream/media_stream_video_track_test.cc
index 2d5a72c..1b178807 100644
--- a/third_party/blink/renderer/modules/mediastream/media_stream_video_track_test.cc
+++ b/third_party/blink/renderer/modules/mediastream/media_stream_video_track_test.cc
@@ -15,6 +15,7 @@
 #include "base/functional/callback_helpers.h"
 #include "base/memory/ptr_util.h"
 #include "base/memory/raw_ptr.h"
+#include "base/memory/weak_ptr.h"
 #include "base/metrics/histogram_macros.h"
 #include "base/run_loop.h"
 #include "base/strings/utf_string_conversions.h"
@@ -24,6 +25,7 @@
 #include "base/test/metrics/histogram_tester.h"
 #include "base/test/test_future.h"
 #include "base/threading/thread_checker.h"
+#include "build/build_config.h"
 #include "media/base/video_frame.h"
 #include "testing/gmock/include/gmock/gmock.h"
 #include "testing/gtest/include/gtest/gtest.h"
@@ -379,6 +381,77 @@
   sink2.DisconnectFromTrack();
 }
 
+class GarbageCollectingSourceObserver final
+    : public GarbageCollected<GarbageCollectingSourceObserver>,
+      public MediaStreamSource::Observer {
+ public:
+  explicit GarbageCollectingSourceObserver(base::OnceClosure on_state_changed)
+      : on_state_changed_(std::move(on_state_changed)) {}
+
+  void SourceChangedState() override {
+    if (on_state_changed_) {
+      std::move(on_state_changed_).Run();
+    }
+  }
+  void SourceChangedCaptureConfiguration() override {}
+  void SourceChangedCaptureHandle() override {}
+#if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS)
+  void SourceChangedZoomLevel(int) override {}
+#endif
+
+  void Trace(Visitor* visitor) const override {
+    MediaStreamSource::Observer::Trace(visitor);
+  }
+
+ private:
+  base::OnceClosure on_state_changed_;
+};
+
+// Regression test for https://crbug.com/576215540: verifies that
+// MediaStreamVideoTrack::StopAndNotify() does not UAF if stopping the source
+// synchronously triggers garbage collection of the owning
+// MediaStreamComponentImpl and destroys the MediaStreamVideoTrack.
+TEST_F(MediaStreamVideoTrackTest,
+       StopAndNotifyComponentGarbageCollectedOnSourceStop) {
+  InitializeSource();
+  WebMediaStreamTrack track = CreateTrack();
+  MediaStreamVideoTrack* raw_track = MediaStreamVideoTrack::From(track);
+  ASSERT_TRUE(raw_track);
+  base::WeakPtr<MediaStreamVideoTrack> weak_track =
+      raw_track->GetWeakPtrForTesting();
+  ASSERT_TRUE(weak_track);
+  EXPECT_EQ(MediaStreamSource::kReadyStateLive,
+            stream_source()->GetReadyState());
+
+  bool observer_called = false;
+  Persistent<GarbageCollectingSourceObserver> observer =
+      MakeGarbageCollected<GarbageCollectingSourceObserver>(
+          base::BindLambdaForTesting([&]() {
+            observer_called = true;
+            // Drop the last reference to the MediaStreamComponentImpl owning
+            // |raw_track| and collect it, which destroys |raw_track| while
+            // StopAndNotify() is still on the stack.
+            track.Reset();
+            WebHeap::CollectAllGarbageForTesting();
+            EXPECT_FALSE(weak_track);
+          }));
+  stream_source()->AddObserver(observer.Get());
+
+  bool notify_callback_called = false;
+  raw_track->StopAndNotify(
+      base::BindLambdaForTesting([&]() { notify_callback_called = true; }));
+
+  EXPECT_TRUE(observer_called);
+  EXPECT_FALSE(weak_track);
+  EXPECT_EQ(MediaStreamSource::kReadyStateEnded,
+            stream_source()->GetReadyState());
+
+  // The source runs the callback once it has actually stopped, which happens
+  // on a separate task.
+  base::RunLoop().RunUntilIdle();
+  EXPECT_TRUE(notify_callback_called);
+}
+
 TEST_F(MediaStreamVideoTrackTest, CheckTrackRequestsFrame) {
   InitializeSource();
   UpdateVideoSourceToRespondToRequestRefreshFrame();
diff --git a/third_party/blink/renderer/platform/mediastream/media_stream_audio_test.cc b/third_party/blink/renderer/platform/mediastream/media_stream_audio_test.cc
index 609e99a..0a875d0 100644
--- a/third_party/blink/renderer/platform/mediastream/media_stream_audio_test.cc
+++ b/third_party/blink/renderer/platform/mediastream/media_stream_audio_test.cc
@@ -15,11 +15,13 @@
 #include "base/numerics/safe_conversions.h"
 #include "base/synchronization/lock.h"
 #include "base/synchronization/waitable_event.h"
+#include "base/test/bind.h"
 #include "base/test/task_environment.h"
 #include "base/test/test_timeouts.h"
 #include "base/threading/platform_thread.h"
 #include "base/threading/thread_checker.h"
 #include "base/time/time.h"
+#include "build/build_config.h"
 #include "build/buildflag.h"
 #include "media/base/audio_bus.h"
 #include "media/base/audio_glitch_info.h"
@@ -632,4 +634,72 @@
 #endif
 }
 
+namespace {
+
+class GarbageCollectingSourceObserver final
+    : public GarbageCollected<GarbageCollectingSourceObserver>,
+      public MediaStreamSource::Observer {
+ public:
+  explicit GarbageCollectingSourceObserver(base::OnceClosure on_state_changed)
+      : on_state_changed_(std::move(on_state_changed)) {}
+
+  void SourceChangedState() override {
+    if (on_state_changed_) {
+      std::move(on_state_changed_).Run();
+    }
+  }
+  void SourceChangedCaptureConfiguration() override {}
+  void SourceChangedCaptureHandle() override {}
+#if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS)
+  void SourceChangedZoomLevel(int) override {}
+#endif
+
+  void Trace(Visitor* visitor) const override {
+    MediaStreamSource::Observer::Trace(visitor);
+  }
+
+ private:
+  base::OnceClosure on_state_changed_;
+};
+
+}  // namespace
+
+// Regression test for https://crbug.com/576215540: verifies that
+// MediaStreamAudioTrack::StopAndNotify() does not UAF if stopping the source
+// synchronously triggers garbage collection of the owning
+// MediaStreamComponentImpl and destroys the MediaStreamAudioTrack.
+TEST_F(MediaStreamAudioTest,
+       StopAndNotifyComponentGarbageCollectedOnSourceStop) {
+  ASSERT_TRUE(source());
+  EXPECT_TRUE(source()->ConnectToInitializedTrack(audio_component_));
+  MediaStreamAudioTrack* raw_track = track();
+  ASSERT_TRUE(raw_track);
+  base::WeakPtr<MediaStreamAudioTrack> weak_track = raw_track->GetWeakPtr();
+  ASSERT_TRUE(weak_track);
+
+  FakeMediaStreamAudioSink sink;
+  raw_track->AddSink(&sink);
+  EXPECT_FALSE(sink.was_ended());
+
+  bool observer_called = false;
+  Persistent<GarbageCollectingSourceObserver> observer =
+      MakeGarbageCollected<GarbageCollectingSourceObserver>(
+          base::BindLambdaForTesting([&]() {
+            observer_called = true;
+            audio_component_ = nullptr;
+            WebHeap::CollectAllGarbageForTesting();
+            EXPECT_FALSE(weak_track);
+          }));
+  audio_source_->AddObserver(observer.Get());
+
+  bool notify_callback_called = false;
+  raw_track->StopAndNotify(
... (truncated)
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.