1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6
7 #include "WorkerError.h"
8
9 #include "mozilla/DOMEventTargetHelper.h"
10 #include "mozilla/dom/ErrorEvent.h"
11 #include "mozilla/dom/ErrorEventBinding.h"
12 #include "mozilla/dom/RemoteWorkerChild.h"
13 #include "mozilla/dom/ServiceWorkerManager.h"
14 #include "mozilla/dom/ServiceWorkerUtils.h"
15 #include "mozilla/dom/SimpleGlobalObject.h"
16 #include "mozilla/dom/WorkerDebuggerGlobalScopeBinding.h"
17 #include "mozilla/dom/WorkerGlobalScopeBinding.h"
18 #include "mozilla/EventDispatcher.h"
19 #include "nsGlobalWindowInner.h"
20 #include "nsIConsoleService.h"
21 #include "nsScriptError.h"
22 #include "WorkerRunnable.h"
23 #include "WorkerPrivate.h"
24 #include "WorkerScope.h"
25
26 namespace mozilla {
27 namespace dom {
28
29 namespace {
30
31 class ReportErrorRunnable final : public WorkerDebuggeeRunnable {
32 UniquePtr<WorkerErrorReport> mReport;
33
34 public:
ReportErrorRunnable(WorkerPrivate * aWorkerPrivate,UniquePtr<WorkerErrorReport> aReport)35 ReportErrorRunnable(WorkerPrivate* aWorkerPrivate,
36 UniquePtr<WorkerErrorReport> aReport)
37 : WorkerDebuggeeRunnable(aWorkerPrivate), mReport(std::move(aReport)) {}
38
39 private:
PostDispatch(WorkerPrivate * aWorkerPrivate,bool aDispatchResult)40 virtual void PostDispatch(WorkerPrivate* aWorkerPrivate,
41 bool aDispatchResult) override {
42 aWorkerPrivate->AssertIsOnWorkerThread();
43
44 // Dispatch may fail if the worker was canceled, no need to report that as
45 // an error, so don't call base class PostDispatch.
46 }
47
WorkerRun(JSContext * aCx,WorkerPrivate * aWorkerPrivate)48 virtual bool WorkerRun(JSContext* aCx,
49 WorkerPrivate* aWorkerPrivate) override {
50 uint64_t innerWindowId;
51 bool fireAtScope = true;
52
53 bool workerIsAcceptingEvents = aWorkerPrivate->IsAcceptingEvents();
54
55 WorkerPrivate* parent = aWorkerPrivate->GetParent();
56 if (parent) {
57 innerWindowId = 0;
58 } else {
59 AssertIsOnMainThread();
60
61 // Once a window has frozen its workers, their
62 // mMainThreadDebuggeeEventTargets should be paused, and their
63 // WorkerDebuggeeRunnables should not be being executed. The same goes for
64 // WorkerDebuggeeRunnables sent from child to parent workers, but since a
65 // frozen parent worker runs only control runnables anyway, that is taken
66 // care of naturally.
67 MOZ_ASSERT(!aWorkerPrivate->IsFrozen());
68
69 // Similarly for paused windows; all its workers should have been
70 // informed. (Subworkers are unaffected by paused windows.)
71 MOZ_ASSERT(!aWorkerPrivate->IsParentWindowPaused());
72
73 if (aWorkerPrivate->IsSharedWorker()) {
74 aWorkerPrivate->GetRemoteWorkerController()
75 ->ErrorPropagationOnMainThread(mReport.get(),
76 /* isErrorEvent */ true);
77 return true;
78 }
79
80 // Service workers do not have a main thread parent global, so normal
81 // worker error reporting will crash. Instead, pass the error to
82 // the ServiceWorkerManager to report on any controlled documents.
83 if (aWorkerPrivate->IsServiceWorker()) {
84 if (ServiceWorkerParentInterceptEnabled()) {
85 RefPtr<RemoteWorkerChild> actor(
86 aWorkerPrivate->GetRemoteWorkerControllerWeakRef());
87
88 Unused << NS_WARN_IF(!actor);
89
90 if (actor) {
91 actor->ErrorPropagationOnMainThread(nullptr, false);
92 }
93
94 } else {
95 RefPtr<ServiceWorkerManager> swm =
96 ServiceWorkerManager::GetInstance();
97 if (swm) {
98 swm->HandleError(aCx, aWorkerPrivate->GetPrincipal(),
99 aWorkerPrivate->ServiceWorkerScope(),
100 aWorkerPrivate->ScriptURL(), EmptyString(),
101 EmptyString(), EmptyString(), 0, 0,
102 nsIScriptError::errorFlag, JSEXN_ERR);
103 }
104 }
105
106 return true;
107 }
108
109 // The innerWindowId is only required if we are going to ReportError
110 // below, which is gated on this condition. The inner window correctness
111 // check is only going to succeed when the worker is accepting events.
112 if (workerIsAcceptingEvents) {
113 aWorkerPrivate->AssertInnerWindowIsCorrect();
114 innerWindowId = aWorkerPrivate->WindowID();
115 }
116 }
117
118 // Don't fire this event if the JS object has been disconnected from the
119 // private object.
120 if (!workerIsAcceptingEvents) {
121 return true;
122 }
123
124 WorkerErrorReport::ReportError(aCx, parent, fireAtScope,
125 aWorkerPrivate->ParentEventTargetRef(),
126 std::move(mReport), innerWindowId);
127 return true;
128 }
129 };
130
131 class ReportGenericErrorRunnable final : public WorkerDebuggeeRunnable {
132 public:
CreateAndDispatch(WorkerPrivate * aWorkerPrivate)133 static void CreateAndDispatch(WorkerPrivate* aWorkerPrivate) {
134 MOZ_ASSERT(aWorkerPrivate);
135 aWorkerPrivate->AssertIsOnWorkerThread();
136
137 RefPtr<ReportGenericErrorRunnable> runnable =
138 new ReportGenericErrorRunnable(aWorkerPrivate);
139 runnable->Dispatch();
140 }
141
142 private:
ReportGenericErrorRunnable(WorkerPrivate * aWorkerPrivate)143 explicit ReportGenericErrorRunnable(WorkerPrivate* aWorkerPrivate)
144 : WorkerDebuggeeRunnable(aWorkerPrivate) {
145 aWorkerPrivate->AssertIsOnWorkerThread();
146 }
147
PostDispatch(WorkerPrivate * aWorkerPrivate,bool aDispatchResult)148 void PostDispatch(WorkerPrivate* aWorkerPrivate,
149 bool aDispatchResult) override {
150 aWorkerPrivate->AssertIsOnWorkerThread();
151
152 // Dispatch may fail if the worker was canceled, no need to report that as
153 // an error, so don't call base class PostDispatch.
154 }
155
WorkerRun(JSContext * aCx,WorkerPrivate * aWorkerPrivate)156 bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override {
157 // Once a window has frozen its workers, their
158 // mMainThreadDebuggeeEventTargets should be paused, and their
159 // WorkerDebuggeeRunnables should not be being executed. The same goes for
160 // WorkerDebuggeeRunnables sent from child to parent workers, but since a
161 // frozen parent worker runs only control runnables anyway, that is taken
162 // care of naturally.
163 MOZ_ASSERT(!aWorkerPrivate->IsFrozen());
164
165 // Similarly for paused windows; all its workers should have been informed.
166 // (Subworkers are unaffected by paused windows.)
167 MOZ_ASSERT(!aWorkerPrivate->IsParentWindowPaused());
168
169 if (aWorkerPrivate->IsSharedWorker()) {
170 aWorkerPrivate->GetRemoteWorkerController()->ErrorPropagationOnMainThread(
171 nullptr, false);
172 return true;
173 }
174
175 if (aWorkerPrivate->IsServiceWorker()) {
176 if (ServiceWorkerParentInterceptEnabled()) {
177 RefPtr<RemoteWorkerChild> actor(
178 aWorkerPrivate->GetRemoteWorkerControllerWeakRef());
179
180 Unused << NS_WARN_IF(!actor);
181
182 if (actor) {
183 actor->ErrorPropagationOnMainThread(nullptr, false);
184 }
185
186 } else {
187 RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
188 if (swm) {
189 swm->HandleError(aCx, aWorkerPrivate->GetPrincipal(),
190 aWorkerPrivate->ServiceWorkerScope(),
191 aWorkerPrivate->ScriptURL(), EmptyString(),
192 EmptyString(), EmptyString(), 0, 0,
193 nsIScriptError::errorFlag, JSEXN_ERR);
194 }
195 }
196
197 return true;
198 }
199
200 if (!aWorkerPrivate->IsAcceptingEvents()) {
201 return true;
202 }
203
204 RefPtr<mozilla::dom::EventTarget> parentEventTarget =
205 aWorkerPrivate->ParentEventTargetRef();
206 RefPtr<Event> event = Event::Constructor(
207 parentEventTarget, NS_LITERAL_STRING("error"), EventInit());
208 event->SetTrusted(true);
209
210 parentEventTarget->DispatchEvent(*event);
211 return true;
212 }
213 };
214
215 } // namespace
216
AssignErrorBase(JSErrorBase * aReport)217 void WorkerErrorBase::AssignErrorBase(JSErrorBase* aReport) {
218 mFilename = NS_ConvertUTF8toUTF16(aReport->filename);
219 mLineNumber = aReport->lineno;
220 mColumnNumber = aReport->column;
221 mErrorNumber = aReport->errorNumber;
222 }
223
AssignErrorNote(JSErrorNotes::Note * aNote)224 void WorkerErrorNote::AssignErrorNote(JSErrorNotes::Note* aNote) {
225 WorkerErrorBase::AssignErrorBase(aNote);
226 xpc::ErrorNote::ErrorNoteToMessageString(aNote, mMessage);
227 }
228
WorkerErrorReport()229 WorkerErrorReport::WorkerErrorReport()
230 : mIsWarning(false), mExnType(JSEXN_ERR), mMutedError(false) {}
231
AssignErrorReport(JSErrorReport * aReport)232 void WorkerErrorReport::AssignErrorReport(JSErrorReport* aReport) {
233 WorkerErrorBase::AssignErrorBase(aReport);
234 xpc::ErrorReport::ErrorReportToMessageString(aReport, mMessage);
235
236 mLine.Assign(aReport->linebuf(), aReport->linebufLength());
237 mIsWarning = aReport->isWarning();
238 MOZ_ASSERT(aReport->exnType >= JSEXN_FIRST && aReport->exnType < JSEXN_LIMIT);
239 mExnType = JSExnType(aReport->exnType);
240 mMutedError = aReport->isMuted;
241
242 if (aReport->notes) {
243 if (!mNotes.SetLength(aReport->notes->length(), fallible)) {
244 return;
245 }
246
247 size_t i = 0;
248 for (auto&& note : *aReport->notes) {
249 mNotes.ElementAt(i).AssignErrorNote(note.get());
250 i++;
251 }
252 }
253 }
254
255 // aWorkerPrivate is the worker thread we're on (or the main thread, if null)
256 // aTarget is the worker object that we are going to fire an error at
257 // (if any).
258 /* static */
ReportError(JSContext * aCx,WorkerPrivate * aWorkerPrivate,bool aFireAtScope,DOMEventTargetHelper * aTarget,UniquePtr<WorkerErrorReport> aReport,uint64_t aInnerWindowId,JS::Handle<JS::Value> aException)259 void WorkerErrorReport::ReportError(
260 JSContext* aCx, WorkerPrivate* aWorkerPrivate, bool aFireAtScope,
261 DOMEventTargetHelper* aTarget, UniquePtr<WorkerErrorReport> aReport,
262 uint64_t aInnerWindowId, JS::Handle<JS::Value> aException) {
263 if (aWorkerPrivate) {
264 aWorkerPrivate->AssertIsOnWorkerThread();
265 } else {
266 AssertIsOnMainThread();
267 }
268
269 // We should not fire error events for warnings but instead make sure that
270 // they show up in the error console.
271 if (!aReport->mIsWarning) {
272 // First fire an ErrorEvent at the worker.
273 RootedDictionary<ErrorEventInit> init(aCx);
274
275 if (aReport->mMutedError) {
276 init.mMessage.AssignLiteral("Script error.");
277 } else {
278 init.mMessage = aReport->mMessage;
279 init.mFilename = aReport->mFilename;
280 init.mLineno = aReport->mLineNumber;
281 init.mError = aException;
282 }
283
284 init.mCancelable = true;
285 init.mBubbles = false;
286
287 if (aTarget) {
288 RefPtr<ErrorEvent> event =
289 ErrorEvent::Constructor(aTarget, NS_LITERAL_STRING("error"), init);
290 event->SetTrusted(true);
291
292 bool defaultActionEnabled =
293 aTarget->DispatchEvent(*event, CallerType::System, IgnoreErrors());
294 if (!defaultActionEnabled) {
295 return;
296 }
297 }
298
299 // Now fire an event at the global object, but don't do that if the error
300 // code is too much recursion and this is the same script threw the error.
301 // XXXbz the interaction of this with worker errors seems kinda broken.
302 // An overrecursion in the debugger or debugger sandbox will get turned
303 // into an error event on our parent worker!
304 // https://bugzilla.mozilla.org/show_bug.cgi?id=1271441 tracks making this
305 // better.
306 if (aFireAtScope &&
307 (aTarget || aReport->mErrorNumber != JSMSG_OVER_RECURSED)) {
308 JS::Rooted<JSObject*> global(aCx, JS::CurrentGlobalOrNull(aCx));
309 NS_ASSERTION(global, "This should never be null!");
310
311 nsEventStatus status = nsEventStatus_eIgnore;
312
313 if (aWorkerPrivate) {
314 WorkerGlobalScope* globalScope = nullptr;
315 UNWRAP_OBJECT(WorkerGlobalScope, &global, globalScope);
316
317 if (!globalScope) {
318 WorkerDebuggerGlobalScope* globalScope = nullptr;
319 UNWRAP_OBJECT(WorkerDebuggerGlobalScope, &global, globalScope);
320
321 MOZ_ASSERT_IF(globalScope,
322 globalScope->GetWrapperPreserveColor() == global);
323 if (globalScope || IsWorkerDebuggerSandbox(global)) {
324 aWorkerPrivate->ReportErrorToDebugger(
325 aReport->mFilename, aReport->mLineNumber, aReport->mMessage);
326 return;
327 }
328
329 MOZ_ASSERT(SimpleGlobalObject::SimpleGlobalType(global) ==
330 SimpleGlobalObject::GlobalType::BindingDetail);
331 // XXXbz We should really log this to console, but unwinding out of
332 // this stuff without ending up firing any events is ... hard. Just
333 // return for now.
334 // https://bugzilla.mozilla.org/show_bug.cgi?id=1271441 tracks
335 // making this better.
336 return;
337 }
338
339 MOZ_ASSERT(globalScope->GetWrapperPreserveColor() == global);
340
341 RefPtr<ErrorEvent> event =
342 ErrorEvent::Constructor(aTarget, NS_LITERAL_STRING("error"), init);
343 event->SetTrusted(true);
344
345 if (NS_FAILED(EventDispatcher::DispatchDOMEvent(
346 ToSupports(globalScope), nullptr, event, nullptr, &status))) {
347 NS_WARNING("Failed to dispatch worker thread error event!");
348 status = nsEventStatus_eIgnore;
349 }
350 } else if (nsGlobalWindowInner* win = xpc::WindowOrNull(global)) {
351 MOZ_ASSERT(NS_IsMainThread());
352
353 if (!win->HandleScriptError(init, &status)) {
354 NS_WARNING("Failed to dispatch main thread error event!");
355 status = nsEventStatus_eIgnore;
356 }
357 }
358
359 // Was preventDefault() called?
360 if (status == nsEventStatus_eConsumeNoDefault) {
361 return;
362 }
363 }
364 }
365
366 // Now fire a runnable to do the same on the parent's thread if we can.
367 if (aWorkerPrivate) {
368 RefPtr<ReportErrorRunnable> runnable =
369 new ReportErrorRunnable(aWorkerPrivate, std::move(aReport));
370 runnable->Dispatch();
371 return;
372 }
373
374 // Otherwise log an error to the error console.
375 WorkerErrorReport::LogErrorToConsole(aCx, *aReport, aInnerWindowId);
376 }
377
378 /* static */
LogErrorToConsole(JSContext * aCx,WorkerErrorReport & aReport,uint64_t aInnerWindowId)379 void WorkerErrorReport::LogErrorToConsole(JSContext* aCx,
380 WorkerErrorReport& aReport,
381 uint64_t aInnerWindowId) {
382 nsTArray<ErrorDataNote> notes;
383 for (size_t i = 0, len = aReport.mNotes.Length(); i < len; i++) {
384 const WorkerErrorNote& note = aReport.mNotes.ElementAt(i);
385 notes.AppendElement(ErrorDataNote(note.mLineNumber, note.mColumnNumber,
386 note.mMessage, note.mFilename));
387 }
388
389 JS::RootedObject stack(aCx, aReport.ReadStack(aCx));
390 JS::RootedObject stackGlobal(aCx, JS::CurrentGlobalOrNull(aCx));
391
392 ErrorData errorData(aReport.mIsWarning, aReport.mLineNumber,
393 aReport.mColumnNumber, aReport.mMessage,
394 aReport.mFilename, aReport.mLine, notes);
395 LogErrorToConsole(errorData, aInnerWindowId, stack, stackGlobal);
396 }
397
398 /* static */
LogErrorToConsole(const ErrorData & aReport,uint64_t aInnerWindowId,JS::HandleObject aStack,JS::HandleObject aStackGlobal)399 void WorkerErrorReport::LogErrorToConsole(const ErrorData& aReport,
400 uint64_t aInnerWindowId,
401 JS::HandleObject aStack,
402 JS::HandleObject aStackGlobal) {
403 AssertIsOnMainThread();
404
405 RefPtr<nsScriptErrorBase> scriptError =
406 CreateScriptError(nullptr, JS::NothingHandleValue, aStack, aStackGlobal);
407
408 NS_WARNING_ASSERTION(scriptError, "Failed to create script error!");
409
410 if (scriptError) {
411 nsAutoCString category("Web Worker");
412 uint32_t flags = aReport.isWarning() ? nsIScriptError::warningFlag
413 : nsIScriptError::errorFlag;
414 if (NS_FAILED(scriptError->nsIScriptError::InitWithWindowID(
415 aReport.message(), aReport.filename(), aReport.line(),
416 aReport.lineNumber(), aReport.columnNumber(), flags, category,
417 aInnerWindowId))) {
418 NS_WARNING("Failed to init script error!");
419 scriptError = nullptr;
420 }
421
422 for (size_t i = 0, len = aReport.notes().Length(); i < len; i++) {
423 const ErrorDataNote& note = aReport.notes().ElementAt(i);
424
425 nsScriptErrorNote* noteObject = new nsScriptErrorNote();
426 noteObject->Init(note.message(), note.filename(), 0, note.lineNumber(),
427 note.columnNumber());
428 scriptError->AddNote(noteObject);
429 }
430 }
431
432 nsCOMPtr<nsIConsoleService> consoleService =
433 do_GetService(NS_CONSOLESERVICE_CONTRACTID);
434 NS_WARNING_ASSERTION(consoleService, "Failed to get console service!");
435
436 if (consoleService) {
437 if (scriptError) {
438 if (NS_SUCCEEDED(consoleService->LogMessage(scriptError))) {
439 return;
440 }
441 NS_WARNING("LogMessage failed!");
442 } else if (NS_SUCCEEDED(consoleService->LogStringMessage(
443 aReport.message().BeginReading()))) {
444 return;
445 }
446 NS_WARNING("LogStringMessage failed!");
447 }
448
449 NS_ConvertUTF16toUTF8 msg(aReport.message());
450 NS_ConvertUTF16toUTF8 filename(aReport.filename());
451
452 static const char kErrorString[] = "JS error in Web Worker: %s [%s:%u]";
453
454 #ifdef ANDROID
455 __android_log_print(ANDROID_LOG_INFO, "Gecko", kErrorString, msg.get(),
456 filename.get(), aReport.lineNumber());
457 #endif
458
459 fprintf(stderr, kErrorString, msg.get(), filename.get(),
460 aReport.lineNumber());
461 fflush(stderr);
462 }
463
464 /* static */
CreateAndDispatchGenericErrorRunnableToParent(WorkerPrivate * aWorkerPrivate)465 void WorkerErrorReport::CreateAndDispatchGenericErrorRunnableToParent(
466 WorkerPrivate* aWorkerPrivate) {
467 ReportGenericErrorRunnable::CreateAndDispatch(aWorkerPrivate);
468 }
469
470 } // namespace dom
471 } // namespace mozilla
472