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-componentWebRTC
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: Mostly restates commit message. Overclaims fix 'always' runs disposal on main thread: in the trigger scenario the main runner is shut down, so the new PostCrossThreadTask can also fail. Race-dependent; DCHECK absent in release builds.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker523296105
Fix commit5643da4a3d7e (chromium/src) +103/-29
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
CreditedGoogle
Reported2026-06-12 (per vendor advisory)
Fix landed2026-07-03 (public git)
Disclosed2026-08-25
Patch gap53 days — fix public in source before the advisory

Background

`WebRtcMediaStreamTrackAdapter`
a Blink object bridging a MediaStreamComponent/blink track and a webrtc::MediaStreamTrackInterface, whose disposal touches main-thread-affine objects such as MediaStreamVideoTrack.
`WebRtcMediaStreamTrackAdapterMap::AdapterRef`
a reference handle into the adapter map; when the last AdapterRef is destroyed the adapter is removed from the map and Dispose()d.
Main thread vs. WebRTC signaling thread
adapter map mutation and track disposal (e.g. MediaStreamVideoTrack::RemoveSink) are main-thread-affine, while replaceTrack and related peerconnection work run cross-thread on the signaling thread.
`BelongsToCurrentThread()` / `PostCrossThreadTask`
the check used to detect the running thread and the mechanism used to hop work back to the main thread.

Root Cause Analysis

AdapterRef::~AdapterRef() asserted DCHECK(map_->main_thread_->BelongsToCurrentThread()) and then, inline, removed the adapter from the map and let the last reference trigger WebRtcMediaStreamTrackAdapter::Dispose() — all on whatever thread happened to run the destructor. When an iframe hosting an RTCPeerConnection was detached while replaceTrack was communicating with the signaling thread, the main-thread task runner could be shut down, so cross-thread tasks posted back failed and the closures holding AdapterRef objects were instead destroyed on the signaling thread. That ran map removal and Dispose() — including main-thread-affine disposal paths such as MediaStreamVideoTrack::RemoveSink — on the signaling thread without synchronization, producing data races and a use-after-free on objects that assume single-threaded main-thread access.

The fix makes both AdapterRef::~AdapterRef() and WebRtcMediaStreamTrackAdapter::Dispose() check BelongsToCurrentThread() and, when not on the main thread, PostCrossThreadTask the work (DisposeAdapterRef / DisposeOnMainThread) to the main thread, so map mutation and track disposal always execute on the main thread as their affinity requires.

Key insight
The core mistake was performing thread-affine map removal and track disposal directly on whichever thread destroyed the last AdapterRef — which, on iframe detach during replaceTrack, was the signaling thread; the fix re-checks the current thread and posts disposal to the main thread instead of asserting/assuming it.

Attack Path

  1. Set up a peerconnection with a track A page creates an RTCPeerConnection (in an iframe) and obtains a WebRtcMediaStreamTrackAdapter/AdapterRef for a media track.
  2. Race replaceTrack against detach The page calls replaceTrack (driving signaling-thread work holding AdapterRef closures) and concurrently detaches the iframe, shutting down the main-thread task runner.
  3. Adapter destroyed on the signaling thread With main-thread posts failing, the closures holding the last AdapterRef are deconstructed on the signaling thread; ~AdapterRef() performs map removal and invokes Dispose() there.
  4. Cross-thread disposal races main-thread state Main-thread-affine disposal (e.g. MediaStreamVideoTrack::RemoveSink) runs unsynchronized on the signaling thread, racing main-thread access to the same objects and yielding a use-after-free.

Impact Assessment

An attacker web page that scripts an RTCPeerConnection in an iframe and races replaceTrack against iframe detachment can cause adapter/track disposal to run on the WebRTC signaling thread instead of the main thread, producing data races and a use-after-free of media-track objects in the renderer process. The advisory classifies this as a use-after-free in WebRTC and does not state in-the-wild exploitation. Any escalation from the resulting UAF/data race to a controlled read/write or code execution is not shown by the patch and is (inferred).

Changed Functions

