Medium chrome Type Confusion 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactType Confusion in WebRTC
DescriptionType Confusion in WebRTC
ComponentWebRTC
Bug ClassType Confusion
Tracker498753456
Fix commitfaec1cd9c75f (chromium/src) +16/-12
CISA KEVNot listed
CreditedGoogle
Disclosed2026-05-05

Files Changed

  • third_party/blink/renderer/modules/peerconnection/rtc_rtp_sender_impl.cc
  • third_party/blink/renderer/modules/peerconnection/rtc_rtp_sender_impl.h
  • third_party/blink/renderer/modules/peerconnection/rtc_rtp_sender_impl_test.cc
From faec1cd9c75ffca4e10584ed555177c6e2141e07 Mon Sep 17 00:00:00 2001
From: Tony Herre <toprice@chromium.org>
Date: Thu, 02 Apr 2026 02:47:36 -0700
Subject: [PATCH] Fix cross-thread passing of Persistent bound params in RTCrtpSenderImpl

Use WrapCrossThreadPersistent to bind params which are eventually
passed cross-thread inside closures.

Bug: 498753456
Change-Id: Ida9b2f23754bc3af0c89d06478b816d09233a7a6
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7725121
Commit-Queue: Tony Herre <toprice@chromium.org>
Auto-Submit: Tony Herre <toprice@chromium.org>
Reviewed-by: Palak Agarwal <agpalak@chromium.org>
Commit-Queue: Palak Agarwal <agpalak@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1609069}
---

diff --git a/third_party/blink/renderer/modules/peerconnection/rtc_rtp_sender_impl.cc b/third_party/blink/renderer/modules/peerconnection/rtc_rtp_sender_impl.cc
index 26cd378..3ed0885 100644
--- a/third_party/blink/renderer/modules/peerconnection/rtc_rtp_sender_impl.cc
+++ b/third_party/blink/renderer/modules/peerconnection/rtc_rtp_sender_impl.cc
@@ -213,7 +213,7 @@
   }
 
   void ReplaceTrack(MediaStreamComponent* with_track,
-                    base::OnceCallback<void(bool)> callback) {
+                    CrossThreadOnceFunction<void(bool)> callback) {
     DCHECK(main_task_runner_->BelongsToCurrentThread());
     std::unique_ptr<blink::WebRtcMediaStreamTrackAdapterMap::AdapterRef>
         track_ref;
@@ -228,7 +228,7 @@
                                 ReplaceTrackOnSignalingThread,
                             WrapRefCounted(this), std::move(track_ref),
                             CrossThreadUnretained(webrtc_track),
-                            CrossThreadBindOnce(std::move(callback))));
+                            std::move(callback)));
   }
 
   std::unique_ptr<blink::RtcDtmfSenderHandler> GetDtmfSender() const {
@@ -250,7 +250,7 @@
   void SetParameters(
       Vector<webrtc::RtpEncodingParameters> encodings,
       std::optional<webrtc::DegradationPreference> degradation_preference,
-      base::OnceCallback<void(webrtc::RTCError)> callback) {
+      CrossThreadOnceFunction<void(webrtc::RTCError)> callback) {
     DCHECK(main_task_runner_->BelongsToCurrentThread());
 
     webrtc::RtpParameters new_parameters = parameters_;
@@ -285,7 +285,7 @@
         CrossThreadBindOnce(&RTCRtpSenderImpl::RTCRtpSenderInternal::
                                 SetParametersOnSignalingThread,
                             WrapRefCounted(this), std::move(new_parameters),
-                            CrossThreadBindOnce(std::move(callback))));
+                            std::move(callback)));
   }
 
   void GetStats(RTCStatsReportCallback callback) {
@@ -495,7 +495,8 @@
 void RTCRtpSenderImpl::ReplaceTrack(MediaStreamComponent* with_track,
                                     RTCVoidRequest* request) {
   internal_->ReplaceTrack(
-      with_track, BindOnce(&OnReplaceTrackCompleted, WrapPersistent(request)));
+      with_track, CrossThreadBindOnce(&OnReplaceTrackCompleted,
+                                      WrapCrossThreadPersistent(request)));
 }
 
 std::unique_ptr<blink::RtcDtmfSenderHandler> RTCRtpSenderImpl::GetDtmfSender()
@@ -513,7 +514,8 @@
     blink::RTCVoidRequest* request) {
   internal_->SetParameters(
       std::move(encodings), degradation_preference,
-      BindOnce(&OnSetParametersCompleted, WrapPersistent(request)));
+      CrossThreadBindOnce(&OnSetParametersCompleted,
+                          WrapCrossThreadPersistent(request)));
 }
 
 void RTCRtpSenderImpl::GetStats(RTCStatsReportCallback callback) {
@@ -524,8 +526,9 @@
   internal_->SetStreams(stream_ids);
 }
 
