Bug 1409852 - Expose a hook to be informed whenever an exception is thrown;r?jandem
This hook should help us diagnose more easily typoes in our chrome code.
To avoid painting ourselves in a corner in case we need to optimize
exceptions at some later point, the API is restricted to Nightly -
which is where it will be the most useful anyway.
MozReview-Commit-ID: FvDnaALKHox
--- a/js/src/jsapi-tests/moz.build
+++ b/js/src/jsapi-tests/moz.build
@@ -132,16 +132,23 @@ if CONFIG['ENABLE_ION']:
'testJitRValueAlloc.cpp',
]
if CONFIG['ENABLE_STREAMS']:
UNIFIED_SOURCES += [
'testReadableStream.cpp',
]
+
+if CONFIG['NIGHTLY_BUILD']:
+ # The Error interceptor only exists on Nightly.
+ UNIFIED_SOURCES += [
+ 'testErrorInterceptor.cpp',
+ ]
+
if CONFIG['JS_BUILD_BINAST'] and CONFIG['JS_STANDALONE']:
# Standalone builds leave the source directory untouched,
# which lets us run tests with the data files intact.
# Otherwise, in the current state of the build system,
# we can't have data files in js/src tests.
UNIFIED_SOURCES += [
'testBinASTReader.cpp',
'testBinTokenReaderTester.cpp'
new file mode 100644
--- /dev/null
+++ b/js/src/jsapi-tests/testErrorInterceptor.cpp
@@ -0,0 +1,143 @@
+#include "jsapi.h"
+
+#include "jsapi-tests/tests.h"
+
+#include "vm/StringBuffer.h"
+
+// Tests for JS_GetErrorInterceptorCallback and JS_SetErrorInterceptorCallback.
+
+
+namespace {
+const double EXN_VALUE = 3.14;
+
+static JS::PersistentRootedString gLatestMessage;
+
+// An interceptor that stores the error in `gLatestMessage`.
+struct SimpleInterceptor: JSErrorInterceptor {
+ virtual void interceptError(JSContext* cx, const JS::Value& val) override {
+ js::StringBuffer buffer(cx);
+ if (!ValueToStringBuffer(cx, val, buffer))
+ MOZ_CRASH("Could not convert to string buffer");
+ gLatestMessage = buffer.finishString();
+ if (!gLatestMessage)
+ MOZ_CRASH("Could not convert to string");
+ }
+};
+
+bool equalStrings(JSContext* cx, JSString* a, JSString* b) {
+ int32_t result = 0;
+ if (!JS_CompareStrings(cx, a, b, &result))
+ MOZ_CRASH("Could not compare strings");
+ return result == 0;
+}
+}
+
+BEGIN_TEST(testErrorInterceptor)
+{
+ // Run the following snippets.
+ const char* SAMPLES[] = {
+ "throw new Error('I am an Error')\0",
+ "throw new TypeError('I am a TypeError')\0",
+ "throw new ReferenceError('I am a ReferenceError')\0",
+ "throw new SyntaxError('I am a SyntaxError')\0",
+ "throw 5\0",
+ "undefined[0]\0",
+ "foo[0]\0",
+ "b[\0",
+ };
+ // With the simpleInterceptor, we should end up with the following error:
+ const char* TO_STRING[] = {
+ "Error: I am an Error\0",
+ "TypeError: I am a TypeError\0",
+ "ReferenceError: I am a ReferenceError\0",
+ "SyntaxError: I am a SyntaxError\0",
+ "5\0",
+ "TypeError: undefined has no properties\0",
+ "ReferenceError: foo is not defined\0",
+ "SyntaxError: expected expression, got end of script\0",
+ };
+ MOZ_ASSERT(mozilla::ArrayLength(SAMPLES) == mozilla::ArrayLength(TO_STRING));
+
+
+ // Save original callback.
+ JSErrorInterceptor* original = JS_GetErrorInterceptorCallback(cx->runtime());
+ gLatestMessage.init(cx);
+
+ // Test without callback.
+ JS_SetErrorInterceptorCallback(cx->runtime(), nullptr);
+ CHECK(gLatestMessage == nullptr);
+
+ for (auto sample: SAMPLES) {
+ if (execDontReport(sample, __FILE__, __LINE__))
+ MOZ_CRASH("This sample should have failed");
+ CHECK(JS_IsExceptionPending(cx));
+ CHECK(gLatestMessage == nullptr);
+ JS_ClearPendingException(cx);
+ }
+
+ // Test with callback.
+ SimpleInterceptor simpleInterceptor;
+ JS_SetErrorInterceptorCallback(cx->runtime(), &simpleInterceptor);
+
+ // Test that we return the right callback.
+ CHECK_EQUAL(JS_GetErrorInterceptorCallback(cx->runtime()), &simpleInterceptor);
+
+ // This shouldn't cause any error.
+ EXEC("function bar() {}");
+ CHECK(gLatestMessage == nullptr);
+
+ // Test error throwing with a callback that succeeds.
+ for (size_t i = 0; i < mozilla::ArrayLength(SAMPLES); ++i) {
+ // This should cause the appropriate error.
+ if (execDontReport(SAMPLES[i], __FILE__, __LINE__))
+ MOZ_CRASH("This sample should have failed");
+ CHECK(JS_IsExceptionPending(cx));
+
+ // Check result of callback.
+ CHECK(gLatestMessage != nullptr);
+ CHECK(js::StringEqualsAscii(&gLatestMessage->asLinear(), TO_STRING[i]));
+
+ // Check the final error.
+ JS::RootedValue exn(cx);
+ CHECK(JS_GetPendingException(cx, &exn));
+ JS_ClearPendingException(cx);
+
+ js::StringBuffer buffer(cx);
+ CHECK(ValueToStringBuffer(cx, exn, buffer));
+ CHECK(equalStrings(cx, buffer.finishString(), gLatestMessage));
+
+ // Cleanup.
+ gLatestMessage = nullptr;
+ }
+
+ // Test again without callback.
+ JS_SetErrorInterceptorCallback(cx->runtime(), nullptr);
+ for (size_t i = 0; i < mozilla::ArrayLength(SAMPLES); ++i) {
+ if (execDontReport(SAMPLES[i], __FILE__, __LINE__))
+ MOZ_CRASH("This sample should have failed");
+ CHECK(JS_IsExceptionPending(cx));
+
+ // Check that the callback wasn't called.
+ CHECK(gLatestMessage == nullptr);
+
+ // Check the final error.
+ JS::RootedValue exn(cx);
+ CHECK(JS_GetPendingException(cx, &exn));
+ JS_ClearPendingException(cx);
+
+ js::StringBuffer buffer(cx);
+ CHECK(ValueToStringBuffer(cx, exn, buffer));
+ CHECK(js::StringEqualsAscii(buffer.finishString(), TO_STRING[i]));
+
+ // Cleanup.
+ gLatestMessage = nullptr;
+ }
+
+ // Cleanup
+ JS_SetErrorInterceptorCallback(cx->runtime(), original);
+ gLatestMessage = nullptr;
+ JS_ClearPendingException(cx);
+
+ return true;
+}
+END_TEST(testErrorInterceptor)
--- a/js/src/jsapi.cpp
+++ b/js/src/jsapi.cpp
@@ -648,16 +648,50 @@ JS_SetSizeOfIncludingThisCompartmentCall
}
JS_PUBLIC_API(void)
JS_SetCompartmentNameCallback(JSContext* cx, JSCompartmentNameCallback callback)
{
cx->runtime()->compartmentNameCallback = callback;
}
+#if defined(NIGHTLY_BUILD)
+JS_PUBLIC_API(void)
+JS_SetErrorInterceptorCallback(JSRuntime* rt, JSErrorInterceptor* callback)
+{
+ rt->errorInterception.interceptor = callback;
+}
+
+JS_PUBLIC_API(JSErrorInterceptor*)
+JS_GetErrorInterceptorCallback(JSRuntime* rt)
+{
+ return rt->errorInterception.interceptor;
+}
+
+JS_PUBLIC_API(Maybe<JSExnType>)
+JS_GetErrorType(const JS::Value& val)
+{
+ // All errors are objects.
+ if (!val.isObject())
+ return mozilla::Nothing();
+
+ const JSObject& obj = val.toObject();
+
+ // All errors are `ErrorObject`.
+ if (!obj.is<js::ErrorObject>()) {
+ // Not one of the primitive errors.
+ return mozilla::Nothing();
+ }
+
+ const js::ErrorObject& err = obj.as<js::ErrorObject>();
+ return mozilla::Some(err.type());
+}
+
+#endif // defined(NIGHTLY_BUILD)
+
JS_PUBLIC_API(void)
JS_SetWrapObjectCallbacks(JSContext* cx, const JSWrapObjectCallbacks* callbacks)
{
cx->runtime()->wrapObjectCallbacks = callbacks;
}
JS_PUBLIC_API(void)
JS_SetExternalStringSizeofCallback(JSContext* cx, JSExternalStringSizeofCallback callback)
--- a/js/src/jsapi.h
+++ b/js/src/jsapi.h
@@ -675,16 +675,28 @@ typedef void
* that corresponds to the size of the allocation that will be released by the
* JSStringFinalizer passed to JS_NewExternalString for this string.
*
* Implementations of this callback MUST NOT do anything that can cause GC.
*/
using JSExternalStringSizeofCallback =
size_t (*)(JSString* str, mozilla::MallocSizeOf mallocSizeOf);
+/**
+ * Callback used to intercept JavaScript errors.
+ */
+struct JSErrorInterceptor {
+ /**
+ * This method is called whenever an error has been raised from JS code.
+ *
+ * This method MUST be infallible.
+ */
+ virtual void interceptError(JSContext* cx, const JS::Value& error) = 0;
+};
+
/************************************************************************/
static MOZ_ALWAYS_INLINE JS::Value
JS_NumberValue(double d)
{
int32_t i;
d = JS::CanonicalizeNaN(d);
if (mozilla::NumberIsInt32(d, &i))
@@ -1322,16 +1334,43 @@ extern JS_PUBLIC_API(void)
JS_SetCompartmentNameCallback(JSContext* cx, JSCompartmentNameCallback callback);
extern JS_PUBLIC_API(void)
JS_SetWrapObjectCallbacks(JSContext* cx, const JSWrapObjectCallbacks* callbacks);
extern JS_PUBLIC_API(void)
JS_SetExternalStringSizeofCallback(JSContext* cx, JSExternalStringSizeofCallback callback);
+#if defined(NIGHTLY_BUILD)
+
+// Set a callback that will be called whenever an error
+// is thrown in this runtime. This is designed as a mechanism
+// for logging errors. Note that the VM makes no attempt to sanitize
+// the contents of the error (so it may contain private data)
+// or to sort out among errors (so it may not be the error you
+// are interested in or for the component in which you are
+// interested).
+//
+// If the callback sets a new error, this new error
+// will replace the original error.
+//
+// May be `nullptr`.
+extern JS_PUBLIC_API(void)
+JS_SetErrorInterceptorCallback(JSRuntime*, JSErrorInterceptor* callback);
+
+extern JS_PUBLIC_API(JSErrorInterceptor*)
+JS_GetErrorInterceptorCallback(JSRuntime*);
+
+// Examine a value to determine if it is one of the built-in Error types.
+// If so, return the error type.
+extern JS_PUBLIC_API(mozilla::Maybe<JSExnType>)
+JS_GetErrorType(const JS::Value& val);
+
+#endif // defined(NIGHTLY_BUILD)
+
extern JS_PUBLIC_API(void)
JS_SetCompartmentPrivate(JSCompartment* compartment, void* data);
extern JS_PUBLIC_API(void*)
JS_GetCompartmentPrivate(JSCompartment* compartment);
extern JS_PUBLIC_API(void)
JS_SetZoneUserData(JS::Zone* zone, void* data);
--- a/js/src/jscntxtinlines.h
+++ b/js/src/jscntxtinlines.h
@@ -429,16 +429,41 @@ inline void
JSContext::minorGC(JS::gcreason::Reason reason)
{
runtime()->gc.minorGC(reason);
}
inline void
JSContext::setPendingException(const js::Value& v)
{
+#if defined(NIGHTLY_BUILD)
+ do {
+ // Do not intercept exceptions if we are already
+ // in the exception interceptor. That would lead
+ // to infinite recursion.
+ if (this->runtime()->errorInterception.isExecuting)
+ break;
+
+ // Check whether we have an interceptor at all.
+ if (!this->runtime()->errorInterception.interceptor)
+ break;
+
+ // Make sure that we do not call the interceptor from within
+ // the interceptor.
+ this->runtime()->errorInterception.isExecuting = true;
+
+ // The interceptor must be infallible.
+ const mozilla::DebugOnly<bool> wasExceptionPending = this->isExceptionPending();
+ this->runtime()->errorInterception.interceptor->interceptError(this, v);
+ MOZ_ASSERT(wasExceptionPending == this->isExceptionPending());
+
+ this->runtime()->errorInterception.isExecuting = false;
+ } while (false);
+#endif // defined(NIGHTLY_BUILD)
+
// overRecursed_ is set after the fact by ReportOverRecursed.
this->overRecursed_ = false;
this->throwing = true;
this->unwrappedException() = v;
// We don't use assertSameCompartment here to allow
// js::SetPendingExceptionCrossContext to work.
MOZ_ASSERT_IF(v.isObject(), v.toObject().compartment() == compartment());
}
--- a/js/src/vm/Runtime.h
+++ b/js/src/vm/Runtime.h
@@ -1076,16 +1076,40 @@ struct JSRuntime : public js::MallocProv
wasmUnwindPC_ = nullptr;
}
void* wasmResumePC() const {
return wasmResumePC_;
}
void* wasmUnwindPC() const {
return wasmUnwindPC_;
}
+
+ public:
+#if defined(NIGHTLY_BUILD)
+ // Support for informing the embedding of any error thrown.
+ // This mechanism is designed to let the embedding
+ // log/report/fail in case certain errors are thrown
+ // (e.g. SyntaxError, ReferenceError or TypeError
+ // in critical code).
+ struct ErrorInterceptionSupport {
+ ErrorInterceptionSupport()
+ : isExecuting(false)
+ , interceptor(nullptr)
+ { }
+
+ // true if the error interceptor is currently executing,
+ // false otherwise. Used to avoid infinite loops.
+ bool isExecuting;
+
+ // if non-null, any call to `setPendingException`
+ // in this runtime will trigger the call to `interceptor`
+ JSErrorInterceptor* interceptor;
+ };
+ ErrorInterceptionSupport errorInterception;
+#endif // defined(NIGHTLY_BUILD)
};
namespace js {
inline void
FreeOp::free_(void* p)
{
js_free(p);