FunctionChangeNotes
if
third_party/blink/renderer/modules/peerconnection/webrtc_media_stream_track_adapter_map.cc
modified
MODULES_EXPORT
third_party/blink/renderer/modules/peerconnection/webrtc_media_stream_track_adapter_map.h
modified
TEST_F
third_party/blink/renderer/modules/peerconnection/webrtc_media_stream_track_adapter_test.cc
modified

Files Changed

  • third_party/blink/renderer/modules/peerconnection/webrtc_media_stream_track_adapter_map.h
  • third_party/blink/renderer/modules/peerconnection/webrtc_media_stream_track_adapter_test.cc
  • third_party/blink/renderer/modules/peerconnection/webrtc_media_stream_track_adapter_map.cc
  • third_party/blink/renderer/modules/peerconnection/webrtc_media_stream_track_adapter.h
  • third_party/blink/renderer/modules/peerconnection/webrtc_media_stream_track_adapter_map_test.cc
  • third_party/blink/renderer/modules/peerconnection/webrtc_media_stream_track_adapter.cc

Audit Directions

  • Destructors with thread affinity
    Audit ref-counted handles whose destructors mutate thread-affine state or call disposal; a destructor can run on any thread that drops the last reference, so it must re-check and post rather than DCHECK the thread.
  • Task-runner shutdown during teardown
    Review cross-thread flows (e.g. replaceTrack, peerconnection signaling) where main-thread task runners can shut down on frame/iframe detach, causing posted-back closures to be destroyed on the wrong thread.
  • Comment-encoded threading contracts
    Find APIs documented as “must be invoked/destroyed on the main thread” and verify the invariant is enforced by runtime posting, not merely by comments or debug-only DCHECKs.
From 5643da4a3d7eca3d0b9b523647c7e14e37f294c4 Mon Sep 17 00:00:00 2001
From: Guido Urdaneta <guidou@chromium.org>
Date: Fri, 03 Jul 2026 08:10:23 -0700
Subject: [PATCH] [WebRTC] Ensure WebRtcMediaStreamTrackAdapter is disposed on main thread

If an iframe hosting an RTCPeerConnection is detached while
replaceTrack is communicating with the WebRTC signaling thread,
cross-thread tasks posted back to the main thread can fail because the
main thread task runner is shut down. When this happens, closures
holding AdapterRef objects are deconstructed on the signaling thread.

Previously, AdapterRef::~AdapterRef() would remove the adapter from the
map and execute removed_adapter->Dispose() directly on whatever thread
invoked ~AdapterRef(). When run on the signaling thread, this triggered
main-thread disposal paths (such as MediaStreamVideoTrack::RemoveSink)
on the signaling thread without synchronization, leading to data races
and Use-After-Free.

This CL updates AdapterRef::~AdapterRef() and
WebRtcMediaStreamTrackAdapter::Dispose() to check whether execution is
occurring on the main thread. If invoked on a non-main thread, map
removal and track disposal are posted to the main thread.

Bug: 523296105
Change-Id: I85f5403e14dbdc7e49b5060bc098014878d19297
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8028963
Commit-Queue: Guido Urdaneta <guidou@chromium.org>
Reviewed-by: Tove Petersson <tovep@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1656585}
---
diff --git a/third_party/blink/renderer/modules/peerconnection/webrtc_media_stream_track_adapter.cc b/third_party/blink/renderer/modules/peerconnection/webrtc_media_stream_track_adapter.cc
index 07f246fe..9e64f4f 100644
--- a/third_party/blink/renderer/modules/peerconnection/webrtc_media_stream_track_adapter.cc
+++ b/third_party/blink/renderer/modules/peerconnection/webrtc_media_stream_track_adapter.cc
@@ -110,6 +110,17 @@
 }
 
 void WebRtcMediaStreamTrackAdapter::Dispose() {
+  if (!main_thread_->BelongsToCurrentThread()) {
+    PostCrossThreadTask(
+        *main_thread_, FROM_HERE,
+        CrossThreadBindOnce(&WebRtcMediaStreamTrackAdapter::DisposeOnMainThread,
+                            WrapRefCounted(this)));
+    return;
+  }
+  DisposeOnMainThread();
+}
+
+void WebRtcMediaStreamTrackAdapter::DisposeOnMainThread() {
   DCHECK(main_thread_->BelongsToCurrentThread());
   DCHECK(is_initialized_);
   if (is_disposed_)
diff --git a/third_party/blink/renderer/modules/peerconnection/webrtc_media_stream_track_adapter.h b/third_party/blink/renderer/modules/peerconnection/webrtc_media_stream_track_adapter.h
index 844dfae8..bc19bf7 100644
--- a/third_party/blink/renderer/modules/peerconnection/webrtc_media_stream_track_adapter.h
+++ b/third_party/blink/renderer/modules/peerconnection/webrtc_media_stream_track_adapter.h
@@ -56,7 +56,7 @@
       const WebRtcMediaStreamTrackAdapter&) = delete;
 
   // Must be called before all external references are released (i.e. before
