High patch-now ✔︎ verified CVSS 8.8 chrome UAF 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in WebRTC
DescriptionUse after free in WebRTC
ComponentWebRTC
Sub-componentMediaStream
Bug ClassUAF
Action tierpatch-now — corruption primitive, web-reachable in default config (confirmed) — upheld by independent review by AI
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Independent review by AIConfirmed
Reviewer notes: none substantive -- diff and commit message confirm raw-pointer-to-base::WeakPtr observers plus a non-GC'd shim for the GCed MediaRecorderHandler; the precise UAF trigger is inferred but consistent with the diff.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker440737137
Fix commit4e6f77f49842 (chromium/src) +112/-42
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
Creditedsherkito
Reported2025-08-23 · bounty $10000 (per vendor advisory)
Fix landed2025-09-04 (public git)
Disclosed2025-09-17
Patch gap13 days — fix public in source before the advisory

Background

`WebMediaStreamObserver`
an interface whose implementers register with a MediaStreamDescriptor to receive TrackAdded/TrackRemoved notifications about a media stream.
`MediaRecorderHandler`
the Blink object backing MediaRecorder; it is GarbageCollected, and prior to the patch it inherited directly from WebMediaStreamObserver and registered itself as an observer.
Oilpan / GC vs. reference-counted lifetimes
MediaRecorderHandler lives on Blink’s garbage-collected (Oilpan) heap, while the MediaStreamDescriptor observer 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.

Key insight
The single mistake was registering a garbage-collected object into a raw-pointer observer list, mixing two incompatible lifetime models; the fix decouples them by inserting a weakly-referenced, non-GC’d shim so the descriptor holds a base::WeakPtr that safely nulls out when the observer dies.

Attack Path

  1. Start recording Web content constructs a MediaRecorder over a MediaStream and calls start(), causing MediaRecorderHandler to register itself on the underlying MediaStreamDescriptor via AddObserver(this).
  2. Free the handler while still registered Through page manipulation that drops references to the recorder, the garbage collector reclaims the MediaRecorderHandler while the descriptor still holds the raw observer pointer (the deregistration path via RemoveObserver(this) does not reliably run first). (inferred)
  3. Trigger a track notification The script mutates the stream (adding/removing a track), so the descriptor iterates its observer list and dispatches TrackAdded/TrackRemoved to the stale pointer. (inferred)
  4. Use-after-free The descriptor dereferences the freed handler, reading and calling into reclaimed memory. (inferred)

Impact Assessment

An attacker controlling a web page in the renderer can drive a use-after-free of a 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

FunctionChangeNotes
if
third_party/blink/renderer/modules/mediarecorder/media_recorder_handler.cc
modified
MODULES_EXPORT
third_party/blink/renderer/modules/mediarecorder/media_recorder_handler.h
modified
MediaStreamObserver
third_party/blink/renderer/modules/mediarecorder/media_recorder_handler.h
modified
MockWebMediaStreamObserver
third_party/blink/renderer/modules/mediastream/media_stream_track_impl_test.cc
modified

Files Changed

  • third_party/blink/public/platform/modules/mediastream/web_media_stream.h
  • third_party/blink/renderer/modules/mediarecorder/media_recorder_handler.cc
  • third_party/blink/renderer/modules/mediarecorder/media_recorder_handler.h
  • third_party/blink/renderer/modules/mediastream/media_stream_track_impl_test.cc
  • third_party/blink/renderer/modules/mediastream/web_media_player_ms.cc

Audit Directions

  • Pattern
    Look 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.
  • Pattern
    Audit AddObserver/RemoveObserver pairs whose deregistration depends on a destructor or teardown path that may not run before the object is reclaimed, especially across the GC/refcount boundary.
  • Pattern
    Search for WebMediaStreamObserver and other cross-module callback interfaces implemented directly by GC’d handlers and convert them to weakly-referenced composition shims.
From 4e6f77f4984209726c6d33475bc4afb13f0e2955 Mon Sep 17 00:00:00 2001
From: Guido Urdaneta <guidou@chromium.org>
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 <toprice@chromium.org>
Commit-Queue: Guido Urdaneta <guidou@chromium.org>
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_));
 
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 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;
Loading diff…

Original Bug Report

reported by ss...@gmail.com

Use-After-Free in MediaStreamDescriptor

Steps to reproduce the problem

  1. 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 \
View on issue tracker