Bug 1080109 - Clear ServiceWorkers when clearing history or forgetting about site. r=baku,ehsan
authorNikhil Marathe <nsm.nikhil@gmail.com>
Thu, 05 Mar 2015 17:37:49 -0800
changeset 274269 a003c1905b9a2724be8b735eda091d6bdfbaccf7
parent 274268 f61b4a514e6ce23896f5d7a6e27d0128ed2a0035
child 274270 de714e5d0a13a905e600afa3242b72e10f2f3e0d
push id863
push userraliiev@mozilla.com
push dateMon, 03 Aug 2015 13:22:43 +0000
treeherdermozilla-release@f6321b14228d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbaku, ehsan
bugs1080109
milestone40.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 1080109 - Clear ServiceWorkers when clearing history or forgetting about site. r=baku,ehsan
build/pgo/server-locations.txt
dom/interfaces/base/nsIServiceWorkerManager.idl
dom/ipc/ContentChild.cpp
dom/ipc/ContentChild.h
dom/ipc/PContent.ipdl
dom/workers/ServiceWorkerManager.cpp
dom/workers/ServiceWorkerManager.h
dom/workers/test/serviceworkers/mochitest.ini
dom/workers/test/serviceworkers/sanitize/example_check_and_unregister.html
dom/workers/test/serviceworkers/sanitize/frame.html
dom/workers/test/serviceworkers/sanitize/register.html
dom/workers/test/serviceworkers/sanitize_worker.js
dom/workers/test/serviceworkers/test_sanitize.html
dom/workers/test/serviceworkers/test_sanitize_domain.html
testing/specialpowers/content/specialpowersAPI.js
--- a/build/pgo/server-locations.txt
+++ b/build/pgo/server-locations.txt
@@ -87,16 +87,18 @@ http://www.example.com:80            pri
 http://test1.example.com:80          privileged
 http://test2.example.com:80          privileged
 http://sub1.test1.example.com:80     privileged
 http://sub1.test2.example.com:80     privileged
 http://sub2.test1.example.com:80     privileged
 http://sub2.test2.example.com:80     privileged
 http://noxul.example.com:80          privileged,noxul
 http://example.net:80                privileged
+# Used to test that clearing Service Workers for domain example.com, does not clear prefixexample.com
+http://prefixexample.com:80
 
 https://example.com:443                privileged
 https://test1.example.com:443          privileged
 https://test2.example.com:443          privileged
 https://sub1.test1.example.com:443     privileged
 https://sub1.test2.example.com:443     privileged
 https://sub2.test1.example.com:443     privileged
 https://sub2.test2.example.com:443     privileged
--- a/dom/interfaces/base/nsIServiceWorkerManager.idl
+++ b/dom/interfaces/base/nsIServiceWorkerManager.idl
@@ -28,17 +28,17 @@ interface nsIServiceWorkerInfo : nsISupp
   readonly attribute DOMString scope;
   readonly attribute DOMString scriptSpec;
   readonly attribute DOMString currentWorkerURL;
 
   readonly attribute DOMString activeCacheName;
   readonly attribute DOMString waitingCacheName;
 };
 
