summaryrefslogtreecommitdiffstats
path: root/dom/workers
diff options
context:
space:
mode:
Diffstat (limited to 'dom/workers')
-rw-r--r--dom/workers/RuntimeService.cpp6
-rw-r--r--dom/workers/ScriptLoader.cpp33
-rw-r--r--dom/workers/Worker.cpp6
-rw-r--r--dom/workers/Worker.h27
-rw-r--r--dom/workers/WorkerLoadInfo.cpp2
-rw-r--r--dom/workers/WorkerLoadInfo.h2
-rw-r--r--dom/workers/WorkerPrivate.cpp106
-rw-r--r--dom/workers/WorkerPrivate.h4
-rw-r--r--dom/workers/WorkerRunnable.cpp16
-rw-r--r--dom/workers/WorkerScope.cpp2
-rw-r--r--dom/workers/WorkerScope.h4
-rw-r--r--dom/workers/WorkerThread.cpp18
-rw-r--r--dom/workers/loader/CacheLoadHandler.cpp11
-rw-r--r--dom/workers/loader/NetworkLoadHandler.cpp4
-rw-r--r--dom/workers/loader/WorkerLoadContext.h2
-rw-r--r--dom/workers/loader/WorkerModuleLoader.cpp9
-rw-r--r--dom/workers/loader/WorkerModuleLoader.h2
-rw-r--r--dom/workers/nsIWorkerDebugger.idl6
-rw-r--r--dom/workers/remoteworkers/RemoteWorkerChild.cpp2
-rw-r--r--dom/workers/remoteworkers/RemoteWorkerTypes.ipdlh2
-rw-r--r--dom/workers/test/call_throws.js4
-rw-r--r--dom/workers/test/chrome.toml4
-rw-r--r--dom/workers/test/crashtests/1858809.html14
-rw-r--r--dom/workers/test/crashtests/crashtests.list1
-rw-r--r--dom/workers/test/importScripts_3rdParty_worker.js199
-rw-r--r--dom/workers/test/mime_type_is_csv.js1
-rw-r--r--dom/workers/test/mime_type_is_csv.js^headers^1
-rw-r--r--dom/workers/test/mochitest.toml5
-rw-r--r--dom/workers/test/redirect_with_query_args.sjs22
-rw-r--r--dom/workers/test/test_importScripts_3rdparty.html633
-rw-r--r--dom/workers/test/test_worker_interfaces.js2
-rw-r--r--dom/workers/test/toplevel_throws.js1
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");