CVE-2025-10501
Overview
Background
- `WebMediaStreamObserver`
- an interface whose implementers register with a
MediaStreamDescriptorto receiveTrackAdded/TrackRemovednotifications about a media stream. - `MediaRecorderHandler`
- the Blink object backing
MediaRecorder; it isGarbageCollected, and prior to the patch it inherited directly fromWebMediaStreamObserverand registered itself as an observer. - Oilpan / GC vs. reference-counted lifetimes
MediaRecorderHandlerlives on Blink’s garbage-collected (Oilpan) heap, while theMediaStreamDescriptorobserver list stored raw pointers with no lifetime tie, so a GC’d observer could be freed while still listed.- `base::WeakPtr`
- a smart pointer that automatically becomes null when the object it points to is destroyed, letting callers detect a dead pointee instead of dereferencing freed memory.
Root Cause Analysis
Before the patch, MediaStreamDescriptor tracked its observers as raw WebMediaStreamObserver* pointers, and MediaRecorderHandler registered itself as such an observer via media_stream_->AddObserver(this). Because MediaRecorderHandler is a garbage-collected object, its lifetime is not controlled by the descriptor’s observer list; the collector can reclaim the handler while the raw pointer remains in the list, or the deregistration in the destruction path can fail to run in time. When the descriptor subsequently dispatches a TrackAdded/TrackRemoved notification, it dereferences a stale raw pointer to the already-freed handler — a use-after-free.
The fix changes AddObserver/RemoveObserver to take base::WeakPtr<WebMediaStreamObserver> and, since the GC’d handler cannot itself own a WeakPtrFactory cleanly, introduces a nested non-GC’d MediaStreamObserver helper (held by std::unique_ptr and referencing the handler through a WeakPersistent) that hands out a weak pointer; a freed observer now resolves to null instead of a dangling pointer.
base::WeakPtr that safely nulls out when the observer dies.Attack Path
- Start recording
Web content constructs a
MediaRecorderover aMediaStreamand callsstart(), causingMediaRecorderHandlerto register itself on the underlyingMediaStreamDescriptorviaAddObserver(this). - Free the handler while still registered
Through page manipulation that drops references to the recorder, the garbage collector reclaims the
MediaRecorderHandlerwhile the descriptor still holds the raw observer pointer (the deregistration path viaRemoveObserver(this)does not reliably run first). (inferred) - Trigger a track notification
The script mutates the stream (adding/removing a track), so the descriptor iterates its observer list and dispatches
TrackAdded/TrackRemovedto the stale pointer. (inferred) - Use-after-free The descriptor dereferences the freed handler, reading and calling into reclaimed memory. (inferred)
Impact Assessment
MediaRecorderHandler in the renderer process by racing garbage collection against media-stream observer notifications. Successful exploitation could yield memory corruption and potentially renderer code execution (inferred); the advisory classifies this as a use-after-free in WebRTC and does not report exploitation in the wild.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifthird_party/blink/renderer/modules/mediarecorder/media_recorder_handler.cc |
modified | |
MODULES_EXPORTthird_party/blink/renderer/modules/mediarecorder/media_recorder_handler.h |
modified | |
MediaStreamObserverthird_party/blink/renderer/modules/mediarecorder/media_recorder_handler.h |
modified | |
MockWebMediaStreamObserverthird_party/blink/renderer/modules/mediastream/media_stream_track_impl_test.cc |
modified |
Files Changed
third_party/blink/public/platform/modules/mediastream/web_media_stream.hthird_party/blink/renderer/modules/mediarecorder/media_recorder_handler.ccthird_party/blink/renderer/modules/mediarecorder/media_recorder_handler.hthird_party/blink/renderer/modules/mediastream/media_stream_track_impl_test.ccthird_party/blink/renderer/modules/mediastream/web_media_player_ms.cc
Audit Directions
- PatternLook for garbage-collected (Oilpan) Blink objects registered into raw-pointer observer/listener lists owned by non-GC’d or differently-scoped objects, where lifetime is not enforced by the container.
- PatternAudit
AddObserver/RemoveObserverpairs whose deregistration depends on a destructor or teardown path that may not run before the object is reclaimed, especially across the GC/refcount boundary. - PatternSearch for
WebMediaStreamObserverand other cross-module callback interfaces implemented directly by GC’d handlers and convert them to weakly-referenced composition shims.
Patch
From 4e6f77f4984209726c6d33475bc4afb13f0e2955 Mon Sep 17 00:00:00 2001 From: Guido Urdaneta <[email protected]> Date: Thu, 04 Sep 2025 02:43:02 -0700 Subject: [PATCH] [MediaStream] Use weak ptrs to track observers in MediaStreamDescriptor Prior to this CL, we were using raw pointers. This was problematic for several reasons, including that one of the observer classes is GCed. The GCed case (MediaRecorderHandler) is handled by using composition instead of inheriting directly from the WebMediaStreamObserver interface. Bug: 440737137 Change-Id: Ibebb8145b3d190b39f04424f4e14cb9020855038 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6912084 Reviewed-by: Tony Herre <[email protected]> Commit-Queue: Guido Urdaneta <[email protected]> Cr-Commit-Position: refs/heads/main@{#1510795} --- diff --git a/third_party/blink/public/platform/modules/mediastream/web_media_stream.h b/third_party/blink/public/platform/modules/mediastream/web_media_stream.h index 72f417ee2..c65116e 100644 --- a/third_party/blink/public/platform/modules/mediastream/web_media_stream.h +++ b/third_party/blink/public/platform/modules/mediastream/web_media_stream.h @@ -25,6 +25,7 @@ #ifndef THIRD_PARTY_BLINK_PUBLIC_PLATFORM_MODULES_MEDIASTREAM_WEB_MEDIA_STREAM_H_ #define THIRD_PARTY_BLINK_PUBLIC_PLATFORM_MODULES_MEDIASTREAM_WEB_MEDIA_STREAM_H_ +#include "base/memory/weak_ptr.h" #include "third_party/blink/public/platform/web_common.h" #include "third_party/blink/public/platform/web_private_ptr.h" @@ -84,8 +85,8 @@ // the WebMediaStream. Observers cannot be null, cannot be added or removed // more than once, and cannot invoke AddObserver/RemoveObserver in their // TrackAdded/TrackRemoved callbacks. - void AddObserver(WebMediaStreamObserver*); - void RemoveObserver(WebMediaStreamObserver*); + void AddObserver(base::WeakPtr<WebMediaStreamObserver>); + void RemoveObserver(base::WeakPtr<WebMediaStreamObserver>); #if INSIDE_BLINK explicit WebMediaStream(MediaStreamDescriptor*); diff --git a/third_party/blink/renderer/modules/mediarecorder/media_recorder_handler.cc b/third_party/blink/renderer/modules/mediarecorder/media_recorder_handler.cc index 92359ac..a212a0e 100644 --- a/third_party/blink/renderer/modules/mediarecorder/media_recorder_handler.cc +++ b/third_party/blink/renderer/modules/mediarecorder/media_recorder_handler.cc @@ -281,7 +281,8 @@ scoped_refptr<base::SingleThreadTaskRunner> main_thread_task_runner, KeyFrameRequestProcessor::Configuration key_frame_config) : key_frame_config_(key_frame_config), - main_thread_task_runner_(std::move(main_thread_task_runner)) {} + main_thread_task_runner_(std::move(main_thread_task_runner)), + media_stream_observer_(std::make_unique<MediaStreamObserver>(this)) {} bool MediaRecorderHandler::CanSupportMimeType(const String& type, const String& web_codecs) { @@ -560,7 +561,7 @@ DCHECK(!muxer_adapter_); DCHECK(!is_media_stream_observer_); - media_stream_->AddObserver(this); + media_stream_->AddObserver(media_stream_observer_->AsWeakPtr()); is_media_stream_observer_ = true; timeslice_ = timeslice == std::numeric_limits<int>::max() @@ -715,7 +716,7 @@ // Unregister from media stream notifications. if (media_stream_ && is_media_stream_observer_) { - media_stream_->RemoveObserver(this); + media_stream_->RemoveObserver(media_stream_observer_->AsWeakPtr()); } is_media_stream_observer_ = false; diff --git a/third_party/blink/renderer/modules/mediarecorder/media_recorder_handler.h b/third_party/blink/renderer/modules/mediarecorder/media_recorder_handler.h index c221640b..30fb95e 100644 --- a/third_party/blink/renderer/modules/mediarecorder/media_recorder_handler.h +++ b/third_party/blink/renderer/modules/mediarecorder/media_recorder_handler.h @@ -10,6 +10,7 @@ #include <string_view> #include "base/feature_list.h" +#include "base/memory/weak_ptr.h" #include "base/task/single_thread_task_runner.h" #include "base/threading/thread_checker.h" #include "base/time/time.h" @@ -22,6 +23,7 @@ #include "third_party/blink/renderer/modules/mediarecorder/video_track_recorder.h" #include "third_party/blink/renderer/modules/modules_export.h" #include "third_party/blink/renderer/platform/heap/collection_support/heap_vector.h" +#include "third_party/blink/renderer/platform/heap/persistent.h" #include "third_party/blink/renderer/platform/heap/weak_cell.h" #include "third_party/blink/renderer/platform/wtf/allocator/allocator.h" #include "third_party/blink/renderer/platform/wtf/text/wtf_string.h" @@ -67,8 +69,7 @@ class MODULES_EXPORT MediaRecorderHandler final : public GarbageCollected<MediaRecorderHandler>, public VideoTrackRecorder::CallbackInterface, - public AudioTrackRecorder::CallbackInterface, - public WebMediaStreamObserver { + public AudioTrackRecorder::CallbackInterface { public: MediaRecorderHandler( scoped_refptr<base::SingleThreadTaskRunner> main_thread_task_runner, @@ -118,10 +119,34 @@ private: friend class MediaRecorderHandlerFixture; friend class MediaRecorderHandlerPassthroughTest; + friend class MediaStreamObserver; - // WebMediaStreamObserver overrides. - void TrackAdded(const WebString& track_id) override; - void TrackRemoved(const WebString& track_id) override; + class MediaStreamObserver : public WebMediaStreamObserver { + public: + explicit MediaStreamObserver(MediaRecorderHandler* handler) + : media_recorder_handler_(handler) {} + + // WebMediaStreamObserver overrides. + void TrackAdded(const WebString& track_id) override { + CHECK(media_recorder_handler_); + media_recorder_handler_->TrackAdded(track_id); + } + void TrackRemoved(const WebString& track_id) override { + CHECK(media_recorder_handler_); + media_recorder_handler_->TrackRemoved(track_id); + } + + base::WeakPtr<WebMediaStreamObserver> AsWeakPtr() { + return weak_factory_.GetWeakPtr(); + } + + private: + WeakPersistent<MediaRecorderHandler> media_recorder_handler_; + base::WeakPtrFactory<WebMediaStreamObserver> weak_factory_{this}; + }; + + void TrackAdded(const WebString& track_id); + void TrackRemoved(const WebString& track_id); // VideoTrackRecorder::CallbackInterface overrides. void OnEncodedVideo( @@ -242,6 +267,8 @@ bool has_codec_description_changed_error_printed_ = false; #endif + std::unique_ptr<MediaStreamObserver> media_stream_observer_; + // For invalidation of in-flight callbacks back to ourselves. Need to track // each callback interface specifically as there seem to be no automatic // coercion. 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 aa2e4a02..65a52d8 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 @@ -7,6 +7,7 @@ #include <tuple> #include "base/feature_list.h" +#include "base/memory/weak_ptr.h" #include "base/run_loop.h" #include "base/test/gmock_callback_support.h" #include "media/base/video_frame.h" @@ -91,6 +92,12 @@ class MockWebMediaStreamObserver : public WebMediaStreamObserver { public: MOCK_METHOD(void, EnabledStateChangedForWebRtcAudio, (bool)); + base::WeakPtr<WebMediaStreamObserver> AsWeakPtr() { + return weak_ptr_factory_.GetWeakPtr(); + } + + private: + base::WeakPtrFactory<MockWebMediaStreamObserver> weak_ptr_factory_{this}; }; MediaStreamComponent* MakeMockVideoComponent() { @@ -344,7 +351,7 @@ MediaStream::Create(v8_scope.GetExecutionContext(), audio_tracks); auto* descriptor = media_stream->Descriptor(); descriptor->SetActive(true); - descriptor->AddObserver(&observer); + descriptor->AddObserver(observer.AsWeakPtr()); // let video = document.createElement('video'); // video.srcObject = media_stream; @@ -379,7 +386,7 @@ MediaStream::Create(v8_scope.GetExecutionContext(), audio_tracks); auto* descriptor = media_stream->Descriptor(); descriptor->SetActive(true); - descriptor->AddObserver(&observer); + descriptor->AddObserver(observer.AsWeakPtr()); // let video = document.createElement('video'); // video.srcObject = media_stream; diff --git a/third_party/blink/renderer/modules/mediastream/web_media_player_ms.cc b/third_party/blink/renderer/modules/mediastream/web_media_player_ms.cc index 870b673..9ebf604 100644 --- a/third_party/blink/renderer/modules/mediastream/web_media_player_ms.cc +++ b/third_party/blink/renderer/modules/mediastream/web_media_player_ms.cc @@ -408,7 +408,7 @@ String::Format("%s() [delegate_id=%d]", __func__, delegate_id_));
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 aa2e4a02..65a52d8 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
@@ -7,6 +7,7 @@
#include <tuple>
#include "base/feature_list.h"
+#include "base/memory/weak_ptr.h"
#include "base/run_loop.h"
#include "base/test/gmock_callback_support.h"
#include "media/base/video_frame.h"
@@ -91,6 +92,12 @@
class MockWebMediaStreamObserver : public WebMediaStreamObserver {
public:
MOCK_METHOD(void, EnabledStateChangedForWebRtcAudio, (bool));
+ base::WeakPtr<WebMediaStreamObserver> AsWeakPtr() {
+ return weak_ptr_factory_.GetWeakPtr();
+ }
+
+ private:
+ base::WeakPtrFactory<MockWebMediaStreamObserver> weak_ptr_factory_{this};
};
MediaStreamComponent* MakeMockVideoComponent() {
@@ -344,7 +351,7 @@
MediaStream::Create(v8_scope.GetExecutionContext(), audio_tracks);
auto* descriptor = media_stream->Descriptor();
descriptor->SetActive(true);
- descriptor->AddObserver(&observer);
+ descriptor->AddObserver(observer.AsWeakPtr());
// let video = document.createElement('video');
// video.srcObject = media_stream;
@@ -379,7 +386,7 @@
MediaStream::Create(v8_scope.GetExecutionContext(), audio_tracks);
auto* descriptor = media_stream->Descriptor();
descriptor->SetActive(true);
- descriptor->AddObserver(&observer);
+ descriptor->AddObserver(observer.AsWeakPtr());
// let video = document.createElement('video');
// video.srcObject = media_stream;
Original Bug Report
Use-After-Free in MediaStreamDescriptor
Steps to reproduce the problem
- chrome.exe –js-flags="–stress-incremental-marking –expose-gc" –no-sandbox
Problem Description
void MediaStreamDescriptor::AddComponent(MediaStreamComponent* component) {
'''
'''
// Iterate over a copy of |observers_| to avoid re-entrancy issues.
Vector<WebMediaStreamObserver*> observers = observers_;
for (auto*& observer : observers)
observer->TrackAdded(WebString(component->Id()));
}
A vulnerability occurs when the garbage collector runs while maintaining GC objects on the stack using a Vector.\
SpaceSplitString::Data* SpaceSplitString::Data::Create(
const AtomicString& string) {
auto result = SharedDataMap().insert(string, nullptr);
SpaceSplitString::Data* data = result.stored_value->value;
if (result.is_new_entry) {
data = MakeGarbageCollected<SpaceSplitString::Data>(string);
result.stored_value->value = data;
}
return data;
}
```\
As shown in the asan.log file, the MakeGarbageCollected function can be called from the SpaceSplitString function and several other functions within the TrackAdded function.\
```c++
void CppHeap::ReportBufferedAllocationSizeIfPossible() {
'''
'''
used_size_.fetch_add(static_cast<size_t>(bytes_to_report),
std::memory_order_relaxed);
allocated_size_ += bytes_to_report;
if (v8_flags.incremental_marking) {
if (allocated_size_ > allocated_size_limit_for_check_) {
Heap* heap = isolate_->heap();
heap->StartIncrementalMarkingIfAllocationLimitIsReached(
heap->main_thread_local_heap(),
heap->GCFlagsForIncrementalMarking(),
kGCCallbackScheduleIdleGarbageCollection);
if (heap->incremental_marking()->IsMajorMarking()) {
if (heap->AllocationLimitOvershotByLargeMargin()) {
heap->FinalizeIncrementalMarkingAtomically(
i::GarbageCollectionReason::kExternalFinalize);
} else {
heap->incremental_marking()->AdvanceOnAllocation();
}
}
allocated_size_limit_for_check_ =
allocated_size_ + kIncrementalMarkingCheckInterval;
}
}
}
}
```\
If there is a lot of memory allocated so far inside the MakeGarbageCollected function, you can call FinalizeIncrementalMarkingAtomically through the ReportBufferedAllocationSizeIfPossible function to execute Full GarbageCollect.\
The probability of the conditions for executing the above GarbageCollect is very low and can change a lot depending on the version, environment, etc., so the probability can be adjusted by taking actions such as setting the ArrayBuffer allocation size and loop count to different values.
# Summary
Use-After-Free in MediaStreamDescriptor
# Custom Questions
#### Type of crash:
Renderer
#### Crash state:
Crash referencing vftable
#### Reporter credit:
sherkito
# Additional Data
Category: Security \
Chrome Channel: Not sure \
Regression: N/A \