CVE-2026-11118
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ExecutionContextthird_party/blink/renderer/modules/peerconnection/rtc_certificate_generator.h |
modified | |
GetStatsCallbackWrapperthird_party/blink/renderer/modules/peerconnection/test_webrtc_stats_report_obtainer.cc |
modified |
Files Changed
third_party/blink/renderer/modules/peerconnection/rtc_certificate_generator.hthird_party/blink/renderer/modules/peerconnection/rtc_peer_connection.ccthird_party/blink/renderer/modules/peerconnection/rtc_peer_connection_handler.ccthird_party/blink/renderer/modules/peerconnection/rtc_rtp_receiver.ccthird_party/blink/renderer/modules/peerconnection/rtc_rtp_sender.ccthird_party/blink/renderer/modules/peerconnection/rtc_rtp_sender_impl.ccthird_party/blink/renderer/modules/peerconnection/test_webrtc_stats_report_obtainer.ccthird_party/blink/renderer/platform/peerconnection/rtc_stats.h
Patch
From 8fc4e8102781e4fdf8bbc4a370474f38d06b1b94 Mon Sep 17 00:00:00 2001 From: Tony Herre <[email protected]> Date: Mon, 13 Apr 2026 03:49:46 -0700 Subject: [PATCH] Use WrapCrossThreadPersistent for callbacks in RTCPeerConnection Switch to WrapCrossThreadPersistent for generateCertificate and getStats callbacks to ensure thread safety. Bug: 501424047 Change-Id: I2379ed85cc1dba90810e891b16387e4667b766ca Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7747791 Reviewed-by: Palak Agarwal <[email protected]> Commit-Queue: Palak Agarwal <[email protected]> Auto-Submit: Tony Herre <[email protected]> Cr-Commit-Position: refs/heads/main@{#1613612} --- diff --git a/third_party/blink/renderer/modules/peerconnection/rtc_certificate_generator.h b/third_party/blink/renderer/modules/peerconnection/rtc_certificate_generator.h index d486bb4..833bd25 100644 --- a/third_party/blink/renderer/modules/peerconnection/rtc_certificate_generator.h +++ b/third_party/blink/renderer/modules/peerconnection/rtc_certificate_generator.h @@ -18,8 +18,8 @@ class ExecutionContext; -using RTCCertificateCallback = - base::OnceCallback<void(webrtc::scoped_refptr<webrtc::RTCCertificate>)>; +using RTCCertificateCallback = CrossThreadOnceFunction<void( + webrtc::scoped_refptr<webrtc::RTCCertificate>)>; // Chromium's WebRTCCertificateGenerator implementation; uses the // PeerConnectionIdentityStore/SSLIdentity::Generate to generate the identity, diff --git a/third_party/blink/renderer/modules/peerconnection/rtc_peer_connection.cc b/third_party/blink/renderer/modules/peerconnection/rtc_peer_connection.cc index c5ba331b..2ef7005 100644 --- a/third_party/blink/renderer/modules/peerconnection/rtc_peer_connection.cc +++ b/third_party/blink/renderer/modules/peerconnection/rtc_peer_connection.cc @@ -123,6 +123,7 @@ #include "third_party/blink/renderer/platform/bindings/exception_state.h" #include "third_party/blink/renderer/platform/bindings/script_state.h" #include "third_party/blink/renderer/platform/bindings/v8_throw_exception.h" +#include "third_party/blink/renderer/platform/heap/cross_thread_persistent.h" #include "third_party/blink/renderer/platform/heap/garbage_collected.h" #include "third_party/blink/renderer/platform/heap/persistent.h" #include "third_party/blink/renderer/platform/instrumentation/instance_counters.h" @@ -1472,8 +1473,8 @@ // Helper closure callback for RTCPeerConnection::generateCertificate. auto completion_callback = - BindOnce(RTCPeerConnection::GenerateCertificateCompleted, - WrapPersistent(resolver)); + CrossThreadBindOnce(RTCPeerConnection::GenerateCertificateCompleted, + WrapCrossThreadPersistent(resolver)); // Generate certificate. The |certificateObserver| will resolve the promise // asynchronously upon completion. The observer will manage its own @@ -1766,8 +1767,9 @@ // while leaving the associated promise pending as specified. resolver->Detach(); } else { - peer_handler_->GetStats(BindOnce(WebRTCStatsReportCallbackResolver, - WrapPersistent(resolver))); + peer_handler_->GetStats( + CrossThreadBindOnce(WebRTCStatsReportCallbackResolver, + WrapCrossThreadPersistent(resolver))); } return promise; } diff --git a/third_party/blink/renderer/modules/peerconnection/rtc_peer_connection_handler.cc b/third_party/blink/renderer/modules/peerconnection/rtc_peer_connection_handler.cc index 76a04bcf..188095b 100644 --- a/third_party/blink/renderer/modules/peerconnection/rtc_peer_connection_handler.cc +++ b/third_party/blink/renderer/modules/peerconnection/rtc_peer_connection_handler.cc @@ -233,19 +233,14 @@ PeerConnectionTracker::Action action_; }; -using RTCStatsReportCallbackInternal = - CrossThreadOnceFunction<void(std::unique_ptr<RTCStatsReportPlatform>)>; - void GetRTCStatsOnSignalingThread( const scoped_refptr<base::SingleThreadTaskRunner>& main_thread, webrtc::scoped_refptr<webrtc::PeerConnectionInterface> native_peer_connection, - RTCStatsReportCallbackInternal callback) { + RTCStatsReportCallback callback) { TRACE_EVENT0("webrtc", "GetRTCStatsOnSignalingThread"); native_peer_connection->GetStats( - CreateRTCStatsCollectorCallback( - main_thread, ConvertToBaseOnceCallback(std::move(callback))) - .get()); + CreateRTCStatsCollectorCallback(main_thread, std::move(callback)).get()); } std::set<RTCPeerConnectionHandler*>* GetPeerConnectionHandlers() { diff --git a/third_party/blink/renderer/modules/peerconnection/rtc_rtp_receiver.cc b/third_party/blink/renderer/modules/peerconnection/rtc_rtp_receiver.cc index 567d85dd..98f8d99 100644 --- a/third_party/blink/renderer/modules/peerconnection/rtc_rtp_receiver.cc +++ b/third_party/blink/renderer/modules/peerconnection/rtc_rtp_receiver.cc @@ -38,6 +38,7 @@ #include "third_party/blink/renderer/modules/peerconnection/rtc_rtp_sender.h" #include "third_party/blink/renderer/modules/peerconnection/rtc_stats_report.h" #include "third_party/blink/renderer/modules/peerconnection/web_rtc_stats_report_callback_resolver.h" +#include "third_party/blink/renderer/platform/heap/cross_thread_persistent.h" #include "third_party/blink/renderer/platform/heap/persistent.h" #include "third_party/blink/renderer/platform/peerconnection/rtc_encoded_video_stream_transformer.h" #include "third_party/blink/renderer/platform/peerconnection/rtc_stats.h" @@ -161,8 +162,8 @@ auto* resolver = MakeGarbageCollected<ScriptPromiseResolver<RTCStatsReport>>(script_state); auto promise = resolver->Promise(); - receiver_->GetStats( - BindOnce(WebRTCStatsReportCallbackResolver, WrapPersistent(resolver))); + receiver_->GetStats(CrossThreadBindOnce(WebRTCStatsReportCallbackResolver, + WrapCrossThreadPersistent(resolver))); return promise; } diff --git a/third_party/blink/renderer/modules/peerconnection/rtc_rtp_sender.cc b/third_party/blink/renderer/modules/peerconnection/rtc_rtp_sender.cc index 6d7519e..916880179 100644 --- a/third_party/blink/renderer/modules/peerconnection/rtc_rtp_sender.cc +++ b/third_party/blink/renderer/modules/peerconnection/rtc_rtp_sender.cc @@ -47,6 +47,7 @@ #include "third_party/blink/renderer/modules/peerconnection/rtc_stats_report.h" #include "third_party/blink/renderer/modules/peerconnection/web_rtc_stats_report_callback_resolver.h" #include "third_party/blink/renderer/platform/bindings/exception_state.h" +#include "third_party/blink/renderer/platform/heap/cross_thread_persistent.h" #include "third_party/blink/renderer/platform/heap/garbage_collected.h" #include "third_party/blink/renderer/platform/heap/persistent.h" #include "third_party/blink/renderer/platform/peerconnection/rtc_dtmf_sender_handler.h" @@ -882,8 +883,8 @@ auto* resolver = MakeGarbageCollected<ScriptPromiseResolver<RTCStatsReport>>(script_state); auto promise = resolver->Promise(); - sender_->GetStats( - BindOnce(WebRTCStatsReportCallbackResolver, WrapPersistent(resolver))); + sender_->GetStats(CrossThreadBindOnce(WebRTCStatsReportCallbackResolver, + WrapCrossThreadPersistent(resolver))); return promise; } 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 3ed0885..7d10622 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 @@ -362,14 +362,11 @@ std::move(callback).Run(result); } - using RTCStatsReportCallbackInternal = - CrossThreadOnceFunction<void(std::unique_ptr<RTCStatsReportPlatform>)>; - - void GetStatsOnSignalingThread(RTCStatsReportCallbackInternal callback) { + void GetStatsOnSignalingThread(RTCStatsReportCallback callback) { native_peer_connection_->GetStats( webrtc::scoped_refptr<webrtc::RtpSenderInterface>(webrtc_sender_.get()), - CreateRTCStatsCollectorCallback( - main_task_runner_, ConvertToBaseOnceCallback(std::move(callback)))); + CreateRTCStatsCollectorCallback(main_task_runner_, + std::move(callback))); } void SetParametersOnSignalingThread( diff --git a/third_party/blink/renderer/modules/peerconnection/test_webrtc_stats_report_obtainer.cc b/third_party/blink/renderer/modules/peerconnection/test_webrtc_stats_report_obtainer.cc index a90e06e..6d32a06 100644 --- a/third_party/blink/renderer/modules/peerconnection/test_webrtc_stats_report_obtainer.cc +++ b/third_party/blink/renderer/modules/peerconnection/test_webrtc_stats_report_obtainer.cc @@ -7,6 +7,7 @@ #include "base/functional/bind.h" #include "base/functional/callback.h" #include "third_party/blink/renderer/platform/peerconnection/rtc_stats.h" +#include "third_party/blink/renderer/platform/wtf/cross_thread_functional.h" namespace blink { @@ -16,7 +17,8 @@ RTCStatsReportCallback TestWebRTCStatsReportObtainer::GetStatsCallbackWrapper() { - return base::BindOnce(&TestWebRTCStatsReportObtainer::OnStatsDelivered, this); + return CrossThreadBindOnce(&TestWebRTCStatsReportObtainer::OnStatsDelivered, + base::RetainedRef(this)); } RTCStatsReportPlatform* TestWebRTCStatsReportObtainer::report() const { diff --git a/third_party/blink/renderer/platform/peerconnection/rtc_stats.h b/third_party/blink/renderer/platform/peerconnection/rtc_stats.h index 7e86745..1438da20 100644 --- a/third_party/blink/renderer/platform/peerconnection/rtc_stats.h +++ b/third_party/blink/renderer/platform/peerconnection/rtc_stats.h @@ -11,6 +11,7 @@ #include "third_party/blink/public/platform/web_string.h" #include "third_party/blink/renderer/platform/allow_discouraged_type.h" #include "third_party/blink/renderer/platform/platform_export.h" +#include "third_party/blink/renderer/platform/wtf/cross_thread_functional.h" #include "third_party/blink/renderer/platform/wtf/hash_map.h" #include "third_party/blink/renderer/platform/wtf/text/wtf_string.h" #include "third_party/webrtc/api/scoped_refptr.h" @@ -56,7 +57,7 @@ }; using RTCStatsReportCallback = - base::OnceCallback<void(std::unique_ptr<RTCStatsReportPlatform>)>;
Original Bug Report
Off-thread cppgc::Persistent destruction in RTCPeerConnection leads to memory 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 Chrome Security team.
Overview: A race condition during asynchronous WebRTC certificate generation allows a thread-unsafe cppgc::Persistent handle to be destroyed on a worker thread if the originating iframe is removed. This off-thread destruction modifies the Oilpan PersistentRegion free-list without locking, causing memory corruption and potential Use-After-Free (UAF).
Affected files:
third_party/blink/renderer/modules/peerconnection/rtc_certificate_generator.ccthird_party/blink/renderer/modules/peerconnection/rtc_peer_connection.ccv8/include/cppgc/internal/persistent-node.hv8/include/cppgc/persistent.h
Estimated timestamp from git blame: 2025-11-26
Summary
There is a potential race condition and thread-safety violation in RTCCertificateGenerator that allows a main-thread cppgc::Persistent handle to be destroyed on a WebRTC worker thread. Because cppgc::PersistentNode freeing is lockless (assuming execution on the creation thread), this off-thread destruction corrupts the PersistentRegion free-list, leading to memory corruption, aliased handles, and potential Remote Code Execution (RCE) in the renderer process.
Vulnerability Details
The potential vulnerability occurs in the following sequence:
- Trigger: An attacker calls
RTCPeerConnection.generateCertificate()from within an iframe, passing a large RSA modulus (e.g., 4096 bits) to intentionally delay the asynchronous generation process. - Promise Binding: On the main thread,
RTCPeerConnection::generateCertificatecreates aScriptPromiseResolverand wraps it in a thread-localcppgc::Persistenthandle (WrapPersistent(resolver)). This handle is bound to the completion callback usingWTF::BindOnce. - Cross-Thread Handoff: The resulting
base::OnceCallbackis passed to the WebRTC worker thread viaCrossThreadBindOnceandPostCrossThreadTaskinRTCCertificateGeneratorRequest::GenerateCertificateAsync. - Assertion Bypass: The cross-thread static assertions (
CheckGCedTypeRestrictions, etc.) verify that abase::OnceCallbackis safe to pass across threads, but they do not recursively inspect the callback’sBindState. The thread-unsafecppgc::Persistenthandle is therefore successfully passed to the worker thread. - Race Condition Window: The worker thread performs synchronous RSA key generation, blocking for several seconds.
- Frame Detachment: During this window, the attacker’s script removes the iframe from the DOM. This detaches the frame, shuts down its
ExecutionContext, and eventually shuts down its detached task queues (includingblink::TaskType::kInternalMedia). - Task Posting Failure: When the worker thread finishes generating the key, it attempts to post the result back to the main thread via
PostCrossThreadTaskusing the now-destroyedkInternalMediatask runner. - Off-Thread Destruction: Because the task queue is shut down,
PostTaskfails.PostCrossThreadTasktakes ownership of the callback and immediately destroys it on the current thread (the worker thread). - Lockless Modification: The callback’s destruction invokes
~BasicPersistent(), which callsPersistentRegionBase::FreeNode(). In release builds, thread-safetyDCHECKs are compiled out.FreeNode()updates the main-threadPersistentRegionfree-list without any locking.
Exploitation and Impact
This lockless modification races with concurrent allocations or frees on the main thread. A “lost update” interleaving can cause a live, in-use PersistentNode to be erroneously linked back into the free-list.
When the main thread subsequently allocates a new Persistent handle, it pops the corrupted node. Two distinct Persistent handles now alias the exact same PersistentNode memory. The new handle overwrites the owner_ and trace_ fields of the node.
During Garbage Collection, the original Persistent handle is completely skipped because the node no longer points to it. The GC will collect the underlying object, resulting in a Use-After-Free (UAF) when the original handle is later accessed by Blink C++. Additionally, overlapping nodes can lead to type confusion.
Note: cppgc::PersistentNode uses raw pointers in a union, which explicitly cannot be protected by MiraclePtr (BackupRefPtr).
Suggested Fix
Ensure that cppgc::Persistent handles are not captured by value in callbacks that might be destroyed off-thread. One potential fix is to bind a CrossThreadPersistent handle instead of a regular Persistent handle, or ensure the callback is wrapped in a mechanism that guarantees destruction on the original sequence (e.g., using base::ScopedClosureRunner bound to the main thread, although care must be taken if the target queue is shut down).
Evaluated with Chrome root at commit: 096fc8fdbfacf2546485756d03f160a3d04fcc9b
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.