CVE-2026-79187
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifthird_party/blink/renderer/modules/peerconnection/webrtc_media_stream_track_adapter_map.cc |
modified | |
MODULES_EXPORTthird_party/blink/renderer/modules/peerconnection/webrtc_media_stream_track_adapter_map.h |
modified |
Files Changed
third_party/blink/renderer/modules/peerconnection/webrtc_media_stream_track_adapter.ccthird_party/blink/renderer/modules/peerconnection/webrtc_media_stream_track_adapter.hthird_party/blink/renderer/modules/peerconnection/webrtc_media_stream_track_adapter_map.ccthird_party/blink/renderer/modules/peerconnection/webrtc_media_stream_track_adapter_map.h
Patch
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
Regression Test / PoC
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
Original Bug Report
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.ccthird_party/blink/renderer/modules/peerconnection/webrtc_media_stream_track_adapter_map.ccthird_party/blink/renderer/modules/peerconnection/webrtc_media_stream_track_adapter.ccthird_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.
- Setup: An attacker creates an
iframeand initializes anRTCPeerConnectioninside it, acquiring a local videoMediaStreamTrack. A reference to this track is leaked to the parent window. - Trigger
replaceTrack: The attacker callsRTCRtpSender.replaceTrack(track). - Task Queued: On the main thread,
RTCRtpSenderImpl::ReplaceTracksynchronously creates aWebRtcMediaStreamTrackAdapterand anAdapterRef. It posts a task (ReplaceTrackOnSignalingThread) to the WebRTC signaling thread, capturing theAdapterRefin the closure. - Iframe Detachment: The attacker uses the parent window to immediately detach the
iframe. This destroys the iframe’s execution context, shutting down itsmain_task_runner_. - Task Execution and Failure: The signaling thread executes
ReplaceTrackOnSignalingThread. It finishes its work and attempts to postReplaceTrackCallbackback to the main thread viaPostCrossThreadTask, moving theAdapterRefinto the new closure. - Closure Destruction: Because
main_task_runner_is dead,PostCrossThreadTaskreturns false. TheOnceClosureis destroyed immediately on the caller thread (the signaling thread). - Unsafe Destruction Chain: The closure’s destruction destroys the
AdapterRef.~AdapterRefobserves it is the final reference and callsadapter->Dispose(). - Thread-Hostile Access: The disposal chain continues on the signaling thread:
DisposeLocalVideoTrack()->~MediaStreamVideoWebRtcSink()->DisconnectFromTrack()->MediaStreamVideoTrack::RemoveSink(). Note that release builds bypass theDCHECK_CALLED_ON_VALID_THREADmacro. - The Memory Operation:
RemoveSink()callsWTF::Vector::erase()onsinks_. Because the vector holds pointers, this translates to amemmoveon the signaling thread to shift elements down. - The Race and UAF: Concurrently, the attacker uses the parent window to interact with the leaked track reference (e.g., calling
clone()). This triggerssinks_.push_back()on the main thread. If this causes the vector toExpandCapacity(), the old backing buffer is freed by the main thread exactly while the signaling thread is executing amemmoveon 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.