CVE-2026-9962
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
connected_to_transport_pc/sctp_data_channel.cc |
modified | |
switchpc/sctp_data_channel.cc |
modified | |
TEST_Fpc/sctp_data_channel_unittest.cc |
modified |
Files Changed
pc/sctp_data_channel.ccpc/sctp_data_channel.hpc/sctp_data_channel_unittest.cc
Patch
From 9b4ffb3281c02327968ed07389082fcb2dd77baa Mon Sep 17 00:00:00 2001 From: Danil Chapovalov <[email protected]> Date: Tue, 05 May 2026 17:44:03 +0200 Subject: [PATCH] In SctpDataChannel use plain bool as safety flag. SctpDataChannel can be destroyed on various threads and thus safety flag couldn't be properly invalidated in the destructor. Instead of relying on it when posting async send, rely on reference counting of the full object Bug: chromium:504716948 Change-Id: I1246fecac26d0761be006b069658d072183546bf Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/469980 Reviewed-by: Tomas Gunnarsson <[email protected]> Commit-Queue: Danil Chapovalov <[email protected]> Cr-Commit-Position: refs/heads/main@{#47635} --- diff --git a/pc/sctp_data_channel.cc b/pc/sctp_data_channel.cc index a7338aa..6d7305a 100644 --- a/pc/sctp_data_channel.cc +++ b/pc/sctp_data_channel.cc @@ -344,16 +344,14 @@ negotiated_(config.negotiated), ordered_(config.ordered), observer_(nullptr), - controller_(std::move(controller)) { + controller_(std::move(controller)), + connected_to_transport_(connected_to_transport) { RTC_DCHECK_RUN_ON(network_thread_); // Since we constructed on the network thread we can't (yet) check the // `controller_` pointer since doing so will trigger a thread check. RTC_UNUSED(network_thread_); RTC_DCHECK(config.IsValid()); - if (connected_to_transport) - network_safety_->SetAlive(); - switch (config.open_handshake_role) { case InternalDataChannelInit::kNone: // pre-negotiated handshake_state_ = kHandshakeReady; @@ -611,14 +609,17 @@ // thread. So we always post to the network thread (even if the current thread // might be the network thread - in theory a call could even come from within // the `on_complete` callback). - network_thread_->PostTask(SafeTask( - network_safety_, [this, buffer = std::move(buffer), - on_complete = std::move(on_complete)]() mutable { - RTC_DCHECK_RUN_ON(network_thread_); - RTCError err = SendImpl(std::move(buffer)); - if (on_complete) - std::move(on_complete)(err); - })); + scoped_refptr<SctpDataChannel> me(this); + network_thread_->PostTask([me = std::move(me), buffer = std::move(buffer), + on_complete = std::move(on_complete)]() mutable { + RTC_DCHECK_RUN_ON(me->network_thread_); + if (!me->connected_to_transport()) { + return; + } + RTCError err = me->SendImpl(std::move(buffer)); + if (on_complete) + std::move(on_complete)(err); + }); } void SctpDataChannel::SetSctpSid_n(StreamId sid) { @@ -661,7 +662,7 @@ void SctpDataChannel::OnTransportChannelCreated() { RTC_DCHECK_RUN_ON(network_thread_); - network_safety_->SetAlive(); + connected_to_transport_ = true; } void SctpDataChannel::OnTransportChannelClosed(RTCError error) { @@ -782,7 +783,7 @@ return; } - network_safety_->SetNotAlive(); + connected_to_transport_ = false; // Still go to "kClosing" before "kClosed", since observers may be expecting // that. diff --git a/pc/sctp_data_channel.h b/pc/sctp_data_channel.h index 2ac0a3e..693661b 100644 --- a/pc/sctp_data_channel.h +++ b/pc/sctp_data_channel.h @@ -273,7 +273,7 @@ RTC_RUN_ON(network_thread_); bool connected_to_transport() const RTC_RUN_ON(network_thread_) { - return network_safety_->alive(); + return connected_to_transport_; } void MaybeSendOnBufferedAmountChanged() RTC_RUN_ON(network_thread_); @@ -306,9 +306,8 @@ kHandshakeInit; // Did we already start the graceful SCTP closing procedure? bool started_closing_procedure_ RTC_GUARDED_BY(network_thread_) = false; + bool connected_to_transport_ RTC_GUARDED_BY(network_thread_) = false; PacketQueue queued_received_data_ RTC_GUARDED_BY(network_thread_); - scoped_refptr<PendingTaskSafetyFlag> network_safety_ = - PendingTaskSafetyFlag::CreateDetachedInactive(); }; } // namespace webrtc diff --git a/pc/sctp_data_channel_unittest.cc b/pc/sctp_data_channel_unittest.cc index 06b4a88..c4e30ee 100644 --- a/pc/sctp_data_channel_unittest.cc +++ b/pc/sctp_data_channel_unittest.cc @@ -613,6 +613,22 @@ EXPECT_EQ(RTCErrorDetailType::SCTP_FAILURE, channel_->error().error_detail()); } +TEST_F(SctpDataChannelTest, ChannelDeletedWhileDataBuffered) { + AddObserver(); + SetChannelReady(); + + CopyOnWriteBuffer buffer(100 * 1024); + memset(buffer.MutableData(), 0, buffer.size()); + DataBuffer packet(buffer, true); + + // Send a very large packet, forcing the message to become buffered. + channel_->SendAsync(packet, nullptr); + + // Delete the channel, expect no crashes. + inner_channel_ = nullptr; + channel_ = nullptr; +} + TEST_F(SctpDataChannelTest, TransportGotErrorCode) { SetChannelReady();
Regression Test / PoC
diff --git a/pc/sctp_data_channel_unittest.cc b/pc/sctp_data_channel_unittest.cc
index 06b4a88..c4e30ee 100644
--- a/pc/sctp_data_channel_unittest.cc
+++ b/pc/sctp_data_channel_unittest.cc
@@ -613,6 +613,22 @@
EXPECT_EQ(RTCErrorDetailType::SCTP_FAILURE, channel_->error().error_detail());
}
+TEST_F(SctpDataChannelTest, ChannelDeletedWhileDataBuffered) {
+ AddObserver();
+ SetChannelReady();
+
+ CopyOnWriteBuffer buffer(100 * 1024);
+ memset(buffer.MutableData(), 0, buffer.size());
+ DataBuffer packet(buffer, true);
+
+ // Send a very large packet, forcing the message to become buffered.
+ channel_->SendAsync(packet, nullptr);
+
+ // Delete the channel, expect no crashes.
+ inner_channel_ = nullptr;
+ channel_ = nullptr;
+}
+
TEST_F(SctpDataChannelTest, TransportGotErrorCode) {
SetChannelReady();
Original Bug Report
Potential Use-After-Free in WebRTC SctpDataChannel::SendAsync
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. 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 (UAF) vulnerability exists in WebRTC’s SctpDataChannel due to improper management of a task safety flag. The network_safety_ flag is not invalidated during object destruction, allowing delayed tasks capturing a raw this pointer to execute after the object is freed. This can be exploited to potentially achieve Remote Code Execution in the renderer process.
Affected files:
third_party/webrtc/pc/sctp_data_channel.ccthird_party/webrtc/pc/sctp_data_channel.hthird_party/webrtc/pc/data_channel_controller.ccthird_party/blink/renderer/modules/peerconnection/rtc_data_channel.cc
Estimated timestamp from git blame: 2023-04-11
Summary
A potential Use-After-Free (UAF) vulnerability has been identified in webrtc::SctpDataChannel. The issue arises because the PendingTaskSafetyFlag used to guard tasks posted to the network thread is not invalidated when the object is destroyed. If a task is queued on the network thread while the object is concurrently destroyed on the signaling thread, the task will still execute on a dangling this pointer. An attacker can exploit this via a malicious webpage to potentially achieve Remote Code Execution (RCE) in the sandboxed renderer process.
Root Cause Analysis
In third_party/webrtc/pc/sctp_data_channel.cc, SendAsync posts a task to the network thread using the SafeTask wrapper, capturing this as a raw pointer:
void SctpDataChannel::SendAsync(
DataBuffer buffer,
absl::AnyInvocable<void(RTCError) &&> on_complete) {
// ...
network_thread_->PostTask(SafeTask(
network_safety_, [this, buffer = std::move(buffer),
on_complete = std::move(on_complete)]() mutable {
RTC_DCHECK_RUN_ON(network_thread_);
RTCError err = SendImpl(std::move(buffer));
// ...
}));
}
The SafeTask helper relies on network_safety_ (a scoped_refptr<PendingTaskSafetyFlag>) to determine if the object is still alive. For SafeTask to prevent execution on a destroyed object, SetNotAlive() must be explicitly called before or during destruction.
However, SctpDataChannel::~SctpDataChannel does not call network_safety_->SetNotAlive(). It is only invoked in the abrupt error path (CloseAbruptlyWithError). Consequently, SafeTask holds a strong reference to the flag, keeping it alive and marked as valid even after the SctpDataChannel object itself has been deallocated. When the delayed task executes, the lambda runs and dereferences the dangling this pointer.
Potential Exploitation Scenario
An attacker can reliably trigger this UAF using JavaScript by widening the race window between task queuing and object destruction:
- The attacker creates an
RTCDataChanneland callschannel.send(largeBlob). Because the payload is a Blob, Blink initiates an asynchronous read and defers posting theSendAsynctask. - While the Blob is being read, the attacker causes the data channel to close (e.g., via a remote peer SCTP stream reset) and drops all local JavaScript references to the
RTCDataChannelobject. - The WebRTC channel reaches the
kClosedstate, causing theDataChannelControllerto drop its internal reference to theSctpDataChannel. - The Blob finishes reading, triggering
SendDataBufferwhich posts theSendAsynctask to the network thread. - Garbage collection in V8 reclaims the JavaScript object, triggering Blink’s
RTCDataChannel::Dispose(), which drops the final WebRTC proxy reference. - The
SctpDataChannelobject is destroyed on the signaling thread. The safety flag remains valid. - The attacker uses heap spraying to reclaim the memory previously occupied by the
SctpDataChannel. - The network thread executes the pending
SendAsynctask on the attacker-controlled memory.
The attacker crafts the sprayed payload to forge the state_ member to appear kOpen and overwrites the controller_ member, which is a webrtc::WeakPtr. By pointing the WeakPtr’s internal reference flag to a sprayed fake valid flag, and pointing the interface pointer to a fake vtable, the attacker can hijack the virtual call to controller_->SendData(...) in SctpDataChannel::SendDataMessage, leading to RCE.
Note: This vulnerability is not mitigated by MiraclePtr (BackupRefPtr) because the WebRTC library is largely outside the scope of BRP, and the lambda captures a raw pointer (T*) rather than a base::raw_ptr.
Suggested Fix
Invalidate the safety flag in the SctpDataChannel destructor to ensure pending tasks are aborted:
SctpDataChannel::~SctpDataChannel() {
network_safety_->SetNotAlive();
if (observer_adapter_)
ObserverAdapter::DeleteOnSignalingThread(std::move(observer_adapter_));
}
Evaluated with Chrome root at commit: 7353d249d9cacf9c7218e1d7b8a39cf39c72d646
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.