Bug 1423412 P1 Actually mark window/worker ClientSource objects controlled when loaded with a controlling service worker. r=baku r=jld
authorBen Kelly <ben@wanderview.com>
Tue, 05 Dec 2017 20:45:22 -0500
changeset 395174 699fd0f2b04666c7b043f0350cf56f0bc07189ac
parent 395173 202ff4e53892c94131974daf3b9637fac064e558
child 395175 09942818c67041082051e9bd25aee3f7899546ba
push id98024
push userbkelly@mozilla.com
push dateWed, 06 Dec 2017 01:45:29 +0000
treeherdermozilla-inbound@7750a46652fa [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbaku, jld
bugs1423412
milestone59.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 1423412 P1 Actually mark window/worker ClientSource objects controlled when loaded with a controlling service worker. r=baku r=jld
docshell/base/nsDocShell.cpp
dom/base/nsGlobalWindowInner.cpp
dom/base/nsGlobalWindowInner.h
dom/base/nsPIDOMWindow.h
dom/clients/manager/ClientManager.h
dom/clients/manager/ClientSource.cpp
dom/clients/manager/ClientSource.h
dom/clients/manager/ClientSourceParent.cpp
dom/clients/manager/ClientSourceParent.h
dom/clients/manager/PClientSource.ipdl
dom/workers/ScriptLoader.cpp
dom/workers/ServiceWorkerManager.cpp
dom/workers/ServiceWorkerManager.h
dom/workers/WorkerPrivate.cpp
dom/workers/WorkerPrivate.h
ipc/ipdl/sync-messages.ini
--- a/docshell/base/nsDocShell.cpp
+++ b/docshell/base/nsDocShell.cpp
@@ -3445,16 +3445,42 @@ nsDocShell::MaybeCreateInitialClientSour
   if (NS_WARN_IF(!mInitialClientSource)) {
     return;
   }
 
   // Mark the initial client as execution ready, but owned by the docshell.
   // If the client is actually used this will cause ClientSource to force
   // the creation of the initial about:blank by calling nsDocShell::GetDocument().
   mInitialClientSource->DocShellExecutionReady(this);
+
+  // Next, check to see if the parent is controlled.
+  nsCOMPtr<nsIDocShell> parent = GetParentDocshell();
+  nsPIDOMWindowOuter* parentOuter = parent ? parent->GetWindow() : nullptr;
+  nsPIDOMWindowInner* parentInner =
+    parentOuter ? parentOuter->GetCurrentInnerWindow() : nullptr;
+  if (!parentInner) {
+    return;
+  }
+
+  Maybe<ServiceWorkerDescriptor> controller(parentInner->GetController());
+  if (controller.isNothing()) {
+    return;
+  }
+
+  // If the parent is controlled then propagate that controller to the
+  // initial about:blank client as well.  This will set the controller
+  // in the ClientManagerService in the parent.
+  RefPtr<ClientHandle> handle =
+    ClientManager::CreateHandle(mInitialClientSource->Info(),
+                                parentInner->EventTargetFor(TaskCategory::Other));
+  handle->Control(controller.ref());
+
+  // Also mark the ClientSource as controlled directly in case script
+  // immediately accesses navigator.serviceWorker.controller.
+  mInitialClientSource->SetController(controller.ref());
 }
 
 Maybe<ClientInfo>
 nsDocShell::GetInitialClientInfo() const
 {
   if (mInitialClientSource) {
     Maybe<ClientInfo> result;
     result.emplace(mInitialClientSource->Info());
--- a/dom/base/nsGlobalWindowInner.cpp
+++ b/dom/base/nsGlobalWindowInner.cpp
@@ -1785,26 +1785,44 @@ nsGlobalWindowInner::EnsureClientSource(
     }
   }
 
   // If we don't have a reserved client or an initial client, then create
   // one now.  This can happen in certain cases where we avoid preallocating
   // the client in the docshell.  This mainly occurs in situations where
   // the principal is not clearly inherited from the parent; e.g. sandboxed
   // iframes, window.open(), etc.
+  // TODO: We may not be marking initial about:blank documents created
+  //       this way as controlled by a service worker properly.  The
+  //       controller should be coming from the same place as the inheritted
+  //       principal.  We do this in docshell, but as mentioned we aren't
+  //       smart enough to handle all cases yet.  For example, a
+  //       window.open() with new URL should inherit the controller from
+  //       the opener, but we probably don't handle that yet.
   if (!mClientSource) {
     mClientSource = ClientManager::CreateSource(ClientType::Window,
                                                 EventTargetFor(TaskCategory::Other),
                                                 mDoc->NodePrincipal());
     if (NS_WARN_IF(!mClientSource)) {
       return NS_ERROR_FAILURE;
     }
     newClientSource = true;
   }
 
+  // The load may have started controlling the Client as well.  If
+  // so, mark it as controlled immediately here.  The actor may
+  // or may not have been notified by the parent side about being
+  // controlled yet.
+  if (loadInfo) {
+    const Maybe<ServiceWorkerDescriptor> controller = loadInfo->GetController();
+    if (controller.isSome()) {
+      mClientSource->SetController(controller.ref());
+    }
+  }
+
   // Its possible that we got a client just after being frozen in
   // the bfcache.  In that case freeze the client immediately.
   if (newClientSource && IsFrozen()) {
     mClientSource->Freeze();
   }
 
   return NS_OK;
 }
