High patch-now ✔︎ verified CVSS 8.8 chrome UAF 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in WebRTC
DescriptionUse after free in WebRTC
ComponentWebRTC
Sub-componentBlink>WebRTC
Bug ClassUAF
Action tierpatch-now — corruption primitive, web-reachable in default config (confirmed) — upheld by independent review by AI
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Independent review by AIConfirmed
Reviewer notes: Analysis overstates SDP munging: the added test reaches MaybeHandleLocallyRejectedTransceiver via transceiver.stop()+renegotiation with no munging, so it is web-reachable by default. Root cause (removing std::move) matches the diff.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker504194151
Fix commitf685c00979b2 (src) +18/-2
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedRayyan Kadar
Reported2026-04-20 (per vendor advisory)
Fix landed2026-04-22 (public git)
Disclosed2026-06-02
Patch gap41 days — fix public in source before the advisory

Background

Transceiver
a WebRTC object pairing a sender and receiver for one media section (m= line) whose lifetime is managed via reference-counted pointers held in per-negotiation state.
SDP munging
the practice of an application editing the raw SDP text (e.g. rejecting a media section) before applying it, which can drive normally-unreachable local code paths such as locally-rejected content handling.
`std::move`
a C++ cast that transfers ownership out of a source object, potentially leaving it empty/null and allowing the moved-from resource (here a transceiver reference) to be released once the callee is done with it.
`worker_tasks.Run()`
a deferred batch of tasks posted to the worker thread that execute later in the same function scope and may still reference the transceiver.

Root Cause Analysis

When applying a local description that contains a locally-rejected media section (a path reached mainly via SDP munging, per the diff comment), MaybeHandleLocallyRejectedTransceiver was called with std::move(update.transceiver), transferring the transceiver reference out of the update element in the transceivers_to_update vector. Because the reference was moved out of the container prematurely, the transceiver could be destroyed before the deferred worker_tasks.Run() — which executes later within the same function — had finished executing tasks that still needed the transceiver, producing a use-after-free. The invariant violated is that an object referenced by pending worker tasks must remain alive until those tasks have run.

The fix removes the std::move, passing update.transceiver by (non-moving) reference so the owning reference stays in the transceivers_to_update vector until the end of the enclosing function, guaranteeing the transceiver outlives worker_tasks.Run() as the added comment states.

Key insight
The single mistake was moving the transceiver’s owning reference out of its container before deferred worker tasks that depend on it had run; dropping the std::move keeps the reference alive in the vector for the remainder of the function, closing the lifetime gap.

Attack Path

  1. Set up a stopped transceiver An application adds an audio transceiver, sets a local offer, then stops the transceiver so that a subsequent re-offer contains a rejected media section (as the regression test LocallyRejectedTransceiverDoesNotCrash demonstrates).
  2. Apply local description with rejected content Setting the re-offer as the local description reaches the locally-rejected-content code path and invokes MaybeHandleLocallyRejectedTransceiver. (SDP munging is the general trigger the comment cites.)
  3. Premature ownership transfer The pre-patch call moves update.transceiver out of the transceivers_to_update vector into the callee, dropping the container’s owning reference to the transceiver.
  4. Deferred task frees/uses the transceiver Later in the same function, worker_tasks.Run() executes; with the reference no longer retained by the vector, the transceiver can be destroyed while a worker task still references it, resulting in a use-after-free (the specific freeing owner and the exact field touched are not shown in the diff) (inferred).

Impact Assessment

An attacker or malfunctioning application controlling the SDP applied to a PeerConnection can trigger a use-after-free of a transceiver in the process hosting WebRTC (the renderer for browser WebRTC) during local SDP negotiation. The advisory classifies this as a use-after-free in WebRTC; it does not state exploited-in-the-wild status, and any escalation from the UAF to memory corruption primitives or code execution is not demonstrated by the patch (inferred). The precondition is applying a local description whose media section is rejected, most readily produced via SDP munging.

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

Audit Directions

  • Move-then-defer
    Look for std::move of a reference-counted object into a callee at a site where the same function later runs deferred/posted tasks (e.g. *_tasks.Run(), posted lambdas) that may still touch that object.
  • Container element lifetime
    Audit code that moves elements out of a container still iterated or relied upon later in the same scope, especially when the container is what keeps an object alive.
  • SDP-munging code paths
    Review locally-rejected/recycled media-section handling and other “only reachable via SDP munging” branches, which receive less real-world exercise and can hide lifetime bugs.
From f685c00979b202fc5b05df337485664e56b6c581 Mon Sep 17 00:00:00 2001
From: Tommi <tommi@webrtc.org>
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 <hta@webrtc.org>
Commit-Queue: Tomas Gunnarsson <tommi@webrtc.org>
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 r2...@gmail.com

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