Merge inbound to mozilla-central. a=merge
authorNoemi Erli <nerli@mozilla.com>
Tue, 13 Nov 2018 18:27:16 +0200
changeset 446033 84b9cfcef11a337689bfb15944cf480e3cec415a
parent 446032 2655a3d8dcd9f1a27f5b1cd8f5cbbccd42d4cc52 (current diff)
parent 445988 2599b449d317ad862d059fdc5fe0d97e9caa15b2 (diff)
child 446034 b0cacbf71b3d7bbca76857adc20c8ddf9a4651f2
child 446081 71905f4204a95679202fb99b7f7354015f09b1c9
push id109819
push usernerli@mozilla.com
push dateTue, 13 Nov 2018 16:35:04 +0000
treeherdermozilla-inbound@b0cacbf71b3d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone65.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
Merge inbound to mozilla-central. a=merge
dom/base/nsContentUtils.h
js/src/gc/GC.cpp
modules/libpref/init/all.js
--- a/dom/base/nsContentUtils.cpp
+++ b/dom/base/nsContentUtils.cpp
@@ -8992,21 +8992,27 @@ nsContentUtils::InternalStorageAllowedFo
   if (uri) {
     bool isAbout = false;
     MOZ_ALWAYS_SUCCEEDS(uri->SchemeIs("about", &isAbout));
     if (isAbout) {
       return access;
     }
   }
 