-  // destruction). Invoke on the main thread. Disposing may finish
+  // destruction). Can be called from any thread. Disposing may finish
   // asynchronously using the webrtc signaling thread and the main thread. After
   // calling this method it is safe to release all external references to the
   // adapter.
@@ -110,9 +110,11 @@
   void FinalizeRemoteTrackInitializationOnMainThread();
   void EnsureTrackIsInitialized();
 
-  // Disposing starts and finishes on the main thread. Local tracks and remote
-  // video tracks are disposed synchronously. Remote audio tracks are disposed
-  // asynchronously with a jump to the webrtc signaling thread and back.
+  // The disposal logic starts and finishes on the main thread, although it can
+  // be initiated from any thread. Local tracks and remote video tracks are
+  // disposed synchronously. Remote audio tracks are disposed asynchronously
+  // with a jump to the webrtc signaling thread and back.
+  void DisposeOnMainThread();
   void DisposeLocalAudioTrack();
   void DisposeLocalVideoTrack();
   void DisposeRemoteAudioTrack();
diff --git a/third_party/blink/renderer/modules/peerconnection/webrtc_media_stream_track_adapter_map.cc b/third_party/blink/renderer/modules/peerconnection/webrtc_media_stream_track_adapter_map.cc
index 2cf70f5..1e4759f 100644
--- a/third_party/blink/renderer/modules/peerconnection/webrtc_media_stream_track_adapter_map.cc
+++ b/third_party/blink/renderer/modules/peerconnection/webrtc_media_stream_track_adapter_map.cc
@@ -25,29 +25,47 @@
 }
 
 WebRtcMediaStreamTrackAdapterMap::AdapterRef::~AdapterRef() {
-  DCHECK(map_->main_thread_->BelongsToCurrentThread());
+  if (!map_->main_thread_->BelongsToCurrentThread()) {
+    scoped_refptr<base::SingleThreadTaskRunner> main_thread =
+        map_->main_thread_;
+    PostCrossThreadTask(
+        *main_thread, FROM_HERE,
+        CrossThreadBindOnce(
+            &WebRtcMediaStreamTrackAdapterMap::DisposeAdapterRef,
+            std::move(map_), type_, std::move(adapter_)));
+    return;
+  }
+  DisposeAdapterRef(std::move(map_), type_, std::move(adapter_));
+}
+
+// static
+void WebRtcMediaStreamTrackAdapterMap::DisposeAdapterRef(
+    scoped_refptr<WebRtcMediaStreamTrackAdapterMap> map,
+    AdapterRef::Type type,
+    scoped_refptr<blink::WebRtcMediaStreamTrackAdapter> adapter) {
+  DCHECK(map->main_thread_->BelongsToCurrentThread());
   scoped_refptr<blink::WebRtcMediaStreamTrackAdapter> removed_adapter;
   {
-    base::AutoLock scoped_lock(map_->lock_);
-    // The adapter is stored in the track adapter map and we have |adapter_|,
+    base::AutoLock scoped_lock(map->lock_);
+    // The adapter is stored in the track adapter map and we have |adapter|,
     // so there must be at least two references to the adapter.
-    DCHECK(!adapter_->HasOneRef());
-    // Using a raw pointer instead of |adapter_| allows the reference count to
+    DCHECK(!adapter->HasOneRef());
+    // Using a raw pointer instead of |adapter| allows the reference count to
     // go down to one if this is the last |AdapterRef|.
-    blink::WebRtcMediaStreamTrackAdapter* adapter = adapter_.get();
-    adapter_ = nullptr;
-    if (adapter->HasOneRef()) {
-      removed_adapter = adapter;
+    blink::WebRtcMediaStreamTrackAdapter* adapter_ptr = adapter.get();
+    adapter = nullptr;
+    if (adapter_ptr->HasOneRef()) {
+      removed_adapter = adapter_ptr;
       // "GetOrCreate..." ensures the adapter is initialized and the secondary
       // key is set before the last |AdapterRef| is destroyed. We can use either
       // the primary or secondary key for removal.
-      DCHECK(adapter->is_initialized());
-      if (type_ == Type::kLocal) {
-        map_->local_track_adapters_.EraseByPrimary(
-            adapter->track()->UniqueId());
+      DCHECK(adapter_ptr->is_initialized());
+      if (type == AdapterRef::Type::kLocal) {
+        map->local_track_adapters_.EraseByPrimary(
+            adapter_ptr->track()->UniqueId());
       } else {
-        map_->remote_track_adapters_.EraseByPrimary(
-            adapter->webrtc_track().get());
+        map->remote_track_adapters_.EraseByPrimary(
+            adapter_ptr->webrtc_track().get());
       }
     }
   }
diff --git a/third_party/blink/renderer/modules/peerconnection/webrtc_media_stream_track_adapter_map.h b/third_party/blink/renderer/modules/peerconnection/webrtc_media_stream_track_adapter_map.h
index 1c7ccbc5..09f06fe 100644
--- a/third_party/blink/renderer/modules/peerconnection/webrtc_media_stream_track_adapter_map.h
+++ b/third_party/blink/renderer/modules/peerconnection/webrtc_media_stream_track_adapter_map.h
@@ -36,8 +36,9 @@
   // is |Dispose|d and removed from the map.
   class MODULES_EXPORT AdapterRef {
    public:
-    // Must be invoked on the main thread. If this was the last reference to the
-    // adapter it will be disposed and removed from the map.
+    // Usually invoked on the main thread. If invoked on another thread,
+    // disposal is posted to the main thread. When the last reference to the
+    // adapter is destroyed, it is disposed and removed from the map.
     ~AdapterRef();
 
     std::unique_ptr<AdapterRef> Copy() const;
@@ -80,9 +81,8 @@
 
   // Gets a new reference to the local track adapter, or null if no such adapter
   // was found. When all references are destroyed the adapter is disposed and
-  // removed from the map. This method can be called from any thread, but
-  // references must be destroyed on the main thread.
-  // The adapter is a associated with a blink and webrtc track, lookup works by
+  // removed from the map. This method can be called from any thread.
+  // The adapter is associated with a blink and webrtc track, lookup works by
   // either track.
   std::unique_ptr<AdapterRef> GetLocalTrackAdapter(
       MediaStreamComponent* component);
@@ -91,16 +91,15 @@
   // Invoke on the main thread. Gets a new reference to the local track adapter
   // for the web track. If no adapter exists for the track one is created and
   // initialized. When all references are destroyed the adapter is disposed and
-  // removed from the map. References must be destroyed on the main thread.
+  // removed from the map.
   std::unique_ptr<AdapterRef> GetOrCreateLocalTrackAdapter(
       MediaStreamComponent* component);
   size_t GetLocalTrackCount() const;
 
   // Gets a new reference to the remote track adapter. When all references are
   // destroyed the adapter is disposed and removed from the map. This method can
-  // be called from any thread, but references must be destroyed on the main
-  // thread. The adapter is a associated with a blink and webrtc track, lookup
-  // works by either track.
+  // be called from any thread. The adapter is a associated with a blink and
+  // webrtc track, lookup works by either track.
   // First variety: If an adapter exists it will already be initialized, if one
   // does not exist null is returned.
   std::unique_ptr<AdapterRef> GetRemoteTrackAdapter(
@@ -114,12 +113,17 @@
   // track adapter for the webrtc track. If no adapter exists for the track one
   // is created and initialization completes on the main thread in a post. When
   // all references are destroyed the adapter is disposed and removed from the
-  // map. References must be destroyed on the main thread.
+  // map.
   std::unique_ptr<AdapterRef> GetOrCreateRemoteTrackAdapter(
       scoped_refptr<webrtc::MediaStreamTrackInterface> webrtc_track);
   size_t GetRemoteTrackCount() const;
 
  private:
+  static void DisposeAdapterRef(
+      scoped_refptr<WebRtcMediaStreamTrackAdapterMap> map,
+      AdapterRef::Type type,
+      scoped_refptr<blink::WebRtcMediaStreamTrackAdapter> adapter);
+
   friend class ThreadSafeRefCounted<WebRtcMediaStreamTrackAdapterMap>;
 
   // "(MediaStreamComponent, webrtc::MediaStreamTrackInterface) ->
diff --git a/third_party/blink/renderer/modules/peerconnection/webrtc_media_stream_track_adapter_map_test.cc b/third_party/blink/renderer/modules/peerconnection/webrtc_media_stream_track_adapter_map_test.cc
index 2337ae1..68d88683 100644
--- a/third_party/blink/renderer/modules/peerconnection/webrtc_media_stream_track_adapter_map_test.cc
+++ b/third_party/blink/renderer/modules/peerconnection/webrtc_media_stream_track_adapter_map_test.cc
@@ -255,6 +255,29 @@
   EXPECT_EQ(nullptr, map_->GetRemoteTrackAdapter(webrtc_track.get()));
 }
 
+TEST_F(WebRtcMediaStreamTrackAdapterMapTest,
+       DestroyAdapterRefOnSignalingThread) {
+  MediaStreamComponent* track = CreateLocalTrack("local_track");
+  std::unique_ptr<blink::WebRtcMediaStreamTrackAdapterMap::AdapterRef>
+      adapter_ref = map_->GetOrCreateLocalTrackAdapter(track);
+  EXPECT_TRUE(adapter_ref->is_initialized());
+  EXPECT_EQ(1u, map_->GetLocalTrackCount());
+
+  // Destroying the AdapterRef on the signaling thread should post the removal
+  // and disposal to the main thread.
+  PostCrossThreadTask(
+      *signaling_thread(), FROM_HERE,
+      CrossThreadBindOnce(
+          [](std::unique_ptr<
+              blink::WebRtcMediaStreamTrackAdapterMap::AdapterRef> ref) {
+            // Destroyed when going out of scope on the signaling thread.
+          },
+          std::move(adapter_ref)));
+  RunMessageLoopsUntilIdle();
+  EXPECT_EQ(0u, map_->GetLocalTrackCount());
+  EXPECT_EQ(nullptr, map_->GetLocalTrackAdapter(track));
+}
+
 // Continuously calls GetOrCreateLocalTrackAdapter() on the main thread and
 // GetOrCreateRemoteTrackAdapter() on the signaling thread hoping to hit
 // deadlocks if the operations were to synchronize with the other thread while
diff --git a/third_party/blink/renderer/modules/peerconnection/webrtc_media_stream_track_adapter_test.cc b/third_party/blink/renderer/modules/peerconnection/webrtc_media_stream_track_adapter_test.cc
index 84fe299..dda0fc5 100644
--- a/third_party/blink/renderer/modules/peerconnection/webrtc_media_stream_track_adapter_test.cc
+++ b/third_party/blink/renderer/modules/peerconnection/webrtc_media_stream_track_adapter_test.cc
@@ -272,4 +272,20 @@
   RunMessageLoopsUntilIdle();
 }
 
+TEST_F(WebRtcMediaStreamTrackAdapterTest, DisposeOnSignalingThread) {
+  track_adapter_ =
+      blink::WebRtcMediaStreamTrackAdapter::CreateLocalTrackAdapter(
+          dependency_factory_.Get(), main_thread_, CreateLocalAudioTrack());
+  EXPECT_TRUE(track_adapter_->is_initialized());
+
+  dependency_factory_->GetWebRtcSignalingTaskRunner()->PostTask(
+      FROM_HERE,
+      base::BindOnce(
+          [](scoped_refptr<blink::WebRtcMediaStreamTrackAdapter> adapter) {
+            adapter->Dispose();
+          },
+          track_adapter_));
+  RunMessageLoopsUntilIdle();
+}
+
 }  // namespace blink
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/third_party/blink/renderer/modules/peerconnection/webrtc_media_stream_track_adapter_map_test.cc b/third_party/blink/renderer/modules/peerconnection/webrtc_media_stream_track_adapter_map_test.cc
index 2337ae1..68d88683 100644
--- a/third_party/blink/renderer/modules/peerconnection/webrtc_media_stream_track_adapter_map_test.cc
+++ b/third_party/blink/renderer/modules/peerconnection/webrtc_media_stream_track_adapter_map_test.cc
@@ -255,6 +255,29 @@
   EXPECT_EQ(nullptr, map_->GetRemoteTrackAdapter(webrtc_track.get()));
 }
 
+TEST_F(WebRtcMediaStreamTrackAdapterMapTest,
+       DestroyAdapterRefOnSignalingThread) {
+  MediaStreamComponent* track = CreateLocalTrack("local_track");
+  std::unique_ptr<blink::WebRtcMediaStreamTrackAdapterMap::AdapterRef>
+      adapter_ref = map_->GetOrCreateLocalTrackAdapter(track);
+  EXPECT_TRUE(adapter_ref->is_initialized());
+  EXPECT_EQ(1u, map_->GetLocalTrackCount());
+
+  // Destroying the AdapterRef on the signaling thread should post the removal
+  // and disposal to the main thread.
+  PostCrossThreadTask(
+      *signaling_thread(), FROM_HERE,
+      CrossThreadBindOnce(
+          [](std::unique_ptr<
+              blink::WebRtcMediaStreamTrackAdapterMap::AdapterRef> ref) {
+            // Destroyed when going out of scope on the signaling thread.
+          },
+          std::move(adapter_ref)));
+  RunMessageLoopsUntilIdle();
+  EXPECT_EQ(0u, map_->GetLocalTrackCount());
+  EXPECT_EQ(nullptr, map_->GetLocalTrackAdapter(track));
+}
+
 // Continuously calls GetOrCreateLocalTrackAdapter() on the main thread and
 // GetOrCreateRemoteTrackAdapter() on the signaling thread hoping to hit
 // deadlocks if the operations were to synchronize with the other thread while
diff --git a/third_party/blink/renderer/modules/peerconnection/webrtc_media_stream_track_adapter_test.cc b/third_party/blink/renderer/modules/peerconnection/webrtc_media_stream_track_adapter_test.cc
index 84fe299..dda0fc5 100644
--- a/third_party/blink/renderer/modules/peerconnection/webrtc_media_stream_track_adapter_test.cc
+++ b/third_party/blink/renderer/modules/peerconnection/webrtc_media_stream_track_adapter_test.cc
@@ -272,4 +272,20 @@
   RunMessageLoopsUntilIdle();
 }
 
