Bug 1546975 - Cache SSL resumption tokens in necko, r=mayhemer
authorMichal Novotny <michal.novotny@gmail.com>
Thu, 09 May 2019 22:04:43 +0000
changeset 532139 7fbf58d80879cbf94f82bddddb747bdfb9e4caa4
parent 532138 e93a97c6e708cd67d514574a14a611ca516906b4
child 532140 3d041b01d6f08fa48a130e02f631132da7d7c0fd
push id11265
push userffxbld-merge
push dateMon, 13 May 2019 10:53:39 +0000
treeherdermozilla-beta@77e0fe8dbdd3 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmayhemer
bugs1546975
milestone68.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1546975 - Cache SSL resumption tokens in necko, r=mayhemer SSLTokensCache is a simple memory only storage for resumption tokens which are get and set using API for external TLS session caches in NSS. Differential Revision: https://phabricator.services.mozilla.com/D29465
modules/libpref/init/all.js
netwerk/base/SSLTokensCache.cpp
netwerk/base/SSLTokensCache.h
netwerk/base/moz.build
netwerk/base/nsIOService.cpp
netwerk/base/nsNetUtil.cpp
netwerk/base/nsSocketTransport2.cpp
netwerk/base/nsSocketTransport2.h
security/nss.symbols
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -2376,16 +2376,21 @@ pref("network.proxy.autoconfig_retry_int
 pref("network.proxy.enable_wpad_over_dhcp", true);
 
 // Use the HSTS preload list by default
 pref("network.stricttransportsecurity.preloadlist", true);
 
 // Use JS mDNS as a fallback
 pref("network.mdns.use_js_fallback", false);
 
+// Cache SSL resumption tokens in necko
+pref("network.ssl_tokens_cache_enabled", false);
+// Capacity of the cache in kilobytes
+pref("network.ssl_tokens_cache_capacity", 2048);
+
 pref("converter.html2txt.structs",          true); // Output structured phrases (strong, em, code, sub, sup, b, i, u)
 pref("converter.html2txt.header_strategy",  1); // 0 = no indention; 1 = indention, increased with header level; 2 = numbering and slight indention
 // Whether we include ruby annotation in the text despite whether it
 // is requested. This was true because we didn't explicitly strip out
 // annotations. Set false by default to provide a better behavior, but
 // we want to be able to pref-off it if user doesn't like it.
 pref("converter.html2txt.always_include_ruby", false);
 
new file mode 100644
--- /dev/null
+++ b/netwerk/base/SSLTokensCache.cpp
@@ -0,0 +1,253 @@
+/* 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/. */
+
+#include "SSLTokensCache.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/Logging.h"
+#include "ssl.h"
+#include "sslexp.h"
+
+namespace mozilla {
+namespace net {
+
+static bool const kDefaultEnabled = false;
+Atomic<bool, Relaxed> SSLTokensCache::sEnabled(kDefaultEnabled);
+
+static uint32_t const kDefaultCapacity = 2048;  // 2MB
+Atomic<uint32_t, Relaxed> SSLTokensCache::sCapacity(kDefaultCapacity);
+
+static LazyLogModule gSSLTokensCacheLog("SSLTokensCache");
+#undef LOG
+#define LOG(args) MOZ_LOG(gSSLTokensCacheLog, mozilla::LogLevel::Debug, args)
+
+class ExpirationComparator {
+ public:
+  bool Equals(SSLTokensCache::HostRecord* a,
+              SSLTokensCache::HostRecord* b) const {
+    return a->mExpirationTime == b->mExpirationTime;
+  }
+  bool LessThan(SSLTokensCache::HostRecord* a,
+                SSLTokensCache::HostRecord* b) const {
+    return a->mExpirationTime < b->mExpirationTime;
+  }
+};
+
+StaticRefPtr<SSLTokensCache> SSLTokensCache::gInstance;
+StaticMutex SSLTokensCache::sLock;
+
+NS_IMPL_ISUPPORTS(SSLTokensCache, nsIMemoryReporter)
+
+// static
+nsresult SSLTokensCache::Init() {
+  StaticMutexAutoLock lock(sLock);
+
+  if (XRE_GetProcessType() != GeckoProcessType_Default) {
+    return NS_OK;
+  }
+
+  MOZ_ASSERT(!gInstance);
+
+  gInstance = new SSLTokensCache();
+  gInstance->InitPrefs();
+
+  RegisterWeakMemoryReporter(gInstance);
+
+  return NS_OK;
+}
+
+// static
+nsresult SSLTokensCache::Shutdown() {
+  StaticMutexAutoLock lock(sLock);
+
+  if (!gInstance) {
+    return NS_ERROR_UNEXPECTED;
+  }
+
+  UnregisterWeakMemoryReporter(gInstance);
+
+  gInstance = nullptr;
+
+  return NS_OK;
+}
+
+SSLTokensCache::SSLTokensCache() : mCacheSize(0) {
+  LOG(("SSLTokensCache::SSLTokensCache"));
+}
+
+SSLTokensCache::~SSLTokensCache() { LOG(("SSLTokensCache::~SSLTokensCache")); }
+
+// static
+nsresult SSLTokensCache::Put(const nsACString& aHost, const uint8_t* aToken,
+                             uint32_t aTokenLen) {
+  StaticMutexAutoLock lock(sLock);
+
+  LOG(("SSLTokensCache::Put [host=%s, tokenLen=%u]",
+       PromiseFlatCString(aHost).get(), aTokenLen));
+
+  if (!gInstance) {
+    LOG(("  service not initialized"));
+    return NS_ERROR_NOT_INITIALIZED;
+  }
+
+  PRUint32 expirationTime;
+  SSLResumptionTokenInfo tokenInfo;
+  if (SSL_GetResumptionTokenInfo(aToken, aTokenLen, &tokenInfo,
+                                 sizeof(tokenInfo)) != SECSuccess) {
+    LOG(("  cannot get expiration time from the token, NSS error %d",
+         PORT_GetError()));
+    return NS_ERROR_FAILURE;
+  }
+  expirationTime = tokenInfo.expirationTime;
+  SSL_DestroyResumptionTokenInfo(&tokenInfo);
+
+  HostRecord* rec = nullptr;
+
+  if (!gInstance->mHostRecs.Get(aHost, &rec)) {
+    rec = new HostRecord();
+    rec->mHost = aHost;
+    gInstance->mHostRecs.Put(aHost, rec);
+    gInstance->mExpirationArray.AppendElement(rec);
+  } else {
+    gInstance->mCacheSize -= rec->mToken.Length();
+    rec->mToken.Clear();
+  }
+
+  rec->mExpirationTime = expirationTime;
+  MOZ_ASSERT(rec->mToken.IsEmpty());
+  rec->mToken.AppendElements(aToken, aTokenLen);
+  gInstance->mCacheSize += rec->mToken.Length();
+
+  gInstance->LogStats();
+
+  gInstance->EvictIfNecessary();
+
+  return NS_OK;
+}
+
+// static
+nsresult SSLTokensCache::Get(const nsACString& aHost,
+                             nsTArray<uint8_t>& aToken) {
+  StaticMutexAutoLock lock(sLock);
+
+  LOG(("SSLTokensCache::Get [host=%s]", PromiseFlatCString(aHost).get()));
+
+  if (!gInstance) {
+    LOG(("  service not initialized"));
+    return NS_ERROR_NOT_INITIALIZED;
+  }
+
+  HostRecord* rec = nullptr;
+
+  if (gInstance->mHostRecs.Get(aHost, &rec)) {
+    if (rec->mToken.Length()) {
+      aToken = rec->mToken;
+      return NS_OK;
+    }
+  }
+
+  LOG(("  token not found"));
+  return NS_ERROR_NOT_AVAILABLE;
+}
+
+// static
+nsresult SSLTokensCache::Remove(const nsACString& aHost) {
+  StaticMutexAutoLock lock(sLock);
+
+  LOG(("SSLTokensCache::Remove [host=%s]", PromiseFlatCString(aHost).get()));
+
+  if (!gInstance) {
+    LOG(("  service not initialized"));
+    return NS_ERROR_NOT_INITIALIZED;
+  }
+
+  return gInstance->RemoveLocked(aHost);
+}
+
+nsresult SSLTokensCache::RemoveLocked(const nsACString& aHost) {
+  sLock.AssertCurrentThreadOwns();
+
+  LOG(("SSLTokensCache::RemoveLocked [host=%s]",
+       PromiseFlatCString(aHost).get()));
+
+  nsAutoPtr<HostRecord> rec;
+
+  if (!mHostRecs.Remove(aHost, &rec)) {
+    LOG(("  token not found"));
+    return NS_ERROR_NOT_AVAILABLE;
+  }
+
+  mCacheSize -= rec->mToken.Length();
+
+  if (!mExpirationArray.RemoveElement(rec)) {
+    MOZ_ASSERT(false, "token not found in mExpirationArray");
+  }
+
+  LogStats();
+
+  return NS_OK;
+}
+
+void SSLTokensCache::InitPrefs() {
+  Preferences::AddAtomicBoolVarCache(
+      &sEnabled, "network.ssl_tokens_cache_enabled", kDefaultEnabled);
+  Preferences::AddAtomicUintVarCache(
+      &sCapacity, "network.ssl_tokens_cache_capacity", kDefaultCapacity);
+}
+
+void SSLTokensCache::EvictIfNecessary() {
+  uint32_t capacity = sCapacity << 10;  // kilobytes to bytes
+  if (mCacheSize <= capacity) {
+    return;
+  }
+
+  LOG(("SSLTokensCache::EvictIfNecessary - evicting"));
+
+  mExpirationArray.Sort(ExpirationComparator());
+
+  while (mCacheSize > capacity && mExpirationArray.Length() > 0) {
+    if (NS_FAILED(RemoveLocked(mExpirationArray[0]->mHost))) {
+      MOZ_ASSERT(false, "mExpirationArray and mHostRecs are out of sync!");
+      mExpirationArray.RemoveElementAt(0);
+    }
+  }
+}
+
+void SSLTokensCache::LogStats() {
+  LOG(("SSLTokensCache::LogStats [count=%zu, cacheSize=%u]",
+       mExpirationArray.Length(), mCacheSize));
+}
+
+size_t SSLTokensCache::SizeOfIncludingThis(
+    mozilla::MallocSizeOf mallocSizeOf) const {
+  size_t n = mallocSizeOf(this);
+
+  n += mHostRecs.ShallowSizeOfExcludingThis(mallocSizeOf);
+  n += mExpirationArray.ShallowSizeOfExcludingThis(mallocSizeOf);
+
+  for (uint32_t i = 0; i < mExpirationArray.Length(); ++i) {
+    n += mallocSizeOf(mExpirationArray[i]);
+    n += mExpirationArray[i]->mHost.SizeOfExcludingThisIfUnshared(mallocSizeOf);
+    n += mExpirationArray[i]->mToken.ShallowSizeOfExcludingThis(mallocSizeOf);
+  }
+
+  return n;
+}
+
+MOZ_DEFINE_MALLOC_SIZE_OF(SSLTokensCacheMallocSizeOf)
+
+NS_IMETHODIMP
+SSLTokensCache::CollectReports(nsIHandleReportCallback* aHandleReport,
+                               nsISupports* aData, bool aAnonymize) {
+  StaticMutexAutoLock lock(sLock);
+
+  MOZ_COLLECT_REPORT("explicit/network/ssl-tokens-cache", KIND_HEAP,
+                     UNITS_BYTES,
+                     SizeOfIncludingThis(SSLTokensCacheMallocSizeOf),
+                     "Memory used for the SSL tokens cache.");
+
+  return NS_OK;
+}
+
+}  // namespace net
+}  // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/netwerk/base/SSLTokensCache.h
@@ -0,0 +1,70 @@
+/* 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/. */
+
+#ifndef SSLTokensCache_h_
+#define SSLTokensCache_h_
+
+#include "nsIMemoryReporter.h"
+#include "nsClassHashtable.h"
+#include "nsTArray.h"
+#include "mozilla/StaticMutex.h"
+#include "mozilla/StaticPtr.h"
+#include "nsXULAppAPI.h"
+
+namespace mozilla {
+namespace net {
+
+class SSLTokensCache : public nsIMemoryReporter {
+ public:
+  NS_DECL_THREADSAFE_ISUPPORTS
+  NS_DECL_NSIMEMORYREPORTER
+
+  friend class ExpirationComparator;
+
+  static nsresult Init();
+  static nsresult Shutdown();
+
+  static bool IsEnabled() { return sEnabled; }
+
+  static nsresult Put(const nsACString& aHost, const uint8_t* aToken,
+                      uint32_t aTokenLen);
+  static nsresult Get(const nsACString& aHost, nsTArray<uint8_t>& aToken);
+  static nsresult Remove(const nsACString& aHost);
+
+ private:
+  SSLTokensCache();
+  virtual ~SSLTokensCache();
+
+  nsresult RemoveLocked(const nsACString& aHost);
+
+  void InitPrefs();
+  void EvictIfNecessary();
+  void LogStats();
+
+  size_t SizeOfIncludingThis(mozilla::MallocSizeOf mallocSizeOf) const;
+
+  static mozilla::StaticRefPtr<SSLTokensCache> gInstance;
+  static StaticMutex sLock;
+
+  static Atomic<bool, Relaxed> sEnabled;
+  // Capacity of the cache in kilobytes
+  static Atomic<uint32_t, Relaxed> sCapacity;
+
+  uint32_t mCacheSize;  // Actual cache size in bytes
+
+  class HostRecord {
+   public:
+    nsCString mHost;
+    PRUint32 mExpirationTime;
+    nsTArray<uint8_t> mToken;
+  };
+
+  nsClassHashtable<nsCStringHashKey, HostRecord> mHostRecs;
+  nsTArray<HostRecord*> mExpirationArray;
+};
+
+}  // namespace net
+}  // namespace mozilla
+
+#endif  // SSLTokensCache_h_
--- a/netwerk/base/moz.build
+++ b/netwerk/base/moz.build
@@ -168,16 +168,17 @@ EXPORTS.mozilla.net += [
     'MemoryDownloader.h',
     'NetworkConnectivityService.h',
     'PartiallySeekableInputStream.h',
     'Predictor.h',
     'RedirectChannelRegistrar.h',
     'ReferrerPolicy.h',
     'RequestContextService.h',
     'SimpleChannelParent.h',
+    'SSLTokensCache.h',
     'TCPFastOpen.h',
 ]
 
 UNIFIED_SOURCES += [
     'ArrayBufferInputStream.cpp',
     'BackgroundFileSaver.cpp',
     'CaptivePortalService.cpp',
     'ChannelDiverterChild.cpp',
@@ -236,16 +237,17 @@ UNIFIED_SOURCES += [
     'PollableEvent.cpp',
     'Predictor.cpp',
     'ProxyAutoConfig.cpp',
     'RedirectChannelRegistrar.cpp',
     'RequestContextService.cpp',
     'SimpleBuffer.cpp',
     'SimpleChannel.cpp',
     'SimpleChannelParent.cpp',
+    'SSLTokensCache.cpp',
     'TCPFastOpenLayer.cpp',
     'ThrottleQueue.cpp',
     'Tickler.cpp',
     'TLSServerSocket.cpp',
 ]
 
 if CONFIG['FUZZING_INTERFACES'] and CONFIG['LIBFUZZER']:
     include('/tools/fuzzing/libfuzzer-flags.mozbuild')
--- a/netwerk/base/nsIOService.cpp
+++ b/netwerk/base/nsIOService.cpp
@@ -50,16 +50,17 @@
 #include "mozilla/net/NeckoParent.h"
 #include "mozilla/dom/ClientInfo.h"
 #include "mozilla/dom/ContentParent.h"
 #include "mozilla/dom/ServiceWorkerDescriptor.h"
 #include "mozilla/net/CaptivePortalService.h"
 #include "mozilla/net/NetworkConnectivityService.h"
 #include "mozilla/net/SocketProcessHost.h"
 #include "mozilla/net/SocketProcessParent.h"
+#include "mozilla/net/SSLTokensCache.h"
 #include "mozilla/Unused.h"
 #include "ReferrerPolicy.h"
 #include "nsContentSecurityManager.h"
 #include "nsContentUtils.h"
 #include "nsExceptionHandler.h"
 
 namespace mozilla {
 namespace net {
@@ -224,16 +225,18 @@ static const char* gCallbackPrefsForSock
 
 nsresult nsIOService::Init() {
   // XXX hack until xpidl supports error info directly (bug 13423)
   nsCOMPtr<nsIErrorService> errorService = nsErrorService::GetOrCreate();
   MOZ_ALWAYS_TRUE(errorService);
   errorService->RegisterErrorStringBundle(NS_ERROR_MODULE_NETWORK,
                                           NECKO_MSGS_URL);
 
+  SSLTokensCache::Init();
+
   InitializeCaptivePortalService();
 
   // setup our bad port list stuff
   for (int i = 0; gBadPortList[i]; i++)
     mRestrictedPortList.AppendElement(gBadPortList[i]);
 
   // Further modifications to the port list come from prefs
   Preferences::RegisterPrefixCallbacks(
@@ -1443,16 +1446,18 @@ nsIOService::Observe(nsISupports* subjec
 
     SetOffline(true);
 
     if (mCaptivePortalService) {
       static_cast<CaptivePortalService*>(mCaptivePortalService.get())->Stop();
       mCaptivePortalService = nullptr;
     }
 
+    SSLTokensCache::Shutdown();
+
     DestroySocketProcess();
   } else if (!strcmp(topic, NS_NETWORK_LINK_TOPIC)) {
     OnNetworkLinkEvent(NS_ConvertUTF16toUTF8(data).get());
   } else if (!strcmp(topic, NS_WIDGET_WAKE_OBSERVER_TOPIC)) {
     // coming back alive from sleep
     // this indirection brought to you by:
     // https://bugzilla.mozilla.org/show_bug.cgi?id=1152048#c19
     nsCOMPtr<nsIRunnable> wakeupNotifier = new nsWakeupNotifier(this);
--- a/netwerk/base/nsNetUtil.cpp
+++ b/netwerk/base/nsNetUtil.cpp
@@ -90,26 +90,30 @@
 #include "nsQueryObject.h"
 #include "mozIThirdPartyUtil.h"
 #include "../mime/nsMIMEHeaderParamImpl.h"
 #include "nsStandardURL.h"
 #include "nsChromeProtocolHandler.h"
 #include "nsJSProtocolHandler.h"
 #include "nsDataHandler.h"
 #include "mozilla/dom/BlobURLProtocolHandler.h"
+#include "nsStreamUtils.h"
+#include "nsSocketTransportService2.h"
 
 #include <limits>
 
 using namespace mozilla;
 using namespace mozilla::net;
 using mozilla::dom::BlobURLProtocolHandler;
 using mozilla::dom::ClientInfo;
 using mozilla::dom::PerformanceStorage;
 using mozilla::dom::ServiceWorkerDescriptor;
 
+#define MAX_RECURSION_COUNT 50
+
 already_AddRefed<nsIIOService> do_GetIOService(nsresult* error /* = 0 */) {
   nsCOMPtr<nsIIOService> io = mozilla::services::GetIOService();
   if (error) *error = io ? NS_OK : NS_ERROR_FAILURE;
   return io.forget();
 }
 
 nsresult NS_NewLocalFileInputStream(nsIInputStream** result, nsIFile* file,
                                     int32_t ioFlags /* = -1 */,
--- a/netwerk/base/nsSocketTransport2.cpp
+++ b/netwerk/base/nsSocketTransport2.cpp
@@ -33,16 +33,18 @@
 #include "nsIClassInfoImpl.h"
 #include "nsURLHelper.h"
 #include "nsIDNSService.h"
 #include "nsIDNSRecord.h"
 #include "nsIDNSByTypeRecord.h"
 #include "nsICancelable.h"
 #include "TCPFastOpenLayer.h"
 #include <algorithm>
+#include "sslexp.h"
+#include "mozilla/net/SSLTokensCache.h"
 
 #include "nsPrintfCString.h"
 #include "xpcpublic.h"
 
 #if defined(FUZZING)
 #  include "FuzzyLayer.h"
 #endif
 
@@ -729,17 +731,18 @@ nsSocketTransport::nsSocketTransport()
       mKeepaliveEnabled(false),
       mKeepaliveIdleTimeS(-1),
       mKeepaliveRetryIntervalS(-1),
       mKeepaliveProbeCount(-1),
       mFastOpenCallback(nullptr),
       mFastOpenLayerHasBufferedData(false),
       mFastOpenStatus(TFO_NOT_SET),
       mFirstRetryError(NS_OK),
-      mDoNotRetryToConnect(false) {
+      mDoNotRetryToConnect(false),
+      mSSLCallbackSet(false) {
   this->mNetAddr.raw.family = 0;
   this->mNetAddr.inet = {};
   this->mSelfAddr.raw.family = 0;
   this->mSelfAddr.inet = {};
   SOCKET_LOG(("creating nsSocketTransport @%p\n", this));
 
   mTimeouts[TIMEOUT_CONNECT] = UINT16_MAX;     // no timeout
   mTimeouts[TIMEOUT_READ_WRITE] = UINT16_MAX;  // no timeout
@@ -1241,16 +1244,32 @@ nsresult nsSocketTransport::BuildSocket(
             fd, mSocketTransportService->IsTelemetryEnabledAndNotSleepPhase());
       }
     }
   }
 
   return rv;
 }
 
+// static
+SECStatus nsSocketTransport::StoreResumptionToken(
+    PRFileDesc* fd, const PRUint8* resumptionToken, unsigned int len,
+    void* ctx) {
+  PRIntn val;
+  if (SSL_OptionGet(fd, SSL_ENABLE_SESSION_TICKETS, &val) != SECSuccess ||
+      val == 0) {
+    return SECFailure;
+  }
+
+  SSLTokensCache::Put(static_cast<nsSocketTransport*>(ctx)->mHost,
+                      resumptionToken, len);
+
+  return SECSuccess;
+}
+
 nsresult nsSocketTransport::InitiateSocket() {
   SOCKET_LOG(("nsSocketTransport::InitiateSocket [this=%p]\n", this));
 
   nsresult rv;
   bool isLocal;
   IsLocal(&isLocal);
 
   if (gIOService->IsNetTearingDown()) {
@@ -1525,16 +1544,33 @@ nsresult nsSocketTransport::InitiateSock
       tfo = true;
       SOCKET_LOG(
           ("nsSocketTransport::InitiateSocket TCP Fast Open "
            "started [this=%p]\n",
            this));
     }
   }
 
+  if (usingSSL && SSLTokensCache::IsEnabled()) {
+    nsTArray<uint8_t> token;
+    nsresult rv2 = SSLTokensCache::Get(mHost, token);
+    if (NS_SUCCEEDED(rv2) && token.Length() != 0) {
+      SECStatus srv =
+          SSL_SetResumptionToken(fd, token.Elements(), token.Length());
+      if (srv == SECFailure) {
+        SOCKET_LOG(("Setting token failed with NSS error %d [host=%s]",
+                    PORT_GetError(), PromiseFlatCString(mHost).get()));
+        SSLTokensCache::Remove(mHost);
+      }
+    }
+
+    SSL_SetResumptionTokenCallback(fd, &StoreResumptionToken, this);
+    mSSLCallbackSet = true;
+  }
+
   bool connectCalled = true;  // This is only needed for telemetry.
   status = PR_Connect(fd, &prAddr, NS_SOCKET_CONNECT_TIMEOUT);
   PRErrorCode code = PR_GetError();
   if (status == PR_SUCCESS) {
     PR_SetFDInheritable(fd, false);
   }
   if ((status == PR_SUCCESS) && tfo) {
     {
@@ -2011,16 +2047,21 @@ void STS_PRCloseOnSocketTransport(PRFile
 }
 
 void nsSocketTransport::ReleaseFD_Locked(PRFileDesc* fd) {
   mLock.AssertCurrentThreadOwns();
 
   NS_ASSERTION(mFD == fd, "wrong fd");
 
   if (--mFDref == 0) {
+    if (mSSLCallbackSet) {
+      SSL_SetResumptionTokenCallback(fd, nullptr, nullptr);
+      mSSLCallbackSet = false;
+    }
+
     if (gIOService->IsNetTearingDown() &&
         ((PR_IntervalNow() - gIOService->NetTearingDownStarted()) >
          gSocketTransportService->MaxTimeForPrClosePref())) {
       // If shutdown last to long, let the socket leak and do not close it.
       SOCKET_LOG(("Intentional leak"));
     } else {
       if (mLingerPolarity || mLingerTimeout) {
         PRSocketOptionData socket_linger;
--- a/netwerk/base/nsSocketTransport2.h
+++ b/netwerk/base/nsSocketTransport2.h
@@ -22,16 +22,17 @@
 #include "nsIClassInfo.h"
 #include "TCPFastOpen.h"
 #include "mozilla/net/DNS.h"
 #include "nsASocketHandler.h"
 #include "mozilla/Telemetry.h"
 
 #include "prerror.h"
 #include "nsAutoPtr.h"
+#include "ssl.h"
 
 class nsICancelable;
 class nsIDNSRecord;
 class nsIInterfaceRequestor;
 
 //-----------------------------------------------------------------------------
 
 // after this short interval, we will return to PR_Poll
@@ -171,16 +172,20 @@ class nsSocketTransport final : public n
       Telemetry::HistogramID aIDConnectivityChange,
       Telemetry::HistogramID aIDLinkChange, Telemetry::HistogramID aIDOffline);
 
  protected:
   virtual ~nsSocketTransport();
   void CleanupTypes();
 
  private:
+  static SECStatus StoreResumptionToken(PRFileDesc* fd,
+                                        const PRUint8* resumptionToken,
+                                        unsigned int len, void* ctx);
+
   // event types
   enum {
     MSG_ENSURE_CONNECT,
     MSG_DNS_LOOKUP_COMPLETE,
     MSG_RETRY_INIT_SOCKET,
     MSG_TIMEOUT_CHANGED,
     MSG_INPUT_CLOSED,
     MSG_INPUT_PENDING,
@@ -468,14 +473,19 @@ class nsSocketTransport final : public n
 
   // A Fast Open callback.
   TCPFastOpen* mFastOpenCallback;
   bool mFastOpenLayerHasBufferedData;
   uint8_t mFastOpenStatus;
   nsresult mFirstRetryError;
 
   bool mDoNotRetryToConnect;
+
+  // True if SSL_SetResumptionTokenCallback was called. We need to clear the
+  // callback when mFD is nulled out to make sure the ssl layer cannot call
+  // the callback after nsSocketTransport is destroyed.
+  bool mSSLCallbackSet;
 };
 
 }  // namespace net
 }  // namespace mozilla
 
 #endif  // !nsSocketTransport_h__
--- a/security/nss.symbols
+++ b/security/nss.symbols
@@ -659,16 +659,17 @@ SSL_GetPreliminaryChannelInfo
 SSL_GetSRTPCipher
 SSL_GetStatistics
 SSL_HandshakeCallback
 SSL_HandshakeNegotiatedExtension
 SSL_ImplementedCiphers @DATA@
 SSL_ImportFD
 SSL_NamedGroupConfig
 SSL_NumImplementedCiphers @DATA@
+SSL_OptionGet
 SSL_OptionSet
 SSL_OptionSetDefault
 SSL_PeerCertificate
 SSL_PeerCertificateChain
 SSL_PeerSignedCertTimestamps
 SSL_PeerStapledOCSPResponses
 SSL_ResetHandshake
 SSL_SendAdditionalKeyShares