CVE-2026-76024
Overview
Background
- `MediaStreamTrack` (Audio/Video)
- Blink platform tracks (
MediaStreamAudioTrack,MediaStreamVideoTrack) that deliver media from aMediaStreamSourceto sinks. Multiple tracks can share one source. - `MediaStreamComponentImpl`
- The Oilpan (
GarbageCollected) object that owns the platform track (held as astd::unique_ptr). Its pre-finalizerMediaStreamComponentImpl::Disposedeletes 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
endedon the otherMediaStreamTrackImpls 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 aWeakPtrtaken before stopping can be used to detect whetherthiswas 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”).
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
- Share a source
Attacker page creates a
MediaStreamwith two tracks backed by the same source; one sibling track has anendedevent listener. (web-reachable via getUserMedia/capture APIs; reachability inferred for the exact GC timing) - Drop references
Arrange that the
MediaStreamComponentImplowning the target platform track is referenced only transiently, so it becomes collectable once stop begins. - Stop the last track
Call
track.stop()on the track that is the source’s last track, enteringStopAndNotify. - Re-enter via `ended`
Stopping the source synchronously fires
endedon the sibling; the listener’s script drops the last reference to the owning component and provokes a GC. - Free during call
Oilpan runs the component’s pre-finalizer
Dispose(), deleting the platform track whileStopAndNotifyis still on the stack. - 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
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
| Function | Change | Notes |
|---|---|---|
ifthird_party/blink/renderer/modules/mediastream/media_stream_video_track.cc |
modified | |
ifthird_party/blink/renderer/platform/mediastream/media_stream_audio_track.cc |
modified | |
TEST_Fthird_party/blink/renderer/modules/mediastream/media_stream_track_impl_test.cc |
modified | |
BindOncethird_party/blink/renderer/modules/mediastream/media_stream_track_impl_test.cc |
modified | |
GarbageCollectingSourceObserverthird_party/blink/renderer/modules/mediastream/media_stream_video_track_test.cc |
modified | |
ifthird_party/blink/renderer/modules/mediastream/media_stream_video_track_test.cc |
modified | |
BindLambdaForTestingthird_party/blink/renderer/modules/mediastream/media_stream_video_track_test.cc |
modified | |
TEST_Fthird_party/blink/renderer/modules/mediastream/media_stream_video_track_test.cc |
modified | |
GarbageCollectingSourceObserverthird_party/blink/renderer/platform/mediastream/media_stream_audio_test.cc |
modified | |
ifthird_party/blink/renderer/platform/mediastream/media_stream_audio_test.cc |
modified |
Files Changed
third_party/blink/renderer/platform/mediastream/media_stream_audio_test.ccthird_party/blink/renderer/modules/mediastream/media_stream_video_track.hthird_party/blink/renderer/modules/mediastream/media_stream_video_track_test.ccthird_party/blink/renderer/modules/mediastream/media_stream_video_track.ccthird_party/blink/renderer/modules/mediastream/media_stream_track_impl_test.ccthird_party/blink/renderer/platform/mediastream/media_stream_audio_track.cc
Audit Directions
- Post-stop member accessAudit other
Stop/StopAndNotify/RemoveTrack/RemoveSinkpaths in mediastream for member access after a call that can synchronously fire events (ended, state-change observers) and thus GCthis. - Pre-finalizer-deletable ownershipReview objects owned via
unique_ptrby an OilpanGarbageCollectedholder with a pre-finalizer (MediaStreamComponentImpl::Dispose); any method that re-enters script must guard its continuation with aWeakPtr. - Synchronous `ended`/event dispatchTrace all synchronous source-stop → sibling-track
endeddispatch sites; verify callers tolerate self-destruction during dispatch.
Patch
From 29268f92bbf3b2a2c5566bab2eb4d16b0e5bee49 Mon Sep 17 00:00:00 2001
From: Guido Urdaneta <guidou@chromium.org>
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 <guidou@chromium.org>
Reviewed-by: Henrik Boström <hbos@chromium.org>
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)
Regression Test / PoC
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)