+TEST_F(WebRtcMediaStreamTrackAdapterTest, DisposeOnSignalingThread) {
+  track_adapter_ =
+      blink::WebRtcMediaStreamTrackAdapter::CreateLocalTrackAdapter(
+          dependency_factory_.Get(), main_thread_, CreateLocalAudioTrack());
+  EXPECT_TRUE(track_adapter_->is_initialized());
+
+  dependency_factory_->GetWebRtcSignalingTaskRunner()->PostTask(
+      FROM_HERE,
+      base::BindOnce(
+          [](scoped_refptr<blink::WebRtcMediaStreamTrackAdapter> adapter) {
+            adapter->Dispose();
+          },
+          track_adapter_));
+  RunMessageLoopsUntilIdle();
+}
+
 }  // namespace blink
Loading diff…

Original Bug Report

reported by rj...@google.com

Potential UAF/Data Race in WebRTC track replacement due to iframe detachment

Flapjack, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: A Use-After-Free and data race can occur when replacing a WebRTC track if the parent iframe is detached during the operation. This detachment kills the main thread task runner, causing a cross-thread task to fail and destroy a track adapter on the wrong thread. Concurrent modification of the track’s sink vector on the main thread leads to a Use-After-Free memmove on the signaling thread.

Affected files:

  • third_party/blink/renderer/modules/peerconnection/rtc_rtp_sender_impl.cc
  • third_party/blink/renderer/modules/peerconnection/webrtc_media_stream_track_adapter_map.cc
  • third_party/blink/renderer/modules/peerconnection/webrtc_media_stream_track_adapter.cc
  • third_party/blink/renderer/modules/mediastream/media_stream_video_track.cc

