diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-15 03:34:50 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-15 03:34:50 +0000 |
commit | def92d1b8e9d373e2f6f27c366d578d97d8960c6 (patch) | |
tree | 2ef34b9ad8bb9a9220e05d60352558b15f513894 /dom/workers | |
parent | Adding debian version 125.0.3-1. (diff) | |
download | firefox-def92d1b8e9d373e2f6f27c366d578d97d8960c6.tar.xz firefox-def92d1b8e9d373e2f6f27c366d578d97d8960c6.zip |
Merging upstream version 126.0.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dom/workers')
32 files changed, 891 insertions, 260 deletions
diff --git a/dom/workers/RuntimeService.cpp b/dom/workers/RuntimeService.cpp index 02efb12053..321895e700 100644 --- a/dom/workers/RuntimeService.cpp +++ b/dom/workers/RuntimeService.cpp @@ -372,6 +372,9 @@ void LoadJSGCMemoryOptions(const char* aPrefName, void* /* aClosure */) { PREF("gc_parallel_marking", JSGC_PARALLEL_MARKING_ENABLED), PREF("gc_parallel_marking_threshold_mb", JSGC_PARALLEL_MARKING_THRESHOLD_MB), +#ifdef NIGHTLY_BUILD + PREF("gc_experimental_semispace_nursery", JSGC_SEMISPACE_NURSERY_ENABLED), +#endif // Note: Workers do not currently trigger eager minor GC, but if that is // desired the following parameters should be added: // javascript.options.mem.nursery_eager_collection_threshold_kb @@ -426,6 +429,9 @@ void LoadJSGCMemoryOptions(const char* aPrefName, void* /* aClosure */) { } case JSGC_COMPACTING_ENABLED: case JSGC_PARALLEL_MARKING_ENABLED: +#ifdef NIGHTLY_BUILD + case JSGC_SEMISPACE_NURSERY_ENABLED: +#endif case JSGC_BALANCED_HEAP_LIMITS_ENABLED: { bool present; bool prefValue = GetPref(pref->fullName, false, &present); diff --git a/dom/workers/ScriptLoader.cpp b/dom/workers/ScriptLoader.cpp index 73997b2725..9dcb1d0c9a 100644 --- a/dom/workers/ScriptLoader.cpp +++ b/dom/workers/ScriptLoader.cpp @@ -517,9 +517,7 @@ already_AddRefed<WorkerScriptLoader> WorkerScriptLoader::Create( } // Set up the module loader, if it has not been initialzied yet. - if (!aWorkerPrivate->IsServiceWorker()) { - self->InitModuleLoader(); - } + self->InitModuleLoader(); return self.forget(); } @@ -1065,8 +1063,8 @@ nsresult WorkerScriptLoader::LoadScript( // This flag reflects the fact that if the worker is created under a // third-party context. nsCOMPtr<nsILoadInfo> loadInfo = channel->LoadInfo(); - loadInfo->SetIsThirdPartyContextToTopWindow( - mWorkerRef->Private()->IsThirdPartyContextToTopWindow()); + loadInfo->SetIsInThirdPartyContext( + mWorkerRef->Private()->IsThirdPartyContext()); Maybe<ClientInfo> clientInfo; clientInfo.emplace(loadContext->mClientInfo.ref()); @@ -1856,12 +1854,6 @@ void ReportLoadError(ErrorResult& aRv, nsresult aLoadResult, 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); @@ -1877,7 +1869,7 @@ void ReportLoadError(ErrorResult& aRv, nsresult aLoadResult, // make it impossible for consumers to realize that our error was // NS_BINDING_ABORTED. aRv.Throw(aLoadResult); - return; + break; case NS_ERROR_DOM_BAD_URI: // This is actually a security error. @@ -1885,15 +1877,16 @@ void ReportLoadError(ErrorResult& aRv, nsresult aLoadResult, aRv.ThrowSecurityError(err); break; + case NS_ERROR_FILE_NOT_FOUND: + case NS_ERROR_NOT_AVAILABLE: + case NS_ERROR_CORRUPTED_CONTENT: + case NS_ERROR_DOM_NETWORK_ERR: + // 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. 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<uint32_t>(aLoadResult))); - return; + aRv.Throw(NS_ERROR_DOM_NETWORK_ERR); + break; } } diff --git a/dom/workers/Worker.cpp b/dom/workers/Worker.cpp index 2348572e65..88df53b877 100644 --- a/dom/workers/Worker.cpp +++ b/dom/workers/Worker.cpp @@ -83,6 +83,12 @@ JSObject* Worker::WrapObject(JSContext* aCx, return wrapper; } +bool Worker::IsEligibleForMessaging() { + NS_ASSERT_OWNINGTHREAD(Worker); + + return mWorkerPrivate && mWorkerPrivate->ParentStatusProtected() <= Running; +} + void Worker::PostMessage(JSContext* aCx, JS::Handle<JS::Value> aMessage, const Sequence<JSObject*>& aTransferable, ErrorResult& aRv) { diff --git a/dom/workers/Worker.h b/dom/workers/Worker.h index 14d0630f28..6a0e295fc2 100644 --- a/dom/workers/Worker.h +++ b/dom/workers/Worker.h @@ -42,6 +42,31 @@ class Worker : public DOMEventTargetHelper, public SupportsWeakPtr { return Some(EventCallbackDebuggerNotificationType::Worker); } + // True if the worker is not yet closing from the perspective of this, the + // owning thread, and therefore it's okay to post a message to the worker. + // This is not a guarantee that the worker will process the message. + // + // This method will return false if `globalThis.close()` is invoked on the + // worker before that method returns control to the caller and without waiting + // for any task to be queued on this thread and run; this biases us to avoid + // doing wasteful work but does mean if you are exposing something to content + // that is specified to only transition as the result of a task, then you + // should not use this method. + // + // The method name comes from + // https://html.spec.whatwg.org/multipage/web-messaging.html#eligible-for-messaging + // and is intended to convey whether it's okay to begin to take the steps to + // create an `EventWithOptionsRunnable` to pass to `PostEventWithOptions`. + // Note that early returning based on calling this method without performing + // the structured serialization steps that would otherwise run is potentially + // observable to content if content is in control of any of the payload in + // such a way that an object with getters or a proxy could be provided. + // + // There is an identically named method on nsIGlobalObject and the semantics + // are intentionally similar but please make sure you document your + // assumptions when calling either method. + bool IsEligibleForMessaging(); + void PostMessage(JSContext* aCx, JS::Handle<JS::Value> aMessage, const Sequence<JSObject*>& aTransferable, ErrorResult& aRv); @@ -49,6 +74,8 @@ class Worker : public DOMEventTargetHelper, public SupportsWeakPtr { const StructuredSerializeOptions& aOptions, ErrorResult& aRv); + // Callers must call `IsEligibleForMessaging` before constructing an + // `EventWithOptionsRunnable` subclass. void PostEventWithOptions(JSContext* aCx, JS::Handle<JS::Value> aOptions, const Sequence<JSObject*>& aTransferable, EventWithOptionsRunnable* aRunnable, diff --git a/dom/workers/WorkerLoadInfo.cpp b/dom/workers/WorkerLoadInfo.cpp index 0dec07a675..29fb69c212 100644 --- a/dom/workers/WorkerLoadInfo.cpp +++ b/dom/workers/WorkerLoadInfo.cpp @@ -101,7 +101,7 @@ WorkerLoadInfoData::WorkerLoadInfoData() mUsingStorageAccess(false), mServiceWorkersTestingInWindow(false), mShouldResistFingerprinting(false), - mIsThirdPartyContextToTopWindow(true), + mIsThirdPartyContext(true), mSecureContext(eNotSet) {} nsresult WorkerLoadInfo::SetPrincipalsAndCSPOnMainThread( diff --git a/dom/workers/WorkerLoadInfo.h b/dom/workers/WorkerLoadInfo.h index 722e71d6f3..c86538145c 100644 --- a/dom/workers/WorkerLoadInfo.h +++ b/dom/workers/WorkerLoadInfo.h @@ -147,7 +147,7 @@ struct WorkerLoadInfoData { bool mShouldResistFingerprinting; Maybe<RFPTarget> mOverriddenFingerprintingSettings; OriginAttributes mOriginAttributes; - bool mIsThirdPartyContextToTopWindow; + bool mIsThirdPartyContext; enum { eNotSet, diff --git a/dom/workers/WorkerPrivate.cpp b/dom/workers/WorkerPrivate.cpp index a8643981aa..df248acda4 100644 --- a/dom/workers/WorkerPrivate.cpp +++ b/dom/workers/WorkerPrivate.cpp @@ -1443,8 +1443,8 @@ nsresult WorkerPrivate::SetCSPFromHeaderValues( } MOZ_ASSERT(selfURI, "need a self URI for CSP"); - rv = csp->SetRequestContextWithPrincipal(mLoadInfo.mPrincipal, selfURI, - u""_ns, 0); + rv = csp->SetRequestContextWithPrincipal(mLoadInfo.mPrincipal, selfURI, ""_ns, + 0); NS_ENSURE_SUCCESS(rv, rv); csp->EnsureEventTarget(mMainThreadEventTarget); @@ -1574,8 +1574,6 @@ nsresult WorkerPrivate::DispatchLockHeld( MOZ_ASSERT_IF(aSyncLoopTarget, mThread); if (mStatus == Dead || (!aSyncLoopTarget && ParentStatus() > Canceling)) { - LOGV(("WorkerPrivate::DispatchLockHeld [%p] runnable %p, parent status: %u", - this, runnable.get(), (uint8_t)(ParentStatus()))); NS_WARNING( "A runnable was posted to a worker that is already shutting " "down!"); @@ -1624,6 +1622,7 @@ nsresult WorkerPrivate::DispatchLockHeld( } if (NS_WARN_IF(NS_FAILED(rv))) { + LOGV(("WorkerPrivate::Dispatch Failed [%p]", this)); return rv; } @@ -2792,8 +2791,7 @@ nsresult WorkerPrivate::GetLoadInfo( loadInfo.mOriginAttributes = aParent->GetOriginAttributes(); loadInfo.mServiceWorkersTestingInWindow = aParent->ServiceWorkersTestingInWindow(); - loadInfo.mIsThirdPartyContextToTopWindow = - aParent->IsThirdPartyContextToTopWindow(); + loadInfo.mIsThirdPartyContext = aParent->IsThirdPartyContext(); loadInfo.mShouldResistFingerprinting = aParent->ShouldResistFingerprinting( RFPTarget::IsAlwaysEnabledForPrecompute); loadInfo.mOverriddenFingerprintingSettings = @@ -2954,7 +2952,7 @@ nsresult WorkerPrivate::GetLoadInfo( StorageAllowedForDocument(document) != StorageAccess::eAllow) { loadInfo.mUsingStorageAccess = false; } - loadInfo.mIsThirdPartyContextToTopWindow = + loadInfo.mIsThirdPartyContext = AntiTrackingUtils::IsThirdPartyWindow(globalWindow, nullptr); loadInfo.mCookieJarSettings = document->CookieJarSettings(); if (loadInfo.mCookieJarSettings) { @@ -3022,7 +3020,7 @@ nsresult WorkerPrivate::GetLoadInfo( cookieJarSettings->Serialize(loadInfo.mCookieJarSettingsArgs); loadInfo.mOriginAttributes = OriginAttributes(); - loadInfo.mIsThirdPartyContextToTopWindow = false; + loadInfo.mIsThirdPartyContext = false; } MOZ_ASSERT(loadInfo.mLoadingPrincipal); @@ -3104,10 +3102,33 @@ void WorkerPrivate::OverrideLoadInfoLoadGroup(WorkerLoadInfo& aLoadInfo, void WorkerPrivate::RunLoopNeverRan() { LOG(WorkerLog(), ("WorkerPrivate::RunLoopNeverRan [%p]", this)); + // RunLoopNeverRan is only called in WorkerThreadPrimaryRunnable::Run() to + // handle cases + // 1. Fail to get BackgroundChild for the worker thread or + // 2. Fail to initialize the worker's JS context + // However, mPreStartRunnables had already dispatched in + // WorkerThread::SetWorkerPrivateInWorkerThread() where beforing above jobs + // start. So we need to clean up these dispatched runnables for the worker + // thread. + + auto data = mWorkerThreadAccessible.Access(); + RefPtr<WorkerThread> thread; { MutexAutoLock lock(mMutex); - + // WorkerPrivate::DoRunLoop() is never called, so CompileScriptRunnable + // should not execute yet. However, the Worker is going to "Dead", flip the + // mCancelBeforeWorkerScopeConstructed to true for the dispatched runnables + // to indicate runnables there is no valid WorkerGlobalScope for executing. + MOZ_ASSERT(!data->mCancelBeforeWorkerScopeConstructed); + data->mCancelBeforeWorkerScopeConstructed.Flip(); + // Switch State to Dead mStatus = Dead; + thread = mThread; + } + + // Clear the dispatched mPreStartRunnables. + if (thread && NS_HasPendingEvents(thread)) { + NS_ProcessPendingEvents(nullptr); } // After mStatus is set to Dead there can be no more @@ -3230,11 +3251,6 @@ void WorkerPrivate::DoRunLoop(JSContext* aCx) { { MutexAutoLock lock(mMutex); - - LOGV( - ("WorkerPrivate::DoRunLoop [%p] mStatus %u before getting events" - " to run", - this, (uint8_t)mStatus)); if (checkFinalGCCC && currentStatus != mStatus) { // Something moved our status while we were supposed to check for a // potentially needed GC/CC. Just check again. @@ -3270,6 +3286,25 @@ void WorkerPrivate::DoRunLoop(JSContext* aCx) { currentStatus = mStatus; } + // Status transitions to Closing/Canceling and there are no SyncLoops, + // set global start dying, disconnect EventTargetObjects and + // WebTaskScheduler. + // The Worker might switch to the "Killing" immediately then directly exits + // DoRunLoop(). Before exiting the DoRunLoop(), explicitly disconnecting the + // WorkerGlobalScope's EventTargetObject here would help to fail runnable + // dispatching when the Worker is in the status changing. + if (currentStatus >= Closing && + !data->mPerformedShutdownAfterLastContentTaskExecuted) { + data->mPerformedShutdownAfterLastContentTaskExecuted.Flip(); + if (data->mScope) { + data->mScope->NoteTerminating(); + data->mScope->DisconnectGlobalTeardownObservers(); + if (data->mScope->GetExistingScheduler()) { + data->mScope->GetExistingScheduler()->Disconnect(); + } + } + } + // Transition from Canceling to Killing and exit this loop when: // * All (non-weak) WorkerRefs have been released. // * There are no runnables pending. This is intended to let same-thread @@ -3335,21 +3370,6 @@ void WorkerPrivate::DoRunLoop(JSContext* aCx) { } } - // Status transitions to Closing/Canceling and there are no SyncLoops, - // set global start dying, disconnect EventTargetObjects and - // WebTaskScheduler. - if (currentStatus >= Closing && - !data->mPerformedShutdownAfterLastContentTaskExecuted) { - data->mPerformedShutdownAfterLastContentTaskExecuted.Flip(); - if (data->mScope) { - data->mScope->NoteTerminating(); - data->mScope->DisconnectGlobalTeardownObservers(); - if (data->mScope->GetExistingScheduler()) { - data->mScope->GetExistingScheduler()->Disconnect(); - } - } - } - if (debuggerRunnablesPending || normalRunnablesPending) { // Start the periodic GC timer if it is not already running. SetGCTimerMode(PeriodicTimer); @@ -4324,7 +4344,6 @@ void WorkerPrivate::AdjustNonblockingCCBackgroundActorCount(int32_t aCount) { } void WorkerPrivate::UpdateCCFlag(const CCFlag aFlag) { - LOGV(("WorkerPrivate::UpdateCCFlag [%p]", this)); AssertIsOnWorkerThread(); auto data = mWorkerThreadAccessible.Access(); @@ -4984,17 +5003,6 @@ bool WorkerPrivate::NotifyInternal(WorkerStatus aStatus) { MOZ_ASSERT_IF(aStatus == Killing, mStatus == Canceling && mParentStatus == Canceling); - if (aStatus >= Canceling) { - MutexAutoUnlock unlock(mMutex); - if (data->mScope) { - if (aStatus == Canceling) { - data->mScope->NoteTerminating(); - } else { - data->mScope->NoteShuttingDown(); - } - } - } - mStatus = aStatus; // Mark parent status as closing immediately to avoid new events being @@ -5005,8 +5013,20 @@ bool WorkerPrivate::NotifyInternal(WorkerStatus aStatus) { // Synchronize the mParentStatus with mStatus, such that event dispatching // will fail in proper after WorkerPrivate gets into Killing status. - if (aStatus == Killing) { - mParentStatus = Killing; + if (aStatus >= Killing) { + mParentStatus = aStatus; + } + } + + // Status transistion to "Canceling"/"Killing", mark the scope as dying when + // "Canceling," or shutdown the StorageManager when "Killing." + if (aStatus >= Canceling) { + if (data->mScope) { + if (aStatus == Canceling) { + data->mScope->NoteTerminating(); + } else { + data->mScope->NoteShuttingDown(); + } } } diff --git a/dom/workers/WorkerPrivate.h b/dom/workers/WorkerPrivate.h index a670d00975..ce754ba9f6 100644 --- a/dom/workers/WorkerPrivate.h +++ b/dom/workers/WorkerPrivate.h @@ -972,9 +972,7 @@ class WorkerPrivate final } // Determine if the worker was created under a third-party context. - bool IsThirdPartyContextToTopWindow() const { - return mLoadInfo.mIsThirdPartyContextToTopWindow; - } + bool IsThirdPartyContext() const { return mLoadInfo.mIsThirdPartyContext; } bool IsWatchedByDevTools() const { return mLoadInfo.mWatchedByDevTools; } diff --git a/dom/workers/WorkerRunnable.cpp b/dom/workers/WorkerRunnable.cpp index ff2178d16e..14a3e5e3f9 100644 --- a/dom/workers/WorkerRunnable.cpp +++ b/dom/workers/WorkerRunnable.cpp @@ -226,22 +226,6 @@ WorkerRunnable::Run() { LOG(("WorkerRunnable::Run [%p]", this)); bool targetIsWorkerThread = mTarget == WorkerThread; - if (targetIsWorkerThread) { - // On a worker thread, a WorkerRunnable should only run when there is an - // underlying WorkerThreadPrimaryRunnable active, which means we should - // find a CycleCollectedJSContext. - if (!CycleCollectedJSContext::Get()) { -#if (defined(MOZ_COLLECTING_RUNNABLE_TELEMETRY) && defined(NIGHTLY_BUILD)) - // We will only leak the static name string of the WorkerRunnable type - // we are trying to execute. - MOZ_CRASH_UNSAFE_PRINTF( - "Runnable '%s' executed after WorkerThreadPrimaryRunnable ended.", - this->mName); -#endif - return NS_OK; - } - } - #ifdef DEBUG if (targetIsWorkerThread) { mWorkerPrivate->AssertIsOnWorkerThread(); diff --git a/dom/workers/WorkerScope.cpp b/dom/workers/WorkerScope.cpp index 92d6c89dca..2121a99cb3 100644 --- a/dom/workers/WorkerScope.cpp +++ b/dom/workers/WorkerScope.cpp @@ -721,7 +721,7 @@ void WorkerGlobalScope::GetJSTestingFunctions( } already_AddRefed<Promise> WorkerGlobalScope::Fetch( - const RequestOrUSVString& aInput, const RequestInit& aInit, + const RequestOrUTF8String& aInput, const RequestInit& aInit, CallerType aCallerType, ErrorResult& aRv) { return FetchRequest(this, aInput, aInit, aCallerType, aRv); } diff --git a/dom/workers/WorkerScope.h b/dom/workers/WorkerScope.h index 12a97e12c3..668755ce4a 100644 --- a/dom/workers/WorkerScope.h +++ b/dom/workers/WorkerScope.h @@ -68,7 +68,7 @@ template <typename T> class Optional; class Performance; class Promise; -class RequestOrUSVString; +class RequestOrUTF8String; template <typename T> class Sequence; class ServiceWorkerDescriptor; @@ -326,7 +326,7 @@ class WorkerGlobalScope : public WorkerGlobalScopeBase { JS::MutableHandle<JS::Value> aRetval, ErrorResult& aError); - already_AddRefed<Promise> Fetch(const RequestOrUSVString& aInput, + already_AddRefed<Promise> Fetch(const RequestOrUTF8String& aInput, const RequestInit& aInit, CallerType aCallerType, ErrorResult& aRv); diff --git a/dom/workers/WorkerThread.cpp b/dom/workers/WorkerThread.cpp index 19cf9cb364..14d944e4d3 100644 --- a/dom/workers/WorkerThread.cpp +++ b/dom/workers/WorkerThread.cpp @@ -13,6 +13,7 @@ #include "mozilla/Atomics.h" #include "mozilla/CycleCollectedJSContext.h" #include "mozilla/EventQueue.h" +#include "mozilla/Logging.h" #include "mozilla/MacroForEach.h" #include "mozilla/NotNull.h" #include "mozilla/ThreadEventQueue.h" @@ -25,8 +26,15 @@ #include "nsIRunnable.h" #include "nsIThreadInternal.h" #include "nsString.h" +#include "nsThreadUtils.h" #include "prthread.h" +static mozilla::LazyLogModule gWorkerThread("WorkerThread"); +#ifdef LOGV +# undef LOGV +#endif +#define LOGV(msg) MOZ_LOG(gWorkerThread, LogLevel::Verbose, msg); + namespace mozilla { using namespace ipc; @@ -143,7 +151,11 @@ void WorkerThread::SetWorker(const WorkerThreadFriendKey& /* aKey */, while (mOtherThreadsDispatchingViaEventTarget) { mWorkerPrivateCondVar.Wait(); } - + // Need to clean up the dispatched runnables if + // mOtherThreadsDispatchingViaEventTarget was non-zero. + if (NS_HasPendingEvents(nullptr)) { + NS_ProcessPendingEvents(nullptr); + } #ifdef DEBUG mAcceptingNonWorkerRunnables = true; #endif @@ -223,6 +235,8 @@ WorkerThread::Dispatch(already_AddRefed<nsIRunnable> aRunnable, // May be called on any thread! nsCOMPtr<nsIRunnable> runnable(aRunnable); // in case we exit early + LOGV(("WorkerThread::Dispatch [%p] runnable: %p", this, runnable.get())); + // Workers only support asynchronous dispatch. if (NS_WARN_IF(aFlags != NS_DISPATCH_NORMAL)) { return NS_ERROR_UNEXPECTED; @@ -282,6 +296,8 @@ WorkerThread::Dispatch(already_AddRefed<nsIRunnable> aRunnable, } if (NS_WARN_IF(NS_FAILED(rv))) { + LOGV(("WorkerThread::Dispatch [%p] failed, runnable: %p", this, + runnable.get())); return rv; } diff --git a/dom/workers/loader/CacheLoadHandler.cpp b/dom/workers/loader/CacheLoadHandler.cpp index 16f992e837..7059249602 100644 --- a/dom/workers/loader/CacheLoadHandler.cpp +++ b/dom/workers/loader/CacheLoadHandler.cpp @@ -305,18 +305,15 @@ void CacheLoadHandler::Load(Cache* aCache) { return; } - nsAutoCString spec; - rv = uri->GetSpec(spec); + MOZ_ASSERT(loadContext->mFullURL.IsEmpty()); + rv = uri->GetSpec(loadContext->mFullURL); if (NS_WARN_IF(NS_FAILED(rv))) { Fail(rv); return; } - MOZ_ASSERT(loadContext->mFullURL.IsEmpty()); - CopyUTF8toUTF16(spec, loadContext->mFullURL); - - mozilla::dom::RequestOrUSVString request; - request.SetAsUSVString().ShareOrDependUpon(loadContext->mFullURL); + mozilla::dom::RequestOrUTF8String request; + request.SetAsUTF8String().ShareOrDependUpon(loadContext->mFullURL); mozilla::dom::CacheQueryOptions params; diff --git a/dom/workers/loader/NetworkLoadHandler.cpp b/dom/workers/loader/NetworkLoadHandler.cpp index 9c2c243066..07398dac0a 100644 --- a/dom/workers/loader/NetworkLoadHandler.cpp +++ b/dom/workers/loader/NetworkLoadHandler.cpp @@ -358,10 +358,10 @@ nsresult NetworkLoadHandler::PrepareForRequest(nsIRequest* aRequest) { RefPtr<mozilla::dom::Response> response = new mozilla::dom::Response( mRequestHandle->GetCacheCreator()->Global(), std::move(ir), nullptr); - mozilla::dom::RequestOrUSVString request; + mozilla::dom::RequestOrUTF8String request; MOZ_ASSERT(!loadContext->mFullURL.IsEmpty()); - request.SetAsUSVString().ShareOrDependUpon(loadContext->mFullURL); + request.SetAsUTF8String().ShareOrDependUpon(loadContext->mFullURL); // This JSContext will not end up executing JS code because here there are // no ReadableStreams involved. diff --git a/dom/workers/loader/WorkerLoadContext.h b/dom/workers/loader/WorkerLoadContext.h index 97362f2871..f5b107a610 100644 --- a/dom/workers/loader/WorkerLoadContext.h +++ b/dom/workers/loader/WorkerLoadContext.h @@ -130,7 +130,7 @@ class WorkerLoadContext : public JS::loader::LoadContextBase { /* TODO: Split out a ServiceWorkerLoadContext */ // This full URL string is populated only if this object is used in a // ServiceWorker. - nsString mFullURL; + nsCString mFullURL; // This promise is set only when the script is for a ServiceWorker but // it's not in the cache yet. The promise is resolved when the full body is diff --git a/dom/workers/loader/WorkerModuleLoader.cpp b/dom/workers/loader/WorkerModuleLoader.cpp index da340c89bd..d76c61c31f 100644 --- a/dom/workers/loader/WorkerModuleLoader.cpp +++ b/dom/workers/loader/WorkerModuleLoader.cpp @@ -147,6 +147,15 @@ already_AddRefed<ModuleLoadRequest> WorkerModuleLoader::CreateDynamicImport( return request.forget(); } +bool WorkerModuleLoader::IsDynamicImportSupported() { + WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate(); + if (workerPrivate->IsServiceWorker()) { + return false; + } + + return true; +} + bool WorkerModuleLoader::CanStartLoad(ModuleLoadRequest* aRequest, nsresult* aRvOut) { return true; diff --git a/dom/workers/loader/WorkerModuleLoader.h b/dom/workers/loader/WorkerModuleLoader.h index 6ad45b42a0..f49d08743c 100644 --- a/dom/workers/loader/WorkerModuleLoader.h +++ b/dom/workers/loader/WorkerModuleLoader.h @@ -63,6 +63,8 @@ class WorkerModuleLoader : public JS::loader::ModuleLoaderBase { JS::Handle<JSString*> aSpecifier, JS::Handle<JSObject*> aPromise) override; + bool IsDynamicImportSupported() override; + bool CanStartLoad(ModuleLoadRequest* aRequest, nsresult* aRvOut) override; // StartFetch is special for worker modules, as we need to move back to the diff --git a/dom/workers/nsIWorkerDebugger.idl b/dom/workers/nsIWorkerDebugger.idl index 931d01a4ac..573e7027b0 100644 --- a/dom/workers/nsIWorkerDebugger.idl +++ b/dom/workers/nsIWorkerDebugger.idl @@ -25,11 +25,11 @@ interface nsIWorkerDebugger : nsISupports const unsigned long TYPE_SHARED = 1; const unsigned long TYPE_SERVICE = 2; - readonly attribute bool isClosed; + readonly attribute boolean isClosed; - readonly attribute bool isChrome; + readonly attribute boolean isChrome; - readonly attribute bool isInitialized; + readonly attribute boolean isInitialized; readonly attribute nsIWorkerDebugger parent; diff --git a/dom/workers/remoteworkers/RemoteWorkerChild.cpp b/dom/workers/remoteworkers/RemoteWorkerChild.cpp index feb294f3fc..bf63c1729c 100644 --- a/dom/workers/remoteworkers/RemoteWorkerChild.cpp +++ b/dom/workers/remoteworkers/RemoteWorkerChild.cpp @@ -286,7 +286,7 @@ nsresult RemoteWorkerChild::ExecWorkerOnMainThread(RemoteWorkerData&& aData) { info.mStorageAccess = aData.storageAccess(); info.mUseRegularPrincipal = aData.useRegularPrincipal(); info.mUsingStorageAccess = aData.usingStorageAccess(); - info.mIsThirdPartyContextToTopWindow = aData.isThirdPartyContextToTopWindow(); + info.mIsThirdPartyContext = aData.isThirdPartyContext(); info.mOriginAttributes = BasePrincipal::Cast(principal)->OriginAttributesRef(); info.mShouldResistFingerprinting = aData.shouldResistFingerprinting(); diff --git a/dom/workers/remoteworkers/RemoteWorkerTypes.ipdlh b/dom/workers/remoteworkers/RemoteWorkerTypes.ipdlh index 8894450b72..08fb5700fe 100644 --- a/dom/workers/remoteworkers/RemoteWorkerTypes.ipdlh +++ b/dom/workers/remoteworkers/RemoteWorkerTypes.ipdlh @@ -76,7 +76,7 @@ struct RemoteWorkerData StorageAccess storageAccess; - bool isThirdPartyContextToTopWindow; + bool isThirdPartyContext; bool shouldResistFingerprinting; diff --git a/dom/workers/test/call_throws.js b/dom/workers/test/call_throws.js new file mode 100644 index 0000000000..dfde155961 --- /dev/null +++ b/dom/workers/test/call_throws.js @@ -0,0 +1,4 @@ +function workerMethod() { + console.log("workerMethod about to throw..."); + throw new Error("Method-Throw-Payload"); +} diff --git a/dom/workers/test/chrome.toml b/dom/workers/test/chrome.toml index 0b2d68da39..239394d2ea 100644 --- a/dom/workers/test/chrome.toml +++ b/dom/workers/test/chrome.toml @@ -89,10 +89,6 @@ support-files = [ ["test_chromeWorkerJSM.xhtml"] ["test_file.xhtml"] -skip-if = [ - "os == 'linux' && bits == 64 && debug", # Bug 1765445 - "apple_catalina && !debug", # Bug 1765445 -] ["test_fileBlobPosting.xhtml"] diff --git a/dom/workers/test/crashtests/1858809.html b/dom/workers/test/crashtests/1858809.html new file mode 100644 index 0000000000..3a5190c300 --- /dev/null +++ b/dom/workers/test/crashtests/1858809.html @@ -0,0 +1,14 @@ +<!DOCTYPE> +<html> +<head> +<meta charset="UTF-8"> +<script> +document.addEventListener("DOMContentLoaded", () => { + const a = new Worker("", {}) + const b = new Blob([""], {}) + a.terminate() + new RTCRtpScriptTransform(a, b, [{}]) +}) +</script> +</head> +</html> diff --git a/dom/workers/test/crashtests/crashtests.list b/dom/workers/test/crashtests/crashtests.list index 528f4c8a10..26a1fbbf80 100644 --- a/dom/workers/test/crashtests/crashtests.list +++ b/dom/workers/test/crashtests/crashtests.list @@ -6,3 +6,4 @@ load 1228456.html load 1348882.html load 1821066.html load 1819146.html +load 1858809.html diff --git a/dom/workers/test/importScripts_3rdParty_worker.js b/dom/workers/test/importScripts_3rdParty_worker.js index 326d48f77a..e55fdc514b 100644 --- a/dom/workers/test/importScripts_3rdParty_worker.js +++ b/dom/workers/test/importScripts_3rdParty_worker.js @@ -1,18 +1,113 @@ const workerURL = "http://mochi.test:8888/tests/dom/workers/test/importScripts_3rdParty_worker.js"; +/** + * An Error can be a JS Error or a DOMException. The primary difference is that + * JS Errors have a SpiderMonkey specific `fileName` for the filename and + * DOMEXCEPTION uses `filename`. + */ +function normalizeError(err) { + if (!err) { + return null; + } + + const isDOMException = "filename" in err; + + return { + message: err.message, + name: err.name, + isDOMException, + code: err.code, + // normalize to fileName + fileName: isDOMException ? err.filename : err.fileName, + hasFileName: !!err.fileName, + hasFilename: !!err.filename, + lineNumber: err.lineNumber, + columnNumber: err.columnNumber, + stack: err.stack, + stringified: err.toString(), + }; +} + +function normalizeErrorEvent(event) { + if (!event) { + return null; + } + + return { + message: event.message, + filename: event.filename, + lineno: event.lineno, + colno: event.colno, + error: normalizeError(event.error), + stringified: event.toString(), + }; +} + +/** + * Normalize the `OnErrorEventHandlerNonNull onerror` invocation. The + * special handling in JSEventHandler::HandleEvent ends up spreading out the + * contents of the ErrorEvent into discrete arguments. The one thing lost is + * we can't toString the ScriptEvent itself, but this should be the same as the + * message anyways. + * + * The spec for the invocation is the "special error event handling" logic + * described in step 4 at: + * https://html.spec.whatwg.org/multipage/webappapis.html#the-event-handler-processing-algorithm + * noting that the step somewhat glosses over that it's only "onerror" that is + * OnErrorEventHandlerNonNull and capable of processing 5 arguments and that's + * why an addEventListener "error" listener doesn't get this handling. + * + * Argument names here are made to match the call-site in JSEventHandler. + */ +function normalizeOnError( + msgOrEvent, + fileName, + lineNumber, + columnNumber, + error +) { + return { + message: msgOrEvent, + filename: fileName, + lineno: lineNumber, + colno: columnNumber, + error: normalizeError(error), + stringified: null, + }; +} + +/** + * Helper to postMessage the provided data after a setTimeout(0) so that any + * error event currently being dispatched that will bubble to our parent will + * be delivered before our postMessage. + */ +function delayedPostMessage(data) { + setTimeout(() => { + postMessage(data); + }, 0); +} + onmessage = function (a) { + const args = a.data; + // Messages are either nested (forward to a nested worker) or should be + // processed locally. if (a.data.nested) { - var worker = new Worker(workerURL); + const worker = new Worker(workerURL); + let firstErrorEvent; + + // When the test mode is "catch" + worker.onmessage = function (event) { - postMessage(event.data); + delayedPostMessage({ + nestedMessage: event.data, + errorEvent: firstErrorEvent, + }); }; worker.onerror = function (event) { + firstErrorEvent = normalizeErrorEvent(event); event.preventDefault(); - postMessage({ - error: event instanceof ErrorEvent && event.filename == workerURL, - }); }; a.data.nested = false; @@ -20,69 +115,47 @@ onmessage = function (a) { return; } - // This first URL will use the same origin of this script. - var sameOriginURL = new URL(a.data.url); - var fileName1 = 42; - - // This is cross-origin URL. - var crossOriginURL = new URL(a.data.url); - crossOriginURL.host = "example.com"; - crossOriginURL.port = 80; - var fileName2 = 42; - - if (a.data.test == "none") { - importScripts(crossOriginURL.href); - return; - } - - try { - importScripts(sameOriginURL.href); - } catch (e) { - if (!(e instanceof SyntaxError)) { - postMessage({ result: false }); - return; - } - - fileName1 = e.fileName; - } - - if (fileName1 != sameOriginURL.href || !fileName1) { - postMessage({ result: false }); - return; - } - - if (a.data.test == "try") { - var exception; + // Local test. + if (a.data.mode === "catch") { try { - importScripts(crossOriginURL.href); - } catch (e) { - fileName2 = e.filename; - exception = e; + importScripts(a.data.url); + workerMethod(); + } catch (ex) { + delayedPostMessage({ + args, + error: normalizeError(ex), + }); } - - postMessage({ - result: - fileName2 == workerURL && - exception.name == "NetworkError" && - exception.code == DOMException.NETWORK_ERR, + } else if (a.data.mode === "uncaught") { + const onerrorPromise = new Promise(resolve => { + self.onerror = (...onerrorArgs) => { + resolve(normalizeOnError(...onerrorArgs)); + }; }); - return; - } - - if (a.data.test == "eventListener") { - addEventListener("error", function (event) { - event.preventDefault(); - postMessage({ - result: event instanceof ErrorEvent && event.filename == workerURL, + const listenerPromise = new Promise(resolve => { + self.addEventListener("error", evt => { + resolve(normalizeErrorEvent(evt)); }); }); - } - if (a.data.test == "onerror") { - onerror = function (...args) { - postMessage({ result: args[1] == workerURL }); - }; - } + Promise.all([onerrorPromise, listenerPromise]).then( + ([onerrorEvent, listenerEvent]) => { + delayedPostMessage({ + args, + onerrorEvent, + listenerEvent, + }); + } + ); - importScripts(crossOriginURL.href); + importScripts(a.data.url); + workerMethod(); + // we will have thrown by this point, which will trigger an "error" event + // on our global and then will propagate to our parent (which could be a + // window or a worker, if nested). + // + // To avoid hangs, throw a different error here that will fail equivalence + // tests. + throw new Error("We expected an error and this is a failsafe for hangs."); + } }; diff --git a/dom/workers/test/mime_type_is_csv.js b/dom/workers/test/mime_type_is_csv.js new file mode 100644 index 0000000000..54d3b70689 --- /dev/null +++ b/dom/workers/test/mime_type_is_csv.js @@ -0,0 +1 @@ +throw new Error("This file has a CSV mime type and should not load."); diff --git a/dom/workers/test/mime_type_is_csv.js^headers^ b/dom/workers/test/mime_type_is_csv.js^headers^ new file mode 100644 index 0000000000..0d848b02c2 --- /dev/null +++ b/dom/workers/test/mime_type_is_csv.js^headers^ @@ -0,0 +1 @@ +Content-Type: text/csv diff --git a/dom/workers/test/mochitest.toml b/dom/workers/test/mochitest.toml index 5ae8094b58..a32dbf3bf6 100644 --- a/dom/workers/test/mochitest.toml +++ b/dom/workers/test/mochitest.toml @@ -12,6 +12,7 @@ support-files = [ "bug998474_worker.js", "bug1063538_worker.js", "bug1063538.sjs", + "call_throws.js", "clearTimeouts_worker.js", "clearTimeoutsImplicit_worker.js", "content_worker.js", @@ -42,6 +43,8 @@ support-files = [ "loadEncoding_worker.js", "location_worker.js", "longThread_worker.js", + "mime_type_is_csv.js", + "mime_type_is_csv.js^headers^", "multi_sharedWorker_frame.html", "multi_sharedWorker_sharedWorker.js", "navigator_languages_worker.js", @@ -58,6 +61,7 @@ support-files = [ "recursion_worker.js", "recursiveOnerror_worker.js", "redirect_to_foreign.sjs", + "redirect_with_query_args.sjs", "rvals_worker.js", "sharedWorker_sharedWorker.js", "simpleThread_worker.js", @@ -66,6 +70,7 @@ support-files = [ "terminate_worker.js", "test_csp.html^headers^", "test_csp.js", + "toplevel_throws.js", "referrer_worker.html", "sourcemap_header_iframe.html", "sourcemap_header_worker.js", diff --git a/dom/workers/test/redirect_with_query_args.sjs b/dom/workers/test/redirect_with_query_args.sjs new file mode 100644 index 0000000000..3359367ee0 --- /dev/null +++ b/dom/workers/test/redirect_with_query_args.sjs @@ -0,0 +1,22 @@ +/** + * This file expects a query string that's the upper-cased version of a file to + * be redirected to in the same directory. The redirect will also include + * added "secret data" as a query string. + * + * So if the request is `/path/redirect_with_query_args.sjs?FOO.JS` the redirect + * will be to `/path/foo.js?SECRET_DATA`. + **/ + +function handleRequest(request, response) { + // The secret data to include in the redirect to make the redirect URL + // easily detectable. + const secretData = "SECRET_DATA"; + + let pathBase = request.path.split("/").slice(0, -1).join("/"); + let targetFile = request.queryString.toLowerCase(); + let newUrl = `${pathBase}/${targetFile}?${secretData}`; + + response.setStatusLine(request.httpVersion, 302, "Found"); + response.setHeader("Cache-Control", "no-cache"); + response.setHeader("Location", newUrl, false); +} diff --git a/dom/workers/test/test_importScripts_3rdparty.html b/dom/workers/test/test_importScripts_3rdparty.html index 7f10f23faf..b1e4913eea 100644 --- a/dom/workers/test/test_importScripts_3rdparty.html +++ b/dom/workers/test/test_importScripts_3rdparty.html @@ -14,123 +14,576 @@ const workerURL = 'http://mochi.test:8888/tests/dom/workers/test/importScripts_3rdParty_worker.js'; -const sameOriginURL = 'http://mochi.test:8888/tests/dom/workers/test/invalid.js' +const sameOriginBaseURL = 'http://mochi.test:8888/tests/dom/workers/test'; +const crossOriginBaseURL = "https://example.com/tests/dom/workers/test"; -var tests = [ - function() { - var worker = new Worker("importScripts_3rdParty_worker.js"); - worker.onmessage = function(event) { - ok("result" in event.data && event.data.result, "It seems we don't share data!"); - next(); - }; +const workerRelativeUrl = 'importScripts_3rdParty_worker.js'; +const workerAbsoluteUrl = `${sameOriginBaseURL}/${workerRelativeUrl}` - worker.postMessage({ url: sameOriginURL, test: 'try', nested: false }); - }, +/** + * This file tests cross-origin error muting in importScripts for workers. In + * particular, we want to test: + * - The errors thrown by the parsing phase of importScripts(). + * - The errors thrown by the top-level evaluation phase of importScripts(). + * - If the error is reported to the parent's Worker binding, including through + * nested workers, as well as the contents of the error. + * - For errors: + * - What type of exception is reported? + * - What fileName is reported on the exception? + * - What are the contents of the stack on the exception? + * + * Relevant specs: + * - https://html.spec.whatwg.org/multipage/webappapis.html#fetch-a-classic-worker-imported-script + * - https://html.spec.whatwg.org/multipage/webappapis.html#creating-a-classic-script + * + * The situation and motivation for error muting is: + * - JS scripts are allowed to be loaded cross-origin without CORS for legacy + * reasons. If a script is cross-origin, its "muted errors" is set to true. + * - The fetch will set the "use-URL-credentials" flag + * https://fetch.spec.whatwg.org/#concept-request-use-url-credentials-flag + * but will have the default "credentials" mode of "omit" + * https://fetch.spec.whatwg.org/#concept-request-credentials-mode which + * means that username/password will be propagated. + * - For legacy reasons, JS scripts aren't required to have an explicit JS MIME + * type which allows attacks that attempt to load a known-non JS file as JS + * in order to derive information from the errors or from side-effects to the + * global for code that does parse and evaluate as legal JS. + **/ - function() { - var worker = new Worker("importScripts_3rdParty_worker.js"); - worker.onmessage = function(event) { - ok("result" in event.data && event.data.result, "It seems we don't share data in nested workers!"); - next(); - }; - worker.postMessage({ url: sameOriginURL, test: 'try', nested: true }); + /** + * - `sameOrigin`: Describes the exception we expect to see for a same-origin + * import. + * - `crossOrigin`: Describes the exception we expect to see for a cross-origin + * import (from example.com while the worker is the mochitest origin). + * + * The exception fields are: + * - `exceptionName`: The `name` of the Error object. + * - `thrownFile`: Describes the filename we expect to see on the error: + * - `importing-worker-script`: The worker script that's doing the importing + * will be the source of the exception, not the imported script. + * - `imported-script-no-redirect`: The (absolute-ified) script as passed to + * importScript(s), regardless of any redirects that occur. + * - `post-redirect-imported-script`: The name of the actual URL that was + * loaded following any redirects. + */ +const scriptPermutations = [ + { + name: 'Invalid script that generates a syntax error', + script: 'invalid.js', + sameOrigin: { + exceptionName: 'SyntaxError', + thrownFile: 'post-redirect-imported-script', + isDOMException: false, + message: "expected expression, got end of script" + }, + crossOrigin: { + exceptionName: 'NetworkError', + thrownFile: 'importing-worker-script', + isDOMException: true, + code: DOMException.NETWORK_ERR, + message: "A network error occurred." + } }, + { + name: 'Non-JS MIME Type', + script: 'mime_type_is_csv.js', + sameOrigin: { + exceptionName: 'NetworkError', + thrownFile: 'importing-worker-script', + isDOMException: true, + code: DOMException.NETWORK_ERR, + message: "A network error occurred." + }, + crossOrigin: { + exceptionName: 'NetworkError', + thrownFile: 'importing-worker-script', + isDOMException: true, + code: DOMException.NETWORK_ERR, + message: "A network error occurred." + } + }, + { + // What happens if the script is a 404? + name: 'Nonexistent script', + script: 'script_does_not_exist.js', + sameOrigin: { + exceptionName: 'NetworkError', + thrownFile: 'importing-worker-script', + isDOMException: true, + code: DOMException.NETWORK_ERR, + message: "A network error occurred." + }, + crossOrigin: { + exceptionName: 'NetworkError', + thrownFile: 'importing-worker-script', + isDOMException: true, + code: DOMException.NETWORK_ERR, + message: "A network error occurred." + } + }, + { + name: 'Script that throws during toplevel execution', + script: 'toplevel_throws.js', + sameOrigin: { + exceptionName: 'Error', + thrownFile: 'post-redirect-imported-script', + isDOMException: false, + message: "Toplevel-Throw-Payload", + }, + crossOrigin: { + exceptionName: 'NetworkError', + thrownFile: 'importing-worker-script', + isDOMException: true, + code: DOMException.NETWORK_ERR, + message: "A network error occurred." + } + }, + { + name: 'Script that exposes a method that throws', + script: 'call_throws.js', + sameOrigin: { + exceptionName: 'Error', + thrownFile: 'post-redirect-imported-script', + isDOMException: false, + message: "Method-Throw-Payload" + }, + crossOrigin: { + exceptionName: 'Error', + thrownFile: 'imported-script-no-redirect', + isDOMException: false, + message: "Method-Throw-Payload" + } + }, +]; - function() { - var worker = new Worker("importScripts_3rdParty_worker.js"); - worker.onmessage = function(event) { - ok("result" in event.data && event.data.result, "It seems we don't share data via eventListener!"); - next(); - }; - - worker.postMessage({ url: sameOriginURL, test: 'eventListener', nested: false }); +/** + * Special fields: + * - `transformScriptImport`: A function that takes the script name as input and + * produces the actual path to use for import purposes, allowing the addition + * of a redirect. + * - `expectedURLAfterRedirect`: A function that takes the script name as + * input and produces the expected script name post-redirect (if there is a + * redirect). In particular, our `redirect_with_query_args.sjs` helper will + * perform a same-origin redirect and append "?SECRET_DATA" onto the end of + * the redirected URL at this time. + * - `partOfTheURLToNotExposeToJS`: A string snippet that is present in the + * post-redirect contents that should absolutely not show up in the error's + * stack if the redirect isn't exposed. This is a secondary check to the + * result of expectedURLAfterRedirect. + */ +const urlPermutations = [ + { + name: 'No Redirect', + transformScriptImport: x => x, + expectedURLAfterRedirect: x => x, + // No redirect means nothing to be paranoid about. + partOfTheURLToNotExposeToJS: null, }, + { + name: 'Same-Origin Redirect With Query Args', + // We mangle the script into uppercase and the redirector undoes this in + // order to minimize the similarity of the pre-redirect and post-redirect + // strings. + transformScriptImport: x => `redirect_with_query_args.sjs?${x.toUpperCase()}`, + expectedURLAfterRedirect: x => `${x}?SECRET_DATA`, + // The redirect will add this when it formulates the redirected URL, and the + // test wants to make sure this doesn't show up in filenames or stacks + // unless the thrownFile is set to 'post-redirect-imported-script'. + partOfTheURLToNotExposeToJS: 'SECRET_DATA', + } +]; +const nestedPermutations = [ + { + name: 'Window Parent', + nested: false, + }, + { + name: 'Worker Parent', + nested: true, + } +]; - function() { - var worker = new Worker("importScripts_3rdParty_worker.js"); - worker.onmessage = function(event) { - ok("result" in event.data && event.data.result, "It seems we don't share data in nested workers via eventListener!"); - next(); - }; + // NOTE: These implementations are copied from importScripts_3rdParty_worker.js + // for reasons of minimizing the number of calls to importScripts for + // debugging. + function normalizeError(err) { + if (!err) { + return null; + } - worker.postMessage({ url: sameOriginURL, test: 'eventListener', nested: true }); - }, + const isDOMException = "filename" in err; - function() { - var worker = new Worker("importScripts_3rdParty_worker.js"); - worker.onmessage = function(event) { - ok("result" in event.data && event.data.result, "It seems we don't share data via onerror!"); - next(); - }; - worker.onerror = function(event) { - event.preventDefault(); + return { + message: err.message, + name: err.name, + isDOMException, + code: err.code, + // normalize to fileName + fileName: isDOMException ? err.filename : err.fileName, + hasFileName: !!err.fileName, + hasFilename: !!err.filename, + lineNumber: err.lineNumber, + columnNumber: err.columnNumber, + stack: err.stack, + stringified: err.toString(), + }; +} + +function normalizeErrorEvent(event) { + if (!event) { + return null; + } + + return { + message: event.message, + filename: event.filename, + lineno: event.lineno, + colno: event.colno, + error: normalizeError(event.error), + stringified: event.toString(), + }; +} +// End duplicated code. + + +/** + * Validate the received error against our expectations and provided context. + * + * For `expectation`, see the `scriptPermutations` doc-block which documents + * its `sameOrigin` and `crossOrigin` properties which are what we expect here. + * + * The `context` should include: + * - `workerUrl`: The absolute URL of the toplevel worker script that the worker + * is running which is the code that calls `importScripts`. + * - `importUrl`: The absolute URL provided to the call to `importScripts`. + * This is the pre-redirect URL if a redirect is involved. + * - `postRedirectUrl`: The same as `importUrl` unless a redirect is involved, + * in which case this will be a different URL. + * - `isRedirected`: Boolean indicating whether a redirect was involved. This + * is a convenience variable that's derived from the above 2 URL's for now. + * - `shouldNotInclude`: Provided by the URL permutation, this is used to check + * that post-redirect data does not creep into the exception unless the + * expected `thrownFile` is `post-redirect-imported-script`. + */ +function checkError(label, expectation, context, err) { + info(`## Checking error: ${JSON.stringify(err)}`); + is(err.name, expectation.exceptionName, + `${label}: Error name matches "${expectation.exceptionName}"?`); + is(err.isDOMException, expectation.isDOMException, + `${label}: Is a DOM Exception == ${expectation.isDOMException}?`); + if (expectation.code) { + is(err.code, expectation.code, + `${label}: Code matches ${expectation.code}?`); + } + + let expectedFile; + switch (expectation.thrownFile) { + case 'importing-worker-script': + expectedFile = context.workerUrl; + break; + case 'imported-script-no-redirect': + expectedFile = context.importUrl; + break; + case 'post-redirect-imported-script': + expectedFile = context.postRedirectUrl; + break; + default: + ok(false, `Unexpected thrownFile parameter: ${expectation.thrownFile}`); + return; + } + + is(err.fileName, expectedFile, + `${label}: Filename from ${expectation.thrownFile} is ${expectedFile}`); + + + let expMessage = expectation.message; + if (typeof(expMessage) === "function") { + expMessage = expectation.message(context); + } + is(err.message, expMessage, + `${label}: Message is ${expMessage}`); + + // If this is a redirect and we expect the error to not be surfacing any + // post-redirect information and there's a `shouldNotInclude` string, then + // check to make sure it's not present. + if (context.isRedirected && context.shouldNotInclude) { + if (expectation.thrownFile !== 'post-redirect-imported-script') { + ok(!err.stack.includes(context.shouldNotInclude), + `${label}: Stack should not include ${context.shouldNotInclude}:\n${err.stack}`); + ok(!err.stringified.includes(context.shouldNotInclude), + `${label}: Stringified error should not include ${context.shouldNotInclude}:\n${err.stringified}`); + } else if (expectation.exceptionName !== 'SyntaxError') { + // We do expect the shouldNotInclude to be present for + // 'post-redirect-imported-script' as long as the exception isn't a + // SyntaxError. SyntaxError stacks inherently do not include the filename + // of the file with the syntax problem as a stack frame. + ok(err.stack.includes(context.shouldNotInclude), + `${label}: Stack should include ${context.shouldNotInclude}:\n${err.stack}`); } + } + let expStringified = `${err.name}: ${expMessage}`; + is(err.stringified, expStringified, + `${label}: Stringified error should be: ${expStringified}`); - worker.postMessage({ url: sameOriginURL, test: 'onerror', nested: false }); - }, + // Add some whitespace in our output. + info(""); +} - function() { - var worker = new Worker("importScripts_3rdParty_worker.js"); - worker.onerror = function(event) { - event.preventDefault(); - ok(event instanceof ErrorEvent, "ErrorEvent received."); - is(event.filename, workerURL, "ErrorEvent.filename is correct"); - next(); - }; +function checkErrorEvent(label, expectation, context, event, viaTask=false) { + info(`## Checking error event: ${JSON.stringify(event)}`); - worker.postMessage({ url: sameOriginURL, test: 'none', nested: false }); - }, + let expectedFile; + switch (expectation.thrownFile) { + case 'importing-worker-script': + expectedFile = context.workerUrl; + break; + case 'imported-script-no-redirect': + expectedFile = context.importUrl; + break; + case 'post-redirect-imported-script': + expectedFile = context.postRedirectUrl; + break; + default: + ok(false, `Unexpected thrownFile parameter: ${expectation.thrownFile}`); + return; + } - function() { - var worker = new Worker("importScripts_3rdParty_worker.js"); - worker.addEventListener("error", function(event) { - event.preventDefault(); - ok(event instanceof ErrorEvent, "ErrorEvent received."); - is(event.filename, workerURL, "ErrorEvent.filename is correct"); - next(); - }); + is(event.filename, expectedFile, + `${label}: Filename from ${expectation.thrownFile} is ${expectedFile}`); - worker.postMessage({ url: sameOriginURL, test: 'none', nested: false }); - }, + let expMessage = expectation.message; + if (typeof(expMessage) === "function") { + expMessage = expectation.message(context); + } + // The error event message prepends the exception name to the Error's message. + expMessage = `${expectation.exceptionName}: ${expMessage}`; - function() { - var worker = new Worker("importScripts_3rdParty_worker.js"); - worker.onerror = function(event) { - ok(false, "No error should be received!"); - }; + is(event.message, expMessage, + `${label}: Message is ${expMessage}`); + + // If this is a redirect and we expect the error to not be surfacing any + // post-redirect information and there's a `shouldNotInclude` string, then + // check to make sure it's not present. + // + // Note that `stringified` may not be present for the "onerror" case. + if (context.isRedirected && + expectation.thrownFile !== 'post-redirect-imported-script' && + context.shouldNotInclude && + event.stringified) { + ok(!event.stringified.includes(context.shouldNotInclude), + `${label}: Stringified error should not include ${context.shouldNotInclude}:\n${event.stringified}`); + } + if (event.stringified) { + is(event.stringified, "[object ErrorEvent]", + `${label}: Stringified event should be "[object ErrorEvent]"`); + } + + // If we received the error via a task queued because it was not handled in + // the worker, then per + // https://html.spec.whatwg.org/multipage/workers.html#runtime-script-errors-2 + // the error will be null. + if (viaTask) { + is(event.error, null, + `${label}: Error is null because it came from an HTML 10.2.5 task.`); + } else { + checkError(label, expectation, context, event.error); + } +} + +/** + * Helper to spawn a worker, postMessage it the given args, and return the + * worker's response payload and the first "error" received on the Worker + * binding by the time the message handler resolves. The worker logic makes + * sure to delay its postMessage using setTimeout(0) so error events will always + * arrive before any message that is sent. + * + * If args includes a truthy `nested` value, then the `message` and + * `bindingErrorEvent` are as perceived by the parent worker. + */ +function asyncWorkerImport(args) { + const worker = new Worker(workerRelativeUrl); + const promise = new Promise((resolve, reject) => { + // The first "error" received on the Worker binding. + let firstErrorEvent = null; worker.onmessage = function(event) { - ok("error" in event.data && event.data.error, "The error has been fully received from a nested worker"); - next(); - }; - worker.postMessage({ url: sameOriginURL, test: 'none', nested: true }); - }, + let message = event.data; + // For the nested case, unwrap and normalize things. + if (args.nested) { + firstErrorEvent = message.errorEvent; + message = message.nestedMessage; + // We need to re-set the argument to be nested because it was set to + // false so that only a single level of nesting occurred. + message.args.nested = true; + } + + // Make sure the args we receive from the worker are the same as the ones + // we sent. + is(JSON.stringify(message.args), JSON.stringify(args), + "Worker re-transmitted args match sent args."); - function() { - var url = URL.createObjectURL(new Blob(["%&%^&%^"])); - var worker = new Worker(url); + resolve({ + message, + bindingErrorEvent: firstErrorEvent + }); + worker.terminate(); + }; worker.onerror = function(event) { + // We don't want this to bubble to the window and cause a test failure. event.preventDefault(); - ok(event instanceof Event, "Event received."); - next(); - }; - } -]; -function next() { - if (!tests.length) { - SimpleTest.finish(); - return; - } + if (firstErrorEvent) { + ok(false, "Worker binding received more than one error"); + reject(new Error("multiple error events received")); + return; + } + firstErrorEvent = normalizeErrorEvent(event); + } + }); + info("Sending args to worker: " + JSON.stringify(args)); + worker.postMessage(args); - var test = tests.shift(); - test(); + return promise; } -SimpleTest.waitForExplicitFinish(); -next(); +function makeTestPermutations() { + for (const urlPerm of urlPermutations) { + for (const scriptPerm of scriptPermutations) { + for (const nestedPerm of nestedPermutations) { + const testName = + `${nestedPerm.name}: ${urlPerm.name}: ${scriptPerm.name}`; + const caseFunc = async () => { + // Make the test name much more obvious when viewing logs. + info(`#############################################################`); + info(`### ${testName}`); + let result, errorEvent; + + const scriptName = urlPerm.transformScriptImport(scriptPerm.script); + const redirectedUrl = urlPerm.expectedURLAfterRedirect(scriptPerm.script); + + // ### Same-Origin Import + // ## What does the error look like when caught? + ({ message, bindingErrorEvent } = await asyncWorkerImport( + { + url: `${sameOriginBaseURL}/${scriptName}`, + mode: "catch", + nested: nestedPerm.nested, + })); + + const sameOriginContext = { + workerUrl: workerAbsoluteUrl, + importUrl: message.args.url, + postRedirectUrl: `${sameOriginBaseURL}/${redirectedUrl}`, + isRedirected: message.args.url !== redirectedUrl, + shouldNotInclude: urlPerm.partOfTheURLToNotExposeToJS, + }; + checkError( + `${testName}: Same-Origin Thrown`, + scriptPerm.sameOrigin, + sameOriginContext, + message.error); + + // ## What does the error events look like when not caught? + ({ message, bindingErrorEvent } = await asyncWorkerImport( + { + url: `${sameOriginBaseURL}/${scriptName}`, + mode: "uncaught", + nested: nestedPerm.nested, + })); + + // The worker will have captured the error event twice, once via + // onerror and once via an "error" event listener. It will have not + // invoked preventDefault(), so the worker's parent will also have + // received a copy of the error event as well. + checkErrorEvent( + `${testName}: Same-Origin Worker global onerror handler`, + scriptPerm.sameOrigin, + sameOriginContext, + message.onerrorEvent); + checkErrorEvent( + `${testName}: Same-Origin Worker global error listener`, + scriptPerm.sameOrigin, + sameOriginContext, + message.listenerEvent); + // Binding events + checkErrorEvent( + `${testName}: Same-Origin Parent binding onerror`, + scriptPerm.sameOrigin, + sameOriginContext, + bindingErrorEvent, "via-task"); + + // ### Cross-Origin Import + // ## What does the error look like when caught? + ({ message, bindingErrorEvent } = await asyncWorkerImport( + { + url: `${crossOriginBaseURL}/${scriptName}`, + mode: "catch", + nested: nestedPerm.nested, + })); + + const crossOriginContext = { + workerUrl: workerAbsoluteUrl, + importUrl: message.args.url, + postRedirectUrl: `${crossOriginBaseURL}/${redirectedUrl}`, + isRedirected: message.args.url !== redirectedUrl, + shouldNotInclude: urlPerm.partOfTheURLToNotExposeToJS, + }; + + checkError( + `${testName}: Cross-Origin Thrown`, + scriptPerm.crossOrigin, + crossOriginContext, + message.error); + + // ## What does the error events look like when not caught? + ({ message, bindingErrorEvent } = await asyncWorkerImport( + { + url: `${crossOriginBaseURL}/${scriptName}`, + mode: "uncaught", + nested: nestedPerm.nested, + })); + + // The worker will have captured the error event twice, once via + // onerror and once via an "error" event listener. It will have not + // invoked preventDefault(), so the worker's parent will also have + // received a copy of the error event as well. + checkErrorEvent( + `${testName}: Cross-Origin Worker global onerror handler`, + scriptPerm.crossOrigin, + crossOriginContext, + message.onerrorEvent); + checkErrorEvent( + `${testName}: Cross-Origin Worker global error listener`, + scriptPerm.crossOrigin, + crossOriginContext, + message.listenerEvent); + // Binding events + checkErrorEvent( + `${testName}: Cross-Origin Parent binding onerror`, + scriptPerm.crossOrigin, + crossOriginContext, + bindingErrorEvent, "via-task"); + }; + + // The mochitest framework uses the name of the caseFunc, which by default + // will be inferred and set on the configurable `name` property. It's not + // writable though, so we need to clobber the property. Devtools will + // xray through this name but this works for the test framework. + Object.defineProperty( + caseFunc, + 'name', + { + value: testName, + writable: false + }); + add_task(caseFunc); + } + } + } +} +makeTestPermutations(); </script> </body> </html> diff --git a/dom/workers/test/test_worker_interfaces.js b/dom/workers/test/test_worker_interfaces.js index efd108f85c..c53c0b2b0f 100644 --- a/dom/workers/test/test_worker_interfaces.js +++ b/dom/workers/test/test_worker_interfaces.js @@ -138,6 +138,8 @@ let interfaceNamesInGlobalScope = [ // IMPORTANT: Do not change this list without review from a DOM peer! { name: "AudioDecoder", nightly: true }, // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "AudioEncoder", nightly: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! { name: "Blob", insecureContext: true }, // IMPORTANT: Do not change this list without review from a DOM peer! { name: "BroadcastChannel", insecureContext: true }, diff --git a/dom/workers/test/toplevel_throws.js b/dom/workers/test/toplevel_throws.js new file mode 100644 index 0000000000..3efe29d5af --- /dev/null +++ b/dom/workers/test/toplevel_throws.js @@ -0,0 +1 @@ +throw new Error("Toplevel-Throw-Payload"); |