CVE-2026-7988
Overview
Files Changed
third_party/blink/renderer/modules/peerconnection/rtc_rtp_sender_impl.ccthird_party/blink/renderer/modules/peerconnection/rtc_rtp_sender_impl.hthird_party/blink/renderer/modules/peerconnection/rtc_rtp_sender_impl_test.cc
Patch
From faec1cd9c75ffca4e10584ed555177c6e2141e07 Mon Sep 17 00:00:00 2001 From: Tony Herre <[email protected]> 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 <[email protected]> Auto-Submit: Tony Herre <[email protected]> Reviewed-by: Palak Agarwal <[email protected]> Commit-Queue: Palak Agarwal <[email protected]> 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,
Regression Test / PoC
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,
Original Bug Report
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.ccthird_party/blink/renderer/modules/peerconnection/rtc_rtp_sender.ccv8/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.
- In an attacker-controlled iframe, create an
RTCPeerConnectionand a sender viaaddTransceiver. - Execute
sender.setParameters(...). - While the task is processing in libwebrtc, detach the iframe (e.g.,
iframe.remove()). This shuts down the Blink task runner. - When the async operation completes, the signaling thread attempts to post back to the main thread but fails, dropping and destroying the
Persistenthandle off-thread. - Concurrently, loop the rapid creation of GC objects (e.g.,
ScriptPromiseResolvercreations) on the main thread to collide with the background thread’sFreeNodeexecution, corrupting thecppgcfree-list. - 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.