High firefox Logic Error 🔧 Commit mapped

Overview

High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Impacthigh
DescriptionPrivilege escalation due to use-after-free in the WebRTC: Signaling component
ComponentDOM
Bug ClassLogic Error
Tracker2074202
Fix commit6e7cf87d85e7 (firefox) +1326/-904
CISA KEVNot listed
CreditedMozilla
Disclosed2026-10-27

Changed Functions

FunctionChangeNotes
MediaTransportHandlerSTS
dom/media/webrtc/jsapi/MediaTransportHandler.cpp
modified
MediaTransportImpl
dom/media/webrtc/jsapi/MediaTransportHandler.cpp
modified
STSShutdownHandler
dom/media/webrtc/jsapi/MediaTransportHandler.cpp
modified
MediaTransportImplNicer
dom/media/webrtc/jsapi/MediaTransportHandler.cpp
modified
if
dom/media/webrtc/jsapi/MediaTransportHandler.cpp
modified
MediaTransportHandlerLocal
dom/media/webrtc/jsapi/MediaTransportHandler.cpp
modified
Post
dom/media/webrtc/jsapi/MediaTransportHandler.cpp
modified

Files Changed

  • dom/media/webrtc/jsapi/MediaTransportHandler.cpp
  • dom/media/webrtc/jsapi/MediaTransportHandler.h
  • dom/media/webrtc/jsapi/MediaTransportHandlerIPC.cpp
  • dom/media/webrtc/jsapi/MediaTransportHandlerIPC.h
  • dom/media/webrtc/jsapi/moz.build
  • dom/media/webrtc/tests/mochitests/mochitest_peerconnection.toml
  • dom/media/webrtc/tests/mochitests/test_peerConnection_offline.html
  • media/webrtc/signaling/gtest/mediapipeline_unittest.cpp
  • media/webrtc/signaling/gtest/mediatransporthandler_unittest.cpp
  • media/webrtc/signaling/gtest/moz.build
diff --git a/dom/media/webrtc/jsapi/MediaTransportHandler.cpp b/dom/media/webrtc/jsapi/MediaTransportHandler.cpp
index ea6b829529b..b22712ae96c 100644
--- a/dom/media/webrtc/jsapi/MediaTransportHandler.cpp
+++ b/dom/media/webrtc/jsapi/MediaTransportHandler.cpp
@@ -36,10 +36,13 @@
 #include "mozilla/ProfilerMarkers.h"
 #include "mozilla/PublicSSL.h"  // For psm::InitializeCipherSuite
 #include "mozilla/ReverseIterator.h"
+#include "mozilla/StopGapEventTarget.h"
+#include "mozilla/UniquePtr.h"
 #include "mozilla/dom/RTCStatsReportBinding.h"
 #include "nsDNSService2.h"
 #include "nsFmtString.h"
 #include "nsISocketTransportService.h"
+#include "nsSocketTransportService2.h"
 #include "nss.h"  // For NSS_NoDB_Init
 #include "sdp/SdpAttribute.h"
 #include "transport/runnable_utils.h"
@@ -62,20 +65,100 @@ static const char* mthLogTag = "MediaTransportHandler";
 // missing keyframe self-heals via PLI/FIR.
 static constexpr uint32_t kOldFlowGraceMs = 2000;
 
