Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in WebRTC
DescriptionUse after free in WebRTC
ComponentWebRTC
Bug ClassUAF
Tracker504194151
Fix commitf685c00979b2 (src) +18/-2
CISA KEVNot listed
CreditedRayyan Kadar
Disclosed2026-06-02

Changed Functions

FunctionChangeNotes
TEST_F
pc/peer_connection_jsep_unittest.cc
modified

Files Changed

  • pc/peer_connection_jsep_unittest.cc
  • pc/sdp_offer_answer.cc
From f685c00979b202fc5b05df337485664e56b6c581 Mon Sep 17 00:00:00 2001
From: Tommi <[email protected]>
Date: Wed, 22 Apr 2026 22:10:46 +0200
Subject: [PATCH] Prevent transceiver crash during SDP local rejection

Ensure that transceivers remain valid when handling locally rejected
media content during SDP updates. Previously, the transceiver was
std::move()ed out of the update vector prematurely, which could cause it
to be destroyed before associated worker tasks had finished executing.

Fixed: chromium:504194151
Change-Id: Id38ce125cbe83a4ea709adfd1951613642cafc8e
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/466140
Reviewed-by: Harald Alvestrand <[email protected]>
Commit-Queue: Tomas Gunnarsson <[email protected]>
Cr-Commit-Position: refs/heads/main@{#47528}
---

diff --git a/pc/peer_connection_jsep_unittest.cc b/pc/peer_connection_jsep_unittest.cc
index 104591b..a4ebec6 100644
--- a/pc/peer_connection_jsep_unittest.cc
+++ b/pc/peer_connection_jsep_unittest.cc
@@ -1031,6 +1031,20 @@
   EXPECT_EQ(second_mid, caller_second_transceiver->mid());
 }
 
+TEST_F(PeerConnectionJsepTest, LocallyRejectedTransceiverDoesNotCrash) {
+  auto caller = CreatePeerConnection();
+  auto transceiver = caller->AddTransceiver(MediaType::AUDIO);
+
+  ASSERT_TRUE(caller->SetLocalDescription(caller->CreateOffer()));
+
+  transceiver->StopInternal();
+
+  // The reoffer will have a rejected media section.
+  // Setting it as local description triggers
+  // MaybeHandleLocallyRejectedTransceiver.
+  ASSERT_TRUE(caller->SetLocalDescription(caller->CreateOffer()));
+}
+
 // Test that an m= section is *not* recycled if the media section is only
 // rejected in the pending remote description and there is no current local
 // description.
diff --git a/pc/sdp_offer_answer.cc b/pc/sdp_offer_answer.cc
index c3a66d6..09f9ca5 100644
--- a/pc/sdp_offer_answer.cc
+++ b/pc/sdp_offer_answer.cc
@@ -4311,9 +4311,11 @@
     // Handle locally rejected content. This code path is only needed for apps
     // that SDP munge. Remote rejected content is handled in
     // ApplyRemoteDescriptionUpdateTransceiverState().
+    // Do not use std::move here to ensure that the transceiver stays alive
+    // in the `transceivers_to_update` vector until the end of this function.
+    // This guarantees that it outlives the execution of `worker_tasks.Run()`.
     MaybeHandleLocallyRejectedTransceiver(source, new_session, update.content,
-                                          std::move(update.transceiver),
-                                          worker_tasks);
+                                          update.transceiver, worker_tasks);
   }
 
   error = network_teardown_tasks.Run();
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/pc/peer_connection_jsep_unittest.cc b/pc/peer_connection_jsep_unittest.cc
index 104591b..a4ebec6 100644
--- a/pc/peer_connection_jsep_unittest.cc
+++ b/pc/peer_connection_jsep_unittest.cc
@@ -1031,6 +1031,20 @@
   EXPECT_EQ(second_mid, caller_second_transceiver->mid());
 }
 
+TEST_F(PeerConnectionJsepTest, LocallyRejectedTransceiverDoesNotCrash) {
+  auto caller = CreatePeerConnection();
+  auto transceiver = caller->AddTransceiver(MediaType::AUDIO);
+
+  ASSERT_TRUE(caller->SetLocalDescription(caller->CreateOffer()));
+
+  transceiver->StopInternal();
+
+  // The reoffer will have a rejected media section.
+  // Setting it as local description triggers
+  // MaybeHandleLocallyRejectedTransceiver.
+  ASSERT_TRUE(caller->SetLocalDescription(caller->CreateOffer()));
+}
+
 // Test that an m= section is *not* recycled if the media section is only
 // rejected in the pending remote description and there is no current local
 // description.
