CVE-2026-10943
Overview
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.
std::move keeps the reference alive in the vector for the remainder of the function, closing the lifetime gap.Attack Path
- 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
LocallyRejectedTransceiverDoesNotCrashdemonstrates). - 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.) - Premature ownership transfer
The pre-patch call moves
update.transceiverout of thetransceivers_to_updatevector into the callee, dropping the container’s owning reference to the transceiver. - 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
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
| Function | Change | Notes |
|---|---|---|
TEST_Fpc/peer_connection_jsep_unittest.cc |
modified |
Files Changed
pc/peer_connection_jsep_unittest.ccpc/sdp_offer_answer.cc
Audit Directions
- Move-then-deferLook for
std::moveof 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 lifetimeAudit 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 pathsReview 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.
Patch
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();
Regression Test / PoC
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.
Original Bug Report
Heap-UAF in RtpSenderBase::DetachTrackAndGetStopTask during SDP rollback
Steps to reproduce the problem
- Create two RTCPeerConnection objects (caller, callee)
- caller.addTransceiver(“audio”)
- offer = await caller.createOffer()
- await callee.setRemoteDescription(offer)
- 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 \