Bug 996060 - Part 1: Save a stack on JSContext::setPendingException; r=sfink
☠☠ backed out by d2b250ce1693 ☠ ☠
authorNick Fitzgerald <fitzgen@gmail.com>
Sat, 21 May 2016 10:15:24 -0700
changeset 298331 6e043a8ce3356d2bb0867cd6f884aadb4b14e606
parent 298330 5e13d58eefb96a66d6d6b17cd97d780157b4ad07
child 298332 d53726702252f8dea95878c721f8b2f652accb89
push id77123
push usernfitzgerald@mozilla.com
push dateSat, 21 May 2016 17:16:19 +0000
treeherdermozilla-inbound@d53726702252 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssfink
bugs996060
milestone49.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 996060 - Part 1: Save a stack on JSContext::setPendingException; r=sfink This commit captures the JS stack in `JSContext::setPendingException` and exposes a new JSAPI function for getting the stack: `JS::GetPendingExceptionStack`. This stack is cleared out when the pending exception is cleared.
js/src/jsapi-tests/testSavedStacks.cpp
js/src/jsapi.cpp
js/src/jsapi.h
js/src/jscntxt.cpp
js/src/jscntxt.h
js/src/jscntxtinlines.h
js/src/jsexn.cpp
js/src/jsiter.cpp
js/src/proxy/Wrapper.cpp
js/src/vm/GeneratorObject.cpp
js/src/vm/Interpreter.cpp
--- a/js/src/jsapi-tests/testSavedStacks.cpp
+++ b/js/src/jsapi-tests/testSavedStacks.cpp
@@ -1,14 +1,16 @@
 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
  * vim: set ts=8 sts=4 et sw=4 tw=99:
  * 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 "mozilla/ScopeExit.h"
+
 #include "jscompartment.h"
 #include "jsfriendapi.h"
 #include "jsstr.h"
 
 #include "builtin/TestingFunctions.h"
 #include "jsapi-tests/tests.h"
 #include "vm/ArrayObject.h"
 #include "vm/SavedStacks.h"
@@ -197,8 +199,105 @@ BEGIN_TEST(testSavedStacks_selfHostedFra
     CHECK(result == JS::SavedFrameResult::Ok);
     lin = str->ensureLinear(cx);
     CHECK(lin);
     CHECK(js::StringEqualsAscii(lin, "filename.js"));
 
     return true;
 }
 END_TEST(testSavedStacks_selfHostedFrames)