-void RTCRtpSenderImpl::ReplaceTrack(MediaStreamComponent* with_track,
-                                    base::OnceCallback<void(bool)> callback) {
+void RTCRtpSenderImpl::ReplaceTrack(
+    MediaStreamComponent* with_track,
+    CrossThreadOnceFunction<void(bool)> callback) {
   internal_->ReplaceTrack(with_track, std::move(callback));
 }
 
diff --git a/third_party/blink/renderer/modules/peerconnection/rtc_rtp_sender_impl.h b/third_party/blink/renderer/modules/peerconnection/rtc_rtp_sender_impl.h
index d31dcce4..7f91018 100644
--- a/third_party/blink/renderer/modules/peerconnection/rtc_rtp_sender_impl.h
+++ b/third_party/blink/renderer/modules/peerconnection/rtc_rtp_sender_impl.h
@@ -17,6 +17,7 @@
 #include "third_party/blink/renderer/platform/peerconnection/rtc_rtp_sender_platform.h"
 #include "third_party/blink/renderer/platform/peerconnection/rtc_rtp_transceiver_platform.h"
 #include "third_party/blink/renderer/platform/peerconnection/rtc_stats.h"
+#include "third_party/blink/renderer/platform/wtf/cross_thread_functional.h"
 #include "third_party/webrtc/api/peer_connection_interface.h"
 #include "third_party/webrtc/api/rtp_sender_interface.h"
 #include "third_party/webrtc/api/scoped_refptr.h"
@@ -160,7 +161,7 @@
   // ReplaceTrack() without having a blink::RTCVoidRequest, which can only be
   // constructed inside of blink.
   void ReplaceTrack(MediaStreamComponent* with_track,
-                    base::OnceCallback<void(bool)> callback);
+                    CrossThreadOnceFunction<void(bool)> callback);
   // Removes this sender's track from its PeerConnection. Only used in Plan B.
   bool RemoveFromPeerConnection(webrtc::PeerConnectionInterface* pc);
 
