sdp munging: forbid removing a content with SDP munging

This is now disabled by default, guarded by
  WebRTC-NoSdpMangleNumberOfContents
which can be disabled if necessary.

BUG=webrtc:40567530

Change-Id: I02a3d4d21678f41e0910144be1aa159828c40757
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/387560
Reviewed-by: Evan Shrubsole <eshr@webrtc.org>
Reviewed-by: Harald Alvestrand <hta@webrtc.org>
Commit-Queue: Philipp Hancke <phancke@meta.com>
Cr-Commit-Position: refs/heads/main@{#44517}
diff --git a/experiments/field_trials.py b/experiments/field_trials.py
index dff2fe0..bf6b833 100755
--- a/experiments/field_trials.py
+++ b/experiments/field_trials.py
@@ -122,6 +122,9 @@
     FieldTrial('WebRTC-NoSdpMangleUfragRestrictedAddresses',
                409713509,
                date(2025, 10, 11)),
+    FieldTrial('WebRTC-NoSdpMangleNumberOfContents',
+               40567530,
+               date(2025, 10, 11)),
     FieldTrial('WebRTC-Pacer-FastRetransmissions',
                40235589,
                date(2024, 4, 1)),
diff --git a/pc/sdp_offer_answer.cc b/pc/sdp_offer_answer.cc
index 31808c5..6f19d1b 100644
--- a/pc/sdp_offer_answer.cc
+++ b/pc/sdp_offer_answer.cc
@@ -2473,11 +2473,20 @@
   SdpMungingType sdp_munging_type =
       DetermineSdpMungingType(desc.get(), last_created_desc);
 
-  if (!disable_sdp_munging_checks_ &&
-      HasUfragSdpMunging(desc.get(), last_created_desc)) {
-    has_sdp_munged_ufrag_ = true;
-    if (pc_->trials().IsEnabled("WebRTC-NoSdpMangleUfrag")) {
-      RTC_LOG(LS_ERROR) << "Rejecting SDP because of ufrag modification";
+  if (!disable_sdp_munging_checks_) {
+    bool reject_error = false;
+    if (HasUfragSdpMunging(desc.get(), last_created_desc)) {
+      has_sdp_munged_ufrag_ = true;
+      if (pc_->trials().IsEnabled("WebRTC-NoSdpMangleUfrag")) {
+        RTC_LOG(LS_ERROR) << "Rejecting SDP because of ufrag modification";
+        reject_error = true;
+      }
+    } else if (sdp_munging_type == kNumberOfContents &&
+               !pc_->trials().IsDisabled(
+                   "WebRTC-NoSdpMangleNumberOfContents")) {
+      reject_error = true;
+    }
+    if (reject_error) {
       observer->OnSetLocalDescriptionComplete(
           RTCError(RTCErrorType::INVALID_MODIFICATION,
                    "SDP is modified in a non-acceptable way"));
diff --git a/pc/sdp_offer_answer_unittest.cc b/pc/sdp_offer_answer_unittest.cc
index c47f086..3fe6e19 100644
--- a/pc/sdp_offer_answer_unittest.cc
+++ b/pc/sdp_offer_answer_unittest.cc
@@ -2089,7 +2089,7 @@
       ElementsAre(Pair(SdpMungingType::kDtlsSetup, 1)));
 }
 
-TEST_F(SdpOfferAnswerMungingTest, RemoveContent) {
+TEST_F(SdpOfferAnswerMungingTest, RemoveContentDefault) {
   auto pc = CreatePeerConnection();
   pc->AddAudioTrack("audio_track", {});
 
@@ -2105,6 +2105,29 @@
       absl::StrReplaceAll(sdp, {{"a=group:BUNDLE " + name, "a=group:BUNDLE"}}));
 
   RTCError error;
+  EXPECT_FALSE(pc->SetLocalDescription(std::move(modified_offer), &error));
+  EXPECT_THAT(
+      metrics::Samples("WebRTC.PeerConnection.SdpMunging.Offer.Initial"),
+      ElementsAre(Pair(SdpMungingType::kNumberOfContents, 1)));
+}
+
+TEST_F(SdpOfferAnswerMungingTest, RemoveContentKillswitch) {
+  auto pc = CreatePeerConnection(FieldTrials::CreateNoGlobal(
+      "WebRTC-NoSdpMangleNumberOfContents/Disabled/"));
+  pc->AddAudioTrack("audio_track", {});
+
+  auto offer = pc->CreateOffer();
+  auto& contents = offer->description()->contents();
+  ASSERT_EQ(contents.size(), 1u);
+  auto name = contents[0].mid();
+  EXPECT_TRUE(offer->description()->RemoveContentByName(contents[0].mid()));
+  std::string sdp;
+  offer->ToString(&sdp);
+  auto modified_offer = CreateSessionDescription(
+      SdpType::kOffer,
+      absl::StrReplaceAll(sdp, {{"a=group:BUNDLE " + name, "a=group:BUNDLE"}}));
+
+  RTCError error;
   EXPECT_TRUE(pc->SetLocalDescription(std::move(modified_offer), &error));
   EXPECT_THAT(
       metrics::Samples("WebRTC.PeerConnection.SdpMunging.Offer.Initial"),