+
+BEGIN_TEST(test_JS_GetPendingExceptionStack)
+{
+    CHECK(js::DefineTestingFunctions(cx, global, false, false));
+
+    const char* sourceText =
+    //            1         2         3
+    //   123456789012345678901234567890123456789
+        "(function one() {                      \n"  // 1
+        "  (function two() {                    \n"  // 2
+        "    (function three() {                \n"  // 3
+        "      throw 5;                         \n"  // 4
+        "    }());                              \n"  // 5
+        "  }());                                \n"  // 6
+        "}())                                   \n"; // 7
+
+    JS::RootedValue val(cx);
+    JS::CompileOptions opts(cx);
+    opts.setFileAndLine("filename.js", 1U);
+    JS::ContextOptionsRef(cx).setDontReportUncaught(true);
+    JSContext* context = cx;
+    auto resetReportUncaught = mozilla::MakeScopeExit([=]() {
+        JS::ContextOptionsRef(context).setDontReportUncaught(false);
+    });
+    bool ok = JS::Evaluate(cx, opts, sourceText, strlen(sourceText), &val);
+
+    CHECK(!ok);
+    CHECK(JS_IsExceptionPending(cx));
+    CHECK(val.isUndefined());
+
+    JS::RootedObject stack(cx);
+    CHECK(JS::GetPendingExceptionStack(cx, &stack));
+    CHECK(stack);
+    CHECK(stack->is<js::SavedFrame>());
+    JS::Rooted<js::SavedFrame*> savedFrameStack(cx, &stack->as<SavedFrame>());
+
+    JS_GetPendingException(cx, &val);
+    CHECK(val.isInt32());
+    CHECK(val.toInt32() == 5);
+
+    struct {
+        uint32_t line;
+        uint32_t column;
+        const char* source;
+        const char* functionDisplayName;
+    } expected[] = {
+        { 4, 7, "filename.js", "three" },
+        { 3, 15, "filename.js", "two" },
+        { 2, 13, "filename.js", "one" },
+        { 1, 11, "filename.js", nullptr }
+    };
+
+    size_t i = 0;
+    for (JS::Handle<js::SavedFrame*> frame : js::SavedFrame::RootedRange(cx, savedFrameStack)) {
+        CHECK(i < 4);
+
+        // Line
+        uint32_t line = 123;
+        JS::SavedFrameResult result = JS::GetSavedFrameLine(cx, frame, &line,
+                                                            JS::SavedFrameSelfHosted::Exclude);
+        CHECK(result == JS::SavedFrameResult::Ok);
+        CHECK_EQUAL(line, expected[i].line);
+
+        // Column
+        uint32_t column = 123;
+        result = JS::GetSavedFrameColumn(cx, frame, &column,
+                                         JS::SavedFrameSelfHosted::Exclude);
+        CHECK(result == JS::SavedFrameResult::Ok);
+        CHECK_EQUAL(column, expected[i].column);
+
+        // Source
+        JS::RootedString str(cx);
+        result = JS::GetSavedFrameSource(cx, frame, &str, JS::SavedFrameSelfHosted::Exclude);
+        CHECK(result == JS::SavedFrameResult::Ok);
+        JSLinearString* linear = str->ensureLinear(cx);
+        CHECK(linear);
+        CHECK(js::StringEqualsAscii(linear, expected[i].source));
+
+        // Function display name
+        result = JS::GetSavedFrameFunctionDisplayName(cx, frame, &str,
+                                                      JS::SavedFrameSelfHosted::Exclude);
+        CHECK(result == JS::SavedFrameResult::Ok);
+        if (auto expectedName = expected[i].functionDisplayName) {
+            CHECK(str);
+            linear = str->ensureLinear(cx);
+            CHECK(linear);
+            CHECK(js::StringEqualsAscii(linear, expectedName));
+        } else {
+            CHECK(!str);
+        }
+
+        i++;
+    }
+
+    return true;
+}
+END_TEST(test_JS_GetPendingExceptionStack)
--- a/js/src/jsapi.cpp
+++ b/js/src/jsapi.cpp
@@ -5989,22 +5989,22 @@ JS_GetPendingException(JSContext* cx, Mu
     AssertHeapIsIdle(cx);
     CHECK_REQUEST(cx);
     if (!cx->isExceptionPending())
         return false;
     return cx->getPendingException(vp);
 }
 
 JS_PUBLIC_API(void)
-JS_SetPendingException(JSContext* cx, HandleValue value)
+JS_SetPendingException(JSContext* cx, HandleValue value, JS::ExceptionStackBehavior behavior)
 {
     AssertHeapIsIdle(cx);
     CHECK_REQUEST(cx);
     assertSameCompartment(cx, value);
-    cx->setPendingException(value);
+    cx->setPendingException(value, behavior);
 }
 
 JS_PUBLIC_API(void)
 JS_ClearPendingException(JSContext* cx)
 {
     AssertHeapIsIdle(cx);
     cx->clearPendingException();
 }
@@ -6697,8 +6697,16 @@ JS::GetObjectZone(JSObject* obj)
 }
 
 JS_PUBLIC_API(JS::TraceKind)
 JS::GCThingTraceKind(void* thing)
 {
     MOZ_ASSERT(thing);
     return static_cast<js::gc::Cell*>(thing)->getTraceKind();
 }
