Chrome · WebRTC
CVE-2026-106357
UAF in WebRTC
Overview
High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifpc/sdp_offer_answer.cc |
modified | |
forpc/transceiver_list.cc |
modified | |
TEST_Fpc/sdp_offer_answer_unittest.cc |
modified |
Files Changed
pc/transceiver_list.hpc/transceiver_list.ccpc/rtp_transceiver.hpc/peer_connection.ccpc/rtp_transceiver.ccpc/sdp_offer_answer.ccpc/sdp_offer_answer_unittest.cc
Patch
From 7a25e1c85a0af15cae8a2d55bf02e5610bf1af53 Mon Sep 17 00:00:00 2001
From: Tommi <tommi@webrtc.org>
Date: Tue, 29 Sep 2026 17:54:50 +0200
Subject: [PATCH] Keep transceiver MID in sync with its channel
When applying a local description, a transceiver could be matched to an
m= section by its index even if it was already associated with a
different MID. The channel of the transceiver keeps using the MID it was
created for, so the two could get out of sync.
- Only match transceivers that are not yet associated by m= section
index.
- Route transport changes to a transceiver based on the MID of its
channel.
Bug: chromium:567160162
Change-Id: Ic9f29b66a8f9f181fd414009f5c1e5cebfa9ca54
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/506421
Commit-Queue: Tomas Gunnarsson <tommi@webrtc.org>
Reviewed-by: Stefan Holmer <stefan@webrtc.org>
Cr-Commit-Position: refs/heads/main@{#48768}
---
diff --git a/pc/peer_connection.cc b/pc/peer_connection.cc
index 035f6b7..15a0e4e 100644
--- a/pc/peer_connection.cc
+++ b/pc/peer_connection.cc
@@ -3130,7 +3130,10 @@
for (const auto& transceiver :
rtp_manager()->transceivers()->UnsafeList()) {
auto internal = transceiver->internal();
- if (internal->mid() == mid) {
+ // Match on the MID that the channel was created for rather than on the
+ // transceiver's current MID. The channel is bound to the transport of
+ // that MID and must be notified when that transport changes.
+ if (internal->channel_mid() == mid) {
ret = internal->SetRtpTransport(rtp_transport);
}
}
diff --git a/pc/rtp_transceiver.cc b/pc/rtp_transceiver.cc
index b951403..6d5c39e 100644
--- a/pc/rtp_transceiver.cc
+++ b/pc/rtp_transceiver.cc
@@ -1579,6 +1579,11 @@
return channel_->transport_name();
}
+absl::string_view RtpTransceiver::channel_mid() const {
+ RTC_DCHECK_RUN_ON(context()->network_thread());
+ return channel_ ? absl::string_view(channel_->mid()) : absl::string_view();
+}
+
MediaSendChannelInterface* RtpTransceiver::media_send_channel() {
RTC_DCHECK_RUN_ON(thread_);
return channel_ ? channel_->media_send_channel() : nullptr;
diff --git a/pc/rtp_transceiver.h b/pc/rtp_transceiver.h
index 3dca197..a215bc9 100644
--- a/pc/rtp_transceiver.h
+++ b/pc/rtp_transceiver.h
@@ -428,6 +428,10 @@
const std::vector<StreamParams>& channel_local_streams() const;
const std::vector<StreamParams>& channel_remote_streams() const;
absl::string_view channel_transport_name() const;
+ // Returns the MID that the channel was created for, or an empty string if
+ // there is no channel. The channel stays bound to the transport of this MID
+ // for its lifetime. Must be called on the network thread.
+ absl::string_view channel_mid() const;
// Accessors for media channels. These return null if there is no channel.
MediaSendChannelInterface* media_send_channel();
diff --git a/pc/sdp_offer_answer.cc b/pc/sdp_offer_answer.cc
index 5cf98d1..d624d81 100644
--- a/pc/sdp_offer_answer.cc
+++ b/pc/sdp_offer_answer.cc
@@ -4787,7 +4787,10 @@
// mapping between transceivers and m= section indices established when
// creating the offer.
if (!transceiver) {
- transceiver = transceivers()->FindByMLineIndex(mline_index);
+ // Only consider transceivers that are not yet associated. The channel of
+ // an associated transceiver stays bound to the MID it was created for
+ // (and to the transport of that MID), so the MID must not change.
+ transceiver = transceivers()->FindUnassociatedByMLineIndex(mline_index);
}
if (!transceiver) {
// This may happen normally when media sections are rejected.
diff --git a/pc/transceiver_list.cc b/pc/transceiver_list.cc
index f2abe48..49654de 100644
--- a/pc/transceiver_list.cc
+++ b/pc/transceiver_list.cc
@@ -85,11 +85,12 @@
return nullptr;
}
-RtpTransceiverProxyRefPtr TransceiverList::FindByMLineIndex(
+RtpTransceiverProxyRefPtr TransceiverList::FindUnassociatedByMLineIndex(
size_t mline_index) const {
RTC_DCHECK_RUN_ON(&sequence_checker_);
for (const auto& transceiver : transceivers_) {
- if (transceiver->internal()->mline_index() == mline_index) {
+ RtpTransceiver* internal = transceiver->internal();
+ if (!internal->mid() && internal->mline_index() == mline_index) {
return transceiver;
}
}
diff --git a/pc/transceiver_list.h b/pc/transceiver_list.h
index 5136449..a30ed99 100644
--- a/pc/transceiver_list.h
+++ b/pc/transceiver_list.h
@@ -127,7 +127,10 @@
RtpTransceiverProxyRefPtr FindBySender(
scoped_refptr<RtpSenderInterface> sender) const;
RtpTransceiverProxyRefPtr FindByMid(absl::string_view mid) const;
- RtpTransceiverProxyRefPtr FindByMLineIndex(size_t mline_index) const;
+ // Returns the transceiver that is not yet associated with an m= section
+ // (i.e. has no MID) and has been assigned `mline_index` by CreateOffer.
+ RtpTransceiverProxyRefPtr FindUnassociatedByMLineIndex(
+ size_t mline_index) const;
// Find or create the stable state for a transceiver.
TransceiverStableState* StableState(RtpTransceiverProxyRefPtr transceiver) {
diff --git a/pc/sdp_offer_answer_unittest.cc b/pc/sdp_offer_answer_unittest.cc
index 82bda92..cb5ad60 100644
--- a/pc/sdp_offer_answer_unittest.cc
+++ b/pc/sdp_offer_answer_unittest.cc
@@ -2388,6 +2388,62 @@
EXPECT_FALSE(has_channel);
}
+// Verifies that a munged local re-offer can not change the MID of a
+// transceiver that is already associated with an m= section.
+TEST_F(SdpOfferAnswerTest, RejectsMidChangeOfAssociatedTransceiver) {
+ auto caller = CreatePeerConnection();
+ auto callee = CreatePeerConnection();
+
+ // Negotiate without BUNDLE so that each m= section gets its own transport.
+ auto negotiate = [&] {
+ auto offer = caller->CreateOffer();
+ ASSERT_THAT(offer, NotNull());
+ offer->description()->RemoveGroupByName(GROUP_TYPE_BUNDLE);
+ ASSERT_TRUE(caller->SetLocalDescription(offer->Clone()));
+ ASSERT_TRUE(callee->SetRemoteDescription(std::move(offer)));
+ auto answer = callee->CreateAnswerAndSetAsLocal();
+ ASSERT_THAT(answer, NotNull());
+ ASSERT_TRUE(caller->SetRemoteDescription(std::move(answer)));
+ };
+
+ auto set_mid = [](SessionDescriptionInterface* sdesc, size_t index,
+ absl::string_view mid) {
+ SessionDescription* desc = sdesc->description();
+ desc->contents()[index].set_mid(mid);
+ desc->transport_infos()[index].content_name = std::string(mid);
+ };
+
+ caller->AddTransceiver(MediaType::AUDIO);
+ scoped_refptr<RtpTransceiverInterface> stopped_transceiver =
+ caller->AddTransceiver(MediaType::AUDIO);
+ ASSERT_NO_FATAL_FAILURE(negotiate());
+
+ // Reject the second m= section in both the local and remote descriptions.
+ ASSERT_TRUE(stopped_transceiver->StopStandard().ok());
+ ASSERT_NO_FATAL_FAILURE(negotiate());
+ ASSERT_THAT(caller->pc()->GetTransceivers(), SizeIs(1));
+
+ // A new transceiver recycles the rejected m= section and gets associated
+ // with it (and gets a channel) by a local offer.
+ scoped_refptr<RtpTransceiverInterface> recycled_transceiver =
+ caller->AddTransceiver(MediaType::AUDIO);
+ auto offer = caller->CreateOffer();
+ ASSERT_THAT(offer, NotNull());
+ ASSERT_THAT(offer->description()->contents(), SizeIs(2));
+ offer->description()->RemoveGroupByName(GROUP_TYPE_BUNDLE);
+ ASSERT_TRUE(caller->SetLocalDescription(std::move(offer)));
+ const std::optional<std::string> recycled_mid = recycled_transceiver->mid();
+ ASSERT_TRUE(recycled_mid.has_value());
+
+ // Munged re-offer that changes the MID of the recycled m= section.
+ auto renamed_offer = caller->CreateOffer();
+ ASSERT_THAT(renamed_offer, NotNull());
+ renamed_offer->description()->RemoveGroupByName(GROUP_TYPE_BUNDLE);
+ set_mid(renamed_offer.get(), 1, "renamed");
+ EXPECT_FALSE(caller->SetLocalDescription(std::move(renamed_offer)));
+ EXPECT_EQ(recycled_transceiver->mid(), recycled_mid);
+}
+
TEST_F(SdpOfferAnswerTest, SubsequentOfferDoesNotAddSctpInit) {
auto pc1 = CreatePeerConnection("WebRTC-Sctp-Snap/Enabled/");
auto pc2 = CreatePeerConnection("WebRTC-Sctp-Snap/Enabled/");
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/pc/sdp_offer_answer_unittest.cc b/pc/sdp_offer_answer_unittest.cc
index 82bda92..cb5ad60 100644
--- a/pc/sdp_offer_answer_unittest.cc
+++ b/pc/sdp_offer_answer_unittest.cc
@@ -2388,6 +2388,62 @@
EXPECT_FALSE(has_channel);
}
+// Verifies that a munged local re-offer can not change the MID of a
+// transceiver that is already associated with an m= section.
+TEST_F(SdpOfferAnswerTest, RejectsMidChangeOfAssociatedTransceiver) {
+ auto caller = CreatePeerConnection();
+ auto callee = CreatePeerConnection();
+
+ // Negotiate without BUNDLE so that each m= section gets its own transport.
+ auto negotiate = [&] {
+ auto offer = caller->CreateOffer();
+ ASSERT_THAT(offer, NotNull());
+ offer->description()->RemoveGroupByName(GROUP_TYPE_BUNDLE);
+ ASSERT_TRUE(caller->SetLocalDescription(offer->Clone()));
+ ASSERT_TRUE(callee->SetRemoteDescription(std::move(offer)));
+ auto answer = callee->CreateAnswerAndSetAsLocal();
+ ASSERT_THAT(answer, NotNull());
+ ASSERT_TRUE(caller->SetRemoteDescription(std::move(answer)));
+ };
+
+ auto set_mid = [](SessionDescriptionInterface* sdesc, size_t index,
+ absl::string_view mid) {
+ SessionDescription* desc = sdesc->description();
+ desc->contents()[index].set_mid(mid);
+ desc->transport_infos()[index].content_name = std::string(mid);
+ };
+
+ caller->AddTransceiver(MediaType::AUDIO);
+ scoped_refptr<RtpTransceiverInterface> stopped_transceiver =
+ caller->AddTransceiver(MediaType::AUDIO);
+ ASSERT_NO_FATAL_FAILURE(negotiate());
+
+ // Reject the second m= section in both the local and remote descriptions.
+ ASSERT_TRUE(stopped_transceiver->StopStandard().ok());
+ ASSERT_NO_FATAL_FAILURE(negotiate());
+ ASSERT_THAT(caller->pc()->GetTransceivers(), SizeIs(1));
+
+ // A new transceiver recycles the rejected m= section and gets associated
+ // with it (and gets a channel) by a local offer.
+ scoped_refptr<RtpTransceiverInterface> recycled_transceiver =
+ caller->AddTransceiver(MediaType::AUDIO);
+ auto offer = caller->CreateOffer();
+ ASSERT_THAT(offer, NotNull());
+ ASSERT_THAT(offer->description()->contents(), SizeIs(2));
+ offer->description()->RemoveGroupByName(GROUP_TYPE_BUNDLE);
+ ASSERT_TRUE(caller->SetLocalDescription(std::move(offer)));
+ const std::optional<std::string> recycled_mid = recycled_transceiver->mid();
+ ASSERT_TRUE(recycled_mid.has_value());
+
+ // Munged re-offer that changes the MID of the recycled m= section.
+ auto renamed_offer = caller->CreateOffer();
+ ASSERT_THAT(renamed_offer, NotNull());
+ renamed_offer->description()->RemoveGroupByName(GROUP_TYPE_BUNDLE);
+ set_mid(renamed_offer.get(), 1, "renamed");
+ EXPECT_FALSE(caller->SetLocalDescription(std::move(renamed_offer)));
+ EXPECT_EQ(recycled_transceiver->mid(), recycled_mid);
+}
+
TEST_F(SdpOfferAnswerTest, SubsequentOfferDoesNotAddSctpInit) {
auto pc1 = CreatePeerConnection("WebRTC-Sctp-Snap/Enabled/");
auto pc2 = CreatePeerConnection("WebRTC-Sctp-Snap/Enabled/");
Loading diff…
Original Bug Report
The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.
References
On This Page