diff --git a/third_party/blink/renderer/modules/peerconnection/rtc_rtp_sender_impl_test.cc b/third_party/blink/renderer/modules/peerconnection/rtc_rtp_sender_impl_test.cc
index c7ad3b6..39ad055a 100644
--- a/third_party/blink/renderer/modules/peerconnection/rtc_rtp_sender_impl_test.cc
+++ b/third_party/blink/renderer/modules/peerconnection/rtc_rtp_sender_impl_test.cc
@@ -114,9 +114,9 @@
     // and the |run_loop| quit.
     sender_->ReplaceTrack(
         component,
-        blink::BindOnce(&RTCRtpSenderImplTest::CallbackOnComplete,
-                        Unretained(this), Unretained(result_holder.get()),
-                        blink::Unretained(run_loop.get())));
+        CrossThreadBindOnce(&RTCRtpSenderImplTest::CallbackOnComplete,
+                            Unretained(this), Unretained(result_holder.get()),
+                            blink::Unretained(run_loop.get())));
     // When the resulting callback is invoked, waits for |run_loop| to complete
     // and returns |*result_holder|.
     return base::BindOnce(&RTCRtpSenderImplTest::RunLoopAndReturnResult,
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/third_party/blink/renderer/modules/peerconnection/rtc_rtp_sender_impl_test.cc b/third_party/blink/renderer/modules/peerconnection/rtc_rtp_sender_impl_test.cc
index c7ad3b6..39ad055a 100644
--- a/third_party/blink/renderer/modules/peerconnection/rtc_rtp_sender_impl_test.cc
+++ b/third_party/blink/renderer/modules/peerconnection/rtc_rtp_sender_impl_test.cc
@@ -114,9 +114,9 @@
     // and the |run_loop| quit.
     sender_->ReplaceTrack(
         component,
-        blink::BindOnce(&RTCRtpSenderImplTest::CallbackOnComplete,
-                        Unretained(this), Unretained(result_holder.get()),
-                        blink::Unretained(run_loop.get())));
+        CrossThreadBindOnce(&RTCRtpSenderImplTest::CallbackOnComplete,
+                            Unretained(this), Unretained(result_holder.get()),
+                            blink::Unretained(run_loop.get())));
     // When the resulting callback is invoked, waits for |run_loop| to complete
     // and returns |*result_holder|.
     return base::BindOnce(&RTCRtpSenderImplTest::RunLoopAndReturnResult,
Loading diff…

Original Bug Report

reported by vm...@google.com

Off-thread cppgc::Persistent destruction in RTCRtpSender leads to heap corruption

Project Fortify, 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 without the security team.

Overview: RTCRtpSenderImpl::SetParameters incorrectly passes a thread-affine cppgc::Persistent handle to a WebRTC background thread. If the frame is detached, the callback is destroyed off-thread, causing an unlocked data race in the cppgc free-list. This leads to heap corruption and potential renderer RCE.

Affected files:

  • third_party/blink/renderer/modules/peerconnection/rtc_rtp_sender_impl.cc
  • third_party/blink/renderer/modules/peerconnection/rtc_rtp_sender.cc
  • v8/include/cppgc/internal/persistent-node.h

Estimated timestamp from git blame: 2025-08-26

Summary

A vulnerability exists in RTCRtpSenderImpl::SetParameters where a thread-affine cppgc::Persistent handle is illicitly transferred to and potentially destroyed on a background thread. This off-thread destruction bypasses cppgc thread-safety expectations, creating an unlocked data race on the PersistentRegion free-list, leading to memory corruption in the renderer process.

Technical Details

When RTCRtpSender::setParameters is called, it allocates a SetParametersRequest on the garbage-collected heap. In RTCRtpSenderImpl::SetParameters (third_party/blink/renderer/modules/peerconnection/rtc_rtp_sender_impl.cc), this request is wrapped using WrapPersistent(request) and bound into a base::OnceCallback.

To pass this callback to the WebRTC signaling thread, the code wraps the callback using CrossThreadBindOnce(std::move(callback)). CrossThreadBindOnce normally employs CheckGCedTypeRestrictions to prevent thread-unsafe types (like Persistent) from crossing thread boundaries. However, because the Persistent handle is embedded inside the functor (base::OnceCallback) rather than passed as a bound argument, the compile-time checks trivially pass, and the handle is successfully wrapped into a CrossThreadOnceFunction.

On the WebRTC signaling thread, this function is captured in a lambda and passed to webrtc_sender_->SetParametersAsync. Under normal conditions, the lambda posts the callback back to the main thread upon completion. However, if the frame’s task runner has been shut down (e.g., due to iframe detachment), PostCrossThreadTask returns false. The task is dropped and destroyed inline on the background thread, invoking the ~BasicPersistent() destructor off-thread.

Impact

PersistentRegionBase::FreeNode is not thread-safe. When invoked from a background thread, it performs a lock-free update of free_list_head_ (node->InitializeAsFreeNode(free_list_head_); free_list_head_ = node; nodes_in_use_--;).

Concurrently, allocations on the main thread will call TryAllocateNodeFromFreeList, which also reads and writes the free-list head without locks. This data race corrupts the free-list, leading to a double-allocation of a PersistentNode. Because PersistentNode is a tagged union (overlapping the owner_ pointer and next_ pointer), an attacker can cause type-confusion and inject controlled owner_ pointers. During the next garbage collection, the GC’s Iterate phase will follow these corrupted pointers, resulting in arbitrary memory read/write and potential Remote Code Execution (RCE) in the renderer process.

Potential Reproduction Steps

Note: These are suggested steps for reproducing the vulnerability based on static analysis, as our tooling agent cannot yet run a live proof of concept.

  1. In an attacker-controlled iframe, create an RTCPeerConnection and a sender via addTransceiver.
  2. Execute sender.setParameters(...).
  3. While the task is processing in libwebrtc, detach the iframe (e.g., iframe.remove()). This shuts down the Blink task runner.
  4. When the async operation completes, the signaling thread attempts to post back to the main thread but fails, dropping and destroying the Persistent handle off-thread.
  5. Concurrently, loop the rapid creation of GC objects (e.g., ScriptPromiseResolver creations) on the main thread to collide with the background thread’s FreeNode execution, corrupting the cppgc free-list.
  6. Observe heap corruption or a crash during the next GC cycle.

Suggested Fix

Update RTCRtpSenderImpl::SetParameters to use WrapCrossThreadPersistent(request) instead of WrapPersistent(request). CrossThreadPersistent safely handles off-thread destruction by locking the region’s free-list operations. Alternatively, the blink layer should correctly type-check nested functors in CrossThreadBindOnce to prevent base::OnceCallback wrappers from obscuring thread-affine arguments.

Evaluated with Chrome root at commit: ff3d2b74fa39431785bd60e51463b08fcc71ee33


Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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