Bug 1626100 - Introduce an ExceptionStack class and StealPendingExceptionStack. r=sfink,mccr8

See also the bug for a more detailed description.

Differential Revision: https://phabricator.services.mozilla.com/D69636

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Tom Schuster 2020-04-09 12:35:29 +00:00
parent 882077ccb1
commit fae4303276
15 changed files with 199 additions and 87 deletions

View File

@ -8,6 +8,7 @@
#include "AudioParamMap.h"
#include "js/Array.h" // JS::{Get,Set}ArrayLength, JS::NewArrayLength
#include "js/Exception.h"
#include "mozilla/dom/AudioWorkletNodeBinding.h"
#include "mozilla/dom/AudioParamMapBinding.h"
#include "mozilla/dom/RootedDictionary.h"
@ -197,26 +198,24 @@ void WorkletNodeEngine::SendProcessorError(AudioNodeTrack* aTrack,
ReleaseJSResources();
// The processor errored out while getting a context, try to tell the node
// anyways.
if (!aCx) {
if (!aCx || !JS_IsExceptionPending(aCx)) {
ProcessorErrorDetails details;
details.mMessage.Assign(u"Unknown processor error");
SendErrorToMainThread(aTrack, details);
return;
}
js::ErrorReport jsReport(aCx);
JS::Rooted<JS::Value> exn(aCx);
JS::Rooted<JSObject*> exnStack(aCx);
if (JS_GetPendingException(aCx, &exn)) {
exnStack.set(JS::GetPendingExceptionStack(aCx));
JS_ClearPendingException(aCx);
if (!jsReport.init(aCx, exn, js::ErrorReport::WithSideEffects)) {
JS::ExceptionStack exnStack(aCx);
if (JS::StealPendingExceptionStack(aCx, &exnStack)) {
js::ErrorReport jsReport(aCx);
if (!jsReport.init(aCx, exnStack, js::ErrorReport::WithSideEffects)) {
ProcessorErrorDetails details;
details.mMessage.Assign(u"Unknown processor error");
SendErrorToMainThread(aTrack, details);
// Set the exception and stack back to have it in the console with a stack
// trace.
JS::SetPendingExceptionAndStack(aCx, exn, exnStack);
JS::SetPendingExceptionAndStack(aCx, exnStack.exception(),
exnStack.stack());
return;
}
@ -235,7 +234,8 @@ void WorkletNodeEngine::SendProcessorError(AudioNodeTrack* aTrack,
// Set the exception and stack back to have it in the console with a stack
// trace.
JS::SetPendingExceptionAndStack(aCx, exn, exnStack);
JS::SetPendingExceptionAndStack(aCx, exnStack.exception(),
exnStack.stack());
} else {
NS_WARNING("No exception, but processor errored out?");
}

View File

@ -556,8 +556,13 @@ bool AutoJSAPI::StealExceptionAndStack(JS::MutableHandle<JS::Value> aVal,
if (!PeekException(aVal)) {
return false;
}
aStack.set(JS::GetPendingExceptionStack(cx()));
JS_ClearPendingException(cx());
JS::ExceptionStack exnStack(cx());
if (!JS::StealPendingExceptionStack(cx(), &exnStack)) {
return false;
}
aStack.set(exnStack.stack());
return true;
}

View File

@ -10,6 +10,7 @@
#include "js/CompilationAndEvaluation.h"
#include "js/ContextOptions.h"
#include "js/Exception.h"
#include "js/LocaleSensitive.h"
#include "js/MemoryMetrics.h"
#include "js/SourceText.h"
@ -4404,27 +4405,30 @@ void WorkerPrivate::ReportError(JSContext* aCx,
data->mErrorHandlerRecursionCount == 1,
"Bad recursion logic!");
JS::Rooted<JS::Value> exn(aCx);
if (!JS_GetPendingException(aCx, &exn)) {
// Probably shouldn't actually happen? But let's go ahead and just use null
// for lack of anything better.
exn.setNull();
}
JS::RootedObject exnStack(aCx, JS::GetPendingExceptionStack(aCx));
JS_ClearPendingException(aCx);
UniquePtr<WorkerErrorReport> report = MakeUnique<WorkerErrorReport>();
if (aReport) {
report->AssignErrorReport(aReport);
}
JS::RootedObject stack(aCx), stackGlobal(aCx);
xpc::FindExceptionStackForConsoleReport(nullptr, exn, exnStack, &stack,
&stackGlobal);
JS::ExceptionStack exnStack(aCx);
if (JS_IsExceptionPending(aCx)) {
if (!JS::StealPendingExceptionStack(aCx, &exnStack)) {
JS_ClearPendingException(aCx);
return;
}
if (stack) {
JSAutoRealm ar(aCx, stackGlobal);
report->SerializeWorkerStack(aCx, this, stack);
JS::RootedObject stack(aCx), stackGlobal(aCx);
xpc::FindExceptionStackForConsoleReport(
nullptr, exnStack.exception(), exnStack.stack(), &stack, &stackGlobal);
if (stack) {
JSAutoRealm ar(aCx, stackGlobal);
report->SerializeWorkerStack(aCx, this, stack);
}
} else {
// ReportError is also used for reporting warnings,
// so there won't be a pending exception.
MOZ_ASSERT(aReport->isWarning());
}
if (report->mMessage.IsEmpty() && aToStringResult) {
@ -4453,7 +4457,7 @@ void WorkerPrivate::ReportError(JSContext* aCx,
JS::CurrentGlobalOrNull(aCx);
WorkerErrorReport::ReportError(aCx, this, fireAtScope, nullptr,
std::move(report), 0, exn);
std::move(report), 0, exnStack.exception());
data->mErrorHandlerRecursionCount--;
}

View File

@ -13,6 +13,7 @@
#include "mozilla/Attributes.h"
#include "mozilla/EventQueue.h"
#include "mozilla/ThreadEventQueue.h"
#include "js/Exception.h"
namespace mozilla {
namespace dom {
@ -186,16 +187,17 @@ void WorkletJSContext::ReportError(JSErrorReport* aReport,
RefPtr<AsyncErrorReporter> reporter = new AsyncErrorReporter(xpcReport);
JSContext* cx = Context();
JS::Rooted<JS::Value> exn(cx);
if (JS_GetPendingException(cx, &exn)) {
JS::Rooted<JSObject*> exnStack(cx, JS::GetPendingExceptionStack(cx));
JS_ClearPendingException(cx);
JS::Rooted<JSObject*> stack(cx);
JS::Rooted<JSObject*> stackGlobal(cx);
xpc::FindExceptionStackForConsoleReport(nullptr, exn, exnStack, &stack,
&stackGlobal);
if (stack) {
reporter->SerializeStack(cx, stack);
if (JS_IsExceptionPending(cx)) {
JS::ExceptionStack exnStack(cx);
if (JS::StealPendingExceptionStack(cx, &exnStack)) {
JS::Rooted<JSObject*> stack(cx);
JS::Rooted<JSObject*> stackGlobal(cx);
xpc::FindExceptionStackForConsoleReport(nullptr, exnStack.exception(),
exnStack.stack(), &stack,
&stackGlobal);
if (stack) {
reporter->SerializeStack(cx, stack);
}
}
}

64
js/public/Exception.h Normal file
View File

@ -0,0 +1,64 @@
/* -*- 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/. */
#ifndef js_Exception_h
#define js_Exception_h
#include "mozilla/Attributes.h"
#include "jspubtd.h"
#include "js/RootingAPI.h" // JS::{Handle,Rooted}
#include "js/Value.h" // JS::Value, JS::Handle<JS::Value>
namespace JS {
// This class encapsulates a (pending) exception and the corresponding optional
// SavedFrame stack object captured when the pending exception was set
// on the JSContext. This fuzzily correlates with a `throw` statement in JS,
// although arbitrary JSAPI consumers or VM code may also set pending exceptions
// via `JS_SetPendingException`.
//
// This is not the same stack as `e.stack` when `e` is an `Error` object.
// (That would be JS::ExceptionStackOrNull).
class MOZ_STACK_CLASS ExceptionStack {
Rooted<Value> exception_;
Rooted<JSObject*> stack_;
friend JS_PUBLIC_API bool GetPendingExceptionStack(
JSContext* cx, JS::ExceptionStack* exceptionStack);
void init(HandleValue exception, HandleObject stack) {
exception_ = exception;
stack_ = stack;
}
public:
explicit ExceptionStack(JSContext* cx) : exception_(cx), stack_(cx) {}
ExceptionStack(JSContext* cx, HandleValue exception, HandleObject stack)
: exception_(cx, exception), stack_(cx, stack) {}
HandleValue exception() const { return exception_; }
// |stack| can be null.
HandleObject stack() const { return stack_; }
};
// Get the current pending exception value and stack.
// This function asserts that there is a pending exception.
// If this function returns false, then retrieving the current pending exception
// failed and might have been overwritten by a new exception.
extern JS_PUBLIC_API bool GetPendingExceptionStack(
JSContext* cx, JS::ExceptionStack* exceptionStack);
// Similar to GetPendingExceptionStack, but also clears the current
// pending exception.
extern JS_PUBLIC_API bool StealPendingExceptionStack(
JSContext* cx, JS::ExceptionStack* exceptionStack);
} // namespace JS
#endif // js_Exception_h

View File

@ -11,6 +11,7 @@
#include "builtin/TestingFunctions.h"
#include "js/CompilationAndEvaluation.h" // JS::Evaluate
#include "js/Exception.h"
#include "js/SavedFrameAPI.h"
#include "js/SourceText.h" // JS::Source{Ownership,Text}
#include "jsapi-tests/tests.h"
@ -297,7 +298,7 @@ BEGIN_TEST(testSavedStacks_selfHostedFrames) {
}
END_TEST(testSavedStacks_selfHostedFrames)
BEGIN_TEST(test_JS_GetPendingExceptionStack) {
BEGIN_TEST(test_GetPendingExceptionStack) {
CHECK(js::DefineTestingFunctions(cx, global, false, false));
JSPrincipals* principals = cx->realm()->principals();
@ -327,14 +328,15 @@ BEGIN_TEST(test_JS_GetPendingExceptionStack) {
CHECK(JS_IsExceptionPending(cx));
CHECK(val.isUndefined());
JS::RootedObject stack(cx, JS::GetPendingExceptionStack(cx));
CHECK(stack);
CHECK(stack->is<js::SavedFrame>());
JS::Rooted<js::SavedFrame*> savedFrameStack(cx, &stack->as<js::SavedFrame>());
JS::ExceptionStack exnStack(cx);
CHECK(JS::GetPendingExceptionStack(cx, &exnStack));
CHECK(exnStack.stack());
CHECK(exnStack.stack()->is<js::SavedFrame>());
JS::Rooted<js::SavedFrame*> savedFrameStack(
cx, &exnStack.stack()->as<js::SavedFrame>());
JS_GetPendingException(cx, &val);
CHECK(val.isInt32());
CHECK(val.toInt32() == 5);
CHECK(exnStack.exception().isInt32());
CHECK(exnStack.exception().toInt32() == 5);
struct {
uint32_t line;
@ -392,4 +394,4 @@ BEGIN_TEST(test_JS_GetPendingExceptionStack) {
return true;
}
END_TEST(test_JS_GetPendingExceptionStack)
END_TEST(test_GetPendingExceptionStack)

View File

@ -4956,12 +4956,6 @@ JS_PUBLIC_API void JS::SetPendingExceptionAndStack(JSContext* cx,
cx->setPendingException(value, nstack);
}
JS_PUBLIC_API JSObject* JS::GetPendingExceptionStack(JSContext* cx) {
AssertHeapIsIdle();
CHECK_THREAD(cx);
return cx->getPendingExceptionStack();
}
JS::AutoSaveExceptionState::AutoSaveExceptionState(JSContext* cx)
: context(cx),
wasPropagatingForcedReturn(cx->propagatingForcedReturn_),

View File

@ -2651,18 +2651,6 @@ class JS_PUBLIC_API AutoSaveExceptionState {
// must be a SavedFrame.
JS_PUBLIC_API void SetPendingExceptionAndStack(JSContext* cx, HandleValue value,
HandleObject stack);
/**
* Get the SavedFrame stack object captured when the pending exception was set
* on the JSContext. This fuzzily correlates with a `throw` statement in JS,
* although arbitrary JSAPI consumers or VM code may also set pending exceptions
* via `JS_SetPendingException`.
*
* This is not the same stack as `e.stack` when `e` is an `Error` object. (That
* would be JS::ExceptionStackOrNull).
*/
MOZ_MUST_USE JS_PUBLIC_API JSObject* GetPendingExceptionStack(JSContext* cx);
} /* namespace JS */
/**

View File

@ -28,6 +28,7 @@
#include "js/CharacterEncoding.h"
#include "js/Class.h"
#include "js/Conversions.h"
#include "js/Exception.h" // JS::ExceptionStack
#include "js/SavedFrameAPI.h"
#include "js/UniquePtr.h"
#include "js/Value.h"
@ -467,6 +468,11 @@ ErrorReport::ErrorReport(JSContext* cx) : reportp(nullptr), exnObject(cx) {}
ErrorReport::~ErrorReport() = default;
bool ErrorReport::init(JSContext* cx, const JS::ExceptionStack& exnStack,
SniffingBehavior sniffingBehavior) {
return init(cx, exnStack.exception(), sniffingBehavior, exnStack.stack());
}
bool ErrorReport::init(JSContext* cx, HandleValue exn,
SniffingBehavior sniffingBehavior,
HandleObject fallbackStack) {

View File

@ -20,6 +20,7 @@
#include "js/CharacterEncoding.h"
#include "js/Class.h"
#include "js/ErrorReport.h"
#include "js/Exception.h"
#include "js/HeapAPI.h"
#include "js/TypeDecls.h"
#include "js/Utility.h"
@ -44,6 +45,8 @@ struct JSJitInfo;
namespace JS {
template <class T>
class Heap;
class ExceptionStack;
} /* namespace JS */
namespace js {
@ -1334,6 +1337,9 @@ struct MOZ_STACK_CLASS JS_FRIEND_API ErrorReport {
SniffingBehavior sniffingBehavior,
JS::HandleObject fallbackStack = nullptr);
bool init(JSContext* cx, const JS::ExceptionStack& exnStack,
SniffingBehavior sniffingBehavior);
JSErrorReport* report() { return reportp; }
const JS::ConstUTF8CharsZ toStringResult() { return toStringResult_; }

View File

@ -141,6 +141,7 @@ EXPORTS.js += [
'../public/Debug.h',
'../public/Equality.h',
'../public/ErrorReport.h',
'../public/Exception.h',
'../public/ForOfIterator.h',
'../public/GCAnnotations.h',
'../public/GCAPI.h',
@ -306,6 +307,7 @@ UNIFIED_SOURCES += [
'vm/EqualityOperations.cpp',
'vm/ErrorObject.cpp',
'vm/ErrorReporting.cpp',
'vm/Exception.cpp',
'vm/ForOfIterator.cpp',
'vm/FrameIter.cpp',
'vm/GeckoProfiler.cpp',

View File

@ -98,6 +98,7 @@
#include "js/ContextOptions.h" // JS::ContextOptions{,Ref}
#include "js/Debug.h"
#include "js/Equality.h" // JS::SameValue
#include "js/Exception.h" // JS::StealPendingExceptionStack
#include "js/experimental/SourceHook.h" // js::{Set,Forget,}SourceHook
#include "js/GCVector.h"
#include "js/Initialization.h"
@ -9579,15 +9580,12 @@ js::shell::AutoReportException::~AutoReportException() {
}
// Get exception object and stack before printing and clearing exception.
RootedValue exn(cx);
(void)JS_GetPendingException(cx, &exn);
RootedObject stack(cx, GetPendingExceptionStack(cx));
JS_ClearPendingException(cx);
JS::ExceptionStack exnStack(cx);
JS::StealPendingExceptionStack(cx, &exnStack);
ShellContext* sc = GetShellContext(cx);
js::ErrorReport report(cx);
if (!report.init(cx, exn, js::ErrorReport::WithSideEffects, stack)) {
if (!report.init(cx, exnStack, js::ErrorReport::WithSideEffects)) {
fprintf(stderr, "out of memory initializing ErrorReport\n");
fflush(stderr);
JS_ClearPendingException(cx);
@ -9600,7 +9598,7 @@ js::shell::AutoReportException::~AutoReportException() {
PrintError(cx, fp, report.toStringResult(), report.report(), reportWarnings);
JS_ClearPendingException(cx);
if (!PrintStackTrace(cx, stack)) {
if (!PrintStackTrace(cx, exnStack.stack())) {
fputs("(Unable to print stack trace)\n", fp);
JS_ClearPendingException(cx);
}

View File

@ -18,6 +18,7 @@
#include "js/CompilationAndEvaluation.h" // JS::Evaluate
#include "js/CompileOptions.h" // JS::CompileOptions
#include "js/Exception.h" // JS::StealPendingExceptionStack
#include "js/RootingAPI.h" // JS::Rooted
#include "js/SourceText.h" // JS::Source{Ownership,Text}
#include "js/Value.h" // JS::Value
@ -34,20 +35,17 @@ static std::string gFuzzModuleName;
static void CrashOnPendingException() {
if (JS_IsExceptionPending(gCx)) {
JS::Rooted<JS::Value> exn(gCx);
(void)JS_GetPendingException(gCx, &exn);
JS::Rooted<JSObject*> stack(gCx, JS::GetPendingExceptionStack(gCx));
JS_ClearPendingException(gCx);
JS::ExceptionStack exnStack(gCx);
(void)JS::StealPendingExceptionStack(gCx, &exnStack);
js::ErrorReport report(gCx);
if (!report.init(gCx, exn, js::ErrorReport::WithSideEffects)) {
if (!report.init(gCx, exnStack, js::ErrorReport::WithSideEffects)) {
fprintf(stderr, "out of memory initializing ErrorReport\n");
fflush(stderr);
} else {
js::PrintError(gCx, stderr, report.toStringResult(), report.report(),
js::shell::reportWarnings);
if (!js::shell::PrintStackTrace(gCx, stack)) {
if (!js::shell::PrintStackTrace(gCx, exnStack.stack())) {
fputs("(Unable to print stack trace)\n", stderr);
}
}

41
js/src/vm/Exception.cpp Normal file
View File

@ -0,0 +1,41 @@
/* -*- 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 "js/Exception.h"
#include "jsapi.h" // AssertHeapIsIdle
#include "vm/JSContext.h"
using namespace js;
bool JS::StealPendingExceptionStack(JSContext* cx,
JS::ExceptionStack* exceptionStack) {
if (!GetPendingExceptionStack(cx, exceptionStack)) {
return false;
}
// "Steal" exception by clearing it.
cx->clearPendingException();
return true;
}
bool JS::GetPendingExceptionStack(JSContext* cx,
JS::ExceptionStack* exceptionStack) {
AssertHeapIsIdle();
CHECK_THREAD(cx);
MOZ_ASSERT(exceptionStack);
MOZ_ASSERT(cx->isExceptionPending());
RootedValue exception(cx);
if (!cx->getPendingException(&exception)) {
return false;
}
RootedObject stack(cx, cx->getPendingExceptionStack());
exceptionStack->init(exception, stack);
return true;
}

View File

@ -8,6 +8,7 @@
#include "WrapperFactory.h"
#include "AccessCheck.h"
#include "jsfriendapi.h"
#include "js/Exception.h"
#include "js/Proxy.h"
#include "js/Wrapper.h"
#include "mozilla/ErrorResult.h"
@ -277,34 +278,35 @@ static void MaybeSanitizeException(JSContext* cx,
// don't end up unnecessarily wrapping exceptions.
{ // Scope for JSAutoRealm
JSAutoRealm ar(cx, unwrappedFun);
JS::Rooted<JS::Value> exn(cx);
// If JS_GetPendingException returns false, this was an uncatchable
JS::ExceptionStack exnStack(cx);
// If JS::GetPendingExceptionStack returns false, this was an uncatchable
// exception, or we somehow failed to wrap the exception into our
// compartment. In either case, treating this as uncatchable exception,
// by returning without setting any exception on the JSContext,
// seems fine.
if (!JS_GetPendingException(cx, &exn)) {
if (!JS::GetPendingExceptionStack(cx, &exnStack)) {
JS_ClearPendingException(cx);
return;
}
// Let through non-objects as-is, because some APIs rely on
// that and accidental exceptions are never non-objects.
if (!exn.isObject() ||
if (!exnStack.exception().isObject() ||
callerPrincipal->Subsumes(nsContentUtils::ObjectPrincipal(
js::UncheckedUnwrap(&exn.toObject())))) {
js::UncheckedUnwrap(&exnStack.exception().toObject())))) {
// Just leave exn as-is.
return;
}
// Whoever we are throwing the exception to should not have access to
// the exception. Sanitize it. First report the existing exception.
JS::Rooted<JSObject*> exnStack(cx, JS::GetPendingExceptionStack(cx));
// the exception. Sanitize it. First clear the existing exception.
JS_ClearPendingException(cx);
{ // Scope for AutoJSAPI
AutoJSAPI jsapi;
if (jsapi.Init(unwrappedFun)) {
JS::SetPendingExceptionAndStack(cx, exn, exnStack);
JS::SetPendingExceptionAndStack(cx, exnStack.exception(),
exnStack.stack());
}
// If Init() fails, we can't report the exception, but oh, well.