-[scriptable, builtinclass, uuid(ff13ee00-5485-4551-af6f-1ab6de843365)]
+[scriptable, builtinclass, uuid(384c9aec-29e5-4bdb-abc2-fba10da83e17)]
 interface nsIServiceWorkerManager : nsISupports
 {
   /**
    * Registers a ServiceWorker with script loaded from `aScriptURI` to act as
    * the ServiceWorker for aScope.  Requires a valid entry settings object on
    * the stack. This means you must call this from content code 'within'
    * a window.
    *
@@ -106,16 +106,32 @@ interface nsIServiceWorkerManager : nsIS
    */
   [noscript] nsISupports GetDocumentController(in nsIDOMWindow aWindow);
 
   /*
    * This implements the soft update algorithm.
    */
   void softUpdate(in DOMString aScope);
 
+  /*
+   * Clears ServiceWorker registrations from memory and disk for the specified
+   * host.
+   * - All ServiceWorker instances change their state to redundant.
+   * - Existing ServiceWorker instances handling fetches will keep running.
+   * - All documents will immediately stop being controlled.
+   * - Unregister jobs will be queued for all registrations.
+   *   This eventually results in the registration being deleted from disk too.
+   */
+  void remove(in AUTF8String aHost);
+
+  /*
+   * Clear all registrations for all hosts. See remove().
+   */
+  void removeAll();
+
   // Testing
   DOMString getScopeForUrl(in DOMString path);
 
   // This is meant to be used only by about:serviceworkers. It returns an array
   // of nsIServiceWorkerInfo.
   nsIArray getAllRegistrations();
 
   void sendPushEvent(in ACString scope, in DOMString data);
--- a/dom/ipc/ContentChild.cpp
+++ b/dom/ipc/ContentChild.cpp
@@ -1248,16 +1248,36 @@ ContentChild::RecvUpdateServiceWorkerReg
 {
     nsCOMPtr<nsIServiceWorkerManager> swm = mozilla::services::GetServiceWorkerManager();
     if (swm) {
         swm->UpdateAllRegistrations();
     }
     return true;
 }
 
+bool
+ContentChild::RecvRemoveServiceWorkerRegistrationsForDomain(const nsString& aDomain)
+{
+    nsCOMPtr<nsIServiceWorkerManager> swm = mozilla::services::GetServiceWorkerManager();
+    if (swm) {
+        swm->Remove(NS_ConvertUTF16toUTF8(aDomain));
+    }
+    return true;
+}
+
+bool
+ContentChild::RecvRemoveServiceWorkerRegistrations()
+{
+    nsCOMPtr<nsIServiceWorkerManager> swm = mozilla::services::GetServiceWorkerManager();
+    if (swm) {
+        swm->RemoveAll();
+    }
+    return true;
+}
+
 static CancelableTask* sFirstIdleTask;
 
 static void FirstIdle(void)
 {
     MOZ_ASSERT(sFirstIdleTask);
     sFirstIdleTask = nullptr;
     ContentChild::GetSingleton()->SendFirstIdle();
 }
--- a/dom/ipc/ContentChild.h
+++ b/dom/ipc/ContentChild.h
@@ -297,16 +297,20 @@ public:
     virtual bool RecvSetConnectivity(const bool& connectivity) override;
 
     virtual bool RecvSpeakerManagerNotify() override;
 
     virtual bool RecvBidiKeyboardNotify(const bool& isLangRTL) override;
 
     virtual bool RecvUpdateServiceWorkerRegistrations() override;
 
+    virtual bool RecvRemoveServiceWorkerRegistrationsForDomain(const nsString& aDomain) override;
+
+    virtual bool RecvRemoveServiceWorkerRegistrations() override;
+
     virtual bool RecvNotifyVisited(const URIParams& aURI) override;
     // auto remove when alertfinished is received.
     nsresult AddRemoteAlertObserver(const nsString& aData, nsIObserver* aObserver);
 
     virtual bool RecvSystemMemoryAvailable(const uint64_t& aGetterId,
                                            const uint32_t& aMemoryAvailable) override;
 
     virtual bool RecvPreferenceUpdate(const PrefSetting& aPref) override;
--- a/dom/ipc/PContent.ipdl
+++ b/dom/ipc/PContent.ipdl
@@ -500,16 +500,20 @@ child:
     /**
      * Communication between the PuppetBidiKeyboard and the actual
      * BidiKeyboard hosted by the parent
      */
     async BidiKeyboardNotify(bool isLangRTL);
 
     async UpdateServiceWorkerRegistrations();
 
+    async RemoveServiceWorkerRegistrationsForDomain(nsString aDomain);
+
+    async RemoveServiceWorkerRegistrations();
+
     async DataStoreNotify(uint32_t aAppId, nsString aName,
                           nsString aManifestURL);
 
     /**
      * Dump this process's GC and CC logs to the provided files.
      *
      * For documentation on the other args, see dumpGCAndCCLogsToFile in
      * nsIMemoryInfoDumper.idl
--- a/dom/workers/ServiceWorkerManager.cpp
+++ b/dom/workers/ServiceWorkerManager.cpp
@@ -21,16 +21,17 @@
 #include "nsScriptLoader.h"
 #include "nsDebug.h"
 
 #include "jsapi.h"
 
 #include "mozilla/ErrorNames.h"
 #include "mozilla/LoadContext.h"
 #include "mozilla/dom/BindingUtils.h"
+#include "mozilla/dom/ContentParent.h"
 #include "mozilla/dom/DOMError.h"
 #include "mozilla/dom/ErrorEvent.h"
 #include "mozilla/dom/Headers.h"
 #include "mozilla/dom/InternalHeaders.h"
 #include "mozilla/dom/Navigator.h"
 #include "mozilla/dom/PromiseNativeHandler.h"
 #include "mozilla/dom/Request.h"
 #include "mozilla/dom/RootedDictionary.h"
@@ -63,16 +64,19 @@
 #endif
 
 using namespace mozilla;
 using namespace mozilla::dom;
 using namespace mozilla::ipc;
 
 BEGIN_WORKERS_NAMESPACE
 
+#define PURGE_DOMAIN_DATA "browser:purge-domain-data"
+#define PURGE_SESSION_HISTORY "browser:purge-session-history"
+
 static_assert(nsIHttpChannelInternal::CORS_MODE_SAME_ORIGIN == static_cast<uint32_t>(RequestMode::Same_origin),
               "RequestMode enumeration value should match Necko CORS mode value.");
 static_assert(nsIHttpChannelInternal::CORS_MODE_NO_CORS == static_cast<uint32_t>(RequestMode::No_cors),
               "RequestMode enumeration value should match Necko CORS mode value.");
 static_assert(nsIHttpChannelInternal::CORS_MODE_CORS == static_cast<uint32_t>(RequestMode::Cors),
               "RequestMode enumeration value should match Necko CORS mode value.");
 static_assert(nsIHttpChannelInternal::CORS_MODE_CORS_WITH_FORCED_PREFLIGHT == static_cast<uint32_t>(RequestMode::Cors_with_forced_preflight),
               "RequestMode enumeration value should match Necko CORS mode value.");
@@ -212,16 +216,17 @@ ServiceWorkerRegistrationInfo::~ServiceW
 //////////////////////////
 
 NS_IMPL_ADDREF(ServiceWorkerManager)
 NS_IMPL_RELEASE(ServiceWorkerManager)
 
 NS_INTERFACE_MAP_BEGIN(ServiceWorkerManager)
   NS_INTERFACE_MAP_ENTRY(nsIServiceWorkerManager)
   NS_INTERFACE_MAP_ENTRY(nsIIPCBackgroundChildCreateCallback)
+  NS_INTERFACE_MAP_ENTRY(nsIObserver)
   if (aIID.Equals(NS_GET_IID(ServiceWorkerManager)))
     foundInterface = static_cast<nsIServiceWorkerManager*>(this);
   else
   NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIServiceWorkerManager)
 NS_INTERFACE_MAP_END
 
 ServiceWorkerManager::ServiceWorkerManager()
   : mActor(nullptr)
@@ -231,16 +236,27 @@ ServiceWorkerManager::ServiceWorkerManag
 
   if (XRE_GetProcessType() == GeckoProcessType_Default) {
     nsRefPtr<ServiceWorkerRegistrar> swr = ServiceWorkerRegistrar::Get();
     MOZ_ASSERT(swr);
 
     nsTArray<ServiceWorkerRegistrationData> data;
     swr->GetRegistrations(data);
     LoadRegistrations(data);
+
+    nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+    if (obs) {
+      DebugOnly<nsresult> rv;
+      rv = obs->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false /* ownsWeak */);
+      MOZ_ASSERT(NS_SUCCEEDED(rv));
+      rv = obs->AddObserver(this, PURGE_SESSION_HISTORY, false /* ownsWeak */);
+      MOZ_ASSERT(NS_SUCCEEDED(rv));
+      rv = obs->AddObserver(this, PURGE_DOMAIN_DATA, false /* ownsWeak */);
+      MOZ_ASSERT(NS_SUCCEEDED(rv));
+    }
   }
 }
 
 ServiceWorkerManager::~ServiceWorkerManager()
 {
   // The map will assert if it is not empty when destroyed.
   mServiceWorkerRegistrationInfos.Clear();
 }
@@ -531,47 +547,65 @@ class ServiceWorkerRegisterJob final : p
   { }
 
   enum
   {
     REGISTER_JOB = 0,
     UPDATE_JOB = 1,
   } mJobType;
 