@@ -2277,16 +2295,22 @@ nsPIDOMWindowInner::SyncStateFromParentW
 }
 
 Maybe<ClientInfo>
 nsPIDOMWindowInner::GetClientInfo() const
 {
   return Move(nsGlobalWindowInner::Cast(this)->GetClientInfo());
 }
 
+Maybe<ServiceWorkerDescriptor>
+nsPIDOMWindowInner::GetController() const
+{
+  return Move(nsGlobalWindowInner::Cast(this)->GetController());
+}
+
 void
 nsGlobalWindowInner::UpdateTopInnerWindow()
 {
   if (IsTopInnerWindow() || !mTopInnerWindow) {
     return;
   }
 
   mTopInnerWindow->UpdateWebSocketCount(-(int32_t)mNumOfOpenWebSockets);
@@ -6113,16 +6137,27 @@ nsGlobalWindowInner::GetClientInfo() con
   MOZ_ASSERT(NS_IsMainThread());
   Maybe<ClientInfo> clientInfo;
   if (mClientSource) {
     clientInfo.emplace(mClientSource->Info());
   }
   return Move(clientInfo);
 }
 
+Maybe<ServiceWorkerDescriptor>
+nsGlobalWindowInner::GetController() const
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  Maybe<ServiceWorkerDescriptor> controller;
+  if (mClientSource) {
+    controller = mClientSource->GetController();
+  }
+  return Move(controller);
+}
+
 nsresult
 nsGlobalWindowInner::FireDelayedDOMEvents()
 {
   if (mApplicationCache) {
     static_cast<nsDOMOfflineResourceList*>(mApplicationCache.get())->FirePendingEvents();
   }
 
   // Fires an offline status event if the offline status has changed
--- a/dom/base/nsGlobalWindowInner.h
+++ b/dom/base/nsGlobalWindowInner.h
@@ -344,16 +344,17 @@ public:
   void Resume();
   virtual bool IsSuspended() const override;
   void Freeze();
   void Thaw();
   virtual bool IsFrozen() const override;
   void SyncStateFromParentWindow();
 
   mozilla::Maybe<mozilla::dom::ClientInfo> GetClientInfo() const;
+  mozilla::Maybe<mozilla::dom::ServiceWorkerDescriptor> GetController() const;
 
   virtual nsresult FireDelayedDOMEvents() override;
 
   virtual nsresult SetNewDocument(nsIDocument *aDocument,
                                   nsISupports *aState,
                                   bool aForceReuseInnerWindow) override;
 
   virtual void SetOpenerWindow(nsPIDOMWindowOuter* aOpener,
--- a/dom/base/nsPIDOMWindow.h
+++ b/dom/base/nsPIDOMWindow.h
@@ -46,16 +46,17 @@ namespace mozilla {
 class ThrottledEventQueue;
 namespace dom {
 class AudioContext;
 class ClientInfo;
 class DocGroup;
 class TabGroup;
 class Element;
 class Performance;
+class ServiceWorkerDescriptor;
 class ServiceWorkerRegistration;
 class Timeout;
 class TimeoutManager;
 class CustomElementRegistry;
 enum class CallerType : uint32_t;
 } // namespace dom
 } // namespace mozilla
 
@@ -321,16 +322,17 @@ public:
   // Increase/Decrease the number of open WebSockets.
   void UpdateWebSocketCount(int32_t aDelta);
 
   // Return true if there are any open WebSockets that could block
   // timeout-throttling.
   bool HasOpenWebSockets() const;
 
   mozilla::Maybe<mozilla::dom::ClientInfo> GetClientInfo() const;
+  mozilla::Maybe<mozilla::dom::ServiceWorkerDescriptor> GetController() const;
 
   mozilla::dom::TabGroup* TabGroup();
 
   virtual nsPIDOMWindowOuter* GetPrivateRoot() = 0;
 
   virtual mozilla::dom::CustomElementRegistry* CustomElements() = 0;
 
   // XXX: This is called on inner windows
--- a/dom/clients/manager/ClientManager.h
+++ b/dom/clients/manager/ClientManager.h
@@ -32,16 +32,17 @@ class WorkerPrivate;
 // The ClientManager provides a per-thread singleton interface workering
 // with the client subsystem.  It allows globals to create ClientSource
 // objects.  It allows other parts of the system to attach to this globals
 // by creating ClientHandle objects.  The ClientManager also provides
 // methods for querying the list of clients active in the system.
 class ClientManager final : public ClientThing<ClientManagerChild>
 {
   friend class ClientManagerChild;
+  friend class ClientSource;
 
   ClientManager();
   ~ClientManager();
 
   // Utility method to trigger a shutdown of the ClientManager.  This
   // is called in various error conditions or when the last reference
   // is dropped.
   void
--- a/dom/clients/manager/ClientSource.cpp
+++ b/dom/clients/manager/ClientSource.cpp
@@ -253,16 +253,27 @@ ClientSource::Thaw()
 
 const ClientInfo&
 ClientSource::Info() const
 {
   return mClientInfo;
 }
 
 void
+ClientSource::WorkerSyncPing(WorkerPrivate* aWorkerPrivate)
+{
+  NS_ASSERT_OWNINGTHREAD(ClientSource);
+  MOZ_DIAGNOSTIC_ASSERT(aWorkerPrivate);
+  MOZ_DIAGNOSTIC_ASSERT(aWorkerPrivate == mManager->GetWorkerPrivate());
+  aWorkerPrivate->AssertIsOnWorkerThread();
+  MOZ_DIAGNOSTIC_ASSERT(GetActor());
+  GetActor()->SendWorkerSyncPing();
+}
+
+void
 ClientSource::SetController(const ServiceWorkerDescriptor& aServiceWorker)
 {
   NS_ASSERT_OWNINGTHREAD(ClientSource);
 
   if (mController.isSome() && mController.ref() == aServiceWorker) {
     return;
   }
 
--- a/dom/clients/manager/ClientSource.h
+++ b/dom/clients/manager/ClientSource.h
@@ -92,16 +92,23 @@ public:
   Freeze();
 
   void
   Thaw();
 
   const ClientInfo&
   Info() const;
 
+  // Trigger a synchronous IPC ping to the parent process to confirm that
+  // the ClientSource actor has been created.  This should only be used
+  // by the WorkerPrivate startup code to deal with a ClientHandle::Control()
+  // call racing on the main thread.  Do not call this in other circumstances!
+  void
+  WorkerSyncPing(mozilla::dom::workers::WorkerPrivate* aWorkerPrivate);
+
   // Synchronously mark the ClientSource as controlled by the given service
   // worker.  This can happen as a result of a remote operation or directly
   // by local code.  For example, if a client's initial network load is
   // intercepted by a controlling service worker then this should be called
   // immediately.
   //
   // Note, there is no way to clear the controlling service worker because
   // the specification does not allow that operation.
--- a/dom/clients/manager/ClientSourceParent.cpp
+++ b/dom/clients/manager/ClientSourceParent.cpp
@@ -15,16 +15,17 @@
 #include "mozilla/dom/PClientManagerParent.h"
 #include "mozilla/ipc/BackgroundParent.h"
 #include "mozilla/SystemGroup.h"
 #include "mozilla/Unused.h"
 
 namespace mozilla {
 namespace dom {
 
+using mozilla::ipc::AssertIsOnBackgroundThread;
 using mozilla::ipc::BackgroundParent;
 using mozilla::ipc::IPCResult;
 using mozilla::ipc::PrincipalInfo;
 
 namespace {
 
 // It would be nice to use a lambda instead of this class, but we cannot
 // move capture in lambdas yet and ContentParent cannot be AddRef'd off
@@ -77,16 +78,25 @@ ClientSourceParent::KillInvalidChild()
   // typically mean someone sent us bogus data over the IPC link.  We can't
   // trust that process any more.  We have to do this on the main thread, so
   // there is a small window of time before we kill the process.  This is why
   // we start the actor destruction immediately above.
   nsCOMPtr<nsIRunnable> r = new KillContentParentRunnable(Move(process));
   MOZ_ALWAYS_SUCCEEDS(SystemGroup::Dispatch(TaskCategory::Other, r.forget()));
 }
 
+mozilla::ipc::IPCResult
+ClientSourceParent::RecvWorkerSyncPing()
+{
+  AssertIsOnBackgroundThread();
+  // Do nothing here.  This is purely a sync message allowing the child to
+  // confirm that the actor has been created on the parent process.
+  return IPC_OK();
+}
+
 IPCResult
 ClientSourceParent::RecvTeardown()
 {
   Unused << Send__delete__(this);
   return IPC_OK();
 }
 
 IPCResult
--- a/dom/clients/manager/ClientSourceParent.h
+++ b/dom/clients/manager/ClientSourceParent.h
@@ -26,16 +26,19 @@ class ClientSourceParent final : public 
   bool mExecutionReady;
   bool mFrozen;
 
   void
   KillInvalidChild();
 
   // PClientSourceParent
   mozilla::ipc::IPCResult
+  RecvWorkerSyncPing() override;
+
+  mozilla::ipc::IPCResult
   RecvTeardown() override;
 
   mozilla::ipc::IPCResult
   RecvExecutionReady(const ClientSourceExecutionReadyArgs& aArgs) override;
 
   mozilla::ipc::IPCResult
   RecvFreeze() override;
 
--- a/dom/clients/manager/PClientSource.ipdl
+++ b/dom/clients/manager/PClientSource.ipdl
@@ -15,16 +15,17 @@ namespace dom {
 
 sync protocol PClientSource
 {
   manager PClientManager;
 
   manages PClientSourceOp;
 
 parent:
+  sync WorkerSyncPing();
   async Teardown();
   async ExecutionReady(ClientSourceExecutionReadyArgs aArgs);
   async Freeze();
   async Thaw();
 
 child:
   async PClientSourceOp(ClientOpConstructorArgs aArgs);
 
--- a/dom/workers/ScriptLoader.cpp
+++ b/dom/workers/ScriptLoader.cpp
@@ -565,16 +565,17 @@ class ScriptLoaderRunnable final : publi
   friend class CachePromiseHandler;
   friend class CacheScriptLoader;
   friend class LoaderListener;
 
   WorkerPrivate* mWorkerPrivate;
   nsCOMPtr<nsIEventTarget> mSyncLoopTarget;
   nsTArray<ScriptLoadInfo> mLoadInfos;
   RefPtr<CacheCreator> mCacheCreator;
+  Maybe<ServiceWorkerDescriptor> mController;
   bool mIsMainScript;
   WorkerScriptType mWorkerScriptType;
   bool mCanceled;
   bool mCanceledMainThread;
   ErrorResult& mRv;
 
 public:
   NS_DECL_THREADSAFE_ISUPPORTS
@@ -1211,16 +1212,20 @@ private:
 
       mWorkerPrivate->SetReferrerPolicyFromHeaderValue(tRPHeaderCValue);
 
       WorkerPrivate* parent = mWorkerPrivate->GetParent();
       if (parent) {
         // XHR Params Allowed
         mWorkerPrivate->SetXHRParamsAllowed(parent->XHRParamsAllowed());
       }
+
+      if (chanLoadInfo) {
+        mController = chanLoadInfo->GetController();
+      }
     }
 
     return NS_OK;
   }
 
   void
   DataReceivedFromCache(uint32_t aIndex, const uint8_t* aString,
                         uint32_t aStringLen,
@@ -1977,18 +1982,21 @@ ScriptExecutorRunnable::WorkerRun(JSCont
       // Top level scripts only!
       if (mIsWorkerScript) {
         aWorkerPrivate->MaybeDispatchLoadFailedRunnable();
       }
       return true;
     }
 
     // If this is a top level script that succeeded, then mark the
-    // Client execution ready.
+    // Client execution ready and possible controlled by a service worker.
     if (mIsWorkerScript) {
+      if (mScriptLoader.mController.isSome()) {
+        aWorkerPrivate->Control(mScriptLoader.mController.ref());
+      }
       aWorkerPrivate->ExecutionReady();
     }
 
     NS_ConvertUTF16toUTF8 filename(loadInfo.mURL);
 
     JS::CompileOptions options(aCx);
     options.setFileAndLine(filename.get(), 1)
            .setNoScriptRval(true);
--- a/dom/workers/ServiceWorkerManager.cpp
+++ b/dom/workers/ServiceWorkerManager.cpp
@@ -32,16 +32,18 @@
 
 #include "mozilla/BasePrincipal.h"
 #include "mozilla/ClearOnShutdown.h"
 #include "mozilla/ErrorNames.h"
 #include "mozilla/LoadContext.h"
 #include "mozilla/SystemGroup.h"
 #include "mozilla/Telemetry.h"
 #include "mozilla/dom/BindingUtils.h"
+#include "mozilla/dom/ClientHandle.h"
+#include "mozilla/dom/ClientManager.h"
 #include "mozilla/dom/ContentParent.h"
 #include "mozilla/dom/ErrorEvent.h"
 #include "mozilla/dom/Headers.h"
 #include "mozilla/dom/InternalHeaders.h"
 #include "mozilla/dom/Navigator.h"
 #include "mozilla/dom/NotificationEvent.h"
 #include "mozilla/dom/PromiseNativeHandler.h"
 #include "mozilla/dom/Request.h"
@@ -2319,35 +2321,57 @@ ServiceWorkerManager::MaybeCheckNavigati
   //    algorithm.
   RefPtr<ServiceWorkerRegistrationInfo> registration;
   mControlledDocuments.Get(aDoc, getter_AddRefs(registration));
   if (registration) {
     registration->MaybeScheduleUpdate();
   }
 }
 
-void
+RefPtr<GenericPromise>
 ServiceWorkerManager::StartControllingADocument(ServiceWorkerRegistrationInfo* aRegistration,
                                                 nsIDocument* aDoc,
                                                 const nsAString& aDocumentId)
 {
   MOZ_ASSERT(aRegistration);
   MOZ_ASSERT(aDoc);
 
 #ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED
   auto storageAllowed = nsContentUtils::StorageAllowedForDocument(aDoc);
   MOZ_DIAGNOSTIC_ASSERT(storageAllowed == nsContentUtils::StorageAccess::eAllow);
 #endif // MOZ_DIAGNOSTIC_ASSERT_ENABLED
 
+  RefPtr<GenericPromise> ref = GenericPromise::CreateAndResolve(true, __func__);
+
   aRegistration->StartControllingADocument();
   mControlledDocuments.Put(aDoc, aRegistration);
   if (!aDocumentId.IsEmpty()) {
     aDoc->SetId(aDocumentId);
   }
+
+  // Mark the document's ClientSource as controlled using the ClientHandle
+  // interface.  While we could get at the ClientSource directly from the
+  // document here, our goal is to move ServiceWorkerManager to a separate
+  // process.  Using the ClientHandle supports this remote operation.
+  ServiceWorkerInfo* activeWorker = aRegistration->GetActive();
+  nsPIDOMWindowInner* innerWindow = aDoc->GetInnerWindow();
+  if (activeWorker && innerWindow) {
+    Maybe<ClientInfo> clientInfo = innerWindow->GetClientInfo();
+    if (clientInfo.isSome()) {
+      RefPtr<ClientHandle> clientHandle =
+        ClientManager::CreateHandle(clientInfo.ref(),
+                                    SystemGroup::EventTargetFor(TaskCategory::Other));
+      if (clientHandle) {
+        ref = Move(clientHandle->Control(activeWorker->Descriptor()));
+      }
+    }
+  }
+
   Telemetry::Accumulate(Telemetry::SERVICE_WORKER_CONTROLLED_DOCUMENTS, 1);
+  return Move(ref);
 }
 
 void
 ServiceWorkerManager::StopControllingADocument(ServiceWorkerRegistrationInfo* aRegistration)
 {
   aRegistration->StopControllingADocument();
   if (aRegistration->IsControllingDocuments() || !aRegistration->IsIdle()) {
     return;
@@ -2648,16 +2672,58 @@ ServiceWorkerManager::DispatchFetchEvent
     // before we get to this point.  Therefore we must handle a nullptr
     // active worker here.
     serviceWorker = registration->GetActive();
     if (!serviceWorker) {
       aRv.Throw(NS_ERROR_FAILURE);
       return;
     }
 
+    // If there is a reserved client it should be marked as controlled before
+    // the FetchEvent is dispatched.
+    nsCOMPtr<nsILoadInfo> loadInfo = internalChannel->GetLoadInfo();
+    if (loadInfo) {
+      Maybe<ClientInfo> clientInfo = loadInfo->GetReservedClientInfo();
+
+      // Also override the initial about:blank controller since the real
+      // network load may be intercepted by a different service worker.  If
+      // the intial about:blank has a controller here its simply been
+      // inherited from its parent.
+      if (clientInfo.isNothing()) {
+        clientInfo = loadInfo->GetInitialClientInfo();
+
+        // TODO: We need to handle the case where the initial about:blank is
+        //       controlled, but the final document load is not.  Right now
+        //       the spec does not really say what to do.  There currently
+        //       is no way for the controller to be cleared from a client in
+        //       the spec or our implementation.  We may want to force a
+        //       new inner window to be created instead of reusing the
+        //       initial about:blank global.  See bug 1419620 and the spec
+        //       issue here: https://github.com/w3c/ServiceWorker/issues/1232
+      }
+
+      if (clientInfo.isSome()) {
+        // First, attempt to mark the reserved client controlled directly.  This
+        // will update the controlled status in the ClientManagerService in the
+        // parent.  It will also eventually propagate back to the ClientSource.
+        RefPtr<ClientHandle> clientHandle =
+          ClientManager::CreateHandle(clientInfo.ref(),
+                                      SystemGroup::EventTargetFor(TaskCategory::Other));
+        if (clientHandle) {
+          clientHandle->Control(serviceWorker->Descriptor());
+        }
+      }
+
+      // But we also note the reserved state on the LoadInfo.  This allows the
+      // ClientSource to be updated immediately after the nsIChannel starts.
+      // This is necessary to have the correct controller in place for immediate
+      // follow-on requests.
+      loadInfo->SetController(serviceWorker->Descriptor());
+    }
+
     AddNavigationInterception(serviceWorker->Scope(), aChannel);
   }
 
   if (NS_WARN_IF(aRv.Failed())) {
     return;
   }
 
   MOZ_DIAGNOSTIC_ASSERT(serviceWorker);
--- a/dom/workers/ServiceWorkerManager.h
+++ b/dom/workers/ServiceWorkerManager.h
@@ -10,16 +10,17 @@
 #include "nsIServiceWorkerManager.h"
 #include "nsCOMPtr.h"
 
 #include "ipc/IPCMessageUtils.h"
 #include "mozilla/Attributes.h"
 #include "mozilla/AutoRestore.h"
 #include "mozilla/ConsoleReportCollector.h"
 #include "mozilla/LinkedList.h"
+#include "mozilla/MozPromise.h"
 #include "mozilla/Preferences.h"
 #include "mozilla/TypedEnumBits.h"
 #include "mozilla/UniquePtr.h"
 #include "mozilla/WeakPtr.h"
 #include "mozilla/dom/BindingUtils.h"
 #include "mozilla/dom/Promise.h"
 #include "mozilla/dom/ServiceWorkerCommon.h"
 #include "mozilla/dom/ServiceWorkerRegistrar.h"
@@ -385,17 +386,17 @@ private:
                                             WhichServiceWorker aWhichOne);
   void
   InvalidateServiceWorkerRegistrationWorker(ServiceWorkerRegistrationInfo* aRegistration,
                                             WhichServiceWorker aWhichOnes);
 
   void
   NotifyServiceWorkerRegistrationRemoved(ServiceWorkerRegistrationInfo* aRegistration);
 
-  void
+  RefPtr<GenericPromise>
   StartControllingADocument(ServiceWorkerRegistrationInfo* aRegistration,
                             nsIDocument* aDoc,
                             const nsAString& aDocumentId);
 
   void
   StopControllingADocument(ServiceWorkerRegistrationInfo* aRegistration);
 
   already_AddRefed<ServiceWorkerRegistrationInfo>
--- a/dom/workers/WorkerPrivate.cpp
+++ b/dom/workers/WorkerPrivate.cpp
@@ -5328,28 +5328,61 @@ WorkerPrivate::EnsureClientSource()
   if (!mClientSource) {
     return false;
   }
 
   if (mFrozen) {
     mClientSource->Freeze();
   }
 
+  // Shortly after the client is reserved we will try loading the main script
+  // for the worker.  This may get intercepted by the ServiceWorkerManager
+  // which will then try to create a ClientHandle.  Its actually possible for
+  // the main thread to create this ClientHandle before our IPC message creating
+  // the ClientSource completes.  To avoid this race we synchronously ping our
+  // parent Client actor here.  This ensure the worker ClientSource is created
+  // in the parent before the main thread might try reaching it with a
+  // ClientHandle.
+  //
+  // An alternative solution would have been to handle the out-of-order operations
+  // on the parent side.  We could have created a small window where we allow
+  // ClientHandle objects to exist without a ClientSource.  We would then time
+  // out these handles if they stayed orphaned for too long.  This approach would
+  // be much more complex, but also avoid this extra bit of latency when starting
+  // workers.
+  //
+  // Note, we only have to do this for workers that can be controlled by a
+  // service worker.  So avoid the sync overhead here if we are starting a
+  // service worker or a chrome worker.
+  if (Type() != WorkerTypeService && !IsChromeWorker()) {
+    mClientSource->WorkerSyncPing(this);
+  }
+
   return true;
 }
 
 const ClientInfo&
 WorkerPrivate::GetClientInfo() const
 {
   AssertIsOnWorkerThread();
   MOZ_DIAGNOSTIC_ASSERT(mClientSource);
   return mClientSource->Info();
 }
 
 void
+WorkerPrivate::Control(const ServiceWorkerDescriptor& aServiceWorker)
+{
+  AssertIsOnWorkerThread();
+  MOZ_DIAGNOSTIC_ASSERT(mClientSource);
+  MOZ_DIAGNOSTIC_ASSERT(!IsChromeWorker());
+  MOZ_DIAGNOSTIC_ASSERT(Type() != WorkerTypeService);
+  mClientSource->SetController(aServiceWorker);
+}
+
+void
 WorkerPrivate::ExecutionReady()
 {
   AssertIsOnWorkerThread();
   MOZ_DIAGNOSTIC_ASSERT(mClientSource);
   mClientSource->WorkerExecutionReady(this);
 }
 
 void
--- a/dom/workers/WorkerPrivate.h
+++ b/dom/workers/WorkerPrivate.h
@@ -1499,16 +1499,19 @@ public:
 
   bool
   EnsureClientSource();
 
   const ClientInfo&
   GetClientInfo() const;
 
   void
+  Control(const ServiceWorkerDescriptor& aServiceWorker);
+
+  void
   ExecutionReady();
 
 private:
   WorkerPrivate(WorkerPrivate* aParent,
                 const nsAString& aScriptURL, bool aIsChromeWorker,
                 WorkerType aWorkerType, const nsAString& aWorkerName,
                 const nsACString& aServiceWorkerScope,
                 WorkerLoadInfo& aLoadInfo);
--- a/ipc/ipdl/sync-messages.ini
+++ b/ipc/ipdl/sync-messages.ini
@@ -1054,8 +1054,10 @@ description =
 [PHandlerService::ExistsForProtocol]
 description = bug 1382323
 [PHandlerService::Exists]
 description =
 [PHandlerService::GetTypeFromExtension]
 description =
 [PLayerTransaction::ShutdownSync]
 description = bug 1363126
+[PClientSource::WorkerSyncPing]
+description = Synchronous ping allowing worker thread to confirm actor is created. Necessary to avoid racing with ClientHandle actors on main thread.