Estimated timestamp from git blame: 2018-01-08

Summary

A Use-After-Free (UAF) and data race vulnerability exists in WebRTC’s track replacement logic. If an attacker detaches the iframe hosting an RTCPeerConnection exactly while a replaceTrack operation is communicating with the WebRTC signaling thread, the callback task intended for the main thread is rejected because the task runner has shut down. The rejection causes the callback closure and its captured AdapterRef to be destroyed synchronously on the signaling thread. This triggers a destruction chain that modifies the MediaStreamVideoTrack’s sink vectors without locks. A concurrent interaction with the track from the main thread can cause the vector to reallocate, resulting in the signaling thread performing a UAF memmove on a freed backing buffer.

Potential Exploitation Steps

Note: These steps trace the theoretical path through the codebase. We do not have a working Proof of Concept.

  1. Setup: An attacker creates an iframe and initializes an RTCPeerConnection inside it, acquiring a local video MediaStreamTrack. A reference to this track is leaked to the parent window.
  2. Trigger replaceTrack: The attacker calls RTCRtpSender.replaceTrack(track).
  3. Task Queued: On the main thread, RTCRtpSenderImpl::ReplaceTrack synchronously creates a WebRtcMediaStreamTrackAdapter and an AdapterRef. It posts a task (ReplaceTrackOnSignalingThread) to the WebRTC signaling thread, capturing the AdapterRef in the closure.
  4. Iframe Detachment: The attacker uses the parent window to immediately detach the iframe. This destroys the iframe’s execution context, shutting down its main_task_runner_.
  5. Task Execution and Failure: The signaling thread executes ReplaceTrackOnSignalingThread. It finishes its work and attempts to post ReplaceTrackCallback back to the main thread via PostCrossThreadTask, moving the AdapterRef into the new closure.
  6. Closure Destruction: Because main_task_runner_ is dead, PostCrossThreadTask returns false. The OnceClosure is destroyed immediately on the caller thread (the signaling thread).
  7. Unsafe Destruction Chain: The closure’s destruction destroys the AdapterRef. ~AdapterRef observes it is the final reference and calls adapter->Dispose().
  8. Thread-Hostile Access: The disposal chain continues on the signaling thread: DisposeLocalVideoTrack() -> ~MediaStreamVideoWebRtcSink() -> DisconnectFromTrack() -> MediaStreamVideoTrack::RemoveSink(). Note that release builds bypass the DCHECK_CALLED_ON_VALID_THREAD macro.
  9. The Memory Operation: RemoveSink() calls WTF::Vector::erase() on sinks_. Because the vector holds pointers, this translates to a memmove on the signaling thread to shift elements down.
  10. The Race and UAF: Concurrently, the attacker uses the parent window to interact with the leaked track reference (e.g., calling clone()). This triggers sinks_.push_back() on the main thread. If this causes the vector to ExpandCapacity(), the old backing buffer is freed by the main thread exactly while the signaling thread is executing a memmove on it.

By carefully timing the reallocation and spraying replacement objects (like an ArrayBuffer backing store), the attacker can cause the signaling thread to shift the contents of the newly allocated object by 8 bytes, corrupting pointers or lengths and leading to arbitrary read/write and Remote Code Execution.

Suggested Fix

Ensure that the destruction of WebRTC adapters and the resulting disconnection from Blink tracks always occurs on the main thread.

In WebRtcMediaStreamTrackAdapterMap::AdapterRef::~AdapterRef(), if the current thread is not the main thread, the call to removed_adapter->Dispose() should be posted to the main thread (using a task runner that is guaranteed to outlive the ExecutionContext, or by ensuring thread-safe disposal). Alternatively, ensure MediaStreamVideoTrack operations use proper locking, though keeping Blink object modification strictly on the main thread is generally preferred.

Evaluated with Chrome root at commit: 2155cb00003ec35716a76ed3246eae995f87b7ff


Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:

  • If you are familiar with the severity guidelines, you may adjust the severity.
  • If this is a false positive, and there’s no work to be done, please close as WAI.
  • If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.

Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.

View on issue tracker