+  bool mCanceled;
+
 public:
   NS_DECL_ISUPPORTS_INHERITED
 
   // [[Register]]
   ServiceWorkerRegisterJob(ServiceWorkerJobQueue* aQueue,
                            const nsCString& aScope,
                            const nsCString& aScriptSpec,
                            ServiceWorkerUpdateFinishCallback* aCallback,
                            nsIPrincipal* aPrincipal)
     : ServiceWorkerJob(aQueue)
     , mScope(aScope)
     , mScriptSpec(aScriptSpec)
     , mCallback(aCallback)
     , mPrincipal(aPrincipal)
     , mJobType(REGISTER_JOB)
+    , mCanceled(false)
   { }
 
   // [[Update]]
   ServiceWorkerRegisterJob(ServiceWorkerJobQueue* aQueue,
                            ServiceWorkerRegistrationInfo* aRegistration,
                            ServiceWorkerUpdateFinishCallback* aCallback)
     : ServiceWorkerJob(aQueue)
     , mRegistration(aRegistration)
     , mCallback(aCallback)
     , mJobType(UPDATE_JOB)
+    , mCanceled(false)
   { }
 
+  bool
+  IsRegisterJob() const override
+  {
+    return true;
+  }
+
+  void
+  Cancel()
+  {
+    mQueue = nullptr;
+    mCanceled = true;
+  }
+
   void
   Start() override
   {
     MOZ_ASSERT(NS_IsMainThread());
+    MOZ_ASSERT(!mCanceled);
 
     nsRefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
     if (!swm->HasBackgroundActor()) {
       nsCOMPtr<nsIRunnable> runnable =
         NS_NewRunnableMethod(this, &ServiceWorkerRegisterJob::Start);
       swm->AppendPendingOperation(runnable);
       return;
     }
@@ -600,16 +634,22 @@ public:
     }
 
     Update();
   }
 
   void
   ComparisonResult(nsresult aStatus, bool aInCacheAndEqual, const nsAString& aNewCacheName) override
   {
+    nsRefPtr<ServiceWorkerRegisterJob> kungFuDeathGrip = this;
+    if (mCanceled) {
+      Fail(NS_ERROR_DOM_TYPE_ERR);
+      return;
+    }
+
     if (NS_WARN_IF(NS_FAILED(aStatus))) {
       Fail(NS_ERROR_DOM_TYPE_ERR);
       return;
     }
 
     if (aInCacheAndEqual) {
       Succeed();
       Done(NS_OK);
@@ -667,47 +707,63 @@ public:
     if (NS_WARN_IF(!ok)) {
       swm->mSetOfScopesBeingUpdated.Remove(mRegistration->mScope);
       Fail(NS_ERROR_DOM_ABORT_ERR);
       return;
     }
   }
 
   // Public so our error handling code can use it.
+  // Callers MUST hold a strong ref before calling this!
   void
   Fail(const ErrorEventInit& aError)
   {
     MOZ_ASSERT(mCallback);
-    mCallback->UpdateFailed(aError);
+    nsRefPtr<ServiceWorkerUpdateFinishCallback> callback = mCallback.forget();
+    // With cancellation support, we may only be running with one reference
+    // from another object like a stream loader or something.
+    // UpdateFailed may do something with that, so hold a ref to ourself since
+    // FailCommon relies on it.
+    // FailCommon does check for cancellation, but let's be safe here.
+    nsRefPtr<ServiceWorkerRegisterJob> kungFuDeathGrip = this;
+    callback->UpdateFailed(aError);
     FailCommon(NS_ERROR_DOM_JS_EXCEPTION);
   }
 
   // Public so our error handling code can continue with a successful worker.
   void
   ContinueInstall()
   {
+    // Even if we are canceled, ensure integrity of mSetOfScopesBeingUpdated
+    // first.
     nsRefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
     MOZ_ASSERT(swm->mSetOfScopesBeingUpdated.Contains(mRegistration->mScope));
     swm->mSetOfScopesBeingUpdated.Remove(mRegistration->mScope);
     // This is effectively the end of Step 4.3 of the [[Update]] algorithm.
     // The invocation of [[Install]] is not part of the atomic block.
 
+    nsRefPtr<ServiceWorkerRegisterJob> kungFuDeathGrip = this;
+    if (mCanceled) {
+      return Fail(NS_ERROR_DOM_ABORT_ERR);
+    }
+
     // Begin [[Install]] atomic step 4.
     if (mRegistration->mInstallingWorker) {
       // FIXME(nsm): Terminate and stuff
       mRegistration->mInstallingWorker->UpdateState(ServiceWorkerState::Redundant);
     }
 
     swm->InvalidateServiceWorkerRegistrationWorker(mRegistration,
                                                    WhichServiceWorker::INSTALLING_WORKER);
 
     mRegistration->mInstallingWorker = mUpdateAndInstallInfo.forget();
     mRegistration->mInstallingWorker->UpdateState(ServiceWorkerState::Installing);
 
     Succeed();
+    // The job should NOT call fail from this point on.
 
     // Step 4.6 "Queue a task..." for updatefound.
     nsCOMPtr<nsIRunnable> upr =
       NS_NewRunnableMethodWithArg<ServiceWorkerRegistrationInfo*>(
         swm,
         &ServiceWorkerManager::FireUpdateFoundOnServiceWorkerRegistrations,
         mRegistration);
 
@@ -738,28 +794,35 @@ public:
     // which sends the install event to the worker.
     r->Dispatch(jsapi.cx());
   }
 
 private:
   void
   Update()
   {
+    // Since Update() is called synchronously from Start(), we can assert this.
+    MOZ_ASSERT(!mCanceled);
     MOZ_ASSERT(mRegistration);
     nsCOMPtr<nsIRunnable> r =
       NS_NewRunnableMethod(this, &ServiceWorkerRegisterJob::ContinueUpdate);
     NS_DispatchToMainThread(r);
   }
 
   // Aspects of (actually the whole algorithm) of [[Update]] after
   // "Run the following steps in parallel."
   void
   ContinueUpdate()
   {
     AssertIsOnMainThread();
+    nsRefPtr<ServiceWorkerRegisterJob> kungFuDeathGrip = this;
+    if (mCanceled) {
+      return Fail(NS_ERROR_DOM_ABORT_ERR);
+    }
+
     if (mRegistration->mInstallingWorker) {
       // FIXME(nsm): "Terminate installing worker".
       mRegistration->mInstallingWorker->UpdateState(ServiceWorkerState::Redundant);
       mRegistration->mInstallingWorker = nullptr;
     }
 
     nsRefPtr<ServiceWorkerInfo> workerInfo = mRegistration->Newest();
     nsAutoString cacheName;
@@ -795,40 +858,52 @@ private:
     if (mRegistration->mInstallingWorker) {
       nsresult rv = serviceWorkerScriptCache::PurgeCache(mRegistration->mPrincipal,
                                                          mRegistration->mInstallingWorker->CacheName());
       if (NS_FAILED(rv)) {
         NS_WARNING("Failed to purge the installing worker cache.");
       }
     }
 
-    mCallback = nullptr;
     nsRefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
     swm->MaybeRemoveRegistration(mRegistration);
     // Ensures that the job can't do anything useful from this point on.
     mRegistration = nullptr;
     unused << NS_WARN_IF(NS_FAILED(aRv));
     Done(aRv);
   }
 
   // This MUST only be called when the job is still performing actions related
   // to registration or update. After the spec resolves the update promise, use
   // Done() with the failure code instead.
+  // Callers MUST hold a strong ref before calling this!
   void
   Fail(nsresult aRv)
   {
     MOZ_ASSERT(mCallback);
-    mCallback->UpdateFailed(aRv);
+    nsRefPtr<ServiceWorkerUpdateFinishCallback> callback = mCallback.forget();
+    // With cancellation support, we may only be running with one reference
+    // from another object like a stream loader or something.
+    // UpdateFailed may do something with that, so hold a ref to ourself since
+    // FailCommon relies on it.
+    // FailCommon does check for cancellation, but let's be safe here.
+    nsRefPtr<ServiceWorkerRegisterJob> kungFuDeathGrip = this;
+    callback->UpdateFailed(aRv);
     FailCommon(aRv);
   }
 
   void
   ContinueAfterInstallEvent(bool aInstallEventSuccess, bool aActivateImmediately)
   {
-    if (NS_WARN_IF(!mRegistration->mInstallingWorker)) {
+    if (mCanceled) {
+      return Done(NS_ERROR_DOM_ABORT_ERR);
+    }
+
+    if (!mRegistration->mInstallingWorker) {
+      NS_WARNING("mInstallingWorker was null.");
       return Done(NS_ERROR_DOM_ABORT_ERR);
     }
 
     nsRefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
 
     // "If installFailed is true"
     if (NS_WARN_IF(!aInstallEventSuccess)) {
       mRegistration->mInstallingWorker->UpdateState(ServiceWorkerState::Redundant);
@@ -861,16 +936,38 @@ private:
     Done(NS_OK);
     // Activate() is invoked out of band of atomic.
     mRegistration->TryToActivate();
   }
 };
 
 NS_IMPL_ISUPPORTS_INHERITED0(ServiceWorkerRegisterJob, ServiceWorkerJob);
 
+void
+ServiceWorkerJobQueue::CancelJobs()
+{
+  if (mJobs.IsEmpty()) {
+    return;
+  }
+
+  // We have to treat the first job specially. It is the running job and needs
+  // to be notified correctly.
+  nsRefPtr<ServiceWorkerJob> runningJob = mJobs[0];
+  // We can just let an Unregister job run to completion.
+  if (runningJob->IsRegisterJob()) {
+    ServiceWorkerRegisterJob* job = static_cast<ServiceWorkerRegisterJob*>(runningJob.get());
+    job->Cancel();
+  }
+
+  // Get rid of everything. Non-main thread objects may still be holding a ref
+  // to the running register job. Since we called Cancel() on it, the job's
+  // main thread functions will just exit.
+  mJobs.Clear();
+}
+
 NS_IMETHODIMP
 ContinueUpdateRunnable::Run()
 {
   AssertIsOnMainThread();
   nsRefPtr<ServiceWorkerJob> job = static_cast<ServiceWorkerJob*>(mJob.get());
   nsRefPtr<ServiceWorkerRegisterJob> upjob = static_cast<ServiceWorkerRegisterJob*>(job.get());
   upjob->ContinueInstall();
   return NS_OK;
@@ -1902,30 +1999,33 @@ ServiceWorkerManager::HandleError(JSCont
     return false;
   }
 
   mSetOfScopesBeingUpdated.Remove(aScope);
 
   ServiceWorkerJobQueue* queue = mJobQueues.Get(aScope);
   MOZ_ASSERT(queue);
   ServiceWorkerJob* job = queue->Peek();
-  ServiceWorkerRegisterJob* regJob = static_cast<ServiceWorkerRegisterJob*>(job);
-  MOZ_ASSERT(regJob);
-
-  RootedDictionary<ErrorEventInit> init(aCx);
-  init.mMessage = aMessage;
-  init.mFilename = aFilename;
-  init.mLineno = aLineNumber;
-  init.mColno = aColumnNumber;
+  if (job) {
+    MOZ_ASSERT(job->IsRegisterJob());
+    nsRefPtr<ServiceWorkerRegisterJob> regJob = static_cast<ServiceWorkerRegisterJob*>(job);
+
+    RootedDictionary<ErrorEventInit> init(aCx);
+    init.mMessage = aMessage;
+    init.mFilename = aFilename;
+    init.mLineno = aLineNumber;
+    init.mColno = aColumnNumber;
 
   NS_WARNING(nsPrintfCString(
               "Script error caused ServiceWorker registration to fail: %s:%u '%s'",
               NS_ConvertUTF16toUTF8(aFilename).get(), aLineNumber,
               NS_ConvertUTF16toUTF8(aMessage).get()).get());
-  regJob->Fail(init);
+    regJob->Fail(init);
+  }
+
   return true;
 }
 
 void
 ServiceWorkerRegistrationInfo::FinishActivate(bool aSuccess)
 {
   if (mPendingUninstall || !mActiveWorker) {
     return;
@@ -2982,46 +3082,40 @@ ServiceWorkerManager::MaybeRemoveRegistr
   MOZ_ASSERT(aRegistration);
   nsRefPtr<ServiceWorkerInfo> newest = aRegistration->Newest();
   if (!newest) {
     RemoveRegistration(aRegistration);
   }
 }
 
 void
-ServiceWorkerManager::RemoveRegistration(ServiceWorkerRegistrationInfo* aRegistration)
+ServiceWorkerManager::RemoveRegistrationInternal(ServiceWorkerRegistrationInfo* aRegistration)
 {
   MOZ_ASSERT(aRegistration);
   MOZ_ASSERT(!aRegistration->IsControllingDocuments());
   MOZ_ASSERT(mServiceWorkerRegistrationInfos.Contains(aRegistration->mScope));
   ServiceWorkerManager::RemoveScope(mOrderedScopes, aRegistration->mScope);
 
-  // Hold a ref since the hashtable may be the last ref.
-  nsRefPtr<ServiceWorkerRegistrationInfo> reg;
-  mServiceWorkerRegistrationInfos.Remove(aRegistration->mScope,
-                                         getter_AddRefs(reg));
-  MOZ_ASSERT(reg);
-
   // All callers should be either from a job in which case the actor is
   // available, or from MaybeStopControlling(), in which case, this will only be
   // called if a valid registration is found. If a valid registration exists,
   // it means the actor is available since the original map of registrations is
   // populated by it, and any new registrations wait until the actor is
   // available before proceeding (See ServiceWorkerRegisterJob::Start).
   MOZ_ASSERT(mActor);
 
   PrincipalInfo principalInfo;
-  if (NS_WARN_IF(NS_FAILED(PrincipalToPrincipalInfo(reg->mPrincipal,
+  if (NS_WARN_IF(NS_FAILED(PrincipalToPrincipalInfo(aRegistration->mPrincipal,
                                                     &principalInfo)))) {
     //XXXnsm I can't think of any other reason a stored principal would fail to
     //convert.
     NS_WARNING("Unable to unregister serviceworker due to possible OOM");
     return;
   }
-  mActor->SendUnregisterServiceWorker(principalInfo, NS_ConvertUTF8toUTF16(reg->mScope));
+  mActor->SendUnregisterServiceWorker(principalInfo, NS_ConvertUTF8toUTF16(aRegistration->mScope));
 }
 
 class ServiceWorkerDataInfo final : public nsIServiceWorkerInfo
 {
 public:
   NS_DECL_ISUPPORTS
   NS_DECL_NSISERVICEWORKERINFO
 
@@ -3037,17 +3131,96 @@ private:
 
   nsCOMPtr<nsIPrincipal> mPrincipal;
   nsString mScope;
   nsString mScriptSpec;
   nsString mCurrentWorkerURL;
   nsString mActiveCacheName;
   nsString mWaitingCacheName;
 };
-
+void
+ServiceWorkerManager::RemoveRegistration(ServiceWorkerRegistrationInfo* aRegistration)
+{
+  RemoveRegistrationInternal(aRegistration);
+  MOZ_ASSERT(mServiceWorkerRegistrationInfos.Contains(aRegistration->mScope));
+  mServiceWorkerRegistrationInfos.Remove(aRegistration->mScope);
+}
+
+namespace {
+/**
+ * See browser/components/sessionstore/Utils.jsm function hasRootDomain().
+ *
+ * Returns true if the |url| passed in is part of the given root |domain|.
+ * For example, if |url| is "www.mozilla.org", and we pass in |domain| as
+ * "mozilla.org", this will return true. It would return false the other way
+ * around.
+ */
+bool
+HasRootDomain(nsIURI* aURI, const nsACString& aDomain)
+{
+  AssertIsOnMainThread();
+  MOZ_ASSERT(aURI);
+
+  nsAutoCString host;
+  nsresult rv = aURI->GetHost(host);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return false;
+  }
+
+  nsACString::const_iterator start, end;
+  host.BeginReading(start);
+  host.EndReading(end);
+  if (!FindInReadable(aDomain, start, end)) {
+    return false;
+  }
+
+  if (host.Equals(aDomain)) {
+    return true;
+  }
+
+  // Beginning of the string matches, can't look at the previous char.
+  if (start.get() == host.BeginReading()) {
+    // Equals failed so this is fine.
+    return false;
+  }
+
+  char prevChar = *(--start);
+  return prevChar == '.';
+}
+
+// If host/aData is null, unconditionally unregisters.
+PLDHashOperator
+UnregisterIfMatchesHost(const nsACString& aScope,
+                        ServiceWorkerRegistrationInfo* aReg,
+                        void* aData)
+{
+  // We avoid setting toRemove = aReg by default since there is a possibility
+  // of failure when aData is passed, in which case we don't want to remove the
+  // registration.
+  ServiceWorkerRegistrationInfo* toRemove = nullptr;
+  if (aData) {
+    const nsACString& domain = *static_cast<nsACString*>(aData);
+    nsCOMPtr<nsIURI> scopeURI;
+    nsresult rv = NS_NewURI(getter_AddRefs(scopeURI), aScope, nullptr, nullptr);
+    // This way subdomains are also cleared.
+    if (NS_SUCCEEDED(rv) && HasRootDomain(scopeURI, domain)) {
+      toRemove = aReg;
+    }
+  } else {
+    toRemove = aReg;
+  }
+
+  if (toRemove) {
+    nsRefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
+    swm->ForceUnregister(toRemove);
+  }
+
+  return PL_DHASH_NEXT;
+}
+} // anonymous namespace
 NS_IMPL_ISUPPORTS(ServiceWorkerDataInfo, nsIServiceWorkerInfo)
 
 /* static */ already_AddRefed<ServiceWorkerDataInfo>
 ServiceWorkerDataInfo::Create(const ServiceWorkerRegistrationData& aData)
 {
   AssertIsOnMainThread();
 
   nsRefPtr<ServiceWorkerDataInfo> info = new ServiceWorkerDataInfo();
@@ -3138,16 +3311,48 @@ ServiceWorkerManager::GetAllRegistration
 
     array->AppendElement(info, false);
   }
 
   array.forget(aResult);
   return NS_OK;
 }
 
+// MUST ONLY BE CALLED FROM UnregisterIfMatchesHost!
+void
+ServiceWorkerManager::ForceUnregister(ServiceWorkerRegistrationInfo* aRegistration)
+{
+  MOZ_ASSERT(aRegistration);
+
+  ServiceWorkerJobQueue* mQueue;
+  mJobQueues.Get(aRegistration->mScope, &mQueue);
+  if (mQueue) {
+    mQueue->CancelJobs();
+  }
+
+  // Since Unregister is async, it is ok to call it in an enumeration.
+  Unregister(aRegistration->mPrincipal, nullptr, NS_ConvertUTF8toUTF16(aRegistration->mScope));
+}
+
+NS_IMETHODIMP
+ServiceWorkerManager::Remove(const nsACString& aHost)
+{
+  AssertIsOnMainThread();
+  mServiceWorkerRegistrationInfos.EnumerateRead(UnregisterIfMatchesHost, &const_cast<nsACString&>(aHost));
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+ServiceWorkerManager::RemoveAll()
+{
+  AssertIsOnMainThread();
+  mServiceWorkerRegistrationInfos.EnumerateRead(UnregisterIfMatchesHost, nullptr);
+  return NS_OK;
+}
+
 static PLDHashOperator
 UpdateEachRegistration(const nsACString& aKey,
                        ServiceWorkerRegistrationInfo* aInfo,
                        void* aUserArg) {
   auto This = static_cast<ServiceWorkerManager*>(aUserArg);
   MOZ_ASSERT(!aInfo->mScope.IsEmpty());
   nsresult res = This->SoftUpdate(NS_ConvertUTF8toUTF16(aInfo->mScope));
   unused << NS_WARN_IF(NS_FAILED(res));
@@ -3160,16 +3365,53 @@ ServiceWorkerManager::UpdateAllRegistrat
 {
   AssertIsOnMainThread();
 
   mServiceWorkerRegistrationInfos.EnumerateRead(UpdateEachRegistration, this);
 
   return NS_OK;
 }
 
+NS_IMETHODIMP
+ServiceWorkerManager::Observe(nsISupports* aSubject,
+                              const char* aTopic,
+                              const char16_t* aData)
+{
+  MOZ_ASSERT(XRE_GetProcessType() == GeckoProcessType_Default);
+
+  nsAutoTArray<ContentParent*,1> children;
+  ContentParent::GetAll(children);
+
+  if (strcmp(aTopic, PURGE_SESSION_HISTORY) == 0) {
+    for (uint32_t i = 0; i < children.Length(); i++) {
+      unused << children[i]->SendRemoveServiceWorkerRegistrations();
+    }
+
+    RemoveAll();
+  } else if (strcmp(aTopic, PURGE_DOMAIN_DATA) == 0) {
+    nsAutoString domain(aData);
+    for (uint32_t i = 0; i < children.Length(); i++) {
+      unused << children[i]->SendRemoveServiceWorkerRegistrationsForDomain(domain);
+    }
+
+    Remove(NS_ConvertUTF16toUTF8(domain));
+  } else if (strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID) == 0) {
+    nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+    if (obs) {
+      obs->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID);
+      obs->RemoveObserver(this, PURGE_SESSION_HISTORY);
+      obs->RemoveObserver(this, PURGE_DOMAIN_DATA);
+    }
+  } else {
+    MOZ_CRASH("Received message we aren't supposed to be registered for!");
+  }
+
+  return NS_OK;
+}
+
 void
 ServiceWorkerInfo::AppendWorker(ServiceWorker* aWorker)
 {
   MOZ_ASSERT(aWorker);
 #ifdef DEBUG
   nsAutoString workerURL;
   aWorker->GetScriptURL(workerURL);
   MOZ_ASSERT(workerURL.Equals(NS_ConvertUTF8toUTF16(mScriptSpec)));
--- a/dom/workers/ServiceWorkerManager.h
+++ b/dom/workers/ServiceWorkerManager.h
@@ -7,16 +7,17 @@
 #ifndef mozilla_dom_workers_serviceworkermanager_h
 #define mozilla_dom_workers_serviceworkermanager_h
 
 #include "nsIServiceWorkerManager.h"
 #include "nsCOMPtr.h"
 
 #include "ipc/IPCMessageUtils.h"
 #include "mozilla/Attributes.h"
+#include "mozilla/AutoRestore.h"
 #include "mozilla/LinkedList.h"
 #include "mozilla/Preferences.h"
 #include "mozilla/TypedEnumBits.h"
 #include "mozilla/WeakPtr.h"
 #include "mozilla/dom/BindingUtils.h"
 #include "mozilla/dom/Promise.h"
 #include "mozilla/dom/ServiceWorkerBinding.h" // For ServiceWorkerState
 #include "mozilla/dom/ServiceWorkerCommon.h"
@@ -55,16 +56,19 @@ protected:
   // queue.
   ServiceWorkerJobQueue* mQueue;
 
 public:
   NS_DECL_ISUPPORTS
 
   virtual void Start() = 0;
 
+  virtual bool
+  IsRegisterJob() const { return false; }
+
 protected:
   explicit ServiceWorkerJob(ServiceWorkerJobQueue* aQueue)
     : mQueue(aQueue)
   {
   }
 
   virtual ~ServiceWorkerJob()
   { }
@@ -73,18 +77,23 @@ protected:
   Done(nsresult aStatus);
 };
 
 class ServiceWorkerJobQueue final
 {
   friend class ServiceWorkerJob;
 
   nsTArray<nsRefPtr<ServiceWorkerJob>> mJobs;
+  bool mPopping;
 
 public:
+  explicit ServiceWorkerJobQueue()
+    : mPopping(false)
+  {}
+
   ~ServiceWorkerJobQueue()
   {
     if (!mJobs.IsEmpty()) {
       NS_WARNING("Pending/running jobs still around on shutdown!");
     }
   }
 
   void
@@ -94,28 +103,37 @@ public:
     MOZ_ASSERT(!mJobs.Contains(aJob));
     bool wasEmpty = mJobs.IsEmpty();
     mJobs.AppendElement(aJob);
     if (wasEmpty) {
       aJob->Start();
     }
   }
 
+  void
+  CancelJobs();
+
   // Only used by HandleError, keep it that way!
   ServiceWorkerJob*
   Peek()
   {
-    MOZ_ASSERT(!mJobs.IsEmpty());
+    if (mJobs.IsEmpty()) {
+      return nullptr;
+    }
     return mJobs[0];
   }
 
 private:
   void
   Pop()
   {
+    MOZ_ASSERT(!mPopping,
+               "Pop() called recursively, did you write a job which calls Done() synchronously from Start()?");
+    AutoRestore<bool> savePopping(mPopping);
+    mPopping = true;
     MOZ_ASSERT(!mJobs.IsEmpty());
     mJobs.RemoveElementAt(0);
     if (!mJobs.IsEmpty()) {
       mJobs[0]->Start();
     }
   }
 
   void
@@ -299,28 +317,30 @@ public:
 /*
  * The ServiceWorkerManager is a per-process global that deals with the
  * installation, querying and event dispatch of ServiceWorkers for all the
  * origins in the process.
  */
 class ServiceWorkerManager final
   : public nsIServiceWorkerManager
   , public nsIIPCBackgroundChildCreateCallback
+  , public nsIObserver
 {
   friend class GetReadyPromiseRunnable;
   friend class GetRegistrationsRunnable;
   friend class GetRegistrationRunnable;
   friend class ServiceWorkerRegisterJob;
   friend class ServiceWorkerRegistrationInfo;
   friend class ServiceWorkerUnregisterJob;
 
 public:
   NS_DECL_ISUPPORTS
   NS_DECL_NSISERVICEWORKERMANAGER
   NS_DECL_NSIIPCBACKGROUNDCHILDCREATECALLBACK
+  NS_DECL_NSIOBSERVER
   NS_DECLARE_STATIC_IID_ACCESSOR(NS_SERVICEWORKERMANAGER_IMPL_IID)
 
   static ServiceWorkerManager* FactoryCreate()
   {
     AssertIsOnMainThread();
 
     ServiceWorkerManager* res = new ServiceWorkerManager;
     NS_ADDREF(res);
@@ -331,17 +351,17 @@ public:
   // Each entry is an absolute URL representing the scope.
   //
   // An array is used for now since the number of controlled scopes per
   // domain is expected to be relatively low. If that assumption was proved
   // wrong this should be replaced with a better structure to avoid the
   // memmoves associated with inserting stuff in the middle of the array.
   nsTArray<nsCString> mOrderedScopes;
 
-  // Scope to registration. 
+  // Scope to registration.
   // The scope should be a fully qualified valid URL.
   nsRefPtrHashtable<nsCStringHashKey, ServiceWorkerRegistrationInfo> mServiceWorkerRegistrationInfos;
 
   nsTObserverArray<ServiceWorkerRegistrationListener*> mServiceWorkerRegistrationListeners;
 
   nsRefPtrHashtable<nsISupportsHashKey, ServiceWorkerRegistrationInfo> mControlledDocuments;
 
   // Maps scopes to job queues.
@@ -393,16 +413,21 @@ public:
                 nsTArray<ServiceWorkerClientInfo>& aControlledDocuments);
 
   static already_AddRefed<ServiceWorkerManager>
   GetInstance();
 
  void LoadRegistrations(
                  const nsTArray<ServiceWorkerRegistrationData>& aRegistrations);
 
+  // Used by remove() and removeAll() when clearing history.
+  // MUST ONLY BE CALLED FROM UnregisterIfMatchesHost!
+  void
+  ForceUnregister(ServiceWorkerRegistrationInfo* aRegistration);
+
   NS_IMETHOD
   AddRegistrationEventListener(const nsAString& aScope,
                                ServiceWorkerRegistrationListener* aListener);
 
   NS_IMETHOD
   RemoveRegistrationEventListener(const nsAString& aScope,
                                   ServiceWorkerRegistrationListener* aListener);
 private:
@@ -498,20 +523,28 @@ private:
   }
 
   static PLDHashOperator
   CheckPendingReadyPromisesEnumerator(nsISupports* aSupports,
                                       nsAutoPtr<PendingReadyPromise>& aData,
                                       void* aUnused);
 
   nsClassHashtable<nsISupportsHashKey, PendingReadyPromise> mPendingReadyPromises;
- 
+
   void
   MaybeRemoveRegistration(ServiceWorkerRegistrationInfo* aRegistration);
 
+  // Does all cleanup except removing the registration from
+  // mServiceWorkerRegistrationInfos. This is useful when we clear
+  // registrations via remove()/removeAll() since we are iterating over the
+  // hashtable and can cleanly remove within the hashtable enumeration
+  // function.
+  void
+  RemoveRegistrationInternal(ServiceWorkerRegistrationInfo* aRegistration);
+
   mozilla::ipc::PBackgroundChild* mActor;
 
   struct PendingOperation;
   nsTArray<PendingOperation> mPendingOperations;
 };
 
 NS_DEFINE_STATIC_IID_ACCESSOR(ServiceWorkerManager,
                               NS_SERVICEWORKERMANAGER_IMPL_IID);
--- a/dom/workers/test/serviceworkers/mochitest.ini
+++ b/dom/workers/test/serviceworkers/mochitest.ini
@@ -73,16 +73,20 @@ support-files =
   updatefoundevent.html
   empty.js
   periodic_update_test.js
   periodic.sjs
   periodic/frame.html
   periodic/register.html
   periodic/wait_for_update.html
   periodic/unregister.html
+  sanitize/frame.html
+  sanitize/register.html
+  sanitize/example_check_and_unregister.html
+  sanitize_worker.js
 
 [test_unregister.html]
 [test_installation_simple.html]
 [test_fetch_event.html]
 [test_https_fetch.html]
 [test_https_fetch_cloned_response.html]
 [test_https_synth_fetch_from_cached_sw.html]
 [test_match_all.html]
@@ -105,8 +109,10 @@ support-files =
 [test_request_context.html]
 [test_importscript.html]
 [test_client_focus.html]
 [test_bug1151916.html]
 [test_workerupdatefoundevent.html]
 [test_empty_serviceworker.html]
 [test_periodic_update.html]
 [test_periodic_https_update.html]
+[test_sanitize.html]
+[test_sanitize_domain.html]
new file mode 100644
--- /dev/null
+++ b/dom/workers/test/serviceworkers/sanitize/example_check_and_unregister.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<script>
+  function done(exists) {
+    parent.postMessage(exists, '*');
+  }
+
+  function fail() {
+    parent.postMessage("FAIL", '*');
+  }
+
+  navigator.serviceWorker.getRegistration(".").then(function(reg) {
+    if (reg) {
+      reg.unregister().then(done.bind(undefined, true), fail);
+    } else {
+      dump("getRegistration() returned undefined registration\n");
+      done(false);
+    }
+  }, function(e) {
+    dump("getRegistration() failed\n");
+    fail();
+  });
+</script>
+
new file mode 100644
--- /dev/null
+++ b/dom/workers/test/serviceworkers/sanitize/frame.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<script>
+  fetch("intercept-this").then(function(r) {
+    if (!r.ok) {
+      return "FAIL";
+    }
+    return r.text();
+  }).then(function(body) {
+    parent.postMessage(body, '*');
+  });
+</script>
new file mode 100644
--- /dev/null
+++ b/dom/workers/test/serviceworkers/sanitize/register.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<script>
+  function done() {
+    parent.postMessage('', '*');
+  }
+
+  navigator.serviceWorker.ready.then(done);
+  navigator.serviceWorker.register("../sanitize_worker.js", {scope: "."});
+</script>
+
new file mode 100644
--- /dev/null
+++ b/dom/workers/test/serviceworkers/sanitize_worker.js
@@ -0,0 +1,5 @@
+onfetch = function(e) {
+  if (e.request.url.indexOf("intercept-this") != -1) {
+    e.respondWith(new Response("intercepted"));
+  }
+}
new file mode 100644
--- /dev/null
+++ b/dom/workers/test/serviceworkers/test_sanitize.html
@@ -0,0 +1,87 @@
+<!--
+  Any copyright is dedicated to the Public Domain.
+  http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Bug 1080109 - Clear ServiceWorker registrations for all domains</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<script class="testbody" type="text/javascript">
+
+  function start() {
+    const Cc = SpecialPowers.Cc;
+    const Ci = SpecialPowers.Ci;
+
+    function testNotIntercepted() {
+      testFrame("sanitize/frame.html").then(function(body) {
+        is(body, "FAIL", "Expected frame to not be controlled");
+        // No need to unregister since that already happened.
+        navigator.serviceWorker.getRegistration("sanitize/foo").then(function(reg) {
+          ok(reg === undefined, "There should no longer be a valid registration");
+        }, function(e) {
+          ok(false, "getRegistration() should not error");
+        }).then(function(e) {
+          SimpleTest.finish();
+        });
+      });
+    }
+
+    registerSW().then(function() {
+      return testFrame("sanitize/frame.html").then(function(body) {
+        is(body, "intercepted", "Expected serviceworker to intercept request");
+      });
+    }).then(function() {
+      return navigator.serviceWorker.getRegistration("sanitize/foo");
+    }).then(function(reg) {
+      reg.active.onstatechange = function(e) {
+        e.target.onstatechange = null;
+        ok(e.target.state, "redundant", "On clearing data, serviceworker should become redundant");
+        testNotIntercepted();
+      };
+    }).then(function() {
+      SpecialPowers.removeAllServiceWorkerData();
+    });
+  }
+
+  function testFrame(src) {
+    return new Promise(function(resolve, reject) {
+      var iframe = document.createElement("iframe");
+      iframe.src = src;
+      window.onmessage = function(message) {
+        window.onmessage = null;
+        iframe.src = "about:blank";
+        document.body.removeChild(iframe);
+        iframe = null;
+        SpecialPowers.exactGC(window, function() {
+          resolve(message.data);
+        });
+      };
+      document.body.appendChild(iframe);
+    });
+  }
+
+  function registerSW() {
+    return testFrame("sanitize/register.html");
+  }
+
+  SimpleTest.waitForExplicitFinish();
+
+  SpecialPowers.pushPrefEnv({"set": [
+    ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+    ["dom.serviceWorkers.enabled", true],
+    ["dom.serviceWorkers.testing.enabled", true],
+  ]}, function() {
+    start();
+  });
+</script>
+</pre>
+</body>
+</html>
+
new file mode 100644
--- /dev/null
+++ b/dom/workers/test/serviceworkers/test_sanitize_domain.html
@@ -0,0 +1,90 @@
+<!--
+  Any copyright is dedicated to the Public Domain.
+  http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Bug 1080109 - Clear ServiceWorker registrations for specific domains</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<script class="testbody" type="text/javascript">
+
+  function start() {
+    const Cc = SpecialPowers.Cc;
+    const Ci = SpecialPowers.Ci;
+
+    function checkDomainRegistration(domain, exists) {
+      return testFrame("http://" + domain + "/tests/dom/workers/test/serviceworkers/sanitize/example_check_and_unregister.html").then(function(body) {
+        if (body === "FAIL") {
+          ok(false, "Error acquiring registration or unregistering for " + domain);
+        } else {
+          if (exists) {
+            ok(body === true, "Expected " + domain + " to still have a registration.");
+          } else {
+            ok(body === false, "Expected " + domain + " to have no registration.");
+          }
+        }
+      });
+    }
+
+    registerSW().then(function() {
+      return testFrame("http://example.com/tests/dom/workers/test/serviceworkers/sanitize/frame.html").then(function(body) {
+        is(body, "intercepted", "Expected serviceworker to intercept request");
+      });
+    }).then(function() {
+      SpecialPowers.removeServiceWorkerDataForExampleDomain();
+    }).then(function() {
+      return checkDomainRegistration("prefixexample.com", true /* exists */)
+        .then(function(e) {
+          return checkDomainRegistration("example.com", false /* exists */);
+        }).then(function(e) {
+          SimpleTest.finish();
+        });
+    })
+  }
+
+  function testFrame(src) {
+    return new Promise(function(resolve, reject) {
+      var iframe = document.createElement("iframe");
+      iframe.src = src;
+      window.onmessage = function(message) {
+        window.onmessage = null;
+        iframe.src = "about:blank";
+        document.body.removeChild(iframe);
+        iframe = null;
+        SpecialPowers.exactGC(window, function() {
+          resolve(message.data);
+        });
+      };
+      document.body.appendChild(iframe);
+    });
+  }
+
+  function registerSW() {
+    return testFrame("http://example.com/tests/dom/workers/test/serviceworkers/sanitize/register.html")
+           .then(function(e) {
+              // Register for prefixexample.com and then ensure it does not get unregistered.
+              return testFrame("http://prefixexample.com/tests/dom/workers/test/serviceworkers/sanitize/register.html");
+            });
+  }
+
+  SimpleTest.waitForExplicitFinish();
+
+  SpecialPowers.pushPrefEnv({"set": [
+    ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+    ["dom.serviceWorkers.enabled", true],
+    ["dom.serviceWorkers.testing.enabled", true],
+  ]}, function() {
+    start();
+  });
+</script>
+</pre>
+</body>
+</html>
+
--- a/testing/specialpowers/content/specialpowersAPI.js
+++ b/testing/specialpowers/content/specialpowersAPI.js
@@ -1921,13 +1921,21 @@ SpecialPowersAPI.prototype = {
 
   createDOMFile: function(path, options) {
     return new File(path, options);
   },
 
   startPeriodicServiceWorkerUpdates: function() {
     return this._sendSyncMessage('SPPeriodicServiceWorkerUpdates', {});
   },
+
+  removeAllServiceWorkerData: function() {
+    this.notifyObserversInParentProcess(null, "browser:purge-session-history", "");
+  },
+
+  removeServiceWorkerDataForExampleDomain: function() {
+    this.notifyObserversInParentProcess(null, "browser:purge-domain-data", "example.com");
+  },
 };
 
 this.SpecialPowersAPI = SpecialPowersAPI;
 this.bindDOMWindowUtils = bindDOMWindowUtils;
 this.getRawComponents = getRawComponents;