Loading diff…

Original Bug Report

reported by [email protected]

Heap-UAF in RtpSenderBase::DetachTrackAndGetStopTask during SDP rollback

Steps to reproduce the problem

  1. Create two RTCPeerConnection objects (caller, callee)
  2. caller.addTransceiver(“audio”)
  3. offer = await caller.createOffer()
  4. await callee.setRemoteDescription(offer)
  5. await callee.setLocalDescription({type:“rollback”, sdp:""})

Step 5 triggers heap-use-after-free at pc/rtp_sender.cc:864.

Tested on WebRTC HEAD commit 67615c4df4 (2026-04-19). 100% reproducible under ASAN with separate threads.

Standalone C++ repro and ASAN stack trace attached as files.

Problem Description

Heap-use-after-free in pc/rtp_sender.cc:864 during SDP rollback.

ROOT CAUSE: SdpOfferAnswerHandler::Rollback() uses ScopedOperationsBatcher to defer GetStopTransceiverProcedure() tasks. DetachTrackAndGetStopTask() returns a lambda capturing raw [this] (RtpSenderBase). During rollback, the transceiver is removed from the stable-states map, dropping the last ref to AudioRtpSender. When the ScopedOperationsBatcher destructor runs the batched lambda on the worker thread, this points to freed heap.

ASAN SUMMARY: READ of size 8, thread T2 (worker) #0 RtpSenderBase::DetachTrackAndGetStopTask()::$_0 rtp_sender.cc:864 freed by thread T3 (signaling) in: #0 AudioRtpSender::Release() #16 SdpOfferAnswerHandler::Rollback()

REGRESSION: 191a6abd692a (Feb 12 2026) “Batch worker thread tasks in [Get]StopTransceiverProcedure” - changed Rollback() from synchronous StopTransceiverProcedure() to deferred GetStopTransceiverProcedure() via ScopedOperationsBatcher. Internal bug: webrtc:42222804 Review: https://webrtc-review.googlesource.com/c/src/+/448460

WEB REACHABLE: Yes - standard RTCPeerConnection JS API, no flags, no user interaction, no extensions. Any webpage can trigger this.

IMPACT: Renderer process heap-UAF. Potential for exploitation via heap grooming to achieve type confusion or controlled read/write.

Additional Comments

Regression: 191a6abd692a (2026-02-12) “Batch worker thread tasks in [Get]StopTransceiverProcedure” - webrtc:42222804

Standalone multi-threaded C++ PoC attached (uaf_repro_standalone.cc). Crashes 100% on first iteration under ASAN. Build with: autoninja -C out/fuzzers test/fuzzers:uaf_repro_standalone Run with: ASAN_OPTIONS=detect_leaks=0 ./out/fuzzers/uaf_repro_standalone

Also found via custom fuzzer (pc_sdp_negotiation_fuzzer) with two independent 4-byte minimal crash inputs attached.

JS equivalent (for Chrome testing): const pc1 = new RTCPeerConnection(); const pc2 = new RTCPeerConnection(); pc1.addTransceiver(“audio”); const o = await pc1.createOffer(); await pc2.setRemoteDescription(o); await pc2.setLocalDescription({type:“rollback”,sdp:""});

Summary

Heap-UAF in RtpSenderBase::DetachTrackAndGetStopTask during SDP rollback

Custom Questions

Type of crash:

Tab

Crash state:

heap-use-after-free READ size 8 at pc/rtp_sender.cc:864 Thread T2 (worker): #0 RtpSenderBase::DetachTrackAndGetStopTask()::$_0 pc/rtp_sender.cc:864 #1 RtpTransceiver::GetStopSendingAndReceiving()::$_0 any_invocable.h:774 #2 ScopedOperationsBatcher::Run()::$_1 any_invocable.h:774 #3 Thread::Dispatch() any_invocable.h:774 #4 Thread::Run() thread.cc:884

Freed by thread T3 (signaling): #0 operator delete #1 AudioRtpSender::Release() ref_counted_object.h:42 #7 RtpTransceiver::~RtpTransceiver() vector.h:259 #16 SdpOfferAnswerHandler::Rollback() tree:1361

Allocated by thread T2 (worker): #1 AudioRtpSender::Create() make_ref_counted.h:90 #7 SdpOfferAnswerHandler::AssociateTransceiver() sdp_offer_answer.cc:4281

Full symbolized ASAN trace attached as stacktrace.txt

Reporter credit:

Rayyan Kadar

Additional Data

Category: Security
Chrome Channel: Dev
Regression: N/A \

View on issue tracker