Bug 1409852 - Expose a hook to be informed whenever an exception is thrown;r?jandem draft
authorDavid Teller <dteller@mozilla.com>
Thu, 16 Nov 2017 10:36:30 +0100
changeset 714000 2ab7c46b2dd79b477372ea4d303604c61b8e95fd
parent 713999 67ab99a089fc65c520f96d5b4d154e5d504be92c
child 714001 47b4b7e3d27066f258a27280c92a124c5641cd36
child 714374 3d93fc8a8971cfd69f193f4aec435f4a4f466a09
child 714377 fe92e6a296c6094a6b7bd37ab93066f650179fd0
push id93804
push userdteller@mozilla.com
push dateThu, 21 Dec 2017 12:36:34 +0000
reviewersjandem
bugs1409852
milestone59.0a1
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
js/src/jsapi-tests/moz.build
js/src/jsapi-tests/testErrorInterceptor.cpp
js/src/jsapi.cpp
js/src/jsapi.h
js/src/jscntxtinlines.h
js/src/vm/Runtime.h
--- 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);