-class MediaTransportHandlerSTS : public MediaTransportHandler,
-                                 public sigslot::has_slots<> {
+using ParsedIceServer = IceServerParser::ParsedIceServer;
+using IceLogPromise = MediaTransportHandler::IceLogPromise;
+
+// An underlying implementation wrapped by MediaTransportHandlerLocal. Unlike
+// MediaTransportHandler, this is not required to be threadsafe or refcounted.
+// Everything except Init must be run on the event target Init returns.
+class MediaTransportImpl {
+ public:
+  // MediaTransportEvents is how this will communicate with the wrapping
+  // MediaTransportHandlerLocal; this decouples the lifecycles of wrapper/impl.
+  explicit MediaTransportImpl(RefPtr<MediaTransportEvents> aEvents)
+      : mEvents(aEvents) {}
+  virtual ~MediaTransportImpl() = default;
+
+  // Called once, on any thread. Returns the event target every other call must
+  // be dispatched to, including our destruction (eg; a no-op runnable carrying
+  // a UniquePtr to us). The contract on this event target is as follows:
+  // 1 The event target either runs or destroys runnables in the order they
+  //   were dispatched. If Dispatch returns an error, this counts as
+  //   destruction of the runnable; all previously dispatched runnables must
+  //   have already been run, destroyed, or leaked. This is all part of the
+  //   contract of nsISerialEventTarget, but it bears repeating here.
+  //   This is what makes it safe to carry bareptrs (MediaTransportImpl*) in
+  //   these runnables, because the last runnable is the one that destroys us.
+  // 2 If this class must be destroyed on the event target, the event target
+  //   must be accepting and running runnables. In other words, if the event
+  //   target will stop accepting runnables (eg; STS shutdown), the impl must
+  //   make itself safe to destroy on any thread *before* that happens.
+  // 3 The event target will accept runnables right away, even if it needs to
+  //   perform async init before it begins actually running them.
+  virtual already_AddRefed<nsISerialEventTarget> Init() = 0;
+
+  virtual void CreateIceCtx(const std::string& aName) = 0;
+
+  virtual RefPtr<IceLogPromise> GetIceLog(const nsCString& aPattern) = 0;
+  virtual void ClearIceLog() = 0;
+  virtual void EnterPrivateMode() = 0;
+  virtual void ExitPrivateMode() = 0;
+
+  virtual void SetIceConfig(nsTArray<ParsedIceServer>&& aIceServers,
+                            dom::RTCIceTransportPolicy aIcePolicy) = 0;
+  virtual void SetProxyConfig(NrSocketProxyConfig&& aProxyConfig) = 0;
+  virtual void EnsureProvisionalTransport(const std::string& aTransportId,
+                                          const std::string& aUfrag,
+                                          const std::string& aPwd,
+                                          int aComponentCount) = 0;
+  virtual void SetTargetForDefaultLocalAddressLookup(
+      const std::string& aTargetIp, uint16_t aTargetPort) = 0;
+  virtual void StartIceGathering(bool aDefaultRouteOnly,
+                                 bool aObfuscateHostAddresses,
+                                 const nsTArray<NrIceStunAddr>& aStunAddrs) = 0;
+  virtual void ActivateTransport(
+      const std::string& aTransportId, const std::string& aLocalUfrag,
+      const std::string& aLocalPwd, size_t aComponentCount,
+      const std::string& aUfrag, const std::string& aPassword,
+      const nsTArray<uint8_t>& aKeyDer, const nsTArray<uint8_t>& aCertDer,
+      SSLKEAType aAuthType, bool aDtlsClient, const DtlsDigestList& aDigests,
+      bool aPrivacyRequested) = 0;
+  virtual void RemoveTransportsExcept(
+      const std::set<std::string>& aTransportIds) = 0;
+  virtual void StartIceChecks(bool aIsControlling,
+                              const std::vector<std::string>& aIceOptions) = 0;
+  virtual void AddIceCandidate(const std::string& aTransportId,
+                               const std::string& aCandidate,
+                               const std::string& aUfrag,
+                               const std::string& aResolvedAddress) = 0;
+  virtual void UpdateNetworkState(bool aOnline) = 0;
+  virtual void SendPacket(const std::string& aTransportId,
+                          MediaPacket&& aPacket) = 0;
+  virtual RefPtr<dom::RTCStatsPromise> GetIceStats(
+      const std::string& aTransportId, DOMHighResTimeStamp aNow) = 0;
+
+ protected:
+  const RefPtr<MediaTransportEvents> mEvents;
+};
+
+class STSShutdownHandler;
+
+class MediaTransportImplNicer final : public MediaTransportImpl,
+                                      public sigslot::has_slots<> {
  public:
-  explicit MediaTransportHandlerSTS();
+  explicit MediaTransportImplNicer(RefPtr<MediaTransportEvents> aEvents);
+  ~MediaTransportImplNicer();
+
+  already_AddRefed<nsISerialEventTarget> Init() override;
+  void CreateIceCtx(const std::string& aName) override;
 
   RefPtr<IceLogPromise> GetIceLog(const nsCString& aPattern) override;
   void ClearIceLog() override;
   void EnterPrivateMode() override;
   void ExitPrivateMode() override;
 
-  void CreateIceCtx(const std::string& aName) override;
-
-  nsresult SetIceConfig(const nsTArray<dom::RTCIceServer>& aIceServers,
-                        dom::RTCIceTransportPolicy aIcePolicy) override;
+  void SetIceConfig(nsTArray<ParsedIceServer>&& aIceServers,
+                    dom::RTCIceTransportPolicy aIcePolicy) override;
 
   // We will probably be able to move the proxy lookup stuff into
   // this class once we move mtransport to its own process.
@@ -125,12 +208,16 @@ class MediaTransportHandlerSTS : public MediaTransportHandler,
   RefPtr<dom::RTCStatsPromise> GetIceStats(const std::string& aTransportId,
                                            DOMHighResTimeStamp aNow) override;
 
-  void Shutdown();
+  // Releases everything that must be released on STS. Afterwards, this may be
+  // destroyed on any thread. Called by STSShutdownHandler, and on destruction.
+  // This is how we satisfy item 2 in the contract on Init().
+  void DeinitSts();
 
  private:
-  void Destroy() override;
-  void DestroyFinal();
-  void Shutdown_s();
+  void InitMain(const RefPtr<StopGapEventTarget>& aTarget);
+  void InitSts(const RefPtr<STSShutdownHandler>& aHandler,
+               const NrIceCtx::GlobalConfig& aConfig,
+               const RefPtr<StopGapEventTarget>& aTarget);
   RefPtr<TransportFlow> CreateTransportFlow(
       const std::string& aTransportId, bool aIsRtcp,
       const RefPtr<DtlsIdentity>& aDtlsIdentity, bool aDtlsClient,
@@ -144,6 +231,12 @@ class MediaTransportHandlerSTS : public MediaTransportHandler,
     Transport() = default;
     Transport(const Transport&) = delete;
     Transport& operator=(const Transport&) = delete;
+    // The close timer's callback holds a plain pointer to us.
+    ~Transport() {
+      if (mCloseTimer) {
+        mCloseTimer->Cancel();
+      }
+    }
 
     RefPtr<TransportFlow> GetCurrent(bool aIsRtcp) const {
       return (aIsRtcp && mRtcpFlow) ? mRtcpFlow : mFlow;
@@ -185,7 +278,7 @@ class MediaTransportHandlerSTS : public MediaTransportHandler,
     // These are set during DTLS restart to avoid interrupting media flow.
     RefPtr<TransportFlow> mOldFlow;
     RefPtr<TransportFlow> mOldRtcpFlow;
-    // Used by MediaTransportHandlerSTS to schedule closure of old flows.
+    // Used by MediaTransportImplNicer to schedule closure of old flows.
     // This class is not very suitable for scheduling these timers itself,
     // because it is not refcounted.
     nsCOMPtr<nsITimer> mCloseTimer;
@@ -222,16 +315,6 @@ class MediaTransportHandlerSTS : public MediaTransportHandler,
     std::string mReportedAlpn;
   };
 
-  using MediaTransportHandler::OnAlpnNegotiated;
-  using MediaTransportHandler::OnCandidate;
-  using MediaTransportHandler::OnCandidateError;
-  using MediaTransportHandler::OnConnectionStateChange;
-  using MediaTransportHandler::OnEncryptedSending;
-  using MediaTransportHandler::OnGatheringStateChange;
-  using MediaTransportHandler::OnPacketReceived;
-  using MediaTransportHandler::OnRtcpStateChange;
-  using MediaTransportHandler::OnStateChange;
-
   void OnGatheringStateChange(const std::string& aTransportId,
                               NrIceMediaStream::GatheringState aState);
   void OnConnectionStateChange(NrIceMediaStream* aIceStream,
@@ -265,11 +348,16 @@ class MediaTransportHandlerSTS : public MediaTransportHandler,
                    dom::RTCStatsCollection* aStats,
                    dom::RTCTransportStats& aTransport) const;
 
-  virtual ~MediaTransportHandlerSTS() = default;
   nsCOMPtr<nsISerialEventTarget> mStsThread;
+  // Only set if we registered with it, only touched on STS.
+  RefPtr<STSShutdownHandler> mShutdownHandler;
   RefPtr<NrIceCtx> mIceCtx;
   RefPtr<NrIceResolver> mDNSResolver;
   std::map<std::string, Transport> mTransports;
+  // We should not need this if STS is keeping its end of the contract around
+  // shutdown, but we're going to double-check just in case. If STS has
+  // messed up, we'll convert the bug into a safe crash.
+  std::atomic<bool> mSTSOnly{false};
   bool mHideLocalPrflx = false;
   bool mTurnDisabled = false;
   uint32_t mMinDtlsVersion = 0;
@@ -280,10 +368,173 @@ class MediaTransportHandlerSTS : public MediaTransportHandler,
   Maybe<NrIceCtx::NatSimulatorConfig> mNatConfig;
 
   std::set<std::string> mSignaledAddresses;
+};
 
-  // Init can only be done on main, but we want this to be usable on any thread
-  using InitPromise = MozPromise<bool, std::string, false>;
-  RefPtr<InitPromise> mInitPromise;
+// Refcounted front for a MediaTransportImpl, usable on any thread. Every call
+// is dispatched to the implementation's event target, and so is the
+// implementation's destruction, which therefore happens after every call that
+// could still act on it.
+class MediaTransportHandlerLocal final : public MediaTransportHandler {
+ public:
+  MediaTransportHandlerLocal()
+      : mImpl(MakeUnique<MediaTransportImplNicer>(mEvents)),
+        mTarget(mImpl->Init()) {}
+
+  RefPtr<IceLogPromise> GetIceLog(const nsCString& aPattern) override {
+    return InvokeAsync(mTarget, __func__, [impl = mImpl.get(), aPattern] {
+      return impl->GetIceLog(aPattern);
+    });
+  }
+
+  void ClearIceLog() override {
+    Post(__func__, [](MediaTransportImpl& aImpl) { aImpl.ClearIceLog(); });
+  }
+
+  void EnterPrivateMode() override {
+    Post(__func__, [](MediaTransportImpl& aImpl) { aImpl.EnterPrivateMode(); });
+  }
+
+  void ExitPrivateMode() override {
+    Post(__func__, [](MediaTransportImpl& aImpl) { aImpl.ExitPrivateMode(); });
+  }
+
+  void CreateIceCtx(const std::string& aName) override {
+    Post(__func__,
+         [aName](MediaTransportImpl& aImpl) { aImpl.CreateIceCtx(aName); });
+  }
+
+  nsresult SetIceConfig(const nsTArray<dom::RTCIceServer>& aIceServers,
+                        dom::RTCIceTransportPolicy aIcePolicy) override {
+    auto result = IceServerParser::Parse(aIceServers);
+    if (result.isErr()) {
+      // Discard the detailed ErrorResult; callers at this level use nsresult.
+      result.unwrapErr().SuppressException();
+      return NS_ERROR_FAILURE;
+    }
+    Post(__func__, [entries = result.unwrap(),
+                    aIcePolicy](MediaTransportImpl& aImpl) mutable {
+      aImpl.SetIceConfig(std::move(entries), aIcePolicy);
+    });
+    return NS_OK;
+  }
+
+  void SetProxyConfig(NrSocketProxyConfig&& aProxyConfig) override {
+    Post(__func__, [aProxyConfig = std::move(aProxyConfig)](
+                       MediaTransportImpl& aImpl) mutable {
+      aImpl.SetProxyConfig(std::move(aProxyConfig));
+    });
+  }
+
+  void EnsureProvisionalTransport(const std::string& aTransportId,
+                                  const std::string& aUfrag,
+                                  const std::string& aPwd,
+                                  int aComponentCount) override {
+    Post(__func__, [=](MediaTransportImpl& aImpl) {
+      aImpl.EnsureProvisionalTransport(aTransportId, aUfrag, aPwd,
+                                       aComponentCount);
+    });
+  }
+
+  void SetTargetForDefaultLocalAddressLookup(const std::string& aTargetIp,
+                                             uint16_t aTargetPort) override {
+    Post(__func__, [=](MediaTransportImpl& aImpl) {
+      aImpl.SetTargetForDefaultLocalAddressLookup(aTargetIp, aTargetPort);
+    });
+  }
+
+  void StartIceGathering(bool aDefaultRouteOnly, bool aObfuscateHostAddresses,
+                         const nsTArray<NrIceStunAddr>& aStunAddrs) override {
+    Post(__func__,
+         [=, stunAddrs = aStunAddrs.Clone()](MediaTransportImpl& aImpl) {
+           aImpl.StartIceGathering(aDefaultRouteOnly, aObfuscateHostAddresses,
+                                   stunAddrs);
+         });
+  }
+
+  void ActivateTransport(
+      const std::string& aTransportId, const std::string& aLocalUfrag,
+      const std::string& aLocalPwd, size_t aComponentCount,
+      const std::string& aUfrag, const std::string& aPassword,
+      const nsTArray<uint8_t>& aKeyDer, const nsTArray<uint8_t>& aCertDer,
+      SSLKEAType aAuthType, bool aDtlsClient, const DtlsDigestList& aDigests,
+      bool aPrivacyRequested) override {
+    Post(__func__, [=, keyDer = aKeyDer.Clone(),
+                    certDer = aCertDer.Clone()](MediaTransportImpl& aImpl) {
+      aImpl.ActivateTransport(aTransportId, aLocalUfrag, aLocalPwd,
+                              aComponentCount, aUfrag, aPassword, keyDer,
+                              certDer, aAuthType, aDtlsClient, aDigests,
+                              aPrivacyRequested);
+    });
+  }
+
+  void RemoveTransportsExcept(
+      const std::set<std::string>& aTransportIds) override {
+    Post(__func__, [=](MediaTransportImpl& aImpl) {
+      aImpl.RemoveTransportsExcept(aTransportIds);
+    });
+  }
+
+  void StartIceChecks(bool aIsControlling,
+                      const std::vector<std::string>& aIceOptions) override {
+    Post(__func__, [=](MediaTransportImpl& aImpl) {
+      aImpl.StartIceChecks(aIsControlling, aIceOptions);
+    });
+  }
+
+  void AddIceCandidate(const std::string& aTransportId,
+                       const std::string& aCandidate, const std::string& aUfrag,
+                       const std::string& aResolvedAddress) override {
+    Post(__func__, [=](MediaTransportImpl& aImpl) {
+      aImpl.AddIceCandidate(aTransportId, aCandidate, aUfrag, aResolvedAddress);
+    });
+  }
+
+  void UpdateNetworkState(bool aOnline) override {
+    Post(__func__,
+         [=](MediaTransportImpl& aImpl) { aImpl.UpdateNetworkState(aOnline); });
+  }
+
+  void SendPacket(const std::string& aTransportId,
+                  MediaPacket&& aPacket) override {
+    Post(__func__, [aTransportId, aPacket = std::move(aPacket)](
+                       MediaTransportImpl& aImpl) mutable {
+      aImpl.SendPacket(aTransportId, std::move(aPacket));
+    });
+  }
+
+  RefPtr<dom::RTCStatsPromise> GetIceStats(const std::string& aTransportId,
+                                           DOMHighResTimeStamp aNow) override {
+    return InvokeAsync(mTarget, __func__,
+                       [impl = mImpl.get(), aTransportId, aNow] {
+                         return impl->GetIceStats(aTransportId, aNow);
+                       });
+  }
+
+ private:
+  ~MediaTransportHandlerLocal() {
+    // If Dispatch fails, the runnable is synchronously destroyed, taking the
+    // impl with it. This is why we need item 2 in the contract on
+    // MediaTransportImpl::Init; the impl must become safe to destroy on any
+    // thread if the event target will stop accepting runnables.
+    (void)mTarget->Dispatch(
+        NS_NewRunnableFunction(
+            __func__, [impl = std::move(mImpl)]() mutable {}),
+        NS_DISPATCH_FALLIBLE);
+  }
+
+  template <typename Function>
+  void Post(const char* aName, Function&& aFunction) {
+    (void)mTarget->Dispatch(
+        NS_NewRunnableFunction(aName,
+                               [impl = mImpl.get(),
+                                function = std::forward<Function>(
+                                    aFunction)]() mutable { function(*impl); }),
+        NS_DISPATCH_FALLIBLE);
+  }
+
+  // Only released on destruction.
+  UniquePtr<MediaTransportImpl> mImpl;
+  const nsCOMPtr<nsISerialEventTarget> mTarget;
 };
 
 /* static */
@@ -294,87 +545,120 @@ already_AddRefed<MediaTransportHandler> MediaTransportHandler::Create() {
       StaticPrefs::network_process_enabled()) {
     result = MakeRefPtr<MediaTransportHandlerIPC>();
   } else {
-    result = MakeRefPtr<MediaTransportHandlerSTS>();
+    result = MakeRefPtr<MediaTransportHandlerLocal>();
   }
   result->Initialize();
   return result.forget();
 }
 
-class STSShutdownHandler : public nsISTSShutdownObserver {
+// Tells every live MediaTransportImplNicer to release what it holds on STS
+// when STS shuts down. Observe() is called on main just before the STS thread
+// is joined, so the work has to be queued there right away.
+class STSShutdownHandler final : public nsISTSShutdownObserver {
  public:
-  NS_DECL_ISUPPORTS
-
... (file diff truncated)
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/dom/media/webrtc/tests/mochitests/mochitest_peerconnection.toml b/dom/media/webrtc/tests/mochitests/mochitest_peerconnection.toml
index 294a37c70f9..91c77d77156 100644
--- a/dom/media/webrtc/tests/mochitests/mochitest_peerconnection.toml
+++ b/dom/media/webrtc/tests/mochitests/mochitest_peerconnection.toml
@@ -302,6 +302,8 @@ scheme = "http"
 
 ["test_peerConnection_offerRequiresReceiveVideoAudio.html"]
 
+["test_peerConnection_offline.html"]
+
 ["test_peerConnection_portRestrictions.html"]
 
 ["test_peerConnection_promiseSendOnly.html"]
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_offline.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_offline.html
new file mode 100644
index 00000000000..97dcf07be53
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_offline.html
@@ -0,0 +1,96 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+  createHTML({
+    bug: "2074202",
+    title: "Going offline and back online with live PeerConnections"
+  });
+
+// Flips the offline state in the parent, which passes it on to every other
+// process, including the one running the ICE stack.
+const offlineScript = SpecialPowers.loadChromeScript(() => {
+  /* eslint-env mozilla/chrome-script */
+  addMessageListener("set-offline", offline => {
+    Services.io.offline = offline;
+  });
+});
+
+async function setOffline(offline) {
+  const changed = new Promise(resolve => {
+    const observer = {
+      observe(subject, topic) {
+        SpecialPowers.removeObserver(observer, topic);
+        resolve();
+      },
+    };
+    SpecialPowers.addObserver(observer, "network:offline-status-changed");
+  });
+  offlineScript.sendAsyncMessage("set-offline", offline);
+  await changed;
+  is(SpecialPowers.Services.io.offline, offline,
+     `This process is ${offline ? "offline" : "online"}`);
+}
+
+async function connect() {
+  const pc1 = new RTCPeerConnection();
+  const pc2 = new RTCPeerConnection();
+  pc1.onicecandidate = e => pc2.addIceCandidate(e.candidate);
+  pc2.onicecandidate = e => pc1.addIceCandidate(e.candidate);
+  const dc1 = pc1.createDataChannel("no-signal-bars");
+  const dc2 = new Promise(r => (pc2.ondatachannel = e => r(e.channel)));
+  const opened = haveEvent(dc1, "open");
+  await pc1.setLocalDescription();
+  await pc2.setRemoteDescription(pc1.localDescription);
+  await pc2.setLocalDescription();
+  await pc1.setRemoteDescription(pc2.localDescription);
+  await opened;
+  return { pc1, pc2, dc1, dc2: await dc2 };
+}
+
+async function sendAcross({ dc1, dc2 }, text) {
+  const received = haveEvent(dc2, "message");
+  dc1.send(text);
+  is((await received).data, text, "Message made it across");
+}
+
+runNetworkTest(async () =>
+  // Otherwise the link service may put us back online behind our back.
+  withPrefs([["network.manage-offline-status", false]], async () => {
+    const before = await connect();
+    await sendAcross(before, "before");
+
+    info("Going offline with a connected pair");
+    await setOffline(true);
+    // The ICE stack loses its sockets here. We do not insist on any particular
+    // reaction, only that nothing crashes and everything can still be torn down.
+    await wait(1000);
+    info(`iceConnectionState while offline: ${before.pc1.iceConnectionState}, ` +
+         `${before.pc2.iceConnectionState}`);
+
+    try {
+      new RTCPeerConnection();
+      ok(false, "Creating an RTCPeerConnection while offline should throw");
+    } catch (e) {
+      is(e.name, "InvalidStateError",
+         "Creating an RTCPeerConnection while offline throws InvalidStateError");
+    }
+
+    await setOffline(false);
+    before.pc1.close();
+    before.pc2.close();
+
+    info("Back online; a new pair must connect");
+    const after = await connect();
+    await sendAcross(after, "after");
+    after.pc1.close();
+    after.pc2.close();
+  }));
+</script>
+</pre>
+</body>
+</html>
Loading diff…