+
+
+JS_PUBLIC_API(bool)
+JS::GetPendingExceptionStack(JSContext* cx, JS::MutableHandleObject stackp)
+{
+    MOZ_ASSERT(cx);
+    return cx->getPendingExceptionStack(stackp);
+}
--- a/js/src/jsapi.h
+++ b/js/src/jsapi.h
@@ -5458,24 +5458,37 @@ JS_ObjectIsRegExp(JSContext* cx, JS::Han
 extern JS_PUBLIC_API(unsigned)
 JS_GetRegExpFlags(JSContext* cx, JS::HandleObject obj);
 
 extern JS_PUBLIC_API(JSString*)
 JS_GetRegExpSource(JSContext* cx, JS::HandleObject obj);
 
 /************************************************************************/
 
+namespace JS {
+
+enum class ExceptionStackBehavior: bool {
+    // Capture the current JS stack when setting the exception. It may be
+    // retrieved by JS::GetPendingExceptionStack.
+    Capture,
+    // Do not capture any stack.
+    DoNotCapture
+};
+
+} // namespace JS
+
 extern JS_PUBLIC_API(bool)
 JS_IsExceptionPending(JSContext* cx);
 
 extern JS_PUBLIC_API(bool)
 JS_GetPendingException(JSContext* cx, JS::MutableHandleValue vp);
 
 extern JS_PUBLIC_API(void)
-JS_SetPendingException(JSContext* cx, JS::HandleValue v);
+JS_SetPendingException(JSContext* cx, JS::HandleValue v,
+                       JS::ExceptionStackBehavior behavior = JS::ExceptionStackBehavior::Capture);
 
 extern JS_PUBLIC_API(void)
 JS_ClearPendingException(JSContext* cx);
 
 extern JS_PUBLIC_API(bool)
 JS_ReportPendingException(JSContext* cx);
 
 namespace JS {
@@ -5975,16 +5988,42 @@ SetOutOfMemoryCallback(JSRuntime* rt, Ou
  * Capture the current call stack as a chain of SavedFrame JSObjects, and set
  * |stackp| to the SavedFrame for the youngest stack frame, or nullptr if there
  * are no JS frames on the stack. If |maxFrameCount| is non-zero, capture at
  * most the youngest |maxFrameCount| frames.
  */
 extern JS_PUBLIC_API(bool)
 CaptureCurrentStack(JSContext* cx, MutableHandleObject stackp, unsigned maxFrameCount = 0);
 
+/**
+ * Get the `SavedFrame` stack object captured when the pending exception was set
+ * on the `JSContext`. This will correspond to a 'throw' statement in JS, or the
+ * state at which `JS_SetPendingException` is called by VM code or a JSAPI
+ * consumer.
+ *
+ * This is not the same stack as `e.stack` when `e` is an `Error` object. (That
+ * would be `JS::ExceptionStackOrNull`). That stack is the stack when the
+ * `Error` object was created, which could be much earlier than when it is
+ * thrown.
+ *
+ * Note that it is possible that `stackp` is null with a `true` return value
+ * when any of the following situations:
+ *   - There is no pending exception (and hence no stack for the pending
+ *     exception).
+ *   - The exception was set on the `cx` when there was no JS on
+ *     the stack (and therefore the stack is correctly empty).
+ *   - The exception being thrown is an out of memory exception.
+ *     a stack via `JS::ExceptionStackBehavior::DoNotCapture`.
+ *
+ * If `stackp` is non-null after this call, it is guaranteed to be in the same
+ * compartment that `cx` is in.
+ */
+extern JS_PUBLIC_API(MOZ_MUST_USE bool)
+GetPendingExceptionStack(JSContext* cx, MutableHandleObject stackp);
+
 /*
  * This is a utility function for preparing an async stack to be used
  * by some other object.  This may be used when you need to treat a
  * given stack trace as an async parent.  If you just need to capture
  * the current stack, async parents and all, use CaptureCurrentStack
  * instead.
  *
  * Here |asyncStack| is the async stack to prepare.  It is copied into
--- a/js/src/jscntxt.cpp
+++ b/js/src/jscntxt.cpp
@@ -297,17 +297,18 @@ js::ReportOutOfMemory(ExclusiveContext* 
     cx->runtime()->hadOutOfMemory = true;
     AutoSuppressGC suppressGC(cx);
 
     /* Report the oom. */
     if (JS::OutOfMemoryCallback oomCallback = cx->runtime()->oomCallback)
         oomCallback(cx, cx->runtime()->oomCallbackData);
 
     if (cx->options().autoJSAPIOwnsErrorReporting() || JS_IsRunning(cx)) {
-        cx->setPendingException(StringValue(cx->names().outOfMemory));
+        RootedValue v(cx, StringValue(cx->names().outOfMemory));
+        cx->setPendingException(v);
         return;
     }
 
     /* Get the message for this error, but we don't expand any arguments. */
     const JSErrorFormatString* efs = GetErrorMessage(nullptr, JSMSG_OUT_OF_MEMORY);
     const char* msg = efs ? efs->format : "Out of memory";
 
     /* Fill out the report, but don't do anything that requires allocation. */
@@ -956,16 +957,17 @@ ExclusiveContext::recoverFromOutOfMemory
     if (ParseTask* task = helperThread()->parseTask())
         task->outOfMemory = false;
 }
 
 JSContext::JSContext(JSRuntime* rt)
   : ExclusiveContext(rt, &rt->mainThread, Context_JS),
     throwing(false),
     unwrappedException_(this),
+    unwrappedPendingExceptionStack_(this),
     options_(),
     overRecursed_(false),
     propagatingForcedReturn_(false),
     liveVolatileJitFrameIterators_(nullptr),
     reportGranularity(JS_DEFAULT_JITREPORT_GRANULARITY),
     resolvingList(nullptr),
     generatingError(false),
     savedFrameChains_(),
@@ -992,22 +994,39 @@ JSContext::getPendingException(MutableHa
     rval.set(unwrappedException_);
     if (IsAtomsCompartment(compartment()))
         return true;
     bool wasOverRecursed = overRecursed_;
     clearPendingException();
     if (!compartment()->wrap(this, rval))
         return false;
     assertSameCompartment(this, rval);
-    setPendingException(rval);
+    setPendingException(rval, JS::ExceptionStackBehavior::DoNotCapture);
     overRecursed_ = wasOverRecursed;
     return true;
 }
 
 bool
+JSContext::getPendingExceptionStack(MutableHandleObject stackp)
+{
+    if (!unwrappedPendingExceptionStack_) {
+        stackp.set(nullptr);
+        return true;
+    }
+
+    stackp.set(unwrappedPendingExceptionStack_);
+    if (!compartment()->wrap(this, stackp)) {
+        stackp.set(nullptr);
+        return false;
+    }
+
+    return true;
+}
+
+bool
 JSContext::isThrowingOutOfMemory()
 {
     return throwing && unwrappedException_ == StringValue(names().outOfMemory);
 }
 
 bool
 JSContext::isClosingGenerator()
 {
--- a/js/src/jscntxt.h
+++ b/js/src/jscntxt.h
@@ -312,16 +312,22 @@ struct JSContext : public js::ExclusiveC
     friend class JS::AutoSaveExceptionState;
     friend class js::jit::DebugModeOSRVolatileJitFrameIterator;
     friend void js::ReportOverRecursed(JSContext*, unsigned errorNumber);
 
   private:
     /* Exception state -- the exception member is a GC root by definition. */
     bool                throwing;            /* is there a pending exception? */
     JS::PersistentRooted<JS::Value> unwrappedException_; /* most-recently-thrown exception */
+    /*
+     * The SavedFrame stack captured when we set a pending exception, and
+     * cleared out when we clear pending exceptions.
+     */
+    JS::PersistentRooted<js::SavedFrame*> unwrappedPendingExceptionStack_;
+
 
     /* Per-context options. */
     JS::ContextOptions  options_;
 
     // True if the exception currently being thrown is by result of
     // ReportOverRecursed. See Debugger::slowPathOnExceptionUnwind.
     bool                overRecursed_;
 
@@ -433,27 +439,32 @@ struct JSContext : public js::ExclusiveC
 
   public:
     bool isExceptionPending() {
         return throwing;
     }
 
     MOZ_MUST_USE
     bool getPendingException(JS::MutableHandleValue rval);
+    MOZ_MUST_USE
+    bool getPendingExceptionStack(JS::MutableHandleObject stackp);
 
     bool isThrowingOutOfMemory();
     bool isThrowingDebuggeeWouldRun();
     bool isClosingGenerator();
 
-    void setPendingException(js::Value v);
+    void setPendingException(
+        js::HandleValue v,
+        JS::ExceptionStackBehavior behavior = JS::ExceptionStackBehavior::Capture);
 
     void clearPendingException() {
         throwing = false;
         overRecursed_ = false;
         unwrappedException_.setUndefined();
+        unwrappedPendingExceptionStack_ = nullptr;
     }
 
     bool isThrowingOverRecursed() const { return throwing && overRecursed_; }
     bool isPropagatingForcedReturn() const { return propagatingForcedReturn_; }
     void setPropagatingForcedReturn() { propagatingForcedReturn_ = true; }
     void clearPropagatingForcedReturn() { propagatingForcedReturn_ = false; }
 
     /*
--- a/js/src/jscntxtinlines.h
+++ b/js/src/jscntxtinlines.h
@@ -360,18 +360,28 @@ inline LifoAlloc&
 ExclusiveContext::typeLifoAlloc()
 {
     return zone()->types.typeLifoAlloc;
 }
 
 }  /* namespace js */
 
 inline void
-JSContext::setPendingException(js::Value v)
+JSContext::setPendingException(js::HandleValue v, JS::ExceptionStackBehavior behavior)
 {
+    if (behavior == JS::ExceptionStackBehavior::Capture &&
+        compartment() &&
+        v != StringValue(names().outOfMemory))
+    {
+        js::RootedSavedFrame stack(this);
+        if (!compartment()->savedStacks().saveCurrentStack(this, &stack))
+            clearPendingException();
+        unwrappedPendingExceptionStack_ = stack;
+    }
+
     // 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/jsexn.cpp
+++ b/js/src/jsexn.cpp
@@ -673,17 +673,17 @@ js::ReportUncaughtException(JSContext* c
     cx->clearPendingException();
 
     ErrorReport err(cx);
     if (!err.init(cx, exn, js::ErrorReport::WithSideEffects)) {
         cx->clearPendingException();
         return false;
     }
 
-    cx->setPendingException(exn);
+    cx->setPendingException(exn, JS::ExceptionStackBehavior::DoNotCapture);
     CallErrorReporter(cx, err.message(), err.report());
     cx->clearPendingException();
     return true;
 }
 
 ErrorReport::ErrorReport(JSContext* cx)
   : reportp(nullptr),
     message_(nullptr),
--- a/js/src/jsiter.cpp
+++ b/js/src/jsiter.cpp
@@ -970,18 +970,20 @@ js::CreateItrResultObject(JSContext* cx,
 bool
 js::ThrowStopIteration(JSContext* cx)
 {
     MOZ_ASSERT(!JS_IsExceptionPending(cx));
 
     // StopIteration isn't a constructor, but it's stored in GlobalObject
     // as one, out of laziness. Hence the GetBuiltinConstructor call here.
     RootedObject ctor(cx);
-    if (GetBuiltinConstructor(cx, JSProto_StopIteration, &ctor))
-        cx->setPendingException(ObjectValue(*ctor));
+    if (GetBuiltinConstructor(cx, JSProto_StopIteration, &ctor)) {
+        RootedValue v(cx, ObjectValue(*ctor));
+        cx->setPendingException(v);
+    }
     return false;
 }
 
 /*** Iterator objects ****************************************************************************/
 
 bool
 js::IteratorConstructor(JSContext* cx, unsigned argc, Value* vp)
 {
@@ -1245,17 +1247,17 @@ js::UnwindIteratorForException(JSContext
 {
     RootedValue v(cx);
     bool getOk = cx->getPendingException(&v);
     cx->clearPendingException();
     if (!CloseIterator(cx, obj))
         return false;
     if (!getOk)
         return false;
-    cx->setPendingException(v);
+    cx->setPendingException(v, JS::ExceptionStackBehavior::DoNotCapture);
     return true;
 }
 
 void
 js::UnwindIteratorForUncatchableException(JSContext* cx, JSObject* obj)
 {
     if (obj->is<PropertyIteratorObject>()) {
         NativeIterator* ni = obj->as<PropertyIteratorObject>().getNativeIterator();
--- a/js/src/proxy/Wrapper.cpp
+++ b/js/src/proxy/Wrapper.cpp
@@ -405,13 +405,15 @@ ErrorCopier::~ErrorCopier()
         !cx->isThrowingDebuggeeWouldRun())
     {
         RootedValue exc(cx);
         if (cx->getPendingException(&exc) && exc.isObject() && exc.toObject().is<ErrorObject>()) {
             cx->clearPendingException();
             ac.reset();
             Rooted<ErrorObject*> errObj(cx, &exc.toObject().as<ErrorObject>());
             JSObject* copyobj = CopyErrorObject(cx, errObj);
-            if (copyobj)
-                cx->setPendingException(ObjectValue(*copyobj));
+            if (copyobj) {
+                RootedValue v(cx, ObjectValue(*copyobj));
+                cx->setPendingException(v, JS::ExceptionStackBehavior::DoNotCapture);
+            }
         }
     }
 }
--- a/js/src/vm/GeneratorObject.cpp
+++ b/js/src/vm/GeneratorObject.cpp
@@ -132,17 +132,18 @@ js::GeneratorThrowOrClose(JSContext* cx,
 
         if (genObj->is<StarGeneratorObject>()) {
             MOZ_ASSERT(arg.isObject());
             frame.setReturnValue(arg);
         } else {
             MOZ_ASSERT(arg.isUndefined());
         }
 
-        cx->setPendingException(MagicValue(JS_GENERATOR_CLOSING));
+        RootedValue v(cx, MagicValue(JS_GENERATOR_CLOSING));
+        cx->setPendingException(v);
         genObj->setClosing();
     }
     return false;
 }
 
 bool
 GeneratorObject::resume(JSContext* cx, InterpreterActivation& activation,
                         HandleObject obj, HandleValue arg, GeneratorObject::ResumeKind resumeKind)
--- a/js/src/vm/Interpreter.cpp
+++ b/js/src/vm/Interpreter.cpp
@@ -3629,17 +3629,18 @@ CASE(JSOP_RETSUB)
     MOZ_ASSERT(lval.isBoolean());
     if (lval.toBoolean()) {
         /*
          * Exception was pending during finally, throw it *before* we adjust
          * pc, because pc indexes into script->trynotes.  This turns out not to
          * be necessary, but it seems clearer.  And it points out a FIXME:
          * 350509, due to Igor Bukanov.
          */
-        cx->setPendingException(rval);
+        ReservedRooted<Value> v(&rootValue0, rval);
+        cx->setPendingException(v);
         goto error;
     }
     MOZ_ASSERT(rval.isInt32());
 
     /* Increment the PC by this much. */
     int32_t len = rval.toInt32() - int32_t(script->pcToOffset(REGS.pc));
     ADVANCE_AND_DISPATCH(len);
 }