/* -*- 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 "ClientManagerService.h" #include "ClientHandleParent.h" #include "ClientManagerParent.h" #include "ClientNavigateOpParent.h" #include "ClientOpenWindowUtils.h" #include "ClientPrincipalUtils.h" #include "ClientSourceParent.h" #include "mozilla/dom/ContentParent.h" #include "mozilla/dom/ServiceWorkerManager.h" #include "mozilla/dom/ServiceWorkerUtils.h" #include "mozilla/ipc/BackgroundParent.h" #include "mozilla/ipc/PBackgroundSharedTypes.h" #include "mozilla/ClearOnShutdown.h" #include "mozilla/MozPromise.h" #include "mozilla/SchedulerGroup.h" #include "mozilla/ScopeExit.h" #include "jsfriendapi.h" #include "nsIAsyncShutdown.h" #include "nsIXULRuntime.h" #include "nsProxyRelease.h" namespace mozilla::dom { using mozilla::ipc::AssertIsOnBackgroundThread; using mozilla::ipc::PrincipalInfo; namespace { ClientManagerService* sClientManagerServiceInstance = nullptr; bool sClientManagerServiceShutdownRegistered = false; class ClientShutdownBlocker final : public nsIAsyncShutdownBlocker { RefPtr mPromise; ~ClientShutdownBlocker() = default; public: explicit ClientShutdownBlocker(GenericPromise::Private* aPromise) : mPromise(aPromise) { MOZ_DIAGNOSTIC_ASSERT(mPromise); } NS_IMETHOD GetName(nsAString& aNameOut) override { aNameOut = nsLiteralString( u"ClientManagerService: start destroying IPC actors early"); return NS_OK; } NS_IMETHOD BlockShutdown(nsIAsyncShutdownClient* aClient) override { mPromise->Resolve(true, __func__); aClient->RemoveBlocker(this); return NS_OK; } NS_IMETHOD GetState(nsIPropertyBag**) override { return NS_OK; } NS_DECL_ISUPPORTS }; NS_IMPL_ISUPPORTS(ClientShutdownBlocker, nsIAsyncShutdownBlocker) // Helper function the resolves a MozPromise when we detect that the browser // has begun to shutdown. RefPtr OnShutdown() { RefPtr ref = new GenericPromise::Private(__func__); nsCOMPtr r = NS_NewRunnableFunction("ClientManagerServer::OnShutdown", [ref]() { nsCOMPtr svc = services::GetAsyncShutdownService(); if (!svc) { ref->Resolve(true, __func__); return; } nsCOMPtr phase; MOZ_ALWAYS_SUCCEEDS(svc->GetXpcomWillShutdown(getter_AddRefs(phase))); if (!phase) { ref->Resolve(true, __func__); return; } nsCOMPtr blocker = new ClientShutdownBlocker(ref); nsresult rv = phase->AddBlocker(blocker, NS_LITERAL_STRING_FROM_CSTRING(__FILE__), __LINE__, u"ClientManagerService shutdown"_ns); if (NS_FAILED(rv)) { ref->Resolve(true, __func__); return; } }); MOZ_ALWAYS_SUCCEEDS( SchedulerGroup::Dispatch(TaskCategory::Other, r.forget())); return ref; } } // anonymous namespace ClientManagerService::FutureClientSourceParent::FutureClientSourceParent( const IPCClientInfo& aClientInfo) : mPrincipalInfo(aClientInfo.principalInfo()), mAssociated(false) {} ClientManagerService::ClientManagerService() : mShutdown(false) { AssertIsOnBackgroundThread(); // Only register one shutdown handler at a time. If a previous service // instance did this, but shutdown has not come, then we can avoid // doing it again. if (!sClientManagerServiceShutdownRegistered) { sClientManagerServiceShutdownRegistered = true; // While the ClientManagerService will be gracefully terminated as windows // and workers are naturally killed, this can cause us to do extra work // relatively late in the shutdown process. To avoid this we eagerly begin // shutdown at the first sign it has begun. Since we handle normal shutdown // gracefully we don't really need to block anything here. We just begin // destroying our IPC actors immediately. OnShutdown()->Then(GetCurrentSerialEventTarget(), __func__, []() { // Look up the latest service instance, if it exists. This may // be different from the instance that registered the shutdown // handler. RefPtr svc = ClientManagerService::GetInstance(); if (svc) { svc->Shutdown(); } }); } } ClientManagerService::~ClientManagerService() { AssertIsOnBackgroundThread(); MOZ_DIAGNOSTIC_ASSERT(mSourceTable.Count() == 0); MOZ_DIAGNOSTIC_ASSERT(mManagerList.IsEmpty()); MOZ_DIAGNOSTIC_ASSERT(sClientManagerServiceInstance == this); sClientManagerServiceInstance = nullptr; } void ClientManagerService::Shutdown() { AssertIsOnBackgroundThread(); MOZ_DIAGNOSTIC_ASSERT(sClientManagerServiceShutdownRegistered); // If many ClientManagerService are created and destroyed quickly we can // in theory get more than one shutdown listener calling us. if (mShutdown) { return; } mShutdown = true; // Begin destroying our various manager actors which will in turn destroy // all source, handle, and operation actors. for (auto actor : CopyableAutoTArray(mManagerList)) { Unused << PClientManagerParent::Send__delete__(actor); } // Destroying manager actors should've also destroyed all source actors, so // the only sources left will be future sources, which need to be aborted. for (auto& entry : mSourceTable) { MOZ_RELEASE_ASSERT(entry.GetData().is()); CopyableErrorResult rv; rv.ThrowInvalidStateError("Client creation aborted."); entry.GetModifiableData() ->as() .RejectPromiseIfExists(rv); } mSourceTable.Clear(); } ClientSourceParent* ClientManagerService::MaybeUnwrapAsExistingSource( const SourceTableEntry& aEntry) const { AssertIsOnBackgroundThread(); if (aEntry.is()) { return nullptr; } return aEntry.as(); } ClientSourceParent* ClientManagerService::FindExistingSource( const nsID& aID, const PrincipalInfo& aPrincipalInfo) const { AssertIsOnBackgroundThread(); auto entry = mSourceTable.Lookup(aID); if (!entry) { return nullptr; } ClientSourceParent* source = MaybeUnwrapAsExistingSource(entry.Data()); if (!source || NS_WARN_IF(!ClientMatchPrincipalInfo( source->Info().PrincipalInfo(), aPrincipalInfo))) { return nullptr; } return source; } // static already_AddRefed ClientManagerService::GetOrCreateInstance() { AssertIsOnBackgroundThread(); if (!sClientManagerServiceInstance) { sClientManagerServiceInstance = new ClientManagerService(); } RefPtr ref(sClientManagerServiceInstance); return ref.forget(); } // static already_AddRefed ClientManagerService::GetInstance() { AssertIsOnBackgroundThread(); if (!sClientManagerServiceInstance) { return nullptr; } RefPtr ref(sClientManagerServiceInstance); return ref.forget(); } namespace { bool IsNullPrincipalInfo(const PrincipalInfo& aPrincipalInfo) { return aPrincipalInfo.type() == PrincipalInfo::TNullPrincipalInfo; } bool AreBothNullPrincipals(const PrincipalInfo& aPrincipalInfo1, const PrincipalInfo& aPrincipalInfo2) { return IsNullPrincipalInfo(aPrincipalInfo1) && IsNullPrincipalInfo(aPrincipalInfo2); } } // anonymous namespace bool ClientManagerService::AddSource(ClientSourceParent* aSource) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aSource); auto entry = mSourceTable.Lookup(aSource->Info().Id()); if (entry) { // Do not permit overwriting an existing ClientSource with the same // UUID. This would allow a spoofed ClientParentSource actor to // intercept postMessage() intended for the real actor. if (entry.Data().is()) { return false; } FutureClientSourceParent& placeHolder = entry.Data().as(); const PrincipalInfo& placeHolderPrincipalInfo = placeHolder.PrincipalInfo(); const PrincipalInfo& sourcePrincipalInfo = aSource->Info().PrincipalInfo(); // The placeholder FutureClientSourceParent's PrincipalInfo must match the // real ClientSourceParent's PrincipalInfo. The only exception is if both // are null principals (two null principals are considered unequal). if (!AreBothNullPrincipals(placeHolderPrincipalInfo, sourcePrincipalInfo) && NS_WARN_IF(!ClientMatchPrincipalInfo(placeHolderPrincipalInfo, sourcePrincipalInfo))) { return false; } placeHolder.ResolvePromiseIfExists(); *entry = AsVariant(aSource); return true; } if (!mSourceTable.WithEntryHandle(aSource->Info().Id(), [aSource](auto&& entry) { if (NS_WARN_IF(entry.HasEntry())) { return false; } entry.Insert(AsVariant(aSource)); return true; })) { return false; } return true; } bool ClientManagerService::RemoveSource(ClientSourceParent* aSource) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aSource); auto entry = mSourceTable.Lookup(aSource->Info().Id()); if (NS_WARN_IF(!entry)) { return false; } entry.Remove(); return true; } bool ClientManagerService::ExpectFutureSource( const IPCClientInfo& aClientInfo) { AssertIsOnBackgroundThread(); if (!mSourceTable.WithEntryHandle( aClientInfo.id(), [&aClientInfo](auto&& entry) { // Prevent overwrites. if (entry.HasEntry()) { return false; } entry.Insert(SourceTableEntry( VariantIndex<0>(), FutureClientSourceParent(aClientInfo))); return true; })) { return false; } return true; } void ClientManagerService::ForgetFutureSource( const IPCClientInfo& aClientInfo) { AssertIsOnBackgroundThread(); auto entry = mSourceTable.Lookup(aClientInfo.id()); if (entry) { if (entry.Data().is()) { return; } // For non-e10s case, ClientChannelHelperParent will be freed before real // ClientSourceParnet be created. In the end this methoed will be called to // release the FutureClientSourceParent. That means a ClientHandle operation // which waits for the FutureClientSourceParent will have no chance to // connect to the ClientSourceParent. So the FutureClientSourceParent should // be keep in this case. // IsAssociated() makes sure there is a ClientHandle operation associated // with it. // More details please refer // https://bugzilla.mozilla.org/show_bug.cgi?id=1730350#c2 if (!XRE_IsE10sParentProcess() && entry.Data().as().IsAssociated()) { return; } CopyableErrorResult rv; rv.ThrowInvalidStateError("Client creation aborted."); entry.Data().as().RejectPromiseIfExists(rv); entry.Remove(); } } RefPtr ClientManagerService::FindSource( const nsID& aID, const PrincipalInfo& aPrincipalInfo) { AssertIsOnBackgroundThread(); auto entry = mSourceTable.Lookup(aID); if (!entry) { CopyableErrorResult rv; rv.ThrowInvalidStateError("Unknown client."); return SourcePromise::CreateAndReject(rv, __func__); } if (entry.Data().is()) { entry.Data().as().SetAsAssociated(); return entry.Data().as().Promise(); } ClientSourceParent* source = entry.Data().as(); if (NS_WARN_IF(!ClientMatchPrincipalInfo(source->Info().PrincipalInfo(), aPrincipalInfo))) { CopyableErrorResult rv; rv.ThrowInvalidStateError("Unknown client."); return SourcePromise::CreateAndReject(rv, __func__); } return SourcePromise::CreateAndResolve(true, __func__); } void ClientManagerService::AddManager(ClientManagerParent* aManager) { AssertIsOnBackgroundThread(); MOZ_DIAGNOSTIC_ASSERT(aManager); MOZ_ASSERT(!mManagerList.Contains(aManager)); mManagerList.AppendElement(aManager); // If shutdown has already begun then immediately destroy the actor. if (mShutdown) { Unused << PClientManagerParent::Send__delete__(aManager); } } void ClientManagerService::RemoveManager(ClientManagerParent* aManager) { AssertIsOnBackgroundThread(); MOZ_DIAGNOSTIC_ASSERT(aManager); DebugOnly removed = mManagerList.RemoveElement(aManager); MOZ_ASSERT(removed); } RefPtr ClientManagerService::Navigate( ThreadsafeContentParentHandle* aOriginContent, const ClientNavigateArgs& aArgs) { ClientSourceParent* source = FindExistingSource(aArgs.target().id(), aArgs.target().principalInfo()); if (!source) { CopyableErrorResult rv; rv.ThrowInvalidStateError("Unknown client"); return ClientOpPromise::CreateAndReject(rv, __func__); } const IPCServiceWorkerDescriptor& serviceWorker = aArgs.serviceWorker(); // Per https://w3c.github.io/ServiceWorker/#dom-windowclient-navigate step 4, // if the service worker does not control the client, reject with a TypeError. const Maybe& controller = source->GetController(); if (controller.isNothing() || controller.ref().Scope() != serviceWorker.scope() || controller.ref().Id() != serviceWorker.id()) { CopyableErrorResult rv; rv.ThrowTypeError("Client is not controlled by this Service Worker"); return ClientOpPromise::CreateAndReject(rv, __func__); } PClientManagerParent* manager = source->Manager(); MOZ_DIAGNOSTIC_ASSERT(manager); // This is safe to do because the ClientSourceChild cannot directly delete // itself. Instead it sends a Teardown message to the parent which then // calls delete. That means we can be sure that we are not racing with // source destruction here. ClientNavigateOpConstructorArgs args(WrapNotNull(source), aArgs.url(), aArgs.baseURL()); RefPtr promise = new ClientOpPromise::Private(__func__); ClientNavigateOpParent* op = new ClientNavigateOpParent(args, promise); PClientNavigateOpParent* result = manager->SendPClientNavigateOpConstructor(op, args); if (!result) { CopyableErrorResult rv; rv.ThrowInvalidStateError("Client is aborted"); promise->Reject(rv, __func__); } return promise; } namespace { class PromiseListHolder final { RefPtr mResultPromise; nsTArray> mPromiseList; nsTArray mResultList; uint32_t mOutstandingPromiseCount; void ProcessSuccess(const ClientInfoAndState& aResult) { mResultList.AppendElement(aResult); ProcessCompletion(); } void ProcessCompletion() { MOZ_DIAGNOSTIC_ASSERT(mOutstandingPromiseCount > 0); mOutstandingPromiseCount -= 1; MaybeFinish(); } ~PromiseListHolder() = default; public: PromiseListHolder() : mResultPromise(new ClientOpPromise::Private(__func__)), mOutstandingPromiseCount(0) {} RefPtr GetResultPromise() { RefPtr kungFuDeathGrip = this; return mResultPromise->Then( GetCurrentSerialEventTarget(), __func__, [kungFuDeathGrip](const ClientOpPromise::ResolveOrRejectValue& aValue) { return ClientOpPromise::CreateAndResolveOrReject(aValue, __func__); }); } void AddPromise(RefPtr&& aPromise) { mPromiseList.AppendElement(std::move(aPromise)); MOZ_DIAGNOSTIC_ASSERT(mPromiseList.LastElement()); mOutstandingPromiseCount += 1; RefPtr self(this); mPromiseList.LastElement()->Then( GetCurrentSerialEventTarget(), __func__, [self](const ClientOpResult& aResult) { // TODO: This is pretty clunky. Try to figure out a better // wait for MatchAll() and Claim() to share this code // even though they expect different return values. if (aResult.type() == ClientOpResult::TClientInfoAndState) { self->ProcessSuccess(aResult.get_ClientInfoAndState()); } else { self->ProcessCompletion(); } }, [self](const CopyableErrorResult& aResult) { self->ProcessCompletion(); }); } void MaybeFinish() { if (!mOutstandingPromiseCount) { mResultPromise->Resolve(CopyableTArray(mResultList.Clone()), __func__); } } NS_INLINE_DECL_REFCOUNTING(PromiseListHolder) }; } // anonymous namespace RefPtr ClientManagerService::MatchAll( ThreadsafeContentParentHandle* aOriginContent, const ClientMatchAllArgs& aArgs) { AssertIsOnBackgroundThread(); ServiceWorkerDescriptor swd(aArgs.serviceWorker()); const PrincipalInfo& principalInfo = swd.PrincipalInfo(); RefPtr promiseList = new PromiseListHolder(); for (const auto& entry : mSourceTable) { ClientSourceParent* source = MaybeUnwrapAsExistingSource(entry.GetData()); if (!source || source->IsFrozen() || !source->ExecutionReady()) { continue; } if (aArgs.type() != ClientType::All && source->Info().Type() != aArgs.type()) { continue; } if (!ClientMatchPrincipalInfo(source->Info().PrincipalInfo(), principalInfo)) { continue; } if (!aArgs.includeUncontrolled()) { const Maybe& controller = source->GetController(); if (controller.isNothing()) { continue; } if (controller.ref().Id() != swd.Id() || controller.ref().Scope() != swd.Scope()) { continue; } } promiseList->AddPromise(source->StartOp(ClientGetInfoAndStateArgs( source->Info().Id(), source->Info().PrincipalInfo()))); } // Maybe finish the promise now in case we didn't find any matching clients. promiseList->MaybeFinish(); return promiseList->GetResultPromise(); } namespace { RefPtr ClaimOnMainThread( const ClientInfo& aClientInfo, const ServiceWorkerDescriptor& aDescriptor) { RefPtr promise = new ClientOpPromise::Private(__func__); nsCOMPtr r = NS_NewRunnableFunction( __func__, [promise, clientInfo = std::move(aClientInfo), desc = std::move(aDescriptor)]() { auto scopeExit = MakeScopeExit([&] { // This will truncate the URLs if they have embedded nulls, if that // can happen, but for // our purposes here that's OK. nsPrintfCString err( "Service worker at <%s> can't claim Client at <%s>", desc.ScriptURL().get(), clientInfo.URL().get()); CopyableErrorResult rv; rv.ThrowInvalidStateError(err); promise->Reject(rv, __func__); }); RefPtr swm = ServiceWorkerManager::GetInstance(); NS_ENSURE_TRUE_VOID(swm); RefPtr inner = swm->MaybeClaimClient(clientInfo, desc); inner->Then( GetMainThreadSerialEventTarget(), __func__, [promise](bool aResult) { promise->Resolve(CopyableErrorResult(), __func__); }, [promise](const CopyableErrorResult& aRv) { promise->Reject(aRv, __func__); }); scopeExit.release(); }); MOZ_ALWAYS_SUCCEEDS( SchedulerGroup::Dispatch(TaskCategory::Other, r.forget())); return promise; } } // anonymous namespace RefPtr ClientManagerService::Claim( ThreadsafeContentParentHandle* aOriginContent, const ClientClaimArgs& aArgs) { AssertIsOnBackgroundThread(); const IPCServiceWorkerDescriptor& serviceWorker = aArgs.serviceWorker(); const PrincipalInfo& principalInfo = serviceWorker.principalInfo(); RefPtr promiseList = new PromiseListHolder(); for (const auto& entry : mSourceTable) { ClientSourceParent* source = MaybeUnwrapAsExistingSource(entry.GetData()); if (!source) { continue; } if (!ClientMatchPrincipalInfo(source->Info().PrincipalInfo(), principalInfo)) { continue; } const Maybe& controller = source->GetController(); if (controller.isSome() && controller.ref().Scope() == serviceWorker.scope() && controller.ref().Id() == serviceWorker.id()) { continue; } // TODO: This logic to determine if a service worker should control // a particular client should be moved to the ServiceWorkerManager. // This can't happen until the SWM is moved to the parent process, // though. if (!source->ExecutionReady() || source->Info().Type() == ClientType::Serviceworker || source->Info().URL().Find(serviceWorker.scope()) != 0) { continue; } if (source->IsFrozen()) { Unused << source->SendEvictFromBFCache(); continue; } promiseList->AddPromise(ClaimOnMainThread( source->Info(), ServiceWorkerDescriptor(serviceWorker))); } // Maybe finish the promise now in case we didn't find any matching clients. promiseList->MaybeFinish(); return promiseList->GetResultPromise(); } RefPtr ClientManagerService::GetInfoAndState( ThreadsafeContentParentHandle* aOriginContent, const ClientGetInfoAndStateArgs& aArgs) { ClientSourceParent* source = FindExistingSource(aArgs.id(), aArgs.principalInfo()); if (!source) { CopyableErrorResult rv; rv.ThrowInvalidStateError("Unknown client"); return ClientOpPromise::CreateAndReject(rv, __func__); } if (!source->ExecutionReady()) { RefPtr self = this; // rejection ultimately converted to `undefined` in Clients::Get return source->ExecutionReadyPromise()->Then( GetCurrentSerialEventTarget(), __func__, [self = std::move(self), aArgs]() -> RefPtr { ClientSourceParent* source = self->FindExistingSource(aArgs.id(), aArgs.principalInfo()); if (!source) { CopyableErrorResult rv; rv.ThrowInvalidStateError("Unknown client"); return ClientOpPromise::CreateAndReject(rv, __func__); } return source->StartOp(aArgs); }); } return source->StartOp(aArgs); } RefPtr ClientManagerService::OpenWindow( ThreadsafeContentParentHandle* aOriginContent, const ClientOpenWindowArgs& aArgs) { return InvokeAsync(GetMainThreadSerialEventTarget(), __func__, [originContent = RefPtr{aOriginContent}, aArgs]() { return ClientOpenWindow(originContent, aArgs); }); } bool ClientManagerService::HasWindow( const Maybe& aContentParentId, const PrincipalInfo& aPrincipalInfo, const nsID& aClientId) { AssertIsOnBackgroundThread(); ClientSourceParent* source = FindExistingSource(aClientId, aPrincipalInfo); if (!source) { return false; } if (!source->ExecutionReady()) { return false; } if (source->Info().Type() != ClientType::Window) { return false; } if (aContentParentId && !source->IsOwnedByProcess(aContentParentId.value())) { return false; } return true; } } // namespace mozilla::dom