bug 1425032 - use new "cancel all connections" notification for PKCS#11 logout r?mgoodwin draft
authorDavid Keeler <dkeeler@mozilla.com>
Wed, 13 Dec 2017 17:41:02 -0600
changeset 712291 e9485122dfefc09b79daef078286d5d9bf47de6e
parent 711178 defccba824aa91e8d4d820b1defaadfdca34bac7
child 744021 664e36a8be01f7f87c82e14ad6cf7700593fde9f
push id93300
push userbmo:dkeeler@mozilla.com
push dateFri, 15 Dec 2017 21:58:58 +0000
reviewersmgoodwin
bugs1425032, 1411316
milestone59.0a1
bug 1425032 - use new "cancel all connections" notification for PKCS#11 logout r?mgoodwin When the user performs a PKCS#11 logout, we need to cancel all in-progress network connections. Before this patch, PSM would track all the sockets it created to implement this feature. However, bug 1411316 added the ability to cancel these connections by sending the notification "net:cancel-all-connections". This patch removes the now-unnecessary tracking machinery in favor of delegating this to necko. MozReview-Commit-ID: 7IzC14bH2R4
security/manager/ssl/TransportSecurityInfo.h
security/manager/ssl/nsNSSComponent.cpp
security/manager/ssl/nsNSSIOLayer.cpp
security/manager/ssl/nsNSSShutDown.cpp
security/manager/ssl/nsNSSShutDown.h
security/manager/ssl/tests/unit/test_logoutAndTeardown.js
--- a/security/manager/ssl/TransportSecurityInfo.h
+++ b/security/manager/ssl/TransportSecurityInfo.h
@@ -25,24 +25,23 @@
 
 namespace mozilla { namespace psm {
 
 enum class SSLErrorMessageType {
   OverridableCert = 1, // for *overridable* certificate errors
   Plain = 2,           // all other errors (or "no error")
 };
 
-class TransportSecurityInfo : public nsITransportSecurityInfo,
-                              public nsIInterfaceRequestor,
-                              public nsISSLStatusProvider,
-                              public nsIAssociatedContentSecurity,
-                              public nsISerializable,
-                              public nsIClassInfo,
-                              public nsNSSShutDownObject,
-                              public nsOnPK11LogoutCancelObject
+class TransportSecurityInfo : public nsITransportSecurityInfo
+                            , public nsIInterfaceRequestor
+                            , public nsISSLStatusProvider
+                            , public nsIAssociatedContentSecurity
+                            , public nsISerializable
+                            , public nsIClassInfo
+                            , public nsNSSShutDownObject
 {
 protected:
   virtual ~TransportSecurityInfo();
 public:
   TransportSecurityInfo();
 
   NS_DECL_THREADSAFE_ISUPPORTS
   NS_DECL_NSITRANSPORTSECURITYINFO
--- a/security/manager/ssl/nsNSSComponent.cpp
+++ b/security/manager/ssl/nsNSSComponent.cpp
@@ -2343,17 +2343,22 @@ nsresult nsNSSComponent::LogoutAuthentic
   if (icos) {
     icos->ClearValidityOverride(
             NS_LITERAL_CSTRING("all:temporary-certificates"),
             0);
   }
 
   nsClientAuthRememberService::ClearAllRememberedDecisions();
 
-  return nsNSSShutDownList::doPK11Logout();
+  nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
+  if (os) {
+    os->NotifyObservers(nullptr, "net:cancel-all-connections", nullptr);
+  }
+
+  return NS_OK;
 }
 
 nsresult
 nsNSSComponent::RegisterObservers()
 {
   nsCOMPtr<nsIObserverService> observerService(
     do_GetService("@mozilla.org/observer-service;1"));
   if (!observerService) {
--- a/security/manager/ssl/nsNSSIOLayer.cpp
+++ b/security/manager/ssl/nsNSSIOLayer.cpp
@@ -359,17 +359,17 @@ nsNSSSocketInfo::GetNegotiatedNPN(nsACSt
 }
 
 NS_IMETHODIMP
 nsNSSSocketInfo::GetAlpnEarlySelection(nsACString& aAlpnSelected)
 {
   aAlpnSelected.Truncate();
 
   nsNSSShutDownPreventionLock locker;
-  if (isAlreadyShutDown() || isPK11LoggedOut()) {
+  if (isAlreadyShutDown()) {
     return NS_ERROR_NOT_AVAILABLE;
   }
 
   SSLPreliminaryChannelInfo info;
   SECStatus rv = SSL_GetPreliminaryChannelInfo(mFd, &info, sizeof(info));
   if (rv != SECSuccess || !info.canSendEarlyData) {
     return NS_ERROR_NOT_AVAILABLE;
   }
@@ -404,17 +404,17 @@ nsNSSSocketInfo::SetEarlyDataAccepted(bo
 {
   mEarlyDataAccepted = aAccepted;
 }
 
 NS_IMETHODIMP
 nsNSSSocketInfo::DriveHandshake()
 {
   nsNSSShutDownPreventionLock locker;
-  if (isAlreadyShutDown() || isPK11LoggedOut()) {
+  if (isAlreadyShutDown()) {
     return NS_ERROR_NOT_AVAILABLE;
   }
   if (!mFd) {
     return NS_ERROR_FAILURE;
   }
   PRErrorCode errorCode = GetErrorCode();
   if (errorCode) {
     return GetXPCOMFromNSSError(errorCode);
@@ -762,17 +762,17 @@ getSocketInfoIfRunning(PRFileDesc* fd, O
       fd->identity != nsSSLIOLayerHelpers::nsSSLIOLayerIdentity) {
     NS_ERROR("bad file descriptor passed to getSocketInfoIfRunning");
     PR_SetError(PR_BAD_DESCRIPTOR_ERROR, 0);
     return nullptr;
   }
 
   nsNSSSocketInfo* socketInfo = (nsNSSSocketInfo*) fd->secret;
 
-  if (socketInfo->isAlreadyShutDown() || socketInfo->isPK11LoggedOut()) {
+  if (socketInfo->isAlreadyShutDown()) {
     PR_SetError(PR_SOCKET_SHUTDOWN_ERROR, 0);
     return nullptr;
   }
 
   if (socketInfo->GetErrorCode()) {
     PRErrorCode err = socketInfo->GetErrorCode();
     PR_SetError(err, 0);
     if (op == reading || op == writing) {
--- a/security/manager/ssl/nsNSSShutDown.cpp
+++ b/security/manager/ssl/nsNSSShutDown.cpp
@@ -38,17 +38,16 @@ static const PLDHashTableOps gSetOps = {
 };
 
 StaticMutex sListLock;
 Atomic<bool> sInShutdown(false);
 nsNSSShutDownList *singleton = nullptr;
 
 nsNSSShutDownList::nsNSSShutDownList()
   : mObjects(&gSetOps, sizeof(ObjectHashEntry))
-  , mPK11LogoutCancelObjects(&gSetOps, sizeof(ObjectHashEntry))
 {
 }
 
 nsNSSShutDownList::~nsNSSShutDownList()
 {
   MOZ_ASSERT(this == singleton);
   MOZ_ASSERT(sInShutdown,
              "evaporateAllNSSResourcesAndShutDown() should have been called");
@@ -72,67 +71,16 @@ void nsNSSShutDownList::forget(nsNSSShut
   if (!singleton) {
     return;
   }
 
   MOZ_ASSERT(o);
   singleton->mObjects.Remove(o);
 }
 
-void nsNSSShutDownList::remember(nsOnPK11LogoutCancelObject *o)
-{
-  StaticMutexAutoLock lock(sListLock);
-  if (!nsNSSShutDownList::construct(lock)) {
-    return;
-  }
-
-  MOZ_ASSERT(o);
-  singleton->mPK11LogoutCancelObjects.Add(o, fallible);
-}
-
-void nsNSSShutDownList::forget(nsOnPK11LogoutCancelObject *o)
-{
-  StaticMutexAutoLock lock(sListLock);
-  if (!singleton) {
-    return;
-  }
-
-  MOZ_ASSERT(o);
-  singleton->mPK11LogoutCancelObjects.Remove(o);
-}
-
-nsresult nsNSSShutDownList::doPK11Logout()
-{
-  StaticMutexAutoLock lock(sListLock);
-  if (!singleton) {
-    return NS_OK;
-  }
-
-  MOZ_LOG(gPIPNSSLog, LogLevel::Debug,
-          ("canceling all open SSL sockets to disallow future IO\n"));
-
-  // During our iteration we will set a bunch of PRBools to true.
-  // Nobody else ever modifies that bool, only we do.
-  // We only must ensure that our objects do not go away.
-  // This is guaranteed by holding the list lock.
-
-  for (auto iter = singleton->mPK11LogoutCancelObjects.Iter();
-       !iter.Done();
-       iter.Next()) {
-    auto entry = static_cast<ObjectHashEntry*>(iter.Get());
-    nsOnPK11LogoutCancelObject* pklco =
-      BitwiseCast<nsOnPK11LogoutCancelObject*, nsNSSShutDownObject*>(entry->obj);
-    if (pklco) {
-      pklco->logout();
-    }
-  }
-
-  return NS_OK;
-}
-
 nsresult nsNSSShutDownList::evaporateAllNSSResourcesAndShutDown()
 {
   MOZ_RELEASE_ASSERT(NS_IsMainThread());
   if (!NS_IsMainThread()) {
     return NS_ERROR_NOT_SAME_THREAD;
   }
 
   // This makes this function idempotent and protects against reentering it
--- a/security/manager/ssl/nsNSSShutDown.h
+++ b/security/manager/ssl/nsNSSShutDown.h
@@ -9,17 +9,16 @@
 #include "mozilla/Assertions.h"
 #include "mozilla/CondVar.h"
 #include "mozilla/Mutex.h"
 #include "mozilla/StaticMutex.h"
 #include "nscore.h"
 #include "nspr.h"
 
 class nsNSSShutDownObject;
-class nsOnPK11LogoutCancelObject;
 
 // Singleton, owned by nsNSSShutDownList
 class nsNSSActivityState
 {
 public:
   nsNSSActivityState();
   ~nsNSSActivityState();
 
@@ -69,44 +68,34 @@ private:
 // which hold NSS resources and support the "early cleanup mechanism".
 class nsNSSShutDownList
 {
 public:
   // track instances that support early cleanup
   static void remember(nsNSSShutDownObject *o);
   static void forget(nsNSSShutDownObject *o);
 
-  // track instances that would like notification when
-  // a PK11 logout operation is performed.
-  static void remember(nsOnPK11LogoutCancelObject *o);
-  static void forget(nsOnPK11LogoutCancelObject *o);
-
   // Release all tracked NSS resources and prevent nsNSSShutDownObjects from
   // using NSS functions.
   static nsresult evaporateAllNSSResourcesAndShutDown();
 
-  // PSM has been asked to log out of a token.
-  // Notify all registered instances that want to react to that event.
-  static nsresult doPK11Logout();
-
   // Signal entering/leaving a scope where shutting down NSS is prohibited.
   // enteredActivityState will be set to true if we actually managed to enter
   // the NSS activity state.
   static void enterActivityState(/*out*/ bool& enteredActivityState);
   static void leaveActivityState();
 
 private:
   static bool construct(const mozilla::StaticMutexAutoLock& /*proofOfLock*/);
 
   nsNSSShutDownList();
   ~nsNSSShutDownList();
 
 protected:
   PLDHashTable mObjects;
-  PLDHashTable mPK11LogoutCancelObjects;
   nsNSSActivityState mActivityState;
 };
 
 /*
   A class deriving from nsNSSShutDownObject will have its instances
   automatically tracked in a list. However, it must follow some rules
   to assure correct behaviour.
 
@@ -238,41 +227,9 @@ public:
   bool isAlreadyShutDown() const;
 
 protected:
   virtual void virtualDestroyNSSReference() = 0;
 private:
   volatile bool mAlreadyShutDown;
 };
 
-class nsOnPK11LogoutCancelObject
-{
-public:
-  nsOnPK11LogoutCancelObject()
-    : mIsLoggedOut(false)
-  {
-    nsNSSShutDownList::remember(this);
-  }
-
-  virtual ~nsOnPK11LogoutCancelObject()
-  {
-    nsNSSShutDownList::forget(this);
-  }
-
-  void logout()
-  {
-    // We do not care for a race condition.
-    // Once the bool arrived at false,
-    // later calls to isPK11LoggedOut() will see it.
-    // This is a one-time change from 0 to 1.
-    mIsLoggedOut = true;
-  }
-
-  bool isPK11LoggedOut()
-  {
-    return mIsLoggedOut;
-  }
-
-private:
-  volatile bool mIsLoggedOut;
-};
-
 #endif // nsNSSShutDown_h
--- a/security/manager/ssl/tests/unit/test_logoutAndTeardown.js
+++ b/security/manager/ssl/tests/unit/test_logoutAndTeardown.js
@@ -1,55 +1,187 @@
-/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+// Any copyright is dedicated to the Public Domain.
+// http://creativecommons.org/publicdomain/zero/1.0/
 
 "use strict";
 
-do_get_profile();
+// This test ensures that in-progress https connections are cancelled when the
+// user logs out of a PKCS#11 token.
 
-function connect_and_teardown() {
-  let socketTransportService =
-    Cc["@mozilla.org/network/socket-transport-service;1"]
-      .getService(Ci.nsISocketTransportService);
-
-  let tearDown = false;
+// Get a profile directory and ensure PSM initializes NSS.
+do_get_profile();
+Cc["@mozilla.org/psm;1"].getService(Ci.nsISupports);
 
-  let reader = {
-    onInputStreamReady(stream) {
-      throws(() => stream.available(), /NS_ERROR_FAILURE/,
-             "stream should be in an error state");
-      ok(tearDown, "A tear down attempt should have occurred");
-      run_next_test();
-    }
-  };
+function getCert() {
+  return new Promise((resolve, reject) => {
+    let certService = Cc["@mozilla.org/security/local-cert-service;1"]
+                        .getService(Ci.nsILocalCertService);
+    certService.getOrCreateCert("beConservative-test", {
+      handleCert: (c, rv) => {
+        if (rv) {
+          reject(rv);
+          return;
+        }
+        resolve(c);
+      }
+    });
+  });
+}
+
+class InputStreamCallback {
+  constructor(output) {
+    this.output = output;
+    this.stopped = false;
+  }
 
-  let sink = {
-    onTransportStatus(transport, status, progress, progressmax) {
-      if (status == Ci.nsISocketTransport.STATUS_CONNECTED_TO) {
-        // Try to logout and tear down the secure decoder ring.
-        // This should close and stream and notify the reader.
-        // The test will time out if this fails.
-        tearDown = true;
-        Cc["@mozilla.org/security/sdr;1"].getService(Ci.nsISecretDecoderRing)
-          .logoutAndTeardown();
-      }
+  onInputStreamReady(stream) {
+    do_print("input stream ready");
+    if (this.stopped) {
+      do_print("input stream callback stopped - bailing");
+      return;
+    }
+    let available = 0;
+    try {
+      available = stream.available();
+    } catch (e) {
+      // onInputStreamReady may fire when the stream has been closed.
+      equal(e.result, Cr.NS_BASE_STREAM_CLOSED,
+            "error should be NS_BASE_STREAM_CLOSED");
     }
-  };
+    if (available > 0) {
+      let request = NetUtil.readInputStreamToString(stream, available,
+                                                    { charset: "utf8" });
+      ok(request.startsWith("GET / HTTP/1.1\r\n"),
+         "Should get a simple GET / HTTP/1.1 request");
+      let response = "HTTP/1.1 200 OK\r\n" +
+                     "Content-Length: 2\r\n" +
+                     "Content-Type: text/plain\r\n" +
+                     "\r\nOK";
+      let written = this.output.write(response, response.length);
+      equal(written, response.length,
+            "should have been able to write entire response");
+    }
+    this.output.close();
+    do_print("done with input stream ready");
+  }
 
-  Services.prefs.setCharPref("network.dns.localDomains",
-                             "ocsp-stapling-none.example.com");
-  let transport = socketTransportService.createTransport(
-    ["ssl"], 1, "ocsp-stapling-none.example.com", 8443, null);
-  transport.setEventSink(sink, Services.tm.currentThread);
-
-  let inStream = transport.openInputStream(0, 0, 0)
-                          .QueryInterface(Ci.nsIAsyncInputStream);
-  inStream.asyncWait(reader, Ci.nsIAsyncInputStream.WAIT_CLOSURE_ONLY, 0,
-                     Services.tm.currentThread);
+  stop() {
+    this.stopped = true;
+    this.output.close();
+  }
 }
 
-function run_test() {
-  add_tls_server_setup("OCSPStaplingServer", "ocsp_certs");
-  add_test(connect_and_teardown);
-  run_next_test();
+class TLSServerSecurityObserver {
+  constructor(input, output) {
+    this.input = input;
+    this.output = output;
+    this.callbacks = [];
+    this.stopped = false;
+  }
+
+  onHandshakeDone(socket, status) {
+    do_print("TLS handshake done");
+    do_print(`TLS version used: ${status.tlsVersionUsed}`);
+
+    if (this.stopped) {
+      do_print("handshake done callback stopped - bailing");
+      return;
+    }
+
+    let callback = new InputStreamCallback(this.output);
+    this.callbacks.push(callback);
+    this.input.asyncWait(callback, 0, 0, Services.tm.currentThread);
+
+    // We've set up everything needed for a successful request/response,
+    // but calling logoutAndTeardown should cause the request to be cancelled.
+    Cc["@mozilla.org/security/sdr;1"].getService(Ci.nsISecretDecoderRing)
+      .logoutAndTeardown();
+  }
+
+  stop() {
+    this.stopped = true;
+    this.input.close();
+    this.output.close();
+    this.callbacks.forEach((callback) => {
+      callback.stop();
+    });
+  }
 }
+
+class ServerSocketListener {
+  constructor() {
+    this.securityObservers = [];
+  }
+
+  onSocketAccepted(socket, transport) {
+    do_print("accepted TLS client connection");
+    let connectionInfo = transport.securityInfo
+                         .QueryInterface(Ci.nsITLSServerConnectionInfo);
+    let input = transport.openInputStream(0, 0, 0);
+    let output = transport.openOutputStream(0, 0, 0);
+    let securityObserver = new TLSServerSecurityObserver(input, output);
+    this.securityObservers.push(securityObserver);
+    connectionInfo.setSecurityObserver(securityObserver);
+  }
+
+  // For some reason we get input stream callback events after we've stopped
+  // listening, so this ensures we just drop those events.
+  onStopListening() {
+    do_print("onStopListening");
+    this.securityObservers.forEach((observer) => {
+      observer.stop();
+    });
+  }
+}
+
+function getStartedServer(cert) {
+  let tlsServer = Cc["@mozilla.org/network/tls-server-socket;1"]
+                    .createInstance(Ci.nsITLSServerSocket);
+  tlsServer.init(-1, true, -1);
+  tlsServer.serverCert = cert;
+  tlsServer.setSessionCache(false);
+  tlsServer.setSessionTickets(false);
+  tlsServer.asyncListen(new ServerSocketListener());
+  return tlsServer;
+}
+
+const hostname = "example.com";
+
+function storeCertOverride(port, cert) {
+  let certOverrideService = Cc["@mozilla.org/security/certoverride;1"]
+                              .getService(Ci.nsICertOverrideService);
+  let overrideBits = Ci.nsICertOverrideService.ERROR_UNTRUSTED |
+                     Ci.nsICertOverrideService.ERROR_MISMATCH;
+  certOverrideService.rememberValidityOverride(hostname, port, cert,
+                                               overrideBits, true);
+}
+
+function startClient(port) {
+  let req = new XMLHttpRequest();
+  req.open("GET", `https://${hostname}:${port}`);
+  return new Promise((resolve, reject) => {
+    req.onload = () => {
+      ok(false, "should not have gotten load event");
+      resolve();
+    };
+    req.onerror = () => {
+      ok(true, "should have gotten an error");
+      resolve();
+    };
+
+    req.send();
+  });
+}
+
+add_task(async function() {
+  Services.prefs.setCharPref("network.dns.localDomains", hostname);
+  let cert = await getCert();
+
+  let server = getStartedServer(cert);
+  storeCertOverride(server.port, cert);
+  await startClient(server.port);
+  server.close();
+});
+
+do_register_cleanup(function() {
+  Services.prefs.clearUserPref("network.dns.localDomains");
+});