Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in WebRTC
DescriptionUse after free in WebRTC
ComponentWebRTC
Bug ClassUAF
Tracker504716948
Fix commit9b4ffb3281c0 (src) +33/-17
CISA KEVNot listed
CreditedGoogle
Disclosed2026-05-27

Changed Functions

FunctionChangeNotes
connected_to_transport_
pc/sctp_data_channel.cc
modified
switch
pc/sctp_data_channel.cc
modified
TEST_F
pc/sctp_data_channel_unittest.cc
modified

Files Changed

  • pc/sctp_data_channel.cc
  • pc/sctp_data_channel.h
  • pc/sctp_data_channel_unittest.cc
From 9b4ffb3281c02327968ed07389082fcb2dd77baa Mon Sep 17 00:00:00 2001
From: Danil Chapovalov <danilchap@webrtc.org>
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 <tommi@webrtc.org>
Commit-Queue: Danil Chapovalov <danilchap@webrtc.org>
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();
 
Loading diff…

Regression Test / PoC

shipped with the fix
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();
Loading diff…

Original Bug Report

reported by vm...@google.com

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.cc
  • third_party/webrtc/pc/sctp_data_channel.h
  • third_party/webrtc/pc/data_channel_controller.cc
  • third_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:

  1. The attacker creates an RTCDataChannel and calls channel.send(largeBlob). Because the payload is a Blob, Blink initiates an asynchronous read and defers posting the SendAsync task.
  2. 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 RTCDataChannel object.
  3. The WebRTC channel reaches the kClosed state, causing the DataChannelController to drop its internal reference to the SctpDataChannel.
  4. The Blob finishes reading, triggering SendDataBuffer which posts the SendAsync task to the network thread.
  5. Garbage collection in V8 reclaims the JavaScript object, triggering Blink’s RTCDataChannel::Dispose(), which drops the final WebRTC proxy reference.
  6. The SctpDataChannel object is destroyed on the signaling thread. The safety flag remains valid.
  7. The attacker uses heap spraying to reclaim the memory previously occupied by the SctpDataChannel.
  8. The network thread executes the pending SendAsync task 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.

View on issue tracker