Bug 1409852 - Expose a hook to be informed whenever an exception is thrown;r=jandem
☠☠ backed out by e574f932b5a8 ☠ ☠
authorDavid Teller <dteller@mozilla.com>
Thu, 16 Nov 2017 10:36:30 +0100
changeset 397145 46fce9a2622d691de05223d1c80d15bf2a564a51
parent 397144 649d7bdf80add985394854ff7829cc45e20dd571
child 397146 06368bf1a32c75852fe4fc940789b5d5af728805
push id33127
push useraiakab@mozilla.com
push dateThu, 21 Dec 2017 22:17:05 +0000
treeherdermozilla-central@d20967c26da5 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjandem
bugs1409852
milestone59.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
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);