-  if (StorageDisabledByAntiTracking(aWindow, aChannel, aPrincipal, aURI)) {
-    return StorageAccess::eDeny;
-  }
-
-  return access;
+  if (!StorageDisabledByAntiTracking(aWindow, aChannel, aPrincipal, aURI)) {
+    return access;
+  }
+
+  static const char* kPrefName =
+    "privacy.restrict3rdpartystorage.partitionedHosts";
+  if (IsURIInPrefList(uri, kPrefName)) {
+    return StorageAccess::ePartitionedOrDeny;
+  }
+
+  return StorageAccess::eDeny;
 }
 
 namespace {
 
 // We put StringBuilder in the anonymous namespace to prevent anything outside
 // this file from accidentally being linked against it.
 class BulkAppender
 {
@@ -11116,8 +11122,78 @@ nsContentUtils::StringifyJSON(JSContext*
   aOutStr.Truncate();
   JS::RootedValue value(aCx, aValue.get());
   nsAutoString serializedValue;
   NS_ENSURE_TRUE(JS_Stringify(aCx, &value, nullptr, JS::NullHandleValue,
                               JSONCreator, &serializedValue), false);
   aOutStr = serializedValue;
   return true;
 }
+
+/* static */ bool
+nsContentUtils::IsURIInPrefList(nsIURI* aURI, const char* aPrefName)
+{
+  MOZ_ASSERT(aPrefName);
+
+  if (!aURI) {
+    return false;
+  }
+
+  nsAutoCString scheme;
+  aURI->GetScheme(scheme);
+  if (!scheme.EqualsLiteral("http") &&
+      !scheme.EqualsLiteral("https")) {
+    return false;
+  }
+
+  nsAutoCString host;
+  aURI->GetHost(host);
+  if (host.IsEmpty()) {
+    return false;
+  }
+
+  nsAutoCString blackList;
+  Preferences::GetCString(aPrefName, blackList);
+  if (blackList.IsEmpty()) {
+    return false;
+  }
+
+  // The list is comma separated domain list.  Each item may start with "*.".
+  // If starts with "*.", it matches any sub-domains.
+
+  for (;;) {
+    int32_t index = blackList.Find(host, false);
+    if (index >= 0 &&
+        static_cast<uint32_t>(index) + host.Length() <= blackList.Length() &&
+        // If start of the black list or next to ","?
+        (!index || blackList[index - 1] == ',')) {
+      // If end of the black list or immediately before ","?
+      size_t indexAfterHost = index + host.Length();
+      if (indexAfterHost == blackList.Length() ||
+          blackList[indexAfterHost] == ',') {
+        return true;
+      }
+      // If next character is '/', we need to check the path too.
+      // We assume the path in blacklist means "/foo" + "*".
+      if (blackList[indexAfterHost] == '/') {
+        int32_t endOfPath = blackList.Find(",", false, indexAfterHost);
+        nsDependentCSubstring::size_type length =
+          endOfPath < 0 ? static_cast<nsDependentCSubstring::size_type>(-1) :
+                          endOfPath - indexAfterHost;
+        nsDependentCSubstring pathInBlackList(blackList,
+                                              indexAfterHost, length);
+        nsAutoCString filePath;
+        aURI->GetFilePath(filePath);
+        if (StringBeginsWith(filePath, pathInBlackList)) {
+          return true;
+        }
+      }
+    }
+    int32_t startIndexOfCurrentLevel = host[0] == '*' ? 1 : 0;
+    int32_t startIndexOfNextLevel =
+      host.Find(".", false, startIndexOfCurrentLevel + 1);
+    if (startIndexOfNextLevel <= 0) {
+      return false;
+    }
+    host = NS_LITERAL_CSTRING("*") +
+             nsDependentCSubstring(host, startIndexOfNextLevel);
+  }
+}
--- a/dom/base/nsContentUtils.h
+++ b/dom/base/nsContentUtils.h
@@ -1205,16 +1205,22 @@ public:
                      uint32_t *aColumnOut, nsString& aMessageOut);
 
   static nsresult CalculateBufferSizeForImage(const uint32_t& aStride,
                                             const mozilla::gfx::IntSize& aImageSize,
                                             const mozilla::gfx::SurfaceFormat& aFormat,
                                             size_t* aMaxBufferSize,
                                             size_t* aUsedBufferSize);
 
+  // Returns true if the URI's host is contained in a pref list which is a comma
+  // separated domain list.  Each item may start with "*.".  If starts with
+  // "*.", it matches any sub-domains.
+  static bool
+  IsURIInPrefList(nsIURI* aURI, const char* aPrefName);
+
 private:
   /**
    * Fill (with the parameters given) the localized string named |aKey| in
    * properties file |aFile|.
    */
   static nsresult FormatLocalizedString(PropertiesFile aFile,
                                         const char* aKey,
                                         const char16_t** aParams,
@@ -2961,16 +2967,19 @@ public:
   static bool IsNonSubresourceRequest(nsIChannel* aChannel);
 
   static bool IsNonSubresourceInternalPolicyType(nsContentPolicyType aType);
 
   // The order of these entries matters, as we use std::min for total ordering
   // of permissions. Private Browsing is considered to be more limiting
   // then session scoping
   enum class StorageAccess {
+    // The storage should be partitioned. if the caller is unable to do it, deny
+    // the storage access.
+    ePartitionedOrDeny = -1,
     // Don't allow access to the storage
     eDeny = 0,
     // Allow access to the storage, but only if it is secure to do so in a
     // private browsing context.
     ePrivateBrowsing = 1,
     // Allow access to the storage, but only persist it for the current session
     eSessionScoped = 2,
     // Allow access to the storage
--- a/dom/base/nsGlobalWindowInner.cpp
+++ b/dom/base/nsGlobalWindowInner.cpp
@@ -18,16 +18,17 @@
 #include "nsDOMNavigationTiming.h"
 #include "nsIDOMStorageManager.h"
 #include "mozilla/AutoplayPermissionManager.h"
 #include "mozilla/dom/ContentFrameMessageManager.h"
 #include "mozilla/dom/DOMJSProxyHandler.h"
 #include "mozilla/dom/DOMPrefs.h"
 #include "mozilla/dom/EventTarget.h"
 #include "mozilla/dom/LocalStorage.h"
+#include "mozilla/dom/PartitionedLocalStorage.h"
 #include "mozilla/dom/Storage.h"
 #include "mozilla/dom/IdleRequest.h"
 #include "mozilla/dom/Performance.h"
 #include "mozilla/dom/StorageEvent.h"
 #include "mozilla/dom/StorageEventBinding.h"
 #include "mozilla/dom/StorageNotifierService.h"
 #include "mozilla/dom/StorageUtils.h"
 #include "mozilla/dom/Timeout.h"
@@ -4879,56 +4880,91 @@ nsGlobalWindowInner::GetSessionStorage(E
 
 Storage*
 nsGlobalWindowInner::GetLocalStorage(ErrorResult& aError)
 {
   if (!Storage::StoragePrefIsEnabled()) {
     return nullptr;
   }
 
-  if (!mLocalStorage) {
-    if (nsContentUtils::StorageAllowedForWindow(this) ==
-          nsContentUtils::StorageAccess::eDeny) {
-      aError.Throw(NS_ERROR_DOM_SECURITY_ERR);
-      return nullptr;
-    }
-
-    nsIPrincipal *principal = GetPrincipal();
-    if (!principal) {
-      return nullptr;
-    }
-
+  // LocalStorage needs to be exposed in every context except for sandboxes and
+  // NullPrincipals (data: URLs, for instance). But we need to keep data
+  // separate in some scenarios: private-browsing and partitioned trackers.
+  // In private-browsing, LocalStorage keeps data in memory, and it shares
+  // StorageEvents just with other origins in the same private-browsing
+  // environment.
+  // For Partitioned Trackers, we expose a partitioned LocalStorage, which
+  // doesn't share data with other contexts, and it's just in memory.
+  // Partitioned localStorage is available only for trackers listed in the
+  // privacy.restrict3rdpartystorage.partitionedHosts pref. See
+  // nsContentUtils::IsURIInPrefList to know the syntax for the pref value.
+  // This is a temporary web-compatibility hack.
+
+  nsContentUtils::StorageAccess access =
+    nsContentUtils::StorageAllowedForWindow(this);
+  if (access == nsContentUtils::StorageAccess::eDeny) {
+    aError.Throw(NS_ERROR_DOM_SECURITY_ERR);
+    return nullptr;
+  }
+
+  // Note that this behavior is observable: if we grant storage permission to a
+  // tracker, we pass from the partitioned LocalStorage to the 'normal'
+  // LocalStorage. The previous data is lost and the 2 window.localStorage
+  // objects, before and after the permission granted, will be different.
+  if (access != nsContentUtils::StorageAccess::ePartitionedOrDeny &&
+      (!mLocalStorage ||
+        mLocalStorage->Type() == Storage::ePartitionedLocalStorage)) {
     nsresult rv;
     nsCOMPtr<nsIDOMStorageManager> storageManager =
       do_GetService("@mozilla.org/dom/localStorage-manager;1", &rv);
     if (NS_FAILED(rv)) {
       aError.Throw(rv);
       return nullptr;
     }
 
     nsString documentURI;
     if (mDoc) {
       aError = mDoc->GetDocumentURI(documentURI);
       if (NS_WARN_IF(aError.Failed())) {
         return nullptr;
       }
     }
 
+    nsIPrincipal *principal = GetPrincipal();
+    if (!principal) {
+      aError.Throw(NS_ERROR_DOM_SECURITY_ERR);
+      return nullptr;
+    }
+
     RefPtr<Storage> storage;
     aError = storageManager->CreateStorage(this, principal, documentURI,
                                            IsPrivateBrowsing(),
                                            getter_AddRefs(storage));
     if (aError.Failed()) {
       return nullptr;
     }
 
     mLocalStorage = storage;
     MOZ_ASSERT(mLocalStorage);
   }
 
+  if (access == nsContentUtils::StorageAccess::ePartitionedOrDeny &&
+      !mLocalStorage) {
+    nsIPrincipal *principal = GetPrincipal();
+    if (!principal) {
+      aError.Throw(NS_ERROR_DOM_SECURITY_ERR);
+      return nullptr;
+    }
+
+    mLocalStorage = new PartitionedLocalStorage(this, principal);
+  }
+
+  MOZ_ASSERT((access == nsContentUtils::StorageAccess::ePartitionedOrDeny) ==
+               (mLocalStorage->Type() == Storage::ePartitionedLocalStorage));
+
   return mLocalStorage;
 }
 
 IDBFactory*
 nsGlobalWindowInner::GetIndexedDB(ErrorResult& aError)
 {
   if (!mIndexedDB) {
     // This may keep mIndexedDB null without setting an error.
@@ -5822,33 +5858,40 @@ nsGlobalWindowInner::CloneStorageEvent(c
 
   // If null, this is a localStorage event received by IPC.
   if (!storageArea) {
     storage = GetLocalStorage(aRv);
     if (aRv.Failed() || !storage) {
       return nullptr;
     }
 
-    MOZ_ASSERT(storage->Type() == Storage::eLocalStorage);
-    RefPtr<LocalStorage> localStorage =
-      static_cast<LocalStorage*>(storage.get());
-
-    // We must apply the current change to the 'local' localStorage.
-    localStorage->ApplyEvent(aEvent);
+    if (storage->Type() == Storage::eLocalStorage) {
+      RefPtr<LocalStorage> localStorage =
+        static_cast<LocalStorage*>(storage.get());
+
+      // We must apply the current change to the 'local' localStorage.
+      localStorage->ApplyEvent(aEvent);
+    }
   } else if (storageArea->Type() == Storage::eSessionStorage) {
     storage = GetSessionStorage(aRv);
   } else {
     MOZ_ASSERT(storageArea->Type() == Storage::eLocalStorage);
     storage = GetLocalStorage(aRv);
   }
 
   if (aRv.Failed() || !storage) {
     return nullptr;
   }
 
+  if (storage->Type() == Storage::ePartitionedLocalStorage) {
+    // This error message is not exposed.
+    aRv.Throw(NS_ERROR_DOM_SECURITY_ERR);
+    return nullptr;
+  }
+
   MOZ_ASSERT(storage);
   MOZ_ASSERT_IF(storageArea, storage->IsForkOf(storageArea));
 
   dict.mStorageArea = storage;
 
   RefPtr<StorageEvent> event = StorageEvent::Constructor(this, aType, dict);
   return event.forget();
 }
--- a/dom/clients/manager/ClientIPCUtils.h
+++ b/dom/clients/manager/ClientIPCUtils.h
@@ -33,14 +33,14 @@ namespace IPC {
     public ContiguousEnumSerializer<mozilla::dom::VisibilityState,
                                     mozilla::dom::VisibilityState::Hidden,
                                     mozilla::dom::VisibilityState::EndGuard_>
   {};
 
   template<>
   struct ParamTraits<nsContentUtils::StorageAccess> :
     public ContiguousEnumSerializer<nsContentUtils::StorageAccess,
-                                    nsContentUtils::StorageAccess::eDeny,
+                                    nsContentUtils::StorageAccess::ePartitionedOrDeny,
                                     nsContentUtils::StorageAccess::eNumValues>
   {};
 } // namespace IPC
 
 #endif // _mozilla_dom_ClientIPCUtils_h
--- a/dom/indexedDB/IDBFactory.cpp
+++ b/dom/indexedDB/IDBFactory.cpp
@@ -314,17 +314,17 @@ IDBFactory::AllowedForWindowInternal(nsP
   }
 
   nsContentUtils::StorageAccess access =
     nsContentUtils::StorageAllowedForWindow(aWindow);
 
   // the factory callsite records whether the browser is in private browsing.
   // and thus we don't have to respect that setting here. IndexedDB has no
   // concept of session-local storage, and thus ignores it.
-  if (access == nsContentUtils::StorageAccess::eDeny) {
+  if (access <= nsContentUtils::StorageAccess::eDeny) {
     return NS_ERROR_DOM_SECURITY_ERR;
   }
 
   nsCOMPtr<nsIScriptObjectPrincipal> sop = do_QueryInterface(aWindow);
   MOZ_ASSERT(sop);
 
   nsCOMPtr<nsIPrincipal> principal = sop->GetPrincipal();
   if (NS_WARN_IF(!principal)) {
new file mode 100644
--- /dev/null
+++ b/dom/storage/PartitionedLocalStorage.cpp
@@ -0,0 +1,157 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 "PartitionedLocalStorage.h"
+#include "SessionStorageCache.h"
+
+#include "mozilla/dom/StorageBinding.h"
+
+namespace mozilla {
+namespace dom {
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(PartitionedLocalStorage, Storage);
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PartitionedLocalStorage)
+NS_INTERFACE_MAP_END_INHERITING(Storage)
+
+NS_IMPL_ADDREF_INHERITED(PartitionedLocalStorage, Storage)
+NS_IMPL_RELEASE_INHERITED(PartitionedLocalStorage, Storage)
+
+PartitionedLocalStorage::PartitionedLocalStorage(nsPIDOMWindowInner* aWindow,
+                                                 nsIPrincipal* aPrincipal)
+  : Storage(aWindow, aPrincipal)
+  , mCache(new SessionStorageCache())
+{
+}
+
+PartitionedLocalStorage::~PartitionedLocalStorage()
+{
+}
+
+int64_t
+PartitionedLocalStorage::GetOriginQuotaUsage() const
+{
+  return mCache->GetOriginQuotaUsage(SessionStorageCache::eSessionSetType);
+}
+
+uint32_t
+PartitionedLocalStorage::GetLength(nsIPrincipal& aSubjectPrincipal,
+                                   ErrorResult& aRv)
+{
+  if (!CanUseStorage(aSubjectPrincipal)) {
+    aRv.Throw(NS_ERROR_DOM_SECURITY_ERR);
+    return 0;
+  }
+
+  return mCache->Length(SessionStorageCache::eSessionSetType);
+}
+
+void
+PartitionedLocalStorage::Key(uint32_t aIndex, nsAString& aResult,
+                             nsIPrincipal& aSubjectPrincipal,
+                             ErrorResult& aRv)
+{
+  if (!CanUseStorage(aSubjectPrincipal)) {
+    aRv.Throw(NS_ERROR_DOM_SECURITY_ERR);
+    return;
+  }
+
+  mCache->Key(SessionStorageCache::eSessionSetType, aIndex, aResult);
+}
+
+void
+PartitionedLocalStorage::GetItem(const nsAString& aKey, nsAString& aResult,
+                                 nsIPrincipal& aSubjectPrincipal,
+                                 ErrorResult& aRv)
+{
+  if (!CanUseStorage(aSubjectPrincipal)) {
+    aRv.Throw(NS_ERROR_DOM_SECURITY_ERR);
+    return;
+  }
+
+  mCache->GetItem(SessionStorageCache::eSessionSetType, aKey, aResult);
+}
+
+void
+PartitionedLocalStorage::GetSupportedNames(nsTArray<nsString>& aKeys)
+{
+  if (!CanUseStorage(*nsContentUtils::SubjectPrincipal())) {
+    // return just an empty array
+    aKeys.Clear();
+    return;
+  }
+
+  mCache->GetKeys(SessionStorageCache::eSessionSetType, aKeys);
+}
+
+void
+PartitionedLocalStorage::SetItem(const nsAString& aKey, const nsAString& aValue,
+                                 nsIPrincipal& aSubjectPrincipal,
+                                 ErrorResult& aRv)
+{
+  if (!CanUseStorage(aSubjectPrincipal)) {
+    aRv.Throw(NS_ERROR_DOM_SECURITY_ERR);
+    return;
+  }
+
+  nsString oldValue;
+  nsresult rv = mCache->SetItem(SessionStorageCache::eSessionSetType, aKey,
+                                aValue, oldValue);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    aRv.Throw(rv);
+    return;
+  }
+
+  if (rv == NS_SUCCESS_DOM_NO_OPERATION) {
+    return;
+  }
+}
+
+void
+PartitionedLocalStorage::RemoveItem(const nsAString& aKey,
+                                    nsIPrincipal& aSubjectPrincipal,
+                                    ErrorResult& aRv)
+{
+  if (!CanUseStorage(aSubjectPrincipal)) {
+    aRv.Throw(NS_ERROR_DOM_SECURITY_ERR);
+    return;
+  }
+
+  nsString oldValue;
+  nsresult rv = mCache->RemoveItem(SessionStorageCache::eSessionSetType, aKey,
+                                   oldValue);
+  MOZ_ASSERT(NS_SUCCEEDED(rv));
+
+  if (rv == NS_SUCCESS_DOM_NO_OPERATION) {
+    return;
+  }
+}
+
+void
+PartitionedLocalStorage::Clear(nsIPrincipal& aSubjectPrincipal,
+                               ErrorResult& aRv)
+{
+  uint32_t length = GetLength(aSubjectPrincipal, aRv);
+  if (!length) {
+    return;
+  }
+
+  mCache->Clear(SessionStorageCache::eSessionSetType);
+}
+
+bool
+PartitionedLocalStorage::IsForkOf(const Storage* aOther) const
+{
+  MOZ_ASSERT(aOther);
+  if (aOther->Type() != eLocalStorage) {
+    return false;
+  }
+
+  return mCache == static_cast<const PartitionedLocalStorage*>(aOther)->mCache;
+}
+
+} // dom namespace
+} // mozilla namespace
new file mode 100644
--- /dev/null
+++ b/dom/storage/PartitionedLocalStorage.h
@@ -0,0 +1,71 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 mozilla_dom_PartitionedLocalStorage_h
+#define mozilla_dom_PartitionedLocalStorage_h
+
+#include "Storage.h"
+
+class nsIPrincipal;
+
+namespace mozilla {
+namespace dom {
+
+class SessionStorageCache;
+
+// PartitionedLocalStorage is a in-memory-only storage exposed to trackers. It
+// doesn't share data with other contexts.
+
+class PartitionedLocalStorage final : public Storage
+{
+public:
+  NS_DECL_ISUPPORTS_INHERITED
+  NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(PartitionedLocalStorage, Storage)
+
+  PartitionedLocalStorage(nsPIDOMWindowInner* aWindow,
+                          nsIPrincipal* aPrincipal);
+
+  StorageType Type() const override { return ePartitionedLocalStorage; }
+
+  int64_t GetOriginQuotaUsage() const override;
+
+  bool IsForkOf(const Storage* aStorage) const override;
+
+  // WebIDL
+  uint32_t GetLength(nsIPrincipal& aSubjectPrincipal,
+                     ErrorResult& aRv) override;
+
+  void Key(uint32_t aIndex, nsAString& aResult,
+           nsIPrincipal& aSubjectPrincipal,
+           ErrorResult& aRv) override;
+
+  void GetItem(const nsAString& aKey, nsAString& aResult,
+               nsIPrincipal& aSubjectPrincipal,
+               ErrorResult& aRv) override;
+
+  void GetSupportedNames(nsTArray<nsString>& aKeys) override;
+
+  void SetItem(const nsAString& aKey, const nsAString& aValue,
+               nsIPrincipal& aSubjectPrincipal,
+               ErrorResult& aRv) override;
+
+  void RemoveItem(const nsAString& aKey,
+                  nsIPrincipal& aSubjectPrincipal,
+                  ErrorResult& aRv) override;
+
+  void Clear(nsIPrincipal& aSubjectPrincipal,
+             ErrorResult& aRv) override;
+
+private:
+  ~PartitionedLocalStorage();
+
+  RefPtr<SessionStorageCache> mCache;
+};
+
+} // dom namespace
+} // mozilla namespace
+
+#endif //mozilla_dom_PartitionedLocalStorage_h
--- a/dom/storage/SessionStorage.h
+++ b/dom/storage/SessionStorage.h
@@ -66,18 +66,16 @@ public:
                   ErrorResult& aRv) override;
 
   void Clear(nsIPrincipal& aSubjectPrincipal,
              ErrorResult& aRv) override;
 
 private:
   ~SessionStorage();
 
-  bool ProcessUsageDelta(int64_t aDelta);
-
   void
   BroadcastChangeNotification(const nsAString& aKey,
                               const nsAString& aOldValue,
                               const nsAString& aNewValue);
 
   RefPtr<SessionStorageCache> mCache;
   RefPtr<SessionStorageManager> mManager;
 
--- a/dom/storage/Storage.cpp
+++ b/dom/storage/Storage.cpp
@@ -49,17 +49,17 @@ Storage::CanUseStorage(nsIPrincipal& aSu
   // This method is responsible for correct setting of mIsSessionOnly.
   if (!StoragePrefIsEnabled()) {
     return false;
   }
 
   nsContentUtils::StorageAccess access =
     nsContentUtils::StorageAllowedForPrincipal(Principal());
 
-  if (access == nsContentUtils::StorageAccess::eDeny) {
+  if (access <= nsContentUtils::StorageAccess::eDeny) {
     return false;
   }
 
   mIsSessionOnly = access <= nsContentUtils::StorageAccess::eSessionScoped;
 
   return aSubjectPrincipal.Subsumes(mPrincipal);
 }
 
--- a/dom/storage/Storage.h
+++ b/dom/storage/Storage.h
@@ -30,16 +30,17 @@ public:
 
   Storage(nsPIDOMWindowInner* aWindow, nsIPrincipal* aPrincipal);
 
   static bool StoragePrefIsEnabled();
 
   enum StorageType {
     eSessionStorage,
     eLocalStorage,
+    ePartitionedLocalStorage,
   };
 
   virtual StorageType Type() const = 0;
 
   virtual bool IsForkOf(const Storage* aStorage) const = 0;
 
   virtual int64_t GetOriginQuotaUsage() const = 0;
 
--- a/dom/storage/moz.build
+++ b/dom/storage/moz.build
@@ -5,29 +5,31 @@
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 with Files("**"):
     BUG_COMPONENT = ("Core", "DOM: Web Storage")
 
 EXPORTS.mozilla.dom += [
     'LocalStorage.h',
     'LocalStorageManager.h',
+    'PartitionedLocalStorage.h',
     'SessionStorageManager.h',
     'Storage.h',
     'StorageActivityService.h',
     'StorageIPC.h',
     'StorageNotifierService.h',
     'StorageObserver.h',
     'StorageUtils.h',
 ]
 
 UNIFIED_SOURCES += [
     'LocalStorage.cpp',
     'LocalStorageCache.cpp',
     'LocalStorageManager.cpp',
+    'PartitionedLocalStorage.cpp',
     'SessionStorage.cpp',
     'SessionStorageCache.cpp',
     'SessionStorageManager.cpp',
     'Storage.cpp',
     'StorageActivityService.cpp',
     'StorageDBThread.cpp',
     'StorageDBUpdater.cpp',
     'StorageIPC.cpp',
--- a/js/src/gc/GC.cpp
+++ b/js/src/gc/GC.cpp
@@ -5855,41 +5855,47 @@ GCRuntime::sweepJitDataOnMainThread(Free
 
         if (initialState != State::NotActive) {
             // Cancel any active or pending off thread compilations. We also did
             // this before marking (in DiscardJITCodeForGC) so this is a no-op
             // for non-incremental GCs.
             js::CancelOffThreadIonCompile(rt, JS::Zone::Sweep);
         }
 
-        for (SweepGroupRealmsIter r(rt); !r.done(); r.next()) {
-            r->sweepJitRealm();
-        }
-
-        for (SweepGroupZonesIter zone(rt); !zone.done(); zone.next()) {
-            if (jit::JitZone* jitZone = zone->jitZone()) {
-                jitZone->sweep();
-            }
-        }
-
         // Bug 1071218: the following method has not yet been refactored to
         // work on a single zone-group at once.
 
         // Sweep entries containing about-to-be-finalized JitCode and
         // update relocated TypeSet::Types inside the JitcodeGlobalTable.
         jit::JitRuntime::SweepJitcodeGlobalTable(rt);
     }
 
     if (initialState != State::NotActive) {
         gcstats::AutoPhase apdc(stats(), gcstats::PhaseKind::SWEEP_DISCARD_CODE);
         for (SweepGroupZonesIter zone(rt); !zone.done(); zone.next()) {
             zone->discardJitCode(fop);
         }
     }
 
+    // JitZone/JitRealm must be swept *after* discarding JIT code, because
+    // Zone::discardJitCode might access CacheIRStubInfos deleted here.
+    {
+        gcstats::AutoPhase ap(stats(), gcstats::PhaseKind::SWEEP_JIT_DATA);
+
+        for (SweepGroupRealmsIter r(rt); !r.done(); r.next()) {
+            r->sweepJitRealm();
+        }
+
+        for (SweepGroupZonesIter zone(rt); !zone.done(); zone.next()) {
+            if (jit::JitZone* jitZone = zone->jitZone()) {
+                jitZone->sweep();
+            }
+        }
+    }
+
     {
         gcstats::AutoPhase ap1(stats(), gcstats::PhaseKind::SWEEP_TYPES);
         gcstats::AutoPhase ap2(stats(), gcstats::PhaseKind::SWEEP_TYPES_BEGIN);
         for (SweepGroupZonesIter zone(rt); !zone.done(); zone.next()) {
             zone->beginSweepTypes();
         }
     }
 }
--- a/js/src/jsapi-tests/testGCUniqueId.cpp
+++ b/js/src/jsapi-tests/testGCUniqueId.cpp
@@ -97,29 +97,33 @@ BEGIN_TEST(testGCUID)
     // Tear holes in the heap by unrooting the even objects and collecting.
     JS::Rooted<ObjectVector> vec2(cx, ObjectVector(cx));
     for (size_t i = 0; i < N; ++i) {
         if (i % 2 == 1) {
             CHECK(vec2.append(vec[i]));
         }
     }
     vec.clear();
-    MinimizeHeap(cx);
 
     // Grab the last object in the vector as our object of interest.
     obj = vec2.back();
     CHECK(obj);
+    CHECK(!js::gc::IsInsideNursery(obj));
     tenuredAddr = uintptr_t(obj.get());
     CHECK(obj->zone()->getOrCreateUniqueId(obj, &uid));
 
     // Force a compaction to move the object and check that the uid moved to
     // the new tenured heap location.
     JS::PrepareForFullGC(cx);
     JS::NonIncrementalGC(cx, GC_SHRINK, JS::gcreason::API);
-    MinimizeHeap(cx);
+
+    // There's a very low probability that this check could fail, but it is
+    // possible.  If it becomes an annoying intermittent then we should make
+    // this test more robust by recording IDs of many objects and then checking
+    // that some have moved.
     CHECK(uintptr_t(obj.get()) != tenuredAddr);
     CHECK(obj->zone()->hasUniqueId(obj));
     CHECK(obj->zone()->getOrCreateUniqueId(obj, &tmp));
     CHECK(uid == tmp);
 
     return true;
 }
 END_TEST(testGCUID)
--- a/js/src/vm/EnvironmentObject.cpp
+++ b/js/src/vm/EnvironmentObject.cpp
@@ -1593,18 +1593,17 @@ class DebugEnvironmentProxyHandler : pub
             return true;
         }
 
         /* Handle unaliased formals, vars, lets, and consts at function scope. */
         if (env->is<CallObject>()) {
             CallObject& callobj = env->as<CallObject>();
             RootedFunction fun(cx, &callobj.callee());
             RootedScript script(cx, JSFunction::getOrCreateScript(cx, fun));
-            AutoKeepTypeScripts keepTypes(cx);
-            if (!script->ensureHasTypes(cx, keepTypes) || !script->ensureHasAnalyzedArgsUsage(cx)) {
+            if (!script->ensureHasAnalyzedArgsUsage(cx)) {
                 return false;
             }
 
             BindingIter bi(script);
             while (bi && NameToId(bi.name()->asPropertyName()) != id) {
                 bi++;
             }
             if (!bi) {
--- a/js/src/vm/JSFunction.h
+++ b/js/src/vm/JSFunction.h
@@ -570,16 +570,29 @@ class JSFunction : public js::NativeObje
             JSScript* script = existingScriptNonDelazifying();
             flags_ &= ~INTERPRETED_LAZY;
             flags_ |= INTERPRETED;
             initScript(script);
         }
         return nonLazyScript();
     }
 
+    // If this is a scripted function, returns its canonical function (the
+    // original function allocated by the frontend). Note that lazy self-hosted
+    // builtins don't have a lazy script so in that case we also return nullptr.
+    JSFunction* maybeCanonicalFunction() const {
+        if (hasScript()) {
+            return nonLazyScript()->functionNonDelazifying();
+        }
+        if (isInterpretedLazy() && !isSelfHostedBuiltin()) {
+            return lazyScript()->functionNonDelazifying();
+        }
+        return nullptr;
+    }
+
     // The state of a JSFunction whose script errored out during bytecode
     // compilation. Such JSFunctions are only reachable via GC iteration and
     // not from script.
     // If u.scripted.s.script_ is non-null, the pointed JSScript is guaranteed
     // to be complete (see the comment above JSScript::initFromFunctionBox
     // callsite in JSScript::fullyInitFromEmitter).
     bool hasUncompletedScript() const {
         MOZ_ASSERT(hasScript());
--- a/js/src/vm/ObjectGroup.cpp
+++ b/js/src/vm/ObjectGroup.cpp
@@ -516,24 +516,17 @@ ObjectGroup::defaultNewGroup(JSContext* 
     // unboxed plain object.
     MOZ_ASSERT_IF(!clasp, !!associated);
 
     if (associated && !associated->is<TypeDescr>()) {
         MOZ_ASSERT(!clasp);
         if (associated->is<JSFunction>()) {
 
             // Canonicalize new functions to use the original one associated with its script.
-            JSFunction* fun = &associated->as<JSFunction>();
-            if (fun->hasScript()) {
-                associated = fun->nonLazyScript()->functionNonDelazifying();
-            } else if (fun->isInterpretedLazy() && !fun->isSelfHostedBuiltin()) {
-                associated = fun->lazyScript()->functionNonDelazifying();
-            } else {
-                associated = nullptr;
-            }
+            associated = associated->as<JSFunction>().maybeCanonicalFunction();
 
             // If we have previously cleared the 'new' script information for this
             // function, don't try to construct another one.
             if (associated && associated->as<JSFunction>().wasNewScriptCleared()) {
                 associated = nullptr;
             }
 
         } else {
--- a/js/src/vm/TypeInference.cpp
+++ b/js/src/vm/TypeInference.cpp
@@ -4020,16 +4020,20 @@ PreliminaryObjectArrayWithTemplate::mayb
 /* static */ bool
 TypeNewScript::make(JSContext* cx, ObjectGroup* group, JSFunction* fun)
 {
     AutoSweepObjectGroup sweep(group);
     MOZ_ASSERT(cx->zone()->types.activeAnalysis);
     MOZ_ASSERT(!group->newScript(sweep));
     MOZ_ASSERT(!group->maybeUnboxedLayout(sweep));
 
+    // rollbackPartiallyInitializedObjects expects function_ to be
+    // canonicalized.
+    MOZ_ASSERT(fun->maybeCanonicalFunction() == fun);
+
     if (group->unknownProperties(sweep)) {
         return true;
     }
 
     auto newScript = cx->make_unique<TypeNewScript>();
     if (!newScript) {
         return false;
     }
@@ -4399,17 +4403,23 @@ TypeNewScript::rollbackPartiallyInitiali
     for (AllScriptFramesIter iter(cx); !iter.done(); ++iter) {
         {
             AutoEnterOOMUnsafeRegion oomUnsafe;
             if (!pcOffsets.append(iter.script()->pcToOffset(iter.pc()))) {
                 oomUnsafe.crash("rollbackPartiallyInitializedObjects");
             }
         }
 
-        if (!iter.isConstructing() || !iter.matchCallee(cx, function)) {
+        if (!iter.isConstructing()) {
+            continue;
+        }
+
+        MOZ_ASSERT(iter.calleeTemplate()->maybeCanonicalFunction());
+
+        if (iter.calleeTemplate()->maybeCanonicalFunction() != function) {
             continue;
         }
 
         // Derived class constructors initialize their this-binding later and
         // we shouldn't run the definite properties analysis on them.
         MOZ_ASSERT(!iter.script()->isDerivedClassConstructor());
 
         Value thisv = iter.thisArgument(cx);
--- a/layout/base/PresShell.cpp
+++ b/layout/base/PresShell.cpp
@@ -7921,84 +7921,16 @@ GetDocumentURIToCompareWithBlacklist(Pre
       continue;
     }
     nsCOMPtr<nsIURI> uri;
     principal->GetURI(getter_AddRefs(uri));
     return uri.forget();
   }
   return nullptr;
 }
-
-static bool
-IsURIInBlacklistPref(nsIURI* aURI,
-                     const char* aBlacklistPrefName)
-{
-  if (!aURI) {
-    return false;
-  }
-
-  nsAutoCString scheme;
-  aURI->GetScheme(scheme);
-  if (!scheme.EqualsLiteral("http") &&
-      !scheme.EqualsLiteral("https")) {
-    return false;
-  }
-
-  nsAutoCString host;
-  aURI->GetHost(host);
-  if (host.IsEmpty()) {
-    return false;
-  }
-
-  // The black list is comma separated domain list.  Each item may start with
-  // "*.".  If starts with "*.", it matches any sub-domains.
-  nsAutoCString blackList;
-  Preferences::GetCString(aBlacklistPrefName, blackList);
-  if (blackList.IsEmpty()) {
-    return false;
-  }
-
-  for (;;) {
-    int32_t index = blackList.Find(host, false);
-    if (index >= 0 &&
-        static_cast<uint32_t>(index) + host.Length() <= blackList.Length() &&
-        // If start of the black list or next to ","?
-        (!index || blackList[index - 1] == ',')) {
-      // If end of the black list or immediately before ","?
-      size_t indexAfterHost = index + host.Length();
-      if (indexAfterHost == blackList.Length() ||
-          blackList[indexAfterHost] == ',') {
-        return true;
-      }
-      // If next character is '/', we need to check the path too.
-      // We assume the path in blacklist means "/foo" + "*".
-      if (blackList[indexAfterHost] == '/') {
-        int32_t endOfPath = blackList.Find(",", false, indexAfterHost);
-        nsDependentCSubstring::size_type length =
-          endOfPath < 0 ? static_cast<nsDependentCSubstring::size_type>(-1) :
-                          endOfPath - indexAfterHost;
-        nsDependentCSubstring pathInBlackList(blackList,
-                                              indexAfterHost, length);
-        nsAutoCString filePath;
-        aURI->GetFilePath(filePath);
-        if (StringBeginsWith(filePath, pathInBlackList)) {
-          return true;
-        }
-      }
-    }
-    int32_t startIndexOfCurrentLevel = host[0] == '*' ? 1 : 0;
-    int32_t startIndexOfNextLevel =
-      host.Find(".", false, startIndexOfCurrentLevel + 1);
-    if (startIndexOfNextLevel <= 0) {
-      return false;
-    }
-    host = NS_LITERAL_CSTRING("*") +
-             nsDependentCSubstring(host, startIndexOfNextLevel);
-  }
-}
 #endif // #ifdef NIGHTLY_BUILD
 
 nsresult
 PresShell::DispatchEventToDOM(WidgetEvent* aEvent,
                               nsEventStatus* aStatus,
                               nsPresShellEventCB* aEventCB)
 {
   nsresult rv = NS_OK;
@@ -8034,20 +7966,20 @@ PresShell::DispatchEventToDOM(WidgetEven
       // Similarly, the other browsers sets non-zero value of keyCode or
       // charCode of keypress event to the other.  Therefore, we should
       // behave so, however, some web apps may be broken.  On such web apps,
       // we should keep using legacy our behavior.
       if (!mInitializedWithKeyPressEventDispatchingBlacklist) {
         mInitializedWithKeyPressEventDispatchingBlacklist = true;
         nsCOMPtr<nsIURI> uri = GetDocumentURIToCompareWithBlacklist(*this);
         mForceDispatchKeyPressEventsForNonPrintableKeys =
-          IsURIInBlacklistPref(uri,
+          nsContentUtils::IsURIInPrefList(uri,
             "dom.keyboardevent.keypress.hack.dispatch_non_printable_keys");
         mForceUseLegacyKeyCodeAndCharCodeValues =
-          IsURIInBlacklistPref(uri,
+          nsContentUtils::IsURIInPrefList(uri,
             "dom.keyboardevent.keypress.hack.use_legacy_keycode_and_charcode");
       }
       if (mForceDispatchKeyPressEventsForNonPrintableKeys) {
         aEvent->mFlags.mOnlySystemGroupDispatchInContent = false;
       }
       if (mForceUseLegacyKeyCodeAndCharCodeValues) {
         aEvent->AsKeyboardEvent()->mUseLegacyKeyCodeAndCharCodeValues = true;
       }
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -1358,16 +1358,19 @@ pref("content.sink.pending_event_mode", 
 
 // Disable popups from plugins by default
 //   0 = openAllowed
 //   1 = openControlled
 //   2 = openBlocked
 //   3 = openAbused
 pref("privacy.popups.disable_from_plugins", 3);
 
+// Enable Paritioned LocalStorage for a list of hosts.
+pref("privacy.restrict3rdpartystorage.partitionedHosts", "accounts.google.com/o/oauth2/");
+
 // Excessive reporting of blocked popups can be a DOS vector,
 // by overloading the main process as popups get blocked and when
 // users try to restore all popups, which is the most visible
 // option in our UI at the time of writing.
 // We will invisibly drop any popups from a page that has already
 // opened more than this number of popups.
 pref("privacy.popups.maxReported", 100);
 
--- a/toolkit/components/antitracking/test/browser/browser.ini
+++ b/toolkit/components/antitracking/test/browser/browser.ini
@@ -55,8 +55,11 @@ skip-if = serviceworker_e10s
 [browser_storageAccessPromiseResolveHandlerUserInteraction.js]
 [browser_storageAccessRemovalNavigateSubframe.js]
 skip-if = serviceworker_e10s
 [browser_storageAccessSandboxed.js]
 skip-if = serviceworker_e10s
 [browser_storageAccessWithHeuristics.js]
 [browser_allowPermissionForTracker.js]
 [browser_denyPermissionForTracker.js]
+[browser_partitionedLocalStorage.js]
+[browser_partitionedLocalStorage_events.js]
+support-files = localStorageEvents.html
new file mode 100644
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_partitionedLocalStorage.js
@@ -0,0 +1,46 @@
+AntiTracking.runTest("localStorage and Storage Access API",
+  async _ => {
+    /* import-globals-from storageAccessAPIHelpers.js */
+    await noStorageAccessInitially();
+
+    localStorage.foo = 42;
+    ok(true, "LocalStorage is allowed");
+    is(localStorage.foo, "42", "The value matches");
+
+    var prevLocalStorage = localStorage;
+
+    /* import-globals-from storageAccessAPIHelpers.js */
+    await callRequestStorageAccess();
+
+    ok(localStorage != prevLocalStorage, "We have a new localStorage");
+    is(localStorage.foo, undefined, "Undefined value after.");
+
+    localStorage.foo = 42;
+    ok(true, "LocalStorage is still allowed");
+    is(localStorage.foo, "42", "The value matches");
+  },
+  async _ => {
+    /* import-globals-from storageAccessAPIHelpers.js */
+    await noStorageAccessInitially();
+
+    localStorage.foo = 42;
+    ok(true, "LocalStorage is allowed");
+    is(localStorage.foo, "42", "The value matches");
+
+    var prevLocalStorage = localStorage;
+
+    /* import-globals-from storageAccessAPIHelpers.js */
+    await callRequestStorageAccess();
+
+    // For non-tracking windows, calling the API is a no-op
+    ok(localStorage == prevLocalStorage, "We have a new localStorage");
+    is(localStorage.foo, "42", "The value matches");
+    ok(true, "LocalStorage is allowed");
+  },
+  async _ => {
+    await new Promise(resolve => {
+      Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => resolve());
+    });
+  },
+  [["privacy.restrict3rdpartystorage.partitionedHosts", "tracking.example.org,tracking.example.com"]],
+  false, false);
new file mode 100644
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_partitionedLocalStorage_events.js
@@ -0,0 +1,537 @@
+// A same origin (and same-process via setting "dom.ipc.processCount" to 1)
+// top-level window with access to real localStorage does not share storage
+// with an ePartitionOrDeny iframe that should have PartitionedLocalStorage and
+// no storage events are received in either direction.  (Same-process in order
+// to avoid having to worry about any e10s propagation issues.)
+add_task(async _ => {
+  await SpecialPowers.pushPrefEnv({"set": [
+    ["dom.ipc.processCount", 1],
+    ["network.cookie.cookieBehavior", Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER],
+    ["privacy.trackingprotection.enabled", false],
+    ["privacy.trackingprotection.pbmode.enabled", false],
+    ["privacy.trackingprotection.annotate_channels", true],
+    ["browser.fastblock.enabled", false], // prevent intermittent failures
+    ["privacy.restrict3rdpartystorage.partitionedHosts", "tracking.example.org,tracking.example.com"],
+  ]});
+
+  await UrlClassifierTestUtils.addTestTrackers();
+
+  info("Creating a non-tracker top-level context");
+  let normalTab = BrowserTestUtils.addTab(gBrowser, TEST_DOMAIN + TEST_PATH + "page.html");
+  let normalBrowser = gBrowser.getBrowserForTab(normalTab);
+  await BrowserTestUtils.browserLoaded(normalBrowser);
+
+  info("Creating a tracker top-level context");
+  let trackerTab = BrowserTestUtils.addTab(gBrowser, TEST_3RD_PARTY_DOMAIN + TEST_PATH + "page.html");
+  let trackerBrowser = gBrowser.getBrowserForTab(trackerTab);
+  await BrowserTestUtils.browserLoaded(trackerBrowser);
+
+  info("The non-tracker page opens a tracker iframe");
+  await ContentTask.spawn(normalBrowser, {
+      page: TEST_3RD_PARTY_DOMAIN + TEST_PATH + "localStorageEvents.html",
+    }, async obj => {
+      let ifr = content.document.createElement("iframe");
+      ifr.setAttribute("id", "ifr");
+      ifr.setAttribute("src", obj.page);
+
+      info("Iframe loading...");
+      await new content.Promise(resolve => {
+        ifr.onload = resolve;
+        content.document.body.appendChild(ifr);
+      });
+
+      info("Setting localStorage value...");
+      ifr.contentWindow.postMessage("setValue", "*");
+
+      info("Getting the value...");
+      let value = await new Promise(resolve => {
+        content.addEventListener("message", e => {
+          resolve(e.data);
+        }, {once: true});
+        ifr.contentWindow.postMessage("getValue", "*");
+      });
+
+      ok(value.startsWith("tracker-"), "The value is correctly set by the tracker");
+    }
+  );
+
+  info("The tracker page should not have received events");
+  await ContentTask.spawn(trackerBrowser, null, async _ => {
+      is(content.localStorage.foo, undefined, "Undefined value!");
+      content.localStorage.foo = "normal-" + Math.random();
+    }
+  );
+
+  info("Let's see if non-tracker page has received events");
+  await ContentTask.spawn(normalBrowser, null, async _ => {
+      let ifr = content.document.getElementById("ifr");
+
+      info("Getting the value...");
+      let value = await new Promise(resolve => {
+        content.addEventListener("message", e => {
+          resolve(e.data);
+        }, {once: true});
+        ifr.contentWindow.postMessage("getValue", "*");
+      });
+
+      ok(value.startsWith("tracker-"), "The value is correctly set by the tracker");
+
+      info("Getting the events...");
+      let events = await new Promise(resolve => {
+        content.addEventListener("message", e => {
+          resolve(e.data);
+        }, {once: true});
+        ifr.contentWindow.postMessage("getEvents", "*");
+      });
+
+      is(events, 0, "No events");
+    }
+  );
+
+  BrowserTestUtils.removeTab(trackerTab);
+  BrowserTestUtils.removeTab(normalTab);
+});
+
+// Two ePartitionOrDeny iframes in the same tab in the same origin don"t see
+// the same localStorage values and no storage events are received from each
+// other.
+add_task(async _ => {
+  await SpecialPowers.pushPrefEnv({"set": [
+    ["dom.ipc.processCount", 1],
+    ["network.cookie.cookieBehavior", Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER],
+    ["privacy.trackingprotection.enabled", false],
+    ["privacy.trackingprotection.pbmode.enabled", false],
+    ["privacy.trackingprotection.annotate_channels", true],
+    ["browser.fastblock.enabled", false], // prevent intermittent failures
+    ["privacy.restrict3rdpartystorage.partitionedHosts", "tracking.example.org,tracking.example.com"],
+  ]});
+
+  await UrlClassifierTestUtils.addTestTrackers();
+
+  info("Creating a non-tracker top-level context");
+  let normalTab = BrowserTestUtils.addTab(gBrowser, TEST_DOMAIN + TEST_PATH + "page.html");
+  let normalBrowser = gBrowser.getBrowserForTab(normalTab);
+  await BrowserTestUtils.browserLoaded(normalBrowser);
+
+  info("The non-tracker page opens a tracker iframe");
+  await ContentTask.spawn(normalBrowser, {
+      page: TEST_3RD_PARTY_DOMAIN + TEST_PATH + "localStorageEvents.html",
+    }, async obj => {
+      let ifr1 = content.document.createElement("iframe");
+      ifr1.setAttribute("id", "ifr1");
+      ifr1.setAttribute("src", obj.page);
+
+      info("Iframe 1 loading...");
+      await new content.Promise(resolve => {
+        ifr1.onload = resolve;
+        content.document.body.appendChild(ifr1);
+      });
+
+      let ifr2 = content.document.createElement("iframe");
+      ifr2.setAttribute("id", "ifr2");
+      ifr2.setAttribute("src", obj.page);
+
+      info("Iframe 2 loading...");
+      await new content.Promise(resolve => {
+        ifr2.onload = resolve;
+        content.document.body.appendChild(ifr2);
+      });
+
+      info("Setting localStorage value in ifr1...");
+      ifr1.contentWindow.postMessage("setValue", "*");
+
+      info("Getting the value from ifr1...");
+      let value = await new Promise(resolve => {
+        content.addEventListener("message", e => {
+          resolve(e.data);
+        }, {once: true});
+        ifr1.contentWindow.postMessage("getValue", "*");
+      });
+
+      ok(value.startsWith("tracker-"), "The value is correctly set in ifr1");
+
+      info("Getting the value from ifr2...");
+      value = await new Promise(resolve => {
+        content.addEventListener("message", e => {
+          resolve(e.data);
+        }, {once: true});
+        ifr2.contentWindow.postMessage("getValue", "*");
+      });
+
+      is(value, null, "The value is nt set in ifr2");
+
+      info("Getting the events received by ifr2...");
+      let events = await new Promise(resolve => {
+        content.addEventListener("message", e => {
+          resolve(e.data);
+        }, {once: true});
+        ifr2.contentWindow.postMessage("getEvents", "*");
+      });
+
+      is(events, 0, "No events");
+    }
+  );
+
+  BrowserTestUtils.removeTab(normalTab);
+});
+
+// Same as the previous test but with a cookie behavior of BEHAVIOR_ACCEPT
+// instead of BEHAVIOR_REJECT_TRACKER so the iframes get real, persistent
+// localStorage instead of partitioned localStorage.
+add_task(async _ => {
+  await SpecialPowers.pushPrefEnv({"set": [
+    ["dom.ipc.processCount", 1],
+    ["network.cookie.cookieBehavior", Ci.nsICookieService.BEHAVIOR_ACCEPT],
+    ["privacy.trackingprotection.enabled", false],
+    ["privacy.trackingprotection.pbmode.enabled", false],
+    ["privacy.trackingprotection.annotate_channels", true],
+    ["browser.fastblock.enabled", false], // prevent intermittent failures
+    ["privacy.restrict3rdpartystorage.partitionedHosts", "tracking.example.org,tracking.example.com"],
+  ]});
+
+  await UrlClassifierTestUtils.addTestTrackers();
+
+  info("Creating a non-tracker top-level context");
+  let normalTab = BrowserTestUtils.addTab(gBrowser, TEST_DOMAIN + TEST_PATH + "page.html");
+  let normalBrowser = gBrowser.getBrowserForTab(normalTab);
+  await BrowserTestUtils.browserLoaded(normalBrowser);
+
+  info("The non-tracker page opens a tracker iframe");
+  await ContentTask.spawn(normalBrowser, {
+      page: TEST_3RD_PARTY_DOMAIN + TEST_PATH + "localStorageEvents.html",
+    }, async obj => {
+      let ifr1 = content.document.createElement("iframe");
+      ifr1.setAttribute("id", "ifr1");
+      ifr1.setAttribute("src", obj.page);
+
+      info("Iframe 1 loading...");
+      await new content.Promise(resolve => {
+        ifr1.onload = resolve;
+        content.document.body.appendChild(ifr1);
+      });
+
+      let ifr2 = content.document.createElement("iframe");
+      ifr2.setAttribute("id", "ifr2");
+      ifr2.setAttribute("src", obj.page);
+
+      info("Iframe 2 loading...");
+      await new content.Promise(resolve => {
+        ifr2.onload = resolve;
+        content.document.body.appendChild(ifr2);
+      });
+
+      info("Setting localStorage value in ifr1...");
+      ifr1.contentWindow.postMessage("setValue", "*");
+
+      info("Getting the value from ifr1...");
+      let value1 = await new Promise(resolve => {
+        content.addEventListener("message", e => {
+          resolve(e.data);
+        }, {once: true});
+        ifr1.contentWindow.postMessage("getValue", "*");
+      });
+
+      ok(value1.startsWith("tracker-"), "The value is correctly set in ifr1");
+
+      info("Getting the value from ifr2...");
+      let value2 = await new Promise(resolve => {
+        content.addEventListener("message", e => {
+          resolve(e.data);
+        }, {once: true});
+        ifr2.contentWindow.postMessage("getValue", "*");
+      });
+
+      is(value2, value1, "The values match");
+
+      info("Getting the events received by ifr2...");
+      let events = await new Promise(resolve => {
+        content.addEventListener("message", e => {
+          resolve(e.data);
+        }, {once: true});
+        ifr2.contentWindow.postMessage("getEvents", "*");
+      });
+
+      is(events, 1, "One event");
+    }
+  );
+
+  BrowserTestUtils.removeTab(normalTab);
+});
+
+// An ePartitionOrDeny iframe navigated between two distinct pages on the same
+// origin does not see the values stored by the previous iframe.
+add_task(async _ => {
+  await SpecialPowers.pushPrefEnv({"set": [
+    ["dom.ipc.processCount", 1],
+    ["network.cookie.cookieBehavior", Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER],
+    ["privacy.trackingprotection.enabled", false],
+    ["privacy.trackingprotection.pbmode.enabled", false],
+    ["privacy.trackingprotection.annotate_channels", true],
+    ["browser.fastblock.enabled", false], // prevent intermittent failures
+    ["privacy.restrict3rdpartystorage.partitionedHosts", "tracking.example.org,tracking.example.com"],
+  ]});
+
+  await UrlClassifierTestUtils.addTestTrackers();
+
+  info("Creating a non-tracker top-level context");
+  let normalTab = BrowserTestUtils.addTab(gBrowser, TEST_DOMAIN + TEST_PATH + "page.html");
+  let normalBrowser = gBrowser.getBrowserForTab(normalTab);
+  await BrowserTestUtils.browserLoaded(normalBrowser);
+
+  info("The non-tracker page opens a tracker iframe");
+  await ContentTask.spawn(normalBrowser, {
+      page: TEST_3RD_PARTY_DOMAIN + TEST_PATH + "localStorageEvents.html",
+    }, async obj => {
+      let ifr = content.document.createElement("iframe");
+      ifr.setAttribute("id", "ifr");
+      ifr.setAttribute("src", obj.page);
+
+      info("Iframe loading...");
+      await new content.Promise(resolve => {
+        ifr.onload = resolve;
+        content.document.body.appendChild(ifr);
+      });
+
+      info("Setting localStorage value in ifr...");
+      ifr.contentWindow.postMessage("setValue", "*");
+
+      info("Getting the value from ifr...");
+      let value = await new Promise(resolve => {
+        content.addEventListener("message", e => {
+          resolve(e.data);
+        }, {once: true});
+        ifr.contentWindow.postMessage("getValue", "*");
+      });
+
+      ok(value.startsWith("tracker-"), "The value is correctly set in ifr");
+
+      info("Navigate...");
+      await new content.Promise(resolve => {
+        ifr.onload = resolve;
+        ifr.setAttribute("src", obj.page + "?" + Math.random());
+      });
+
+      info("Getting the value from ifr...");
+      value = await new Promise(resolve => {
+        content.addEventListener("message", e => {
+          resolve(e.data);
+        }, {once: true});
+        ifr.contentWindow.postMessage("getValue", "*");
+      });
+
+      is(value, null, "The value is undefined");
+    }
+  );
+
+  BrowserTestUtils.removeTab(normalTab);
+});
+
+// Like the previous test, but accepting trackers
+add_task(async _ => {
+  await SpecialPowers.pushPrefEnv({"set": [
+    ["dom.ipc.processCount", 1],
+    ["network.cookie.cookieBehavior", Ci.nsICookieService.BEHAVIOR_ACCEPT],
+    ["privacy.trackingprotection.enabled", false],
+    ["privacy.trackingprotection.pbmode.enabled", false],
+    ["privacy.trackingprotection.annotate_channels", true],
+    ["browser.fastblock.enabled", false], // prevent intermittent failures
+    ["privacy.restrict3rdpartystorage.partitionedHosts", "tracking.example.org,tracking.example.com"],
+  ]});
+
+  await UrlClassifierTestUtils.addTestTrackers();
+
+  info("Creating a non-tracker top-level context");
+  let normalTab = BrowserTestUtils.addTab(gBrowser, TEST_DOMAIN + TEST_PATH + "page.html");
+  let normalBrowser = gBrowser.getBrowserForTab(normalTab);
+  await BrowserTestUtils.browserLoaded(normalBrowser);
+
+  info("The non-tracker page opens a tracker iframe");
+  await ContentTask.spawn(normalBrowser, {
+      page: TEST_3RD_PARTY_DOMAIN + TEST_PATH + "localStorageEvents.html",
+    }, async obj => {
+      let ifr = content.document.createElement("iframe");
+      ifr.setAttribute("id", "ifr");
+      ifr.setAttribute("src", obj.page);
+
+      info("Iframe loading...");
+      await new content.Promise(resolve => {
+        ifr.onload = resolve;
+        content.document.body.appendChild(ifr);
+      });
+
+      info("Setting localStorage value in ifr...");
+      ifr.contentWindow.postMessage("setValue", "*");
+
+      info("Getting the value from ifr...");
+      let value = await new Promise(resolve => {
+        content.addEventListener("message", e => {
+          resolve(e.data);
+        }, {once: true});
+        ifr.contentWindow.postMessage("getValue", "*");
+      });
+
+      ok(value.startsWith("tracker-"), "The value is correctly set in ifr");
+
+      info("Navigate...");
+      await new content.Promise(resolve => {
+        ifr.onload = resolve;
+        ifr.setAttribute("src", obj.page + "?" + Math.random());
+      });
+
+      info("Getting the value from ifr...");
+      let value2 = await new Promise(resolve => {
+        content.addEventListener("message", e => {
+          resolve(e.data);
+        }, {once: true});
+        ifr.contentWindow.postMessage("getValue", "*");
+      });
+
+      is(value, value2, "The value is undefined");
+    }
+  );
+
+  BrowserTestUtils.removeTab(normalTab);
+});
+
+// An ePartitionOrDeny iframe on the same origin that is navigated to itself
+// via window.location.reload() or equivalent does not see the values stored by
+// its previous self.
+add_task(async _ => {
+  await SpecialPowers.pushPrefEnv({"set": [
+    ["dom.ipc.processCount", 1],
+    ["network.cookie.cookieBehavior", Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER],
+    ["privacy.trackingprotection.enabled", false],
+    ["privacy.trackingprotection.pbmode.enabled", false],
+    ["privacy.trackingprotection.annotate_channels", true],
+    ["browser.fastblock.enabled", false], // prevent intermittent failures
+    ["privacy.restrict3rdpartystorage.partitionedHosts", "tracking.example.org,tracking.example.com"],
+  ]});
+
+  await UrlClassifierTestUtils.addTestTrackers();
+
+  info("Creating a non-tracker top-level context");
+  let normalTab = BrowserTestUtils.addTab(gBrowser, TEST_DOMAIN + TEST_PATH + "page.html");
+  let normalBrowser = gBrowser.getBrowserForTab(normalTab);
+  await BrowserTestUtils.browserLoaded(normalBrowser);
+
+  info("The non-tracker page opens a tracker iframe");
+  await ContentTask.spawn(normalBrowser, {
+      page: TEST_3RD_PARTY_DOMAIN + TEST_PATH + "localStorageEvents.html",
+    }, async obj => {
+      let ifr = content.document.createElement("iframe");
+      ifr.setAttribute("id", "ifr");
+      ifr.setAttribute("src", obj.page);
+
+      info("Iframe loading...");
+      await new content.Promise(resolve => {
+        ifr.onload = resolve;
+        content.document.body.appendChild(ifr);
+      });
+
+      info("Setting localStorage value in ifr...");
+      ifr.contentWindow.postMessage("setValue", "*");
+
+      info("Getting the value from ifr...");
+      let value = await new Promise(resolve => {
+        content.addEventListener("message", e => {
+          resolve(e.data);
+        }, {once: true});
+        ifr.contentWindow.postMessage("getValue", "*");
+      });
+
+      ok(value.startsWith("tracker-"), "The value is correctly set in ifr");
+
+      info("Reload...");
+      await new content.Promise(resolve => {
+        ifr.onload = resolve;
+        ifr.contentWindow.postMessage("reload", "*");
+      });
+
+      info("Getting the value from ifr...");
+      value = await new Promise(resolve => {
+        content.addEventListener("message", e => {
+          resolve(e.data);
+        }, {once: true});
+        ifr.contentWindow.postMessage("getValue", "*");
+      });
+
+      is(value, null, "The value is undefined");
+    }
+  );
+
+  BrowserTestUtils.removeTab(normalTab);
+});
+
+// Like the previous test, but accepting trackers
+add_task(async _ => {
+  await SpecialPowers.pushPrefEnv({"set": [
+    ["dom.ipc.processCount", 1],
+    ["network.cookie.cookieBehavior", Ci.nsICookieService.BEHAVIOR_ACCEPT],
+    ["privacy.trackingprotection.enabled", false],
+    ["privacy.trackingprotection.pbmode.enabled", false],
+    ["privacy.trackingprotection.annotate_channels", true],
+    ["browser.fastblock.enabled", false], // prevent intermittent failures
+    ["privacy.restrict3rdpartystorage.partitionedHosts", "tracking.example.org,tracking.example.com"],
+  ]});
+
+  await UrlClassifierTestUtils.addTestTrackers();
+
+  info("Creating a non-tracker top-level context");
+  let normalTab = BrowserTestUtils.addTab(gBrowser, TEST_DOMAIN + TEST_PATH + "page.html");
+  let normalBrowser = gBrowser.getBrowserForTab(normalTab);
+  await BrowserTestUtils.browserLoaded(normalBrowser);
+
+  info("The non-tracker page opens a tracker iframe");
+  await ContentTask.spawn(normalBrowser, {
+      page: TEST_3RD_PARTY_DOMAIN + TEST_PATH + "localStorageEvents.html",
+    }, async obj => {
+      let ifr = content.document.createElement("iframe");
+      ifr.setAttribute("id", "ifr");
+      ifr.setAttribute("src", obj.page);
+
+      info("Iframe loading...");
+      await new content.Promise(resolve => {
+        ifr.onload = resolve;
+        content.document.body.appendChild(ifr);
+      });
+
+      info("Setting localStorage value in ifr...");
+      ifr.contentWindow.postMessage("setValue", "*");
+
+      info("Getting the value from ifr...");
+      let value = await new Promise(resolve => {
+        content.addEventListener("message", e => {
+          resolve(e.data);
+        }, {once: true});
+        ifr.contentWindow.postMessage("getValue", "*");
+      });
+
+      ok(value.startsWith("tracker-"), "The value is correctly set in ifr");
+
+      info("Reload...");
+      await new content.Promise(resolve => {
+        ifr.onload = resolve;
+        ifr.contentWindow.postMessage("reload", "*");
+      });
+
+      info("Getting the value from ifr...");
+      let value2 = await new Promise(resolve => {
+        content.addEventListener("message", e => {
+          resolve(e.data);
+        }, {once: true});
+        ifr.contentWindow.postMessage("getValue", "*");
+      });
+
+      is(value, value2, "The value is undefined");
+    }
+  );
+
+  BrowserTestUtils.removeTab(normalTab);
+});
+
+// Cleanup data.
+add_task(async _ => {
+  await new Promise(resolve => {
+    Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => resolve());
+  });
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/localStorageEvents.html
@@ -0,0 +1,30 @@
+<script>
+
+let eventCounter = 0;
+
+onmessage = e => {
+  if (e.data == "getValue") {
+    parent.postMessage(localStorage.foo, "*");
+    return;
+  }
+
+  if (e.data == "setValue") {
+    localStorage.foo = "tracker-" + Math.random();
+    return;
+  }
+
+  if (e.data == "getEvents") {
+    parent.postMessage(eventCounter, "*");
+    return;
+  }
+
+  if (e.data == "reload") {
+    window.location.reload();
+  }
+};
+
+addEventListener("storage", _ => {
+  ++eventCounter;
+});
+
+</script>