/* -*- 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 "ScriptLoader.h" #include #include #include "nsIChannel.h" #include "nsIContentPolicy.h" #include "nsIContentSecurityPolicy.h" #include "nsICookieJarSettings.h" #include "nsIDocShell.h" #include "nsIHttpChannel.h" #include "nsIHttpChannelInternal.h" #include "nsIIOService.h" #include "nsIOService.h" #include "nsIPrincipal.h" #include "nsIProtocolHandler.h" #include "nsIScriptError.h" #include "nsIScriptSecurityManager.h" #include "nsIStreamListenerTee.h" #include "nsIThreadRetargetableRequest.h" #include "nsIURI.h" #include "nsIXPConnect.h" #include "jsapi.h" #include "jsfriendapi.h" #include "js/CompilationAndEvaluation.h" #include "js/Exception.h" #include "js/SourceText.h" #include "js/TypeDecls.h" #include "nsError.h" #include "nsComponentManagerUtils.h" #include "nsContentSecurityManager.h" #include "nsContentPolicyUtils.h" #include "nsContentUtils.h" #include "nsDocShellCID.h" #include "nsJSEnvironment.h" #include "nsNetUtil.h" #include "nsIPipe.h" #include "nsIOutputStream.h" #include "nsPrintfCString.h" #include "nsString.h" #include "nsTArray.h" #include "nsThreadUtils.h" #include "nsXPCOM.h" #include "xpcpublic.h" #include "mozilla/AntiTrackingUtils.h" #include "mozilla/ArrayAlgorithm.h" #include "mozilla/Assertions.h" #include "mozilla/Encoding.h" #include "mozilla/LoadContext.h" #include "mozilla/Maybe.h" #include "mozilla/ipc/BackgroundUtils.h" #include "mozilla/dom/ClientChannelHelper.h" #include "mozilla/dom/ClientInfo.h" #include "mozilla/dom/Exceptions.h" #include "mozilla/dom/nsCSPService.h" #include "mozilla/dom/nsCSPUtils.h" #include "mozilla/dom/PerformanceStorage.h" #include "mozilla/dom/Response.h" #include "mozilla/dom/ReferrerInfo.h" #include "mozilla/dom/ScriptSettings.h" #include "mozilla/dom/SerializedStackHolder.h" #include "mozilla/dom/workerinternals/CacheLoadHandler.h" #include "mozilla/dom/workerinternals/NetworkLoadHandler.h" #include "mozilla/dom/workerinternals/ScriptResponseHeaderProcessor.h" #include "mozilla/Result.h" #include "mozilla/ResultExtensions.h" #include "mozilla/StaticPrefs_browser.h" #include "mozilla/UniquePtr.h" #include "WorkerRunnable.h" #include "WorkerScope.h" #define MAX_CONCURRENT_SCRIPTS 1000 using JS::loader::ScriptKind; using JS::loader::ScriptLoadRequest; using mozilla::ipc::PrincipalInfo; namespace mozilla::dom::workerinternals { namespace { nsresult ConstructURI(const nsAString& aScriptURL, nsIURI* baseURI, const mozilla::Encoding* aDocumentEncoding, nsIURI** aResult) { nsresult rv; // Only top level workers' main script use the document charset for the // script uri encoding. Otherwise, default encoding (UTF-8) is applied. if (aDocumentEncoding) { nsAutoCString charset; aDocumentEncoding->Name(charset); rv = NS_NewURI(aResult, aScriptURL, charset.get(), baseURI); } else { rv = NS_NewURI(aResult, aScriptURL, nullptr, baseURI); } if (NS_FAILED(rv)) { return NS_ERROR_DOM_SYNTAX_ERR; } return NS_OK; } nsresult ChannelFromScriptURL( nsIPrincipal* principal, Document* parentDoc, WorkerPrivate* aWorkerPrivate, nsILoadGroup* loadGroup, nsIIOService* ios, nsIScriptSecurityManager* secMan, nsIURI* aScriptURL, const Maybe& aClientInfo, const Maybe& aController, bool aIsMainScript, WorkerScriptType aWorkerScriptType, nsContentPolicyType aContentPolicyType, nsLoadFlags aLoadFlags, uint32_t aSecFlags, nsICookieJarSettings* aCookieJarSettings, nsIReferrerInfo* aReferrerInfo, nsIChannel** aChannel) { AssertIsOnMainThread(); nsresult rv; nsCOMPtr uri = aScriptURL; // Only use the document when its principal matches the principal of the // current request. This means scripts fetched using the Workers' own // principal won't inherit properties of the document, in particular the CSP. if (parentDoc && parentDoc->NodePrincipal() != principal) { parentDoc = nullptr; } // The main service worker script should never be loaded over the network // in this path. It should always be offlined by ServiceWorkerScriptCache. // We assert here since this error should also be caught by the runtime // check in CacheLoadHandler. // // Note, if we ever allow service worker scripts to be loaded from network // here we need to configure the channel properly. For example, it must // not allow redirects. MOZ_DIAGNOSTIC_ASSERT(aContentPolicyType != nsIContentPolicy::TYPE_INTERNAL_SERVICE_WORKER); nsCOMPtr channel; if (parentDoc) { // This is the path for top level dedicated worker scripts with a document rv = NS_NewChannel(getter_AddRefs(channel), uri, parentDoc, aSecFlags, aContentPolicyType, nullptr, // aPerformanceStorage loadGroup, nullptr, // aCallbacks aLoadFlags, ios); NS_ENSURE_SUCCESS(rv, NS_ERROR_DOM_SECURITY_ERR); } else { // This branch is used in the following cases: // * Shared and ServiceWorkers (who do not have a doc) // * Static Module Imports // * ImportScripts // We must have a loadGroup with a load context for the principal to // traverse the channel correctly. MOZ_ASSERT(loadGroup); MOZ_ASSERT(NS_LoadGroupMatchesPrincipal(loadGroup, principal)); RefPtr performanceStorage; nsCOMPtr cspEventListener; if (aWorkerPrivate && !aIsMainScript) { performanceStorage = aWorkerPrivate->GetPerformanceStorage(); cspEventListener = aWorkerPrivate->CSPEventListener(); } if (aClientInfo.isSome()) { // If we have an existing clientInfo (true for all modules and // importScripts), we will use this branch rv = NS_NewChannel(getter_AddRefs(channel), uri, principal, aClientInfo.ref(), aController, aSecFlags, aContentPolicyType, aCookieJarSettings, performanceStorage, loadGroup, nullptr, // aCallbacks aLoadFlags, ios); } else { rv = NS_NewChannel(getter_AddRefs(channel), uri, principal, aSecFlags, aContentPolicyType, aCookieJarSettings, performanceStorage, loadGroup, nullptr, // aCallbacks aLoadFlags, ios); } NS_ENSURE_SUCCESS(rv, NS_ERROR_DOM_SECURITY_ERR); if (cspEventListener) { nsCOMPtr loadInfo = channel->LoadInfo(); rv = loadInfo->SetCspEventListener(cspEventListener); NS_ENSURE_SUCCESS(rv, rv); } } if (aReferrerInfo) { nsCOMPtr httpChannel = do_QueryInterface(channel); if (httpChannel) { rv = httpChannel->SetReferrerInfo(aReferrerInfo); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } } channel.forget(aChannel); return rv; } void LoadAllScripts(WorkerPrivate* aWorkerPrivate, UniquePtr aOriginStack, const nsTArray& aScriptURLs, bool aIsMainScript, WorkerScriptType aWorkerScriptType, ErrorResult& aRv, const mozilla::Encoding* aDocumentEncoding = nullptr) { aWorkerPrivate->AssertIsOnWorkerThread(); NS_ASSERTION(!aScriptURLs.IsEmpty(), "Bad arguments!"); AutoSyncLoopHolder syncLoop(aWorkerPrivate, Canceling); nsCOMPtr syncLoopTarget = syncLoop.GetSerialEventTarget(); if (!syncLoopTarget) { aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); return; } RefPtr loader = new loader::WorkerScriptLoader(aWorkerPrivate, std::move(aOriginStack), syncLoopTarget, aWorkerScriptType, aRv); if (NS_WARN_IF(aRv.Failed())) { return; } bool ok = loader->CreateScriptRequests(aScriptURLs, aDocumentEncoding, aIsMainScript); if (!ok) { return; } // Bug 1817259 - For now, we force loading the debugger script as Classic, // even if the debugged worker is a Module. if (aWorkerPrivate->WorkerType() == WorkerType::Module && aWorkerScriptType != DebuggerScript) { if (!StaticPrefs::dom_workers_modules_enabled()) { aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); return; } MOZ_ASSERT(aIsMainScript); // Module Load RefPtr mainScript = loader->GetMainScript(); if (mainScript && mainScript->IsModuleRequest()) { if (NS_FAILED(mainScript->AsModuleRequest()->StartModuleLoad())) { return; } syncLoop.Run(); return; } } if (loader->DispatchLoadScripts()) { syncLoop.Run(); } } class ChannelGetterRunnable final : public WorkerMainThreadRunnable { const nsAString& mScriptURL; const WorkerType& mWorkerType; const RequestCredentials& mCredentials; const ClientInfo mClientInfo; WorkerLoadInfo& mLoadInfo; nsresult mResult; public: ChannelGetterRunnable(WorkerPrivate* aParentWorker, const nsAString& aScriptURL, const WorkerType& aWorkerType, const RequestCredentials& aCredentials, WorkerLoadInfo& aLoadInfo) : WorkerMainThreadRunnable(aParentWorker, "ScriptLoader :: ChannelGetter"_ns), mScriptURL(aScriptURL) // ClientInfo should always be present since this should not be called // if parent's status is greater than Running. , mWorkerType(aWorkerType), mCredentials(aCredentials), mClientInfo(aParentWorker->GlobalScope()->GetClientInfo().ref()), mLoadInfo(aLoadInfo), mResult(NS_ERROR_FAILURE) { MOZ_ASSERT(aParentWorker); aParentWorker->AssertIsOnWorkerThread(); } virtual bool MainThreadRun() override { AssertIsOnMainThread(); // Initialize the WorkerLoadInfo principal to our triggering principal // before doing anything else. Normally we do this in the WorkerPrivate // Constructor, but we can't do so off the main thread when creating // a nested worker. So do it here instead. mLoadInfo.mLoadingPrincipal = mWorkerPrivate->GetPrincipal(); MOZ_DIAGNOSTIC_ASSERT(mLoadInfo.mLoadingPrincipal); mLoadInfo.mPrincipal = mLoadInfo.mLoadingPrincipal; // Figure out our base URI. nsCOMPtr baseURI = mWorkerPrivate->GetBaseURI(); MOZ_ASSERT(baseURI); // May be null. nsCOMPtr parentDoc = mWorkerPrivate->GetDocument(); mLoadInfo.mLoadGroup = mWorkerPrivate->GetLoadGroup(); mLoadInfo.mCookieJarSettings = mWorkerPrivate->CookieJarSettings(); // Nested workers use default uri encoding. nsCOMPtr url; mResult = ConstructURI(mScriptURL, baseURI, nullptr, getter_AddRefs(url)); NS_ENSURE_SUCCESS(mResult, true); Maybe clientInfo; clientInfo.emplace(mClientInfo); nsCOMPtr channel; nsCOMPtr referrerInfo = ReferrerInfo::CreateForFetch(mLoadInfo.mLoadingPrincipal, nullptr); mLoadInfo.mReferrerInfo = static_cast(referrerInfo.get()) ->CloneWithNewPolicy(mWorkerPrivate->GetReferrerPolicy()); mResult = workerinternals::ChannelFromScriptURLMainThread( mLoadInfo.mLoadingPrincipal, parentDoc, mLoadInfo.mLoadGroup, url, mWorkerType, mCredentials, clientInfo, // Nested workers are always dedicated. nsIContentPolicy::TYPE_INTERNAL_WORKER, mLoadInfo.mCookieJarSettings, mLoadInfo.mReferrerInfo, getter_AddRefs(channel)); NS_ENSURE_SUCCESS(mResult, true); mResult = mLoadInfo.SetPrincipalsAndCSPFromChannel(channel); NS_ENSURE_SUCCESS(mResult, true); mLoadInfo.mChannel = std::move(channel); return true; } nsresult GetResult() const { return mResult; } private: virtual ~ChannelGetterRunnable() = default; }; nsresult GetCommonSecFlags(bool aIsMainScript, nsIURI* uri, nsIPrincipal* principal, WorkerScriptType aWorkerScriptType, uint32_t& secFlags) { bool inheritAttrs = nsContentUtils::ChannelShouldInheritPrincipal( principal, uri, true /* aInheritForAboutBlank */, false /* aForceInherit */); bool isData = uri->SchemeIs("data"); if (inheritAttrs && !isData) { secFlags |= nsILoadInfo::SEC_FORCE_INHERIT_PRINCIPAL; } if (aWorkerScriptType == DebuggerScript) { // A DebuggerScript needs to be a local resource like chrome: or resource: bool isUIResource = false; nsresult rv = NS_URIChainHasFlags( uri, nsIProtocolHandler::URI_IS_UI_RESOURCE, &isUIResource); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (!isUIResource) { return NS_ERROR_DOM_SECURITY_ERR; } secFlags |= nsILoadInfo::SEC_ALLOW_CHROME; } // Note: this is for backwards compatibility and goes against spec. // We should find a better solution. if (aIsMainScript && isData) { secFlags = nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL; } return NS_OK; } nsresult GetModuleSecFlags(bool aIsTopLevel, nsIPrincipal* principal, WorkerScriptType aWorkerScriptType, nsIURI* aURI, RequestCredentials aCredentials, uint32_t& secFlags) { // Implements "To fetch a single module script," // Step 9. If destination is "worker", "sharedworker", or "serviceworker", // and the top-level module fetch flag is set, then set request's // mode to "same-origin". secFlags = aIsTopLevel ? nsILoadInfo::SEC_REQUIRE_SAME_ORIGIN_DATA_IS_BLOCKED : nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_INHERITS_SEC_CONTEXT; // Step 8. Let request be a new request whose [...] mode is "cors" [...] // This implements the same Cookie settings as nsContentSecurityManager's // ComputeSecurityFlags. The main difference is the line above, Step 9, // setting to same origin. if (aCredentials == RequestCredentials::Include) { secFlags |= nsILoadInfo::nsILoadInfo::SEC_COOKIES_INCLUDE; } else if (aCredentials == RequestCredentials::Same_origin) { secFlags |= nsILoadInfo::nsILoadInfo::SEC_COOKIES_SAME_ORIGIN; } else if (aCredentials == RequestCredentials::Omit) { secFlags |= nsILoadInfo::nsILoadInfo::SEC_COOKIES_OMIT; } return GetCommonSecFlags(aIsTopLevel, aURI, principal, aWorkerScriptType, secFlags); } nsresult GetClassicSecFlags(bool aIsMainScript, nsIURI* uri, nsIPrincipal* principal, WorkerScriptType aWorkerScriptType, uint32_t& secFlags) { secFlags = aIsMainScript ? nsILoadInfo::SEC_REQUIRE_SAME_ORIGIN_DATA_IS_BLOCKED : nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_INHERITS_SEC_CONTEXT; return GetCommonSecFlags(aIsMainScript, uri, principal, aWorkerScriptType, secFlags); } } // anonymous namespace namespace loader { class ScriptExecutorRunnable final : public MainThreadWorkerSyncRunnable { RefPtr mScriptLoader; const Span> mLoadedRequests; public: ScriptExecutorRunnable(WorkerScriptLoader* aScriptLoader, WorkerPrivate* aWorkerPrivate, nsISerialEventTarget* aSyncLoopTarget, Span> aLoadedRequests); private: ~ScriptExecutorRunnable() = default; virtual bool IsDebuggerRunnable() const override; virtual bool PreRun(WorkerPrivate* aWorkerPrivate) override; bool ProcessModuleScript(JSContext* aCx, WorkerPrivate* aWorkerPrivate); bool ProcessClassicScripts(JSContext* aCx, WorkerPrivate* aWorkerPrivate); virtual bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override; nsresult Cancel() override; }; template static bool EvaluateSourceBuffer(JSContext* aCx, const JS::CompileOptions& aOptions, JS::loader::ClassicScript* aClassicScript, JS::SourceText& aSourceBuffer) { static_assert(std::is_same::value || std::is_same::value, "inferred units must be UTF-8 or UTF-16"); JS::Rooted script(aCx, JS::Compile(aCx, aOptions, aSourceBuffer)); if (!script) { return false; } if (aClassicScript) { aClassicScript->AssociateWithScript(script); } JS::Rooted unused(aCx); return JS_ExecuteScript(aCx, script, &unused); } WorkerScriptLoader::WorkerScriptLoader( WorkerPrivate* aWorkerPrivate, UniquePtr aOriginStack, nsISerialEventTarget* aSyncLoopTarget, WorkerScriptType aWorkerScriptType, ErrorResult& aRv) : mOriginStack(std::move(aOriginStack)), mSyncLoopTarget(aSyncLoopTarget), mWorkerScriptType(aWorkerScriptType), mRv(aRv), mLoadingModuleRequestCount(0), mCleanedUp(false), mCleanUpLock("cleanUpLock") { aWorkerPrivate->AssertIsOnWorkerThread(); RefPtr workerRef = StrongWorkerRef::Create(aWorkerPrivate, "ScriptLoader"); if (workerRef) { mWorkerRef = new ThreadSafeWorkerRef(workerRef); } else { mRv.Throw(NS_ERROR_FAILURE); return; } nsIGlobalObject* global = GetGlobal(); mController = global->GetController(); if (!StaticPrefs::dom_workers_modules_enabled()) { return; } // Set up the module loader, if it has not been initialzied yet. if (!aWorkerPrivate->IsServiceWorker()) { InitModuleLoader(); } } ScriptLoadRequest* WorkerScriptLoader::GetMainScript() { mWorkerRef->Private()->AssertIsOnWorkerThread(); ScriptLoadRequest* request = mLoadingRequests.getFirst(); if (request->GetWorkerLoadContext()->IsTopLevel()) { return request; } return nullptr; } void WorkerScriptLoader::InitModuleLoader() { mWorkerRef->Private()->AssertIsOnWorkerThread(); if (GetGlobal()->GetModuleLoader(nullptr)) { return; } RefPtr moduleLoader = new WorkerModuleLoader(this, GetGlobal(), mSyncLoopTarget.get()); if (mWorkerScriptType == WorkerScript) { mWorkerRef->Private()->GlobalScope()->InitModuleLoader(moduleLoader); return; } mWorkerRef->Private()->DebuggerGlobalScope()->InitModuleLoader(moduleLoader); } bool WorkerScriptLoader::CreateScriptRequests( const nsTArray& aScriptURLs, const mozilla::Encoding* aDocumentEncoding, bool aIsMainScript) { mWorkerRef->Private()->AssertIsOnWorkerThread(); // If a worker has been loaded as a module worker, ImportScripts calls are // disallowed -- then the operation is invalid. // // 10.3.1 Importing scripts and libraries. // Step 1. If worker global scope's type is "module", throw a TypeError // exception. // // Also, for now, the debugger script is always loaded as Classic, // even if the debugged worker is a Module. We still want to allow // it to use importScripts. if (mWorkerRef->Private()->WorkerType() == WorkerType::Module && !aIsMainScript && !IsDebuggerScript()) { // This should only run for non-main scripts, as only these are // importScripts mRv.ThrowTypeError( "Using `ImportScripts` inside a Module Worker is " "disallowed."); return false; } for (const nsString& scriptURL : aScriptURLs) { RefPtr request = CreateScriptLoadRequest(scriptURL, aDocumentEncoding, aIsMainScript); if (!request) { return false; } mLoadingRequests.AppendElement(request); } return true; } nsTArray> WorkerScriptLoader::GetLoadingList() { mWorkerRef->Private()->AssertIsOnWorkerThread(); nsTArray> list; for (ScriptLoadRequest* req = mLoadingRequests.getFirst(); req; req = req->getNext()) { RefPtr handle = new ThreadSafeRequestHandle(req, mSyncLoopTarget.get()); list.AppendElement(handle.forget()); } return list; } bool WorkerScriptLoader::IsDynamicImport(ScriptLoadRequest* aRequest) { return aRequest->IsModuleRequest() && aRequest->AsModuleRequest()->IsDynamicImport(); } nsContentPolicyType WorkerScriptLoader::GetContentPolicyType( ScriptLoadRequest* aRequest) { if (aRequest->GetWorkerLoadContext()->IsTopLevel()) { // Implements https://html.spec.whatwg.org/#worker-processing-model // Step 13: Let destination be "sharedworker" if is shared is true, and // "worker" otherwise. return mWorkerRef->Private()->ContentPolicyType(); } if (aRequest->IsModuleRequest()) { // Implements the destination for Step 14 in // https://html.spec.whatwg.org/#worker-processing-model // // We need a special subresource type in order to correctly implement // the graph fetch, where the destination is set to "worker" or // "sharedworker". return nsIContentPolicy::TYPE_INTERNAL_WORKER_STATIC_MODULE; } return nsIContentPolicy::TYPE_INTERNAL_WORKER_IMPORT_SCRIPTS; } already_AddRefed WorkerScriptLoader::CreateScriptLoadRequest( const nsString& aScriptURL, const mozilla::Encoding* aDocumentEncoding, bool aIsMainScript) { mWorkerRef->Private()->AssertIsOnWorkerThread(); WorkerLoadContext::Kind kind = WorkerLoadContext::GetKind(aIsMainScript, IsDebuggerScript()); Maybe clientInfo = GetGlobal()->GetClientInfo(); // (For non-serviceworkers, this variable does not matter, but false best // captures their behavior.) bool onlyExistingCachedResourcesAllowed = false; if (mWorkerRef->Private()->IsServiceWorker()) { // https://w3c.github.io/ServiceWorker/#importscripts step 4: // > 4. If serviceWorker’s state is not "parsed" or "installing": // > 1. Return map[url] if it exists and a network error otherwise. // // So if our state is beyond installing, it's too late to make a request // that would perform a new fetch which would be cached. onlyExistingCachedResourcesAllowed = mWorkerRef->Private()->GetServiceWorkerDescriptor().State() > ServiceWorkerState::Installing; } RefPtr loadContext = new WorkerLoadContext( kind, clientInfo, this, onlyExistingCachedResourcesAllowed); // Create ScriptLoadRequests for this WorkerScriptLoader ReferrerPolicy referrerPolicy = mWorkerRef->Private()->GetReferrerPolicy(); // Only top level workers' main script use the document charset for the // script uri encoding. Otherwise, default encoding (UTF-8) is applied. MOZ_ASSERT_IF(bool(aDocumentEncoding), aIsMainScript && !mWorkerRef->Private()->GetParent()); nsCOMPtr baseURI = aIsMainScript ? GetInitialBaseURI() : GetBaseURI(); nsCOMPtr uri; nsresult rv = ConstructURI(aScriptURL, baseURI, aDocumentEncoding, getter_AddRefs(uri)); // If we failed to construct the URI, handle it in the LoadContext so it is // thrown in the right order. if (NS_WARN_IF(NS_FAILED(rv))) { loadContext->mLoadResult = rv; } RefPtr fetchOptions = new ScriptFetchOptions(CORSMode::CORS_NONE, referrerPolicy, nullptr); RefPtr request = nullptr; // Bug 1817259 - For now the debugger scripts are always loaded a Classic. if (mWorkerRef->Private()->WorkerType() == WorkerType::Classic || IsDebuggerScript()) { request = new ScriptLoadRequest(ScriptKind::eClassic, uri, fetchOptions, SRIMetadata(), nullptr, // mReferrer loadContext); } else { // Implements part of "To fetch a worklet/module worker script graph" // including, setting up the request with a credentials mode, // destination. // Step 1. Let options be a script fetch options. // We currently don't track credentials in our ScriptFetchOptions // implementation, so we are defaulting the fetchOptions object defined // above. This behavior is handled fully in GetModuleSecFlags. if (!StaticPrefs::dom_workers_modules_enabled()) { mRv.ThrowTypeError("Modules in workers are currently disallowed."); return nullptr; } RefPtr moduleLoader = GetGlobal()->GetModuleLoader(nullptr); // Implements the referrer for "To fetch a single module script" // Our implementation does not have a "client" as a referrer. // However, when client is resolved (per 8.3. Determine request’s // Referrer in // https://w3c.github.io/webappsec-referrer-policy/#determine-requests-referrer) // This should result in the referrer source being the creation URL. // // In subresource modules, the referrer is the importing script. nsCOMPtr referrer = mWorkerRef->Private()->GetReferrerInfo()->GetOriginalReferrer(); // Part of Step 2. This sets the Top-level flag to true request = new ModuleLoadRequest( uri, fetchOptions, SRIMetadata(), referrer, loadContext, true, /* is top level */ false, /* is dynamic import */ moduleLoader, ModuleLoadRequest::NewVisitedSetForTopLevelImport(uri), nullptr); } // Set the mURL, it will be used for error handling and debugging. request->mURL = NS_ConvertUTF16toUTF8(aScriptURL); return request.forget(); } bool WorkerScriptLoader::DispatchLoadScript(ScriptLoadRequest* aRequest) { mWorkerRef->Private()->AssertIsOnWorkerThread(); IncreaseLoadingModuleRequestCount(); nsTArray> scriptLoadList; RefPtr handle = new ThreadSafeRequestHandle(aRequest, mSyncLoopTarget.get()); scriptLoadList.AppendElement(handle.forget()); RefPtr runnable = new ScriptLoaderRunnable(this, std::move(scriptLoadList)); RefPtr workerRef = StrongWorkerRef::Create( mWorkerRef->Private(), "ScriptLoader", [runnable]() { NS_DispatchToMainThread(NewRunnableMethod( "ScriptLoaderRunnable::CancelMainThreadWithBindingAborted", runnable, &ScriptLoaderRunnable::CancelMainThreadWithBindingAborted)); }); if (NS_FAILED(NS_DispatchToMainThread(runnable))) { NS_ERROR("Failed to dispatch!"); mRv.Throw(NS_ERROR_FAILURE); return false; } return true; } bool WorkerScriptLoader::DispatchLoadScripts() { mWorkerRef->Private()->AssertIsOnWorkerThread(); nsTArray> scriptLoadList = GetLoadingList(); RefPtr runnable = new ScriptLoaderRunnable(this, std::move(scriptLoadList)); RefPtr workerRef = StrongWorkerRef::Create( mWorkerRef->Private(), "ScriptLoader", [runnable]() { NS_DispatchToMainThread(NewRunnableMethod( "ScriptLoaderRunnable::CancelMainThreadWithBindingAborted", runnable, &ScriptLoaderRunnable::CancelMainThreadWithBindingAborted)); }); if (NS_FAILED(NS_DispatchToMainThread(runnable))) { NS_ERROR("Failed to dispatch!"); mRv.Throw(NS_ERROR_FAILURE); return false; } return true; } nsIURI* WorkerScriptLoader::GetInitialBaseURI() { MOZ_ASSERT(mWorkerRef->Private()); nsIURI* baseURI; WorkerPrivate* parentWorker = mWorkerRef->Private()->GetParent(); if (parentWorker) { baseURI = parentWorker->GetBaseURI(); } else { // May be null. baseURI = mWorkerRef->Private()->GetBaseURI(); } return baseURI; } nsIURI* WorkerScriptLoader::GetBaseURI() const { MOZ_ASSERT(mWorkerRef); nsIURI* baseURI; baseURI = mWorkerRef->Private()->GetBaseURI(); NS_ASSERTION(baseURI, "Should have been set already!"); return baseURI; } nsIGlobalObject* WorkerScriptLoader::GetGlobal() { mWorkerRef->Private()->AssertIsOnWorkerThread(); return mWorkerScriptType == WorkerScript ? static_cast( mWorkerRef->Private()->GlobalScope()) : mWorkerRef->Private()->DebuggerGlobalScope(); } void WorkerScriptLoader::MaybeMoveToLoadedList(ScriptLoadRequest* aRequest) { mWorkerRef->Private()->AssertIsOnWorkerThread(); // Only set to ready for regular scripts. Module loader will set the script to // ready if it is a Module Request. if (!aRequest->IsModuleRequest()) { aRequest->SetReady(); } // If the request is not in a list, we are in an illegal state. MOZ_RELEASE_ASSERT(aRequest->isInList()); while (!mLoadingRequests.isEmpty()) { ScriptLoadRequest* request = mLoadingRequests.getFirst(); // We need to move requests in post order. If prior requests have not // completed, delay execution. if (!request->IsReadyToRun()) { break; } RefPtr req = mLoadingRequests.Steal(request); mLoadedRequests.AppendElement(req); } } bool WorkerScriptLoader::StoreCSP() { // We must be on the same worker as we started on. mWorkerRef->Private()->AssertIsOnWorkerThread(); if (!mWorkerRef->Private()->GetJSContext()) { return false; } MOZ_ASSERT(!mRv.Failed()); // Move the CSP from the workerLoadInfo in the corresponding Client // where the CSP code expects it! mWorkerRef->Private()->StoreCSPOnClient(); return true; } bool WorkerScriptLoader::ProcessPendingRequests(JSContext* aCx) { mWorkerRef->Private()->AssertIsOnWorkerThread(); // Don't run if something else has already failed. if (mExecutionAborted) { mLoadedRequests.CancelRequestsAndClear(); TryShutdown(); return true; } // If nothing else has failed, our ErrorResult better not be a failure // either. MOZ_ASSERT(!mRv.Failed(), "Who failed it and why?"); // Slightly icky action at a distance, but there's no better place to stash // this value, really. JS::Rooted global(aCx, JS::CurrentGlobalOrNull(aCx)); MOZ_ASSERT(global); while (!mLoadedRequests.isEmpty()) { // Take a reference, but do not remove it from the list yet. There is a // possibility that this will need to be cancelled. RefPtr req = mLoadedRequests.getFirst(); // We don't have a ProcessRequest method (like we do on the DOM), as there // isn't much processing that we need to do per request that isn't related // to evaluation (the processsing done for the DOM is handled in // DataRecievedFrom{Cache,Network} for workers. // So, this inner loop calls EvaluateScript directly. This will change // once modules are introduced as we will have some extra work to do. if (!EvaluateScript(aCx, req)) { mExecutionAborted = true; WorkerLoadContext* loadContext = req->GetWorkerLoadContext(); mMutedErrorFlag = loadContext->mMutedErrorFlag.valueOr(true); mLoadedRequests.CancelRequestsAndClear(); break; } // remove the element from the list. mLoadedRequests.Remove(req); } TryShutdown(); return true; } nsresult WorkerScriptLoader::LoadScript( ThreadSafeRequestHandle* aRequestHandle) { AssertIsOnMainThread(); WorkerLoadContext* loadContext = aRequestHandle->GetContext(); ScriptLoadRequest* request = aRequestHandle->GetRequest(); MOZ_ASSERT_IF(loadContext->IsTopLevel(), !IsDebuggerScript()); // The URL passed to us for loading was invalid, stop loading at this point. if (loadContext->mLoadResult != NS_ERROR_NOT_INITIALIZED) { return loadContext->mLoadResult; } WorkerPrivate* parentWorker = mWorkerRef->Private()->GetParent(); // For JavaScript debugging, the devtools server must run on the same // thread as the debuggee, indicating the worker uses content principal. // However, in Bug 863246, web content will no longer be able to load // resource:// URIs by default, so we need system principal to load // debugger scripts. nsIPrincipal* principal = (IsDebuggerScript()) ? nsContentUtils::GetSystemPrincipal() : mWorkerRef->Private()->GetPrincipal(); nsCOMPtr loadGroup = mWorkerRef->Private()->GetLoadGroup(); MOZ_DIAGNOSTIC_ASSERT(principal); NS_ENSURE_TRUE(NS_LoadGroupMatchesPrincipal(loadGroup, principal), NS_ERROR_FAILURE); // May be null. nsCOMPtr parentDoc = mWorkerRef->Private()->GetDocument(); nsCOMPtr channel; if (loadContext->IsTopLevel()) { // May be null. channel = mWorkerRef->Private()->ForgetWorkerChannel(); } nsCOMPtr ios(do_GetIOService()); nsIScriptSecurityManager* secMan = nsContentUtils::GetSecurityManager(); NS_ASSERTION(secMan, "This should never be null!"); nsresult& rv = loadContext->mLoadResult; nsLoadFlags loadFlags = mWorkerRef->Private()->GetLoadFlags(); // Get the top-level worker. WorkerPrivate* topWorkerPrivate = mWorkerRef->Private(); WorkerPrivate* parent = topWorkerPrivate->GetParent(); while (parent) { topWorkerPrivate = parent; parent = topWorkerPrivate->GetParent(); } // If the top-level worker is a dedicated worker and has a window, and the // window has a docshell, the caching behavior of this worker should match // that of that docshell. if (topWorkerPrivate->IsDedicatedWorker()) { nsCOMPtr window = topWorkerPrivate->GetWindow(); if (window) { nsCOMPtr docShell = window->GetDocShell(); if (docShell) { nsresult rv = docShell->GetDefaultLoadFlags(&loadFlags); NS_ENSURE_SUCCESS(rv, rv); } } } if (!channel) { nsCOMPtr referrerInfo; uint32_t secFlags; if (request->IsModuleRequest()) { // https://fetch.spec.whatwg.org/#concept-main-fetch // Step 8. If request’s referrer policy is the empty string, then set // request’s referrer policy to request’s policy container’s // referrer policy. ReferrerPolicy policy = request->ReferrerPolicy() == ReferrerPolicy::_empty ? mWorkerRef->Private()->GetReferrerPolicy() : request->ReferrerPolicy(); referrerInfo = new ReferrerInfo(request->mReferrer, policy); rv = GetModuleSecFlags( loadContext->IsTopLevel(), principal, mWorkerScriptType, request->mURI, mWorkerRef->Private()->WorkerCredentials(), secFlags); } else { referrerInfo = ReferrerInfo::CreateForFetch(principal, nullptr); if (parentWorker && !loadContext->IsTopLevel()) { referrerInfo = static_cast(referrerInfo.get()) ->CloneWithNewPolicy(parentWorker->GetReferrerPolicy()); } rv = GetClassicSecFlags(loadContext->IsTopLevel(), request->mURI, principal, mWorkerScriptType, secFlags); } if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } nsContentPolicyType contentPolicyType = GetContentPolicyType(request); rv = ChannelFromScriptURL( principal, parentDoc, mWorkerRef->Private(), loadGroup, ios, secMan, request->mURI, loadContext->mClientInfo, mController, loadContext->IsTopLevel(), mWorkerScriptType, contentPolicyType, loadFlags, secFlags, mWorkerRef->Private()->CookieJarSettings(), referrerInfo, getter_AddRefs(channel)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } // Associate any originating stack with the channel. if (!mOriginStackJSON.IsEmpty()) { NotifyNetworkMonitorAlternateStack(channel, mOriginStackJSON); } // We need to know which index we're on in OnStreamComplete so we know // where to put the result. RefPtr listener = new NetworkLoadHandler(this, aRequestHandle); RefPtr headerProcessor = nullptr; // For each debugger script, a non-debugger script load of the same script // should have occured prior that processed the headers. if (!IsDebuggerScript()) { headerProcessor = MakeRefPtr( mWorkerRef->Private(), loadContext->IsTopLevel() && !IsDynamicImport(request), GetContentPolicyType(request) == nsIContentPolicy::TYPE_INTERNAL_WORKER_IMPORT_SCRIPTS); } nsCOMPtr loader; rv = NS_NewStreamLoader(getter_AddRefs(loader), listener, headerProcessor); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (loadContext->IsTopLevel()) { MOZ_DIAGNOSTIC_ASSERT(loadContext->mClientInfo.isSome()); // In order to get the correct foreign partitioned prinicpal, we need to // set the `IsThirdPartyContextToTopWindow` to the channel's loadInfo. // This flag reflects the fact that if the worker is created under a // third-party context. nsCOMPtr loadInfo = channel->LoadInfo(); loadInfo->SetIsThirdPartyContextToTopWindow( mWorkerRef->Private()->IsThirdPartyContextToTopWindow()); Maybe clientInfo; clientInfo.emplace(loadContext->mClientInfo.ref()); rv = AddClientChannelHelper(channel, std::move(clientInfo), Maybe(), mWorkerRef->Private()->HybridEventTarget()); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } if (StaticPrefs::browser_tabs_remote_useCrossOriginEmbedderPolicy()) { nsILoadInfo::CrossOriginEmbedderPolicy respectedCOEP = mWorkerRef->Private()->GetEmbedderPolicy(); if (mWorkerRef->Private()->IsDedicatedWorker() && respectedCOEP == nsILoadInfo::EMBEDDER_POLICY_NULL) { respectedCOEP = mWorkerRef->Private()->GetOwnerEmbedderPolicy(); } nsCOMPtr channelLoadInfo = channel->LoadInfo(); channelLoadInfo->SetLoadingEmbedderPolicy(respectedCOEP); } if (loadContext->mCacheStatus != WorkerLoadContext::ToBeCached) { rv = channel->AsyncOpen(loader); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } else { nsCOMPtr writer; // In case we return early. loadContext->mCacheStatus = WorkerLoadContext::Cancel; NS_NewPipe(getter_AddRefs(loadContext->mCacheReadStream), getter_AddRefs(writer), 0, UINT32_MAX, // unlimited size to avoid writer WOULD_BLOCK case true, false); // non-blocking reader, blocking writer nsCOMPtr tee = do_CreateInstance(NS_STREAMLISTENERTEE_CONTRACTID); rv = tee->Init(loader, writer, listener); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } nsresult rv = channel->AsyncOpen(tee); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } loadContext->mChannel.swap(channel); return NS_OK; } nsresult WorkerScriptLoader::FillCompileOptionsForRequest( JSContext* cx, ScriptLoadRequest* aRequest, JS::CompileOptions* aOptions, JS::MutableHandle aIntroductionScript) { // The full URL shouldn't be exposed to the debugger. See Bug 1634872 aOptions->setFileAndLine(aRequest->mURL.get(), 1); aOptions->setNoScriptRval(true); aOptions->setMutedErrors( aRequest->GetWorkerLoadContext()->mMutedErrorFlag.value()); if (aRequest->mSourceMapURL) { aOptions->setSourceMapURL(aRequest->mSourceMapURL->get()); } return NS_OK; } bool WorkerScriptLoader::EvaluateScript(JSContext* aCx, ScriptLoadRequest* aRequest) { mWorkerRef->Private()->AssertIsOnWorkerThread(); MOZ_ASSERT(!IsDynamicImport(aRequest)); WorkerLoadContext* loadContext = aRequest->GetWorkerLoadContext(); NS_ASSERTION(!loadContext->mChannel, "Should no longer have a channel!"); NS_ASSERTION(aRequest->IsReadyToRun(), "Should be scheduled!"); MOZ_ASSERT(!mRv.Failed(), "Who failed it and why?"); mRv.MightThrowJSException(); if (NS_FAILED(loadContext->mLoadResult)) { ReportErrorToConsole(aRequest, loadContext->mLoadResult); return false; } // If this is a top level script that succeeded, then mark the // Client execution ready and possible controlled by a service worker. if (loadContext->IsTopLevel()) { if (mController.isSome()) { MOZ_ASSERT(mWorkerScriptType == WorkerScript, "Debugger clients can't be controlled."); mWorkerRef->Private()->GlobalScope()->Control(mController.ref()); } mWorkerRef->Private()->ExecutionReady(); } if (aRequest->IsModuleRequest()) { // Only the top level module of the module graph will be executed from here, // the rest will be executed from SpiderMonkey as part of the execution of // the module graph. MOZ_ASSERT(aRequest->IsTopLevel()); ModuleLoadRequest* request = aRequest->AsModuleRequest(); if (!request->mModuleScript) { return false; } // Implements To fetch a worklet/module worker script graph // Step 5. Fetch the descendants of and link result. if (!request->InstantiateModuleGraph()) { return false; } nsresult rv = request->EvaluateModule(); return NS_SUCCEEDED(rv); } JS::CompileOptions options(aCx); // The introduction script is used by the DOM script loader as a way // to fill the Debugger Metadata for the JS Execution context. We don't use // the JS Execution context as we are not making use of async compilation // (delegation to another worker to produce bytecode or compile a string to a // JSScript), so it is not used in this context. JS::Rooted unusedIntroductionScript(aCx); nsresult rv = FillCompileOptionsForRequest(aCx, aRequest, &options, &unusedIntroductionScript); MOZ_ASSERT(NS_SUCCEEDED(rv), "Filling compile options should not fail"); // Our ErrorResult still shouldn't be a failure. MOZ_ASSERT(!mRv.Failed(), "Who failed it and why?"); // Get the source text. ScriptLoadRequest::MaybeSourceText maybeSource; rv = aRequest->GetScriptSource(aCx, &maybeSource); if (NS_FAILED(rv)) { mRv.StealExceptionFromJSContext(aCx); return false; } RefPtr classicScript = nullptr; if (StaticPrefs::dom_workers_modules_enabled() && !mWorkerRef->Private()->IsServiceWorker()) { // We need a LoadedScript to be associated with the JSScript in order to // correctly resolve the referencing private for dynamic imports. In turn // this allows us to correctly resolve the BaseURL. // // Dynamic import is disallowed on service workers. Additionally, causes // crashes because the life cycle isn't completed for service workers. To // keep things simple, we don't create a classic script for ServiceWorkers. // If this changes then we will need to ensure that the reference that is // held is released appropriately. nsCOMPtr requestBaseURI; if (loadContext->mMutedErrorFlag.valueOr(false)) { NS_NewURI(getter_AddRefs(requestBaseURI), "about:blank"_ns); } else { requestBaseURI = aRequest->mBaseURL; } classicScript = new JS::loader::ClassicScript(aRequest->mFetchOptions, requestBaseURI); } bool successfullyEvaluated = aRequest->IsUTF8Text() ? EvaluateSourceBuffer(aCx, options, classicScript, maybeSource.ref>()) : EvaluateSourceBuffer(aCx, options, classicScript, maybeSource.ref>()); if (aRequest->IsCanceled()) { return false; } if (!successfullyEvaluated) { mRv.StealExceptionFromJSContext(aCx); return false; } // steal the loadContext so that the cycle is broken and cycle collector can // collect the scriptLoadRequest. return true; } void WorkerScriptLoader::TryShutdown() { if (AllScriptsExecuted() && AllModuleRequestsLoaded()) { ShutdownScriptLoader(!mExecutionAborted, mMutedErrorFlag); } } void WorkerScriptLoader::ShutdownScriptLoader(bool aResult, bool aMutedError) { MOZ_ASSERT(AllScriptsExecuted()); MOZ_ASSERT(AllModuleRequestsLoaded()); mWorkerRef->Private()->AssertIsOnWorkerThread(); if (!aResult) { // At this point there are two possibilities: // // 1) mRv.Failed(). In that case we just want to leave it // as-is, except if it has a JS exception and we need to mute JS // exceptions. In that case, we log the exception without firing any // events and then replace it on the ErrorResult with a NetworkError, // per spec. // // 2) mRv succeeded. As far as I can tell, this can only // happen when loading the main worker script and // GetOrCreateGlobalScope() fails or if ScriptExecutorRunnable::Cancel // got called. Does it matter what we throw in this case? I'm not // sure... if (mRv.Failed()) { if (aMutedError && mRv.IsJSException()) { LogExceptionToConsole(mWorkerRef->Private()->GetJSContext(), mWorkerRef->Private()); mRv.Throw(NS_ERROR_DOM_NETWORK_ERR); } } else { mRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); } } // Lock, shutdown, and cleanup state. After this the Loader is closed. { MutexAutoLock lock(CleanUpLock()); if (CleanedUp()) { return; } mWorkerRef->Private()->AssertIsOnWorkerThread(); // Module loader doesn't use sync loop for dynamic import if (mSyncLoopTarget) { mWorkerRef->Private()->StopSyncLoop(mSyncLoopTarget, aResult ? NS_OK : NS_ERROR_FAILURE); } // Signal cleanup mCleanedUp = true; // Allow worker shutdown. mWorkerRef = nullptr; } } void WorkerScriptLoader::ReportErrorToConsole(ScriptLoadRequest* aRequest, nsresult aResult) const { nsAutoString url = NS_ConvertUTF8toUTF16(aRequest->mURL); workerinternals::ReportLoadError(mRv, aResult, url); } void WorkerScriptLoader::LogExceptionToConsole(JSContext* aCx, WorkerPrivate* aWorkerPrivate) { aWorkerPrivate->AssertIsOnWorkerThread(); MOZ_ASSERT(mRv.IsJSException()); JS::Rooted exn(aCx); if (!ToJSValue(aCx, std::move(mRv), &exn)) { return; } // Now the exception state should all be in exn. MOZ_ASSERT(!JS_IsExceptionPending(aCx)); MOZ_ASSERT(!mRv.Failed()); JS::ExceptionStack exnStack(aCx, exn, nullptr); JS::ErrorReportBuilder report(aCx); if (!report.init(aCx, exnStack, JS::ErrorReportBuilder::WithSideEffects)) { JS_ClearPendingException(aCx); return; } RefPtr xpcReport = new xpc::ErrorReport(); xpcReport->Init(report.report(), report.toStringResult().c_str(), aWorkerPrivate->IsChromeWorker(), aWorkerPrivate->WindowID()); RefPtr r = new AsyncErrorReporter(xpcReport); NS_DispatchToMainThread(r); } bool WorkerScriptLoader::AllModuleRequestsLoaded() const { mWorkerRef->Private()->AssertIsOnWorkerThread(); return mLoadingModuleRequestCount == 0; } void WorkerScriptLoader::IncreaseLoadingModuleRequestCount() { mWorkerRef->Private()->AssertIsOnWorkerThread(); ++mLoadingModuleRequestCount; } void WorkerScriptLoader::DecreaseLoadingModuleRequestCount() { mWorkerRef->Private()->AssertIsOnWorkerThread(); --mLoadingModuleRequestCount; } NS_IMPL_ISUPPORTS(ScriptLoaderRunnable, nsIRunnable, nsINamed) NS_IMPL_ISUPPORTS(WorkerScriptLoader, nsINamed) ScriptLoaderRunnable::ScriptLoaderRunnable( WorkerScriptLoader* aScriptLoader, nsTArray> aLoadingRequests) : mScriptLoader(aScriptLoader), mWorkerRef(aScriptLoader->mWorkerRef), mLoadingRequests(std::move(aLoadingRequests)), mCancelMainThread(Nothing()) { MOZ_ASSERT(aScriptLoader); } nsresult ScriptLoaderRunnable::Run() { AssertIsOnMainThread(); // Convert the origin stack to JSON (which must be done on the main // thread) explicitly, so that we can use the stack to notify the net // monitor about every script we load. We do this, rather than pass // the stack directly to the netmonitor, in order to be able to use this // for all subsequent scripts. if (mScriptLoader->mOriginStack && mScriptLoader->mOriginStackJSON.IsEmpty()) { ConvertSerializedStackToJSON(std::move(mScriptLoader->mOriginStack), mScriptLoader->mOriginStackJSON); } if (!mWorkerRef->Private()->IsServiceWorker() || mScriptLoader->IsDebuggerScript()) { for (ThreadSafeRequestHandle* handle : mLoadingRequests) { handle->mRunnable = this; } for (ThreadSafeRequestHandle* handle : mLoadingRequests) { nsresult rv = mScriptLoader->LoadScript(handle); if (NS_WARN_IF(NS_FAILED(rv))) { LoadingFinished(handle, rv); CancelMainThread(rv); return rv; } } return NS_OK; } MOZ_ASSERT(!mCacheCreator); mCacheCreator = new CacheCreator(mWorkerRef->Private()); for (ThreadSafeRequestHandle* handle : mLoadingRequests) { handle->mRunnable = this; WorkerLoadContext* loadContext = handle->GetContext(); mCacheCreator->AddLoader(MakeNotNull>( mWorkerRef, handle, loadContext->IsTopLevel(), loadContext->mOnlyExistingCachedResourcesAllowed, mScriptLoader)); } // The worker may have a null principal on first load, but in that case its // parent definitely will have one. nsIPrincipal* principal = mWorkerRef->Private()->GetPrincipal(); if (!principal) { WorkerPrivate* parentWorker = mWorkerRef->Private()->GetParent(); MOZ_ASSERT(parentWorker, "Must have a parent!"); principal = parentWorker->GetPrincipal(); } nsresult rv = mCacheCreator->Load(principal); if (NS_WARN_IF(NS_FAILED(rv))) { CancelMainThread(rv); return rv; } return NS_OK; } nsresult ScriptLoaderRunnable::OnStreamComplete( ThreadSafeRequestHandle* aRequestHandle, nsresult aStatus) { AssertIsOnMainThread(); LoadingFinished(aRequestHandle, aStatus); return NS_OK; } void ScriptLoaderRunnable::LoadingFinished( ThreadSafeRequestHandle* aRequestHandle, nsresult aRv) { AssertIsOnMainThread(); WorkerLoadContext* loadContext = aRequestHandle->GetContext(); loadContext->mLoadResult = aRv; MOZ_ASSERT(!loadContext->mLoadingFinished); loadContext->mLoadingFinished = true; if (loadContext->IsTopLevel() && NS_SUCCEEDED(aRv)) { MOZ_DIAGNOSTIC_ASSERT( mWorkerRef->Private()->PrincipalURIMatchesScriptURL()); } MaybeExecuteFinishedScripts(aRequestHandle); } void ScriptLoaderRunnable::MaybeExecuteFinishedScripts( ThreadSafeRequestHandle* aRequestHandle) { AssertIsOnMainThread(); // We execute the last step if we don't have a pending operation with the // cache and the loading is completed. WorkerLoadContext* loadContext = aRequestHandle->GetContext(); if (!loadContext->IsAwaitingPromise()) { if (aRequestHandle->GetContext()->IsTopLevel()) { mWorkerRef->Private()->WorkerScriptLoaded(); } DispatchProcessPendingRequests(); } } void ScriptLoaderRunnable::CancelMainThreadWithBindingAborted() { AssertIsOnMainThread(); CancelMainThread(NS_BINDING_ABORTED); } void ScriptLoaderRunnable::CancelMainThread(nsresult aCancelResult) { AssertIsOnMainThread(); if (IsCancelled()) { return; } { MutexAutoLock lock(mScriptLoader->CleanUpLock()); // Check if we have already cancelled, or if the worker has been killed // before we cancel. if (mScriptLoader->CleanedUp()) { return; } mCancelMainThread = Some(aCancelResult); for (ThreadSafeRequestHandle* handle : mLoadingRequests) { if (handle->IsEmpty()) { continue; } bool callLoadingFinished = true; WorkerLoadContext* loadContext = handle->GetContext(); if (!loadContext) { continue; } if (loadContext->IsAwaitingPromise()) { MOZ_ASSERT(mWorkerRef->Private()->IsServiceWorker()); loadContext->mCachePromise->MaybeReject(NS_BINDING_ABORTED); loadContext->mCachePromise = nullptr; callLoadingFinished = false; } if (loadContext->mChannel) { if (NS_SUCCEEDED(loadContext->mChannel->Cancel(aCancelResult))) { callLoadingFinished = false; } else { NS_WARNING("Failed to cancel channel!"); } } if (callLoadingFinished && !loadContext->mLoadingFinished) { LoadingFinished(handle, aCancelResult); } } DispatchProcessPendingRequests(); } } void ScriptLoaderRunnable::DispatchProcessPendingRequests() { AssertIsOnMainThread(); const auto begin = mLoadingRequests.begin(); const auto end = mLoadingRequests.end(); using Iterator = decltype(begin); const auto maybeRangeToExecute = [begin, end]() -> Maybe> { // firstItToExecute is the first loadInfo where mExecutionScheduled is // unset. auto firstItToExecute = std::find_if( begin, end, [](const RefPtr& requestHandle) { return !requestHandle->mExecutionScheduled; }); if (firstItToExecute == end) { return Nothing(); } // firstItUnexecutable is the first loadInfo that is not yet finished. // Update mExecutionScheduled on the ones we're about to schedule for // execution. const auto firstItUnexecutable = std::find_if(firstItToExecute, end, [](RefPtr& requestHandle) { MOZ_ASSERT(!requestHandle->IsEmpty()); if (!requestHandle->Finished()) { return true; } // We can execute this one. requestHandle->mExecutionScheduled = true; return false; }); return firstItUnexecutable == firstItToExecute ? Nothing() : Some(std::pair(firstItToExecute, firstItUnexecutable)); }(); // If there are no unexecutable load infos, we can unuse things before the // execution of the scripts and the stopping of the sync loop. if (maybeRangeToExecute) { if (maybeRangeToExecute->second == end) { mCacheCreator = nullptr; } RefPtr runnable = new ScriptExecutorRunnable( mScriptLoader, mWorkerRef->Private(), mScriptLoader->mSyncLoopTarget, Span>{maybeRangeToExecute->first, maybeRangeToExecute->second}); if (!runnable->Dispatch() && mScriptLoader->mSyncLoopTarget) { MOZ_ASSERT(false, "This should never fail!"); } } } ScriptExecutorRunnable::ScriptExecutorRunnable( WorkerScriptLoader* aScriptLoader, WorkerPrivate* aWorkerPrivate, nsISerialEventTarget* aSyncLoopTarget, Span> aLoadedRequests) : MainThreadWorkerSyncRunnable(aWorkerPrivate, aSyncLoopTarget), mScriptLoader(aScriptLoader), mLoadedRequests(aLoadedRequests) {} bool ScriptExecutorRunnable::IsDebuggerRunnable() const { // ScriptExecutorRunnable is used to execute both worker and debugger scripts. // In the latter case, the runnable needs to be dispatched to the debugger // queue. return mScriptLoader->IsDebuggerScript(); } bool ScriptExecutorRunnable::PreRun(WorkerPrivate* aWorkerPrivate) { aWorkerPrivate->AssertIsOnWorkerThread(); // We must be on the same worker as we started on. MOZ_ASSERT( mScriptLoader->mSyncLoopTarget == mSyncLoopTarget, "Unexpected SyncLoopTarget. Check if the sync loop was closed early"); { // There is a possibility that we cleaned up while this task was waiting to // run. If this has happened, return and exit. MutexAutoLock lock(mScriptLoader->CleanUpLock()); if (mScriptLoader->CleanedUp()) { return true; } const auto& requestHandle = mLoadedRequests[0]; // Check if the request is still valid. if (requestHandle->IsEmpty() || !requestHandle->GetContext()->IsTopLevel()) { return true; } } return mScriptLoader->StoreCSP(); } bool ScriptExecutorRunnable::ProcessModuleScript( JSContext* aCx, WorkerPrivate* aWorkerPrivate) { // We should only ever have one script when processing modules MOZ_ASSERT(mLoadedRequests.Length() == 1); RefPtr request; { // There is a possibility that we cleaned up while this task was waiting to // run. If this has happened, return and exit. MutexAutoLock lock(mScriptLoader->CleanUpLock()); if (mScriptLoader->CleanedUp()) { return true; } MOZ_ASSERT(mLoadedRequests.Length() == 1); const auto& requestHandle = mLoadedRequests[0]; // The request must be valid. MOZ_ASSERT(!requestHandle->IsEmpty()); // Release the request to the worker. From this point on, the Request Handle // is empty. request = requestHandle->ReleaseRequest(); // release lock. We will need it later if we cleanup. } MOZ_ASSERT(request->IsModuleRequest()); WorkerLoadContext* loadContext = request->GetWorkerLoadContext(); ModuleLoadRequest* moduleRequest = request->AsModuleRequest(); // DecreaseLoadingModuleRequestCount must be called before OnFetchComplete. // OnFetchComplete will call ProcessPendingRequests, and in // ProcessPendingRequests it will try to shutdown if // AllModuleRequestsLoaded() returns true. mScriptLoader->DecreaseLoadingModuleRequestCount(); moduleRequest->OnFetchComplete(loadContext->mLoadResult); if (NS_FAILED(loadContext->mLoadResult)) { if (moduleRequest->IsDynamicImport()) { if (request->isInList()) { moduleRequest->CancelDynamicImport(loadContext->mLoadResult); mScriptLoader->TryShutdown(); } } else if (!moduleRequest->IsTopLevel()) { moduleRequest->Cancel(); mScriptLoader->TryShutdown(); } else { moduleRequest->LoadFailed(); } } return true; } bool ScriptExecutorRunnable::ProcessClassicScripts( JSContext* aCx, WorkerPrivate* aWorkerPrivate) { // There is a possibility that we cleaned up while this task was waiting to // run. If this has happened, return and exit. { MutexAutoLock lock(mScriptLoader->CleanUpLock()); if (mScriptLoader->CleanedUp()) { return true; } for (const auto& requestHandle : mLoadedRequests) { // The request must be valid. MOZ_ASSERT(!requestHandle->IsEmpty()); // Release the request to the worker. From this point on, the Request // Handle is empty. RefPtr request = requestHandle->ReleaseRequest(); mScriptLoader->MaybeMoveToLoadedList(request); } } return mScriptLoader->ProcessPendingRequests(aCx); } bool ScriptExecutorRunnable::WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) { aWorkerPrivate->AssertIsOnWorkerThread(); // We must be on the same worker as we started on. MOZ_ASSERT( mScriptLoader->mSyncLoopTarget == mSyncLoopTarget, "Unexpected SyncLoopTarget. Check if the sync loop was closed early"); if (mLoadedRequests.begin()->get()->GetRequest()->IsModuleRequest()) { return ProcessModuleScript(aCx, aWorkerPrivate); } return ProcessClassicScripts(aCx, aWorkerPrivate); } nsresult ScriptExecutorRunnable::Cancel() { // We need to check first if cancel is called twice nsresult rv = MainThreadWorkerSyncRunnable::Cancel(); NS_ENSURE_SUCCESS(rv, rv); if (mScriptLoader->AllScriptsExecuted() && mScriptLoader->AllModuleRequestsLoaded()) { mScriptLoader->ShutdownScriptLoader(false, false); } return NS_OK; } } /* namespace loader */ nsresult ChannelFromScriptURLMainThread( nsIPrincipal* aPrincipal, Document* aParentDoc, nsILoadGroup* aLoadGroup, nsIURI* aScriptURL, const WorkerType& aWorkerType, const RequestCredentials& aCredentials, const Maybe& aClientInfo, nsContentPolicyType aMainScriptContentPolicyType, nsICookieJarSettings* aCookieJarSettings, nsIReferrerInfo* aReferrerInfo, nsIChannel** aChannel) { AssertIsOnMainThread(); nsCOMPtr ios(do_GetIOService()); nsIScriptSecurityManager* secMan = nsContentUtils::GetSecurityManager(); NS_ASSERTION(secMan, "This should never be null!"); uint32_t secFlags; nsresult rv; if (aWorkerType == WorkerType::Module) { rv = GetModuleSecFlags(true, aPrincipal, WorkerScript, aScriptURL, aCredentials, secFlags); } else { rv = GetClassicSecFlags(true, aScriptURL, aPrincipal, WorkerScript, secFlags); } if (NS_FAILED(rv)) { return rv; } return ChannelFromScriptURL( aPrincipal, aParentDoc, nullptr, aLoadGroup, ios, secMan, aScriptURL, aClientInfo, Maybe(), true, WorkerScript, aMainScriptContentPolicyType, nsIRequest::LOAD_NORMAL, secFlags, aCookieJarSettings, aReferrerInfo, aChannel); } nsresult ChannelFromScriptURLWorkerThread( JSContext* aCx, WorkerPrivate* aParent, const nsAString& aScriptURL, const WorkerType& aWorkerType, const RequestCredentials& aCredentials, WorkerLoadInfo& aLoadInfo) { aParent->AssertIsOnWorkerThread(); RefPtr getter = new ChannelGetterRunnable( aParent, aScriptURL, aWorkerType, aCredentials, aLoadInfo); ErrorResult rv; getter->Dispatch(Canceling, rv); if (rv.Failed()) { NS_ERROR("Failed to dispatch!"); return rv.StealNSResult(); } return getter->GetResult(); } void ReportLoadError(ErrorResult& aRv, nsresult aLoadResult, const nsAString& aScriptURL) { MOZ_ASSERT(!aRv.Failed()); nsPrintfCString err("Failed to load worker script at \"%s\"", NS_ConvertUTF16toUTF8(aScriptURL).get()); switch (aLoadResult) { case NS_ERROR_FILE_NOT_FOUND: case NS_ERROR_NOT_AVAILABLE: case NS_ERROR_CORRUPTED_CONTENT: aRv.Throw(NS_ERROR_DOM_NETWORK_ERR); break; case NS_ERROR_MALFORMED_URI: case NS_ERROR_DOM_SYNTAX_ERR: aRv.ThrowSyntaxError(err); break; case NS_BINDING_ABORTED: // Note: we used to pretend like we didn't set an exception for // NS_BINDING_ABORTED, but then ShutdownScriptLoader did it anyway. The // other callsite, in WorkerPrivate::Constructor, never passed in // NS_BINDING_ABORTED. So just throw it directly here. Consumers will // deal as needed. But note that we do NOT want to use one of the // Throw*Error() methods on ErrorResult for this case, because that will // make it impossible for consumers to realize that our error was // NS_BINDING_ABORTED. aRv.Throw(aLoadResult); return; case NS_ERROR_DOM_BAD_URI: // This is actually a security error. case NS_ERROR_DOM_SECURITY_ERR: aRv.ThrowSecurityError(err); break; default: // For lack of anything better, go ahead and throw a NetworkError here. // We don't want to throw a JS exception, because for toplevel script // loads that would get squelched. aRv.ThrowNetworkError(nsPrintfCString( "Failed to load worker script at %s (nsresult = 0x%" PRIx32 ")", NS_ConvertUTF16toUTF8(aScriptURL).get(), static_cast(aLoadResult))); return; } } void LoadMainScript(WorkerPrivate* aWorkerPrivate, UniquePtr aOriginStack, const nsAString& aScriptURL, WorkerScriptType aWorkerScriptType, ErrorResult& aRv, const mozilla::Encoding* aDocumentEncoding) { nsTArray scriptURLs; scriptURLs.AppendElement(aScriptURL); LoadAllScripts(aWorkerPrivate, std::move(aOriginStack), scriptURLs, true, aWorkerScriptType, aRv, aDocumentEncoding); } void Load(WorkerPrivate* aWorkerPrivate, UniquePtr aOriginStack, const nsTArray& aScriptURLs, WorkerScriptType aWorkerScriptType, ErrorResult& aRv) { const uint32_t urlCount = aScriptURLs.Length(); if (!urlCount) { return; } if (urlCount > MAX_CONCURRENT_SCRIPTS) { aRv.Throw(NS_ERROR_OUT_OF_MEMORY); return; } LoadAllScripts(aWorkerPrivate, std::move(aOriginStack), aScriptURLs, false, aWorkerScriptType, aRv); } } // namespace mozilla::dom::workerinternals