Medium firefox Logic Error 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
Impactmoderate
DescriptionDenial-of-service in the WebRTC: Signaling component
ComponentDOM
Bug ClassLogic Error
Tracker2014868
Fix commitf4cef2bfa7d6 (firefox) +96/-5
CISA KEVNot listed
CreditedEvyatar Ben Asher, Keane Lucas, Nicholas Carlini, Newton Cheng, Daniel Freeman, Alex Gaynor, and Joel Weinberger using Claude from Anthropic
Disclosed2026-03-24

Changed Functions

FunctionChangeNotes
if
dom/media/webrtc/tests/crashtests/2014868.html
modified
for
dom/media/webrtc/tests/crashtests/2014868.html
modified

Files Changed

  • dom/media/webrtc/jsep/JsepSessionImpl.cpp
  • dom/media/webrtc/tests/crashtests/2014868.html
  • dom/media/webrtc/tests/crashtests/crashtests.list
diff --git a/dom/media/webrtc/jsep/JsepSessionImpl.cpp b/dom/media/webrtc/jsep/JsepSessionImpl.cpp
index 709cc2f7808..c20ee9afb20 100644
--- a/dom/media/webrtc/jsep/JsepSessionImpl.cpp
+++ b/dom/media/webrtc/jsep/JsepSessionImpl.cpp
@@ -1919,17 +1919,17 @@ nsresult JsepSessionImpl::ValidateRemoteDescription(const Sdp& description) {
     const SdpMediaSection& oldMsection =
         mCurrentRemoteDescription->GetMediaSection(i);
 
-    if (mSdpHelper.MsectionIsDisabled(newMsection) ||
-        mSdpHelper.MsectionIsDisabled(oldMsection)) {
-      continue;
-    }
-
     if (oldMsection.GetMediaType() != newMsection.GetMediaType()) {
       JSEP_SET_ERROR("Remote description changes the media type of m-line "
                      << i);
       return NS_ERROR_INVALID_ARG;
     }
 
+    if (mSdpHelper.MsectionIsDisabled(newMsection) ||
+        mSdpHelper.MsectionIsDisabled(oldMsection)) {
+      continue;
+    }
+
     bool differ = mSdpHelper.IceCredentialsDiffer(newMsection, oldMsection);
 
     if (mIsPendingOfferer.isSome() && *mIsPendingOfferer && differ &&
diff --git a/dom/media/webrtc/tests/crashtests/2014868.html b/dom/media/webrtc/tests/crashtests/2014868.html
new file mode 100644
index 00000000000..6ccf59568a0
--- /dev/null
+++ b/dom/media/webrtc/tests/crashtests/2014868.html
@@ -0,0 +1,90 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<head>
+  <meta charset="utf-8">
+  <title>Bug 2014868 - Reject SDP when MID changes at existing m-line index</title>
+</head>
+<body>
+<script>
+async function run() {
+  try {
+    const pc1 = new RTCPeerConnection();
+    const pc2 = new RTCPeerConnection();
+
+    pc1.addTransceiver("audio", { direction: "sendrecv" });
+    pc1.addTransceiver("video", { direction: "recvonly" });
+    pc1.createDataChannel("test");
+
+    let offer = await pc1.createOffer();
+    await pc1.setLocalDescription(offer);
+    await pc2.setRemoteDescription(offer);
+    let answer = await pc2.createAnswer();
+    await pc2.setLocalDescription(answer);
+    await pc1.setRemoteDescription(answer);
+
+    // Stop video transceiver to trigger recycling logic
+    for (const t of pc1.getTransceivers()) {
+      if (t.receiver.track.kind === "video") {
+        t.stop();
+      }
+    }
+
+    offer = await pc1.createOffer();
+    await pc1.setLocalDescription(offer);
+    await pc2.setRemoteDescription(offer);
+    answer = await pc2.createAnswer();
+    await pc2.setLocalDescription(answer);
+    await pc1.setRemoteDescription(answer);
+
+    // Munge the offer: insert a duplicate video m-line at the application m-line
+    // position. This displaces the application section and creates a media type
+    // mismatch at that index, testing that we reject invalid type changes.
+    const sdpLines = offer.sdp.split("\r\n");
+    let mLineCount = 0;
+    let insertIndex = -1;
+    for (let i = 0; i < sdpLines.length; i++) {
+      if (sdpLines[i].startsWith("m=")) {
+        mLineCount++;
+        if (mLineCount === 2) {
+          insertIndex = i;
+        }
+      }
+    }
+
+    // Duplicate the second m-line section with a changed MID
+    if (insertIndex !== -1) {
+      let endIndex = insertIndex + 1;
+      for (let i = insertIndex + 1; i < sdpLines.length; i++) {
+        if (sdpLines[i].startsWith("m=")) {
+          endIndex = i;
+          break;
+        }
+      }
+      if (endIndex === sdpLines.length) endIndex = sdpLines.length;
+
+      const mlineSection = sdpLines.slice(insertIndex, endIndex);
+      // Change the MID in the copied section
+      for (let i = 0; i < mlineSection.length; i++) {
+        if (mlineSection[i].startsWith("a=mid:")) {
+          mlineSection[i] = "a=mid:changed-mid";
+          break;
+        }
+      }
+      sdpLines.splice(endIndex, 0, ...mlineSection);
+    }
+
+    const modifiedAnswer = { type: "offer", sdp: sdpLines.join("\r\n") };
+    await pc1.setRemoteDescription(modifiedAnswer);
+
+    pc1.close();
+    pc2.close();
+  } catch (e) {
+  }
+
+  document.documentElement.removeAttribute("class");
+}
+
+run();
+</script>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/crashtests/crashtests.list b/dom/media/webrtc/tests/crashtests/crashtests.list
index f2c3444137e..26774133e8d 100644
--- a/dom/media/webrtc/tests/crashtests/crashtests.list
+++ b/dom/media/webrtc/tests/crashtests/crashtests.list
@@ -16,4 +16,5 @@ skip-if(Android) skip-if(cocoaWidget&&/^15\.30$/.test(os_version)) load 1991492_
 skip-if(Android) skip-if(cocoaWidget&&/^15\.30$/.test(os_version)) load 1991494.html # No screenshare on Android, macOS: bug 1984994
 pref(media.navigator.streams.fake,false) pref(media.getusermedia.camera.fake.force,true) load 2009260.html
 pref(media.navigator.permission.device,true) pref(media.audio_loopback_dev,"real-device-request-with-clear-cache") load getUserMedia-audio.html # bug 1767893
+load 2014868.html
 load pc-video-size1.html
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/dom/media/webrtc/tests/crashtests/2014868.html b/dom/media/webrtc/tests/crashtests/2014868.html
new file mode 100644
index 00000000000..6ccf59568a0
--- /dev/null
+++ b/dom/media/webrtc/tests/crashtests/2014868.html
@@ -0,0 +1,90 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<head>
+  <meta charset="utf-8">
+  <title>Bug 2014868 - Reject SDP when MID changes at existing m-line index</title>
+</head>
+<body>
+<script>
+async function run() {
+  try {
+    const pc1 = new RTCPeerConnection();
+    const pc2 = new RTCPeerConnection();
+
+    pc1.addTransceiver("audio", { direction: "sendrecv" });
+    pc1.addTransceiver("video", { direction: "recvonly" });
+    pc1.createDataChannel("test");
+
+    let offer = await pc1.createOffer();
+    await pc1.setLocalDescription(offer);
+    await pc2.setRemoteDescription(offer);
+    let answer = await pc2.createAnswer();
+    await pc2.setLocalDescription(answer);
+    await pc1.setRemoteDescription(answer);
+
+    // Stop video transceiver to trigger recycling logic
+    for (const t of pc1.getTransceivers()) {
+      if (t.receiver.track.kind === "video") {
+        t.stop();
+      }
+    }
+
+    offer = await pc1.createOffer();
+    await pc1.setLocalDescription(offer);
+    await pc2.setRemoteDescription(offer);
+    answer = await pc2.createAnswer();
+    await pc2.setLocalDescription(answer);
+    await pc1.setRemoteDescription(answer);
+
+    // Munge the offer: insert a duplicate video m-line at the application m-line
+    // position. This displaces the application section and creates a media type
+    // mismatch at that index, testing that we reject invalid type changes.
+    const sdpLines = offer.sdp.split("\r\n");
+    let mLineCount = 0;
+    let insertIndex = -1;
+    for (let i = 0; i < sdpLines.length; i++) {
+      if (sdpLines[i].startsWith("m=")) {
+        mLineCount++;
+        if (mLineCount === 2) {
+          insertIndex = i;
+        }
+      }
+    }
+
+    // Duplicate the second m-line section with a changed MID
+    if (insertIndex !== -1) {
+      let endIndex = insertIndex + 1;
+      for (let i = insertIndex + 1; i < sdpLines.length; i++) {
+        if (sdpLines[i].startsWith("m=")) {
+          endIndex = i;
+          break;
+        }
+      }
+      if (endIndex === sdpLines.length) endIndex = sdpLines.length;
+
+      const mlineSection = sdpLines.slice(insertIndex, endIndex);
+      // Change the MID in the copied section
+      for (let i = 0; i < mlineSection.length; i++) {
+        if (mlineSection[i].startsWith("a=mid:")) {
+          mlineSection[i] = "a=mid:changed-mid";
+          break;
+        }
+      }
+      sdpLines.splice(endIndex, 0, ...mlineSection);
+    }
+
+    const modifiedAnswer = { type: "offer", sdp: sdpLines.join("\r\n") };
+    await pc1.setRemoteDescription(modifiedAnswer);
+
+    pc1.close();
+    pc2.close();
+  } catch (e) {
+  }
+
+  document.documentElement.removeAttribute("class");
+}
+
+run();
+</script>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/crashtests/crashtests.list b/dom/media/webrtc/tests/crashtests/crashtests.list
index f2c3444137e..26774133e8d 100644
--- a/dom/media/webrtc/tests/crashtests/crashtests.list
+++ b/dom/media/webrtc/tests/crashtests/crashtests.list
@@ -16,4 +16,5 @@ skip-if(Android) skip-if(cocoaWidget&&/^15\.30$/.test(os_version)) load 1991492_
 skip-if(Android) skip-if(cocoaWidget&&/^15\.30$/.test(os_version)) load 1991494.html # No screenshare on Android, macOS: bug 1984994
 pref(media.navigator.streams.fake,false) pref(media.getusermedia.camera.fake.force,true) load 2009260.html
 pref(media.navigator.permission.device,true) pref(media.audio_loopback_dev,"real-device-request-with-clear-cache") load getUserMedia-audio.html # bug 1767893
+load 2014868.html
 load pc-video-size1.html
Loading diff…