/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "WorkerError.h" #include #include #include #include "MainThreadUtils.h" #include "WorkerPrivate.h" #include "WorkerRunnable.h" #include "WorkerScope.h" #include "js/ComparisonOperators.h" #include "js/UniquePtr.h" #include "js/friend/ErrorMessages.h" #include "jsapi.h" #include "mozilla/ArrayAlgorithm.h" #include "mozilla/ArrayIterator.h" #include "mozilla/Assertions.h" #include "mozilla/BasicEvents.h" #include "mozilla/DOMEventTargetHelper.h" #include "mozilla/ErrorResult.h" #include "mozilla/EventDispatcher.h" #include "mozilla/RefPtr.h" #include "mozilla/Span.h" #include "mozilla/ThreadSafeWeakPtr.h" #include "mozilla/Unused.h" #include "mozilla/dom/BindingDeclarations.h" #include "mozilla/dom/BindingUtils.h" #include "mozilla/dom/ErrorEvent.h" #include "mozilla/dom/ErrorEventBinding.h" #include "mozilla/dom/Event.h" #include "mozilla/dom/EventBinding.h" #include "mozilla/dom/EventTarget.h" #include "mozilla/dom/RemoteWorkerChild.h" #include "mozilla/dom/RemoteWorkerTypes.h" #include "mozilla/dom/RootedDictionary.h" #include "mozilla/dom/ServiceWorkerManager.h" #include "mozilla/dom/ServiceWorkerUtils.h" #include "mozilla/dom/SimpleGlobalObject.h" #include "mozilla/dom/Worker.h" #include "mozilla/dom/WorkerCommon.h" #include "mozilla/dom/WorkerDebuggerGlobalScopeBinding.h" #include "mozilla/dom/WorkerGlobalScopeBinding.h" #include "mozilla/fallible.h" #include "nsCOMPtr.h" #include "nsDebug.h" #include "nsGlobalWindowOuter.h" #include "nsIConsoleService.h" #include "nsIScriptError.h" #include "nsScriptError.h" #include "nsServiceManagerUtils.h" #include "nsString.h" #include "nsWrapperCacheInlines.h" #include "nscore.h" #include "xpcpublic.h" namespace mozilla::dom { namespace { class ReportErrorRunnable final : public WorkerDebuggeeRunnable { UniquePtr mReport; public: ReportErrorRunnable(WorkerPrivate* aWorkerPrivate, UniquePtr aReport) : WorkerDebuggeeRunnable(aWorkerPrivate), mReport(std::move(aReport)) {} private: virtual void PostDispatch(WorkerPrivate* aWorkerPrivate, bool aDispatchResult) override { aWorkerPrivate->AssertIsOnWorkerThread(); // Dispatch may fail if the worker was canceled, no need to report that as // an error, so don't call base class PostDispatch. } virtual bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override { uint64_t innerWindowId; bool fireAtScope = true; bool workerIsAcceptingEvents = aWorkerPrivate->IsAcceptingEvents(); WorkerPrivate* parent = aWorkerPrivate->GetParent(); if (parent) { innerWindowId = 0; } else { AssertIsOnMainThread(); // Once a window has frozen its workers, their // mMainThreadDebuggeeEventTargets should be paused, and their // WorkerDebuggeeRunnables should not be being executed. The same goes for // WorkerDebuggeeRunnables sent from child to parent workers, but since a // frozen parent worker runs only control runnables anyway, that is taken // care of naturally. MOZ_ASSERT(!aWorkerPrivate->IsFrozen()); // Similarly for paused windows; all its workers should have been // informed. (Subworkers are unaffected by paused windows.) MOZ_ASSERT(!aWorkerPrivate->IsParentWindowPaused()); if (aWorkerPrivate->IsSharedWorker()) { aWorkerPrivate->GetRemoteWorkerController() ->ErrorPropagationOnMainThread(mReport.get(), /* isErrorEvent */ true); return true; } // Service workers do not have a main thread parent global, so normal // worker error reporting will crash. Instead, pass the error to // the ServiceWorkerManager to report on any controlled documents. if (aWorkerPrivate->IsServiceWorker()) { RefPtr actor( aWorkerPrivate->GetRemoteWorkerControllerWeakRef()); Unused << NS_WARN_IF(!actor); if (actor) { actor->ErrorPropagationOnMainThread(nullptr, false); } return true; } // The innerWindowId is only required if we are going to ReportError // below, which is gated on this condition. The inner window correctness // check is only going to succeed when the worker is accepting events. if (workerIsAcceptingEvents) { aWorkerPrivate->AssertInnerWindowIsCorrect(); innerWindowId = aWorkerPrivate->WindowID(); } } // Don't fire this event if the JS object has been disconnected from the // private object. if (!workerIsAcceptingEvents) { return true; } WorkerErrorReport::ReportError(aCx, parent, fireAtScope, aWorkerPrivate->ParentEventTargetRef(), std::move(mReport), innerWindowId); return true; } }; class ReportGenericErrorRunnable final : public WorkerDebuggeeRunnable { public: static void CreateAndDispatch(WorkerPrivate* aWorkerPrivate) { MOZ_ASSERT(aWorkerPrivate); aWorkerPrivate->AssertIsOnWorkerThread(); RefPtr runnable = new ReportGenericErrorRunnable(aWorkerPrivate); runnable->Dispatch(); } private: explicit ReportGenericErrorRunnable(WorkerPrivate* aWorkerPrivate) : WorkerDebuggeeRunnable(aWorkerPrivate) { aWorkerPrivate->AssertIsOnWorkerThread(); } void PostDispatch(WorkerPrivate* aWorkerPrivate, bool aDispatchResult) override { aWorkerPrivate->AssertIsOnWorkerThread(); // Dispatch may fail if the worker was canceled, no need to report that as // an error, so don't call base class PostDispatch. } bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override { // Once a window has frozen its workers, their // mMainThreadDebuggeeEventTargets should be paused, and their // WorkerDebuggeeRunnables should not be being executed. The same goes for // WorkerDebuggeeRunnables sent from child to parent workers, but since a // frozen parent worker runs only control runnables anyway, that is taken // care of naturally. MOZ_ASSERT(!aWorkerPrivate->IsFrozen()); // Similarly for paused windows; all its workers should have been informed. // (Subworkers are unaffected by paused windows.) MOZ_ASSERT(!aWorkerPrivate->IsParentWindowPaused()); if (aWorkerPrivate->IsSharedWorker()) { aWorkerPrivate->GetRemoteWorkerController()->ErrorPropagationOnMainThread( nullptr, false); return true; } if (aWorkerPrivate->IsServiceWorker()) { RefPtr actor( aWorkerPrivate->GetRemoteWorkerControllerWeakRef()); Unused << NS_WARN_IF(!actor); if (actor) { actor->ErrorPropagationOnMainThread(nullptr, false); } return true; } if (!aWorkerPrivate->IsAcceptingEvents()) { return true; } RefPtr parentEventTarget = aWorkerPrivate->ParentEventTargetRef(); RefPtr event = Event::Constructor(parentEventTarget, u"error"_ns, EventInit()); event->SetTrusted(true); parentEventTarget->DispatchEvent(*event); return true; } }; } // namespace void WorkerErrorBase::AssignErrorBase(JSErrorBase* aReport) { CopyUTF8toUTF16(MakeStringSpan(aReport->filename), mFilename); mLineNumber = aReport->lineno; mColumnNumber = aReport->column; mErrorNumber = aReport->errorNumber; } void WorkerErrorNote::AssignErrorNote(JSErrorNotes::Note* aNote) { WorkerErrorBase::AssignErrorBase(aNote); xpc::ErrorNote::ErrorNoteToMessageString(aNote, mMessage); } WorkerErrorReport::WorkerErrorReport() : mIsWarning(false), mExnType(JSEXN_ERR), mMutedError(false) {} void WorkerErrorReport::AssignErrorReport(JSErrorReport* aReport) { WorkerErrorBase::AssignErrorBase(aReport); xpc::ErrorReport::ErrorReportToMessageString(aReport, mMessage); mLine.Assign(aReport->linebuf(), aReport->linebufLength()); mIsWarning = aReport->isWarning(); MOZ_ASSERT(aReport->exnType >= JSEXN_FIRST && aReport->exnType < JSEXN_LIMIT); mExnType = JSExnType(aReport->exnType); mMutedError = aReport->isMuted; if (aReport->notes) { if (!mNotes.SetLength(aReport->notes->length(), fallible)) { return; } size_t i = 0; for (auto&& note : *aReport->notes) { mNotes.ElementAt(i).AssignErrorNote(note.get()); i++; } } } // aWorkerPrivate is the worker thread we're on (or the main thread, if null) // aTarget is the worker object that we are going to fire an error at // (if any). /* static */ void WorkerErrorReport::ReportError( JSContext* aCx, WorkerPrivate* aWorkerPrivate, bool aFireAtScope, DOMEventTargetHelper* aTarget, UniquePtr aReport, uint64_t aInnerWindowId, JS::Handle aException) { if (aWorkerPrivate) { aWorkerPrivate->AssertIsOnWorkerThread(); } else { AssertIsOnMainThread(); } // We should not fire error events for warnings but instead make sure that // they show up in the error console. if (!aReport->mIsWarning) { // First fire an ErrorEvent at the worker. RootedDictionary init(aCx); if (aReport->mMutedError) { init.mMessage.AssignLiteral("Script error."); } else { init.mMessage = aReport->mMessage; init.mFilename = aReport->mFilename; init.mLineno = aReport->mLineNumber; init.mColno = aReport->mColumnNumber; init.mError = aException; } init.mCancelable = true; init.mBubbles = false; if (aTarget) { RefPtr event = ErrorEvent::Constructor(aTarget, u"error"_ns, init); event->SetTrusted(true); bool defaultActionEnabled = aTarget->DispatchEvent(*event, CallerType::System, IgnoreErrors()); if (!defaultActionEnabled) { return; } } // Now fire an event at the global object, but don't do that if the error // code is too much recursion and this is the same script threw the error. // XXXbz the interaction of this with worker errors seems kinda broken. // An overrecursion in the debugger or debugger sandbox will get turned // into an error event on our parent worker! // https://bugzilla.mozilla.org/show_bug.cgi?id=1271441 tracks making this // better. if (aFireAtScope && (aTarget || aReport->mErrorNumber != JSMSG_OVER_RECURSED)) { JS::Rooted global(aCx, JS::CurrentGlobalOrNull(aCx)); NS_ASSERTION(global, "This should never be null!"); nsEventStatus status = nsEventStatus_eIgnore; if (aWorkerPrivate) { RefPtr globalScope; UNWRAP_OBJECT(WorkerGlobalScope, &global, globalScope); if (!globalScope) { WorkerDebuggerGlobalScope* globalScope = nullptr; UNWRAP_OBJECT(WorkerDebuggerGlobalScope, &global, globalScope); MOZ_ASSERT_IF(globalScope, globalScope->GetWrapperPreserveColor() == global); if (globalScope || IsWorkerDebuggerSandbox(global)) { aWorkerPrivate->ReportErrorToDebugger( aReport->mFilename, aReport->mLineNumber, aReport->mMessage); return; } MOZ_ASSERT(SimpleGlobalObject::SimpleGlobalType(global) == SimpleGlobalObject::GlobalType::BindingDetail); // XXXbz We should really log this to console, but unwinding out of // this stuff without ending up firing any events is ... hard. Just // return for now. // https://bugzilla.mozilla.org/show_bug.cgi?id=1271441 tracks // making this better. return; } MOZ_ASSERT(globalScope->GetWrapperPreserveColor() == global); RefPtr event = ErrorEvent::Constructor(aTarget, u"error"_ns, init); event->SetTrusted(true); // TODO: Bug 1506441 if (NS_FAILED(EventDispatcher::DispatchDOMEvent( MOZ_KnownLive(ToSupports(globalScope)), nullptr, event, nullptr, &status))) { NS_WARNING("Failed to dispatch worker thread error event!"); status = nsEventStatus_eIgnore; } } else if (nsGlobalWindowInner* win = xpc::WindowOrNull(global)) { MOZ_ASSERT(NS_IsMainThread()); if (!win->HandleScriptError(init, &status)) { NS_WARNING("Failed to dispatch main thread error event!"); status = nsEventStatus_eIgnore; } } // Was preventDefault() called? if (status == nsEventStatus_eConsumeNoDefault) { return; } } } // Now fire a runnable to do the same on the parent's thread if we can. if (aWorkerPrivate) { RefPtr runnable = new ReportErrorRunnable(aWorkerPrivate, std::move(aReport)); runnable->Dispatch(); return; } // Otherwise log an error to the error console. WorkerErrorReport::LogErrorToConsole(aCx, *aReport, aInnerWindowId); } /* static */ void WorkerErrorReport::LogErrorToConsole(JSContext* aCx, WorkerErrorReport& aReport, uint64_t aInnerWindowId) { JS::Rooted stack(aCx, aReport.ReadStack(aCx)); JS::Rooted stackGlobal(aCx, JS::CurrentGlobalOrNull(aCx)); ErrorData errorData( aReport.mIsWarning, aReport.mLineNumber, aReport.mColumnNumber, aReport.mMessage, aReport.mFilename, aReport.mLine, TransformIntoNewArray(aReport.mNotes, [](const WorkerErrorNote& note) { return ErrorDataNote(note.mLineNumber, note.mColumnNumber, note.mMessage, note.mFilename); })); LogErrorToConsole(errorData, aInnerWindowId, stack, stackGlobal); } /* static */ void WorkerErrorReport::LogErrorToConsole(const ErrorData& aReport, uint64_t aInnerWindowId, JS::Handle aStack, JS::Handle aStackGlobal) { AssertIsOnMainThread(); RefPtr scriptError = CreateScriptError(nullptr, JS::NothingHandleValue, aStack, aStackGlobal); NS_WARNING_ASSERTION(scriptError, "Failed to create script error!"); if (scriptError) { nsAutoCString category("Web Worker"); uint32_t flags = aReport.isWarning() ? nsIScriptError::warningFlag : nsIScriptError::errorFlag; if (NS_FAILED(scriptError->nsIScriptError::InitWithWindowID( aReport.message(), aReport.filename(), aReport.line(), aReport.lineNumber(), aReport.columnNumber(), flags, category, aInnerWindowId))) { NS_WARNING("Failed to init script error!"); scriptError = nullptr; } for (const ErrorDataNote& note : aReport.notes()) { nsScriptErrorNote* noteObject = new nsScriptErrorNote(); noteObject->Init(note.message(), note.filename(), 0, note.lineNumber(), note.columnNumber()); scriptError->AddNote(noteObject); } } nsCOMPtr consoleService = do_GetService(NS_CONSOLESERVICE_CONTRACTID); NS_WARNING_ASSERTION(consoleService, "Failed to get console service!"); if (consoleService) { if (scriptError) { if (NS_SUCCEEDED(consoleService->LogMessage(scriptError))) { return; } NS_WARNING("LogMessage failed!"); } else if (NS_SUCCEEDED(consoleService->LogStringMessage( aReport.message().BeginReading()))) { return; } NS_WARNING("LogStringMessage failed!"); } NS_ConvertUTF16toUTF8 msg(aReport.message()); NS_ConvertUTF16toUTF8 filename(aReport.filename()); static const char kErrorString[] = "JS error in Web Worker: %s [%s:%u]"; #ifdef ANDROID __android_log_print(ANDROID_LOG_INFO, "Gecko", kErrorString, msg.get(), filename.get(), aReport.lineNumber()); #endif fprintf(stderr, kErrorString, msg.get(), filename.get(), aReport.lineNumber()); fflush(stderr); } /* static */ void WorkerErrorReport::CreateAndDispatchGenericErrorRunnableToParent( WorkerPrivate* aWorkerPrivate) { ReportGenericErrorRunnable::CreateAndDispatch(aWorkerPrivate); } } // namespace mozilla::dom