Bug 1280818 part 1 - Add the ability to capture the stack until the first non-self-hosted frame with the given principals; r=bz,jimb
authorNick Fitzgerald <fitzgen@gmail.com>
Thu, 21 Jul 2016 23:40:59 -0400
changeset 331288 0916f44729ff5048a81117573c627089946b0c1a
parent 331287 4baf0f3644aae5a52847a2f2edbe33034847d02f
child 331289 3ad6f81739fb5ee9cfed230439415e1ce995f57e
push id9858
push userjlund@mozilla.com
push dateMon, 01 Aug 2016 14:37:10 +0000
treeherdermozilla-aurora@203106ef6cb6 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbz, jimb
bugs1280818
milestone50.0a1
Bug 1280818 part 1 - Add the ability to capture the stack until the first non-self-hosted frame with the given principals; r=bz,jimb Before this commit, one could either capture all stack frames (by passing maxFrameCount = 0) or a maximum of N frames (by passing maxFrameCount = N). This commit introduces the ability to capture the first frame (by default ignoring self hosted frames) with some target principals. This new option required replacing the `unsigned maxFrameCount` parameter with the introduction of a new sum type to describe the stack capturing behavior: StackCapture = AllFrames | MaxFrames(unsigned n) | FirstSubsumedFrame(JSPrincipals* p, bool ignoreSelfHosted) This is obviously more wordy in C++ than we'd like, but does make the stack capturing more explicit rather than relying on the sentinal 0 to stand in for infinity.
dom/bindings/Exceptions.cpp
js/src/builtin/Promise.cpp
js/src/builtin/TestingFunctions.cpp
js/src/jit-test/tests/saved-stacks/capture-first-frame-with-principals.js
js/src/jsapi.cpp
js/src/jsapi.h
js/src/jsexn.cpp
js/src/vm/SavedStacks.cpp
js/src/vm/SavedStacks.h
--- a/dom/bindings/Exceptions.cpp
+++ b/dom/bindings/Exceptions.cpp
@@ -668,17 +668,20 @@ already_AddRefed<nsIStackFrame>
 CreateStack(JSContext* aCx, int32_t aMaxDepth)
 {
   static const unsigned MAX_FRAMES = 100;
   if (aMaxDepth < 0) {
     aMaxDepth = MAX_FRAMES;
   }
 
   JS::Rooted<JSObject*> stack(aCx);
-  if (!JS::CaptureCurrentStack(aCx, &stack, aMaxDepth)) {
+  JS::StackCapture capture = aMaxDepth == 0
+    ? JS::StackCapture(JS::AllFrames())
+    : JS::StackCapture(JS::MaxFrames(aMaxDepth));
+  if (!JS::CaptureCurrentStack(aCx, &stack, mozilla::Move(capture))) {
     return nullptr;
   }
 
   if (!stack) {
     return nullptr;
   }
 
   nsCOMPtr<nsIStackFrame> frame = new JSStackFrame(stack);
--- a/js/src/builtin/Promise.cpp
+++ b/js/src/builtin/Promise.cpp
@@ -130,17 +130,17 @@ PromiseObject::create(JSContext* cx, Han
         promise->setFixedSlot(PROMISE_IS_HANDLED_SLOT,
                               Int32Value(PROMISE_IS_HANDLED_STATE_UNHANDLED));
 
         // Store an allocation stack so we can later figure out what the
         // control flow was for some unexpected results. Frightfully expensive,
         // but oh well.
         RootedObject stack(cx);
         if (cx->options().asyncStack() || cx->compartment()->isDebuggee()) {
-            if (!JS::CaptureCurrentStack(cx, &stack, 0))
+            if (!JS::CaptureCurrentStack(cx, &stack, JS::StackCapture(JS::AllFrames())))
                 return nullptr;
         }
         promise->setFixedSlot(PROMISE_ALLOCATION_SITE_SLOT, ObjectOrNullValue(stack));
         promise->setFixedSlot(PROMISE_ALLOCATION_TIME_SLOT,
                               DoubleValue(MillisecondsSinceStartup()));
     }
 
     RootedValue promiseVal(cx, ObjectValue(*promise));
@@ -420,17 +420,17 @@ PromiseObject::reject(JSContext* cx, Han
 }
 
 void
 PromiseObject::onSettled(JSContext* cx)
 {
     Rooted<PromiseObject*> promise(cx, this);
     RootedObject stack(cx);
     if (cx->options().asyncStack() || cx->compartment()->isDebuggee()) {
-        if (!JS::CaptureCurrentStack(cx, &stack, 0)) {
+        if (!JS::CaptureCurrentStack(cx, &stack, JS::StackCapture(JS::AllFrames()))) {
             cx->clearPendingException();
             return;
         }
     }
     promise->setFixedSlot(PROMISE_RESOLUTION_SITE_SLOT, ObjectOrNullValue(stack));
     promise->setFixedSlot(PROMISE_RESOLUTION_TIME_SLOT, DoubleValue(MillisecondsSinceStartup()));
 
     if (promise->state() == JS::PromiseState::Rejected &&
--- a/js/src/builtin/TestingFunctions.cpp
+++ b/js/src/builtin/TestingFunctions.cpp
@@ -1098,28 +1098,29 @@ GetSavedFrameCount(JSContext* cx, unsign
     return true;
 }
 
 static bool
 SaveStack(JSContext* cx, unsigned argc, Value* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
 
-    unsigned maxFrameCount = 0;
+    JS::StackCapture capture((JS::AllFrames()));
     if (args.length() >= 1) {
         double d;
         if (!ToNumber(cx, args[0], &d))
             return false;
         if (d < 0) {
             ReportValueErrorFlags(cx, JSREPORT_ERROR, JSMSG_UNEXPECTED_TYPE,
                                   JSDVG_SEARCH_STACK, args[0], nullptr,
                                   "not a valid maximum frame count", NULL);
             return false;
         }
-        maxFrameCount = d;
+        if (d > 0)
+            capture = JS::StackCapture(JS::MaxFrames(d));
     }
 
     JSCompartment* targetCompartment = cx->compartment();
     if (args.length() >= 2) {
         if (!args[1].isObject()) {
             ReportValueErrorFlags(cx, JSREPORT_ERROR, JSMSG_UNEXPECTED_TYPE,
                                   JSDVG_SEARCH_STACK, args[0], nullptr,
                                   "not an object", NULL);
@@ -1129,28 +1130,59 @@ SaveStack(JSContext* cx, unsigned argc, 
         if (!obj)
             return false;
         targetCompartment = obj->compartment();
     }
 
     RootedObject stack(cx);
     {
         AutoCompartment ac(cx, targetCompartment);
-        if (!JS::CaptureCurrentStack(cx, &stack, maxFrameCount))
+        if (!JS::CaptureCurrentStack(cx, &stack, mozilla::Move(capture)))
             return false;
     }
 
     if (stack && !cx->compartment()->wrap(cx, &stack))
         return false;
 
     args.rval().setObjectOrNull(stack);
     return true;
 }
 
 static bool
+CaptureFirstSubsumedFrame(JSContext* cx, unsigned argc, JS::Value* vp)
+{
+    CallArgs args = CallArgsFromVp(argc, vp);
+    if (!args.requireAtLeast(cx, "captureFirstSubsumedFrame", 1))
+        return false;
+
+    if (!args[0].isObject()) {
+        JS_ReportError(cx, "The argument must be an object");
+        return false;
+    }
+
+    RootedObject obj(cx, &args[0].toObject());
+    obj = CheckedUnwrap(obj);
+    if (!obj) {
+        JS_ReportError(cx, "Denied permission to object.");
+        return false;
+    }
+
+    JS::StackCapture capture(JS::FirstSubsumedFrame(cx, obj->compartment()->principals()));
+    if (args.length() > 1)
+        capture.as<JS::FirstSubsumedFrame>().ignoreSelfHosted = JS::ToBoolean(args[1]);
+
+    JS::RootedObject capturedStack(cx);
+    if (!JS::CaptureCurrentStack(cx, &capturedStack, mozilla::Move(capture)))
+        return false;
+
+    args.rval().setObjectOrNull(capturedStack);
+    return true;
+}
+
+static bool
 CallFunctionFromNativeFrame(JSContext* cx, unsigned argc, Value* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
 
     if (args.length() != 1) {
         JS_ReportError(cx, "The function takes exactly one argument.");
         return false;
     }
@@ -3598,16 +3630,22 @@ static const JSFunctionSpecWithHelp Test
 "  SavedStacks cache."),
 
     JS_FN_HELP("saveStack", SaveStack, 0, 0,
 "saveStack([maxDepth [, compartment]])",
 "  Capture a stack. If 'maxDepth' is given, capture at most 'maxDepth' number\n"
 "  of frames. If 'compartment' is given, allocate the js::SavedFrame instances\n"
 "  with the given object's compartment."),
 
+    JS_FN_HELP("captureFirstSubsumedFrame", CaptureFirstSubsumedFrame, 1, 0,
+"saveStack(object [, shouldIgnoreSelfHosted = true]])",
+"  Capture a stack back to the first frame whose principals are subsumed by the\n"
+"  object's compartment's principals. If 'shouldIgnoreSelfHosted' is given,\n"
+"  control whether self-hosted frames are considered when checking principals."),
+
     JS_FN_HELP("callFunctionFromNativeFrame", CallFunctionFromNativeFrame, 1, 0,
 "callFunctionFromNativeFrame(function)",
 "  Call 'function' with a (C++-)native frame on stack.\n"
 "  Required for testing that SaveStack properly handles native frames."),
 
     JS_FN_HELP("callFunctionWithAsyncStack", CallFunctionWithAsyncStack, 0, 0,
 "callFunctionWithAsyncStack(function, stack, asyncCause)",
 "  Call 'function', using the provided stack as the async stack responsible\n"
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/saved-stacks/capture-first-frame-with-principals.js
@@ -0,0 +1,92 @@
+// Create two different globals whose compartments have two different
+// principals. Test getting the first frame on the stack with some given
+// principals in various configurations of JS stack and of wanting self-hosted
+// frames or not.
+
+const g1 = newGlobal({
+  principal: 0xffff
+});
+
+const g2 = newGlobal({
+  principal: 0xff
+});
+
+// Introduce everyone to themselves and each other.
+g1.g2 = g2.g2 = g2;
+g1.g1 = g2.g1 = g1;
+
+g1.g2obj = g2.eval("new Object");
+
+g1.evaluate(`
+  const global = this;
+
+  // Capture the stack back to the first frame in the g2 global.
+  function capture(shouldIgnoreSelfHosted = true) {
+    return captureFirstSubsumedFrame(global.g2obj, shouldIgnoreSelfHosted);
+  }
+`, {
+  fileName: "script1.js"
+});
+
+g2.evaluate(`
+  const capture = g1.capture;
+
+  // Use our Function.prototype.bind, not capture.bind (which is ===
+  // g1.Function.prototype.bind) so that the generated bound function is in our
+  // compartment and has our principals.
+  const boundTrue = Function.prototype.bind.call(capture, null, true);
+  const boundFalse = Function.prototype.bind.call(capture, null, false);
+
+  function getOldestFrame(stack) {
+    while (stack.parent) {
+      stack = stack.parent;
+    }
+    return stack;
+  }
+
+  function dumpStack(name, stack) {
+    print("Stack " + name + " =");
+    while (stack) {
+      print("    " + stack.functionDisplayName + " @ " + stack.source);
+      stack = stack.parent;
+    }
+    print();
+  }
+
+  // When the youngest frame is not self-hosted, it doesn't matter whether or not
+  // we specify that we should ignore self hosted frames when capturing the first
+  // frame with the given principals.
+  //
+  // Stack: iife1 (g2) <- capture (g1)
+
+  (function iife1() {
+    const captureTrueStack = capture(true);
+    dumpStack("captureTrueStack", captureTrueStack);
+    assertEq(getOldestFrame(captureTrueStack).functionDisplayName, "iife1");
+    assertEq(getOldestFrame(captureTrueStack).source, "script2.js");
+
+    const captureFalseStack = capture(false);
+    dumpStack("captureFalseStack", captureFalseStack);
+    assertEq(getOldestFrame(captureFalseStack).functionDisplayName, "iife1");
+    assertEq(getOldestFrame(captureFalseStack).source, "script2.js");
+  }());
+
+  // When the youngest frame is a self hosted frame, we get two different
+  // captured stacks depending on whether or not we ignore self-hosted frames.
+  //
+  // Stack: iife2 (g2) <- bound function (g2) <- capture (g1)
+
+  (function iife2() {
+    const boundTrueStack = boundTrue();
+    dumpStack("boundTrueStack", boundTrueStack);
+    assertEq(getOldestFrame(boundTrueStack).functionDisplayName, "iife2");
+    assertEq(getOldestFrame(boundTrueStack).source, "script2.js");
+
+    const boundFalseStack = boundFalse();
+    dumpStack("boundFalseStack", boundFalseStack);
+    assertEq(getOldestFrame(boundFalseStack).functionDisplayName !== "iife2", true);
+    assertEq(getOldestFrame(boundFalseStack).source, "self-hosted");
+  }());
+`, {
+  fileName: "script2.js"
+});
--- a/js/src/jsapi.cpp
+++ b/js/src/jsapi.cpp
@@ -6531,26 +6531,32 @@ JS::SetLargeAllocationFailureCallback(JS
 
 JS_PUBLIC_API(void)
 JS::SetOutOfMemoryCallback(JSContext* cx, OutOfMemoryCallback cb, void* data)
 {
     cx->oomCallback = cb;
     cx->oomCallbackData = data;
 }
 
-JS_PUBLIC_API(bool)
-JS::CaptureCurrentStack(JSContext* cx, JS::MutableHandleObject stackp, unsigned maxFrameCount)
+JS::FirstSubsumedFrame::FirstSubsumedFrame(JSContext* cx,
+                                           bool ignoreSelfHostedFrames /* = true */)
+  : JS::FirstSubsumedFrame(cx, cx->compartment()->principals(), ignoreSelfHostedFrames)
+{ }
+
+JS_PUBLIC_API(bool)
+JS::CaptureCurrentStack(JSContext* cx, JS::MutableHandleObject stackp,
+                        JS::StackCapture&& capture /* = JS::StackCapture(JS::AllFrames()) */)
 {
     AssertHeapIsIdle(cx);
     CHECK_REQUEST(cx);
     MOZ_RELEASE_ASSERT(cx->compartment());
 
     JSCompartment* compartment = cx->compartment();
     Rooted<SavedFrame*> frame(cx);
-    if (!compartment->savedStacks().saveCurrentStack(cx, &frame, maxFrameCount))
+    if (!compartment->savedStacks().saveCurrentStack(cx, &frame, mozilla::Move(capture)))
         return false;
     stackp.set(frame.get());
     return true;
 }
 
 JS_PUBLIC_API(bool)
 JS::CopyAsyncStack(JSContext* cx, JS::HandleObject asyncStack,
                    JS::HandleString asyncCause, JS::MutableHandleObject stackp,
--- a/js/src/jsapi.h
+++ b/js/src/jsapi.h
@@ -5892,23 +5892,105 @@ SetLargeAllocationFailureCallback(JSCont
 
 typedef void
 (* OutOfMemoryCallback)(JSContext* cx, void* data);
 
 extern JS_PUBLIC_API(void)
 SetOutOfMemoryCallback(JSContext* cx, OutOfMemoryCallback cb, void* data);
 
 /**
+ * Capture all frames.
+ */
+struct AllFrames { };
+
+/**
+ * Capture at most this many frames.
+ */
+struct MaxFrames
+{
+    unsigned maxFrames;
+
+    explicit MaxFrames(unsigned max)
+      : maxFrames(max)
+    {
+        MOZ_ASSERT(max > 0);
+    }
+};
+
+/**
+ * Capture the first frame with the given principals. By default, do not
+ * consider self-hosted frames with the given principals as satisfying the stack
+ * capture.
+ */
+struct FirstSubsumedFrame
+{
+    JSContext* cx;
+    JSPrincipals* principals;
+    bool ignoreSelfHosted;
+
+    /**
+     * Use the cx's current compartment's principals.
+     */
+    explicit FirstSubsumedFrame(JSContext* cx, bool ignoreSelfHostedFrames = true);
+
+    explicit FirstSubsumedFrame(JSContext* ctx, JSPrincipals* p, bool ignoreSelfHostedFrames = true)
+      : cx(ctx)
+      , principals(p)
+      , ignoreSelfHosted(ignoreSelfHostedFrames)
+    {
+        JS_HoldPrincipals(principals);
+    }
+
+    // No copying because we want to avoid holding and dropping principals
+    // unnecessarily.
+    FirstSubsumedFrame(const FirstSubsumedFrame&) = delete;
+    FirstSubsumedFrame& operator=(const FirstSubsumedFrame&) = delete;
+
+    FirstSubsumedFrame(FirstSubsumedFrame&& rhs)
+      : principals(rhs.principals)
+      , ignoreSelfHosted(rhs.ignoreSelfHosted)
+    {
+        MOZ_ASSERT(this != &rhs, "self move disallowed");
+        rhs.principals = nullptr;
+    }
+
+    FirstSubsumedFrame& operator=(FirstSubsumedFrame&& rhs) {
+        new (this) FirstSubsumedFrame(mozilla::Move(rhs));
+        return *this;
+    }
+
+    ~FirstSubsumedFrame() {
+        if (principals)
+            JS_DropPrincipals(cx, principals);
+    }
+};
+
+using StackCapture = mozilla::Variant<AllFrames, MaxFrames, FirstSubsumedFrame>;
+
+/**
  * 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);
+ * are no JS frames on the stack.
+ *
+ * The |capture| parameter describes the portion of the JS stack to capture:
+ *
+ *   * |JS::AllFrames|: Capture all frames on the stack.
+ *
+ *   * |JS::MaxFrames|: Capture no more than |JS::MaxFrames::maxFrames| from the
+ *      stack.
+ *
+ *   * |JS::FirstSubsumedFrame|: Capture the first frame whose principals are
+ *     subsumed by |JS::FirstSubsumedFrame::principals|. By default, do not
+ *     consider self-hosted frames; this can be controlled via the
+ *     |JS::FirstSubsumedFrame::ignoreSelfHosted| flag. Do not capture any async
+ *     stack.
+ */
+extern JS_PUBLIC_API(bool)
+CaptureCurrentStack(JSContext* cx, MutableHandleObject stackp,
+                    StackCapture&& capture = StackCapture(AllFrames()));
 
 /*
  * 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.
  *
--- a/js/src/jsexn.cpp
+++ b/js/src/jsexn.cpp
@@ -261,17 +261,18 @@ struct SuppressErrorsGuard
 
 // Cut off the stack if it gets too deep (most commonly for infinite recursion
 // errors).
 static const size_t MAX_REPORTED_STACK_DEPTH = 1u << 7;
 
 static bool
 CaptureStack(JSContext* cx, MutableHandleObject stack)
 {
-    return CaptureCurrentStack(cx, stack, MAX_REPORTED_STACK_DEPTH);
+    return CaptureCurrentStack(cx, stack,
+                               JS::StackCapture(JS::MaxFrames(MAX_REPORTED_STACK_DEPTH)));
 }
 
 JSString*
 js::ComputeStackString(JSContext* cx)
 {
     SuppressErrorsGuard seg(cx);
 
     RootedObject stack(cx);
--- a/js/src/vm/SavedStacks.cpp
+++ b/js/src/vm/SavedStacks.cpp
@@ -1063,34 +1063,35 @@ SavedFrame::toStringMethod(JSContext* cx
 bool
 SavedStacks::init()
 {
     return frames.init() &&
            pcLocationMap.init();
 }
 
 bool
-SavedStacks::saveCurrentStack(JSContext* cx, MutableHandleSavedFrame frame, unsigned maxFrameCount)
+SavedStacks::saveCurrentStack(JSContext* cx, MutableHandleSavedFrame frame,
+                              JS::StackCapture&& capture /* = JS::StackCapture(JS::AllFrames()) */)
 {
     MOZ_ASSERT(initialized());
     MOZ_RELEASE_ASSERT(cx->compartment());
     assertSameCompartment(cx, this);
 
     if (creatingSavedFrame ||
         cx->isExceptionPending() ||
         !cx->global() ||
         !cx->global()->isStandardClassResolved(JSProto_Object))
     {
         frame.set(nullptr);
         return true;
     }
 
     AutoSPSEntry psuedoFrame(cx->runtime(), "js::SavedStacks::saveCurrentStack");
     FrameIter iter(cx);
-    return insertFrames(cx, iter, frame, maxFrameCount);
+    return insertFrames(cx, iter, frame, mozilla::Move(capture));
 }
 
 bool
 SavedStacks::copyAsyncStack(JSContext* cx, HandleObject asyncStack, HandleString asyncCause,
                             MutableHandleSavedFrame adoptedStack, unsigned maxFrameCount)
 {
     MOZ_ASSERT(initialized());
     MOZ_RELEASE_ASSERT(cx->compartment());
@@ -1132,19 +1133,59 @@ SavedStacks::clear()
 
 size_t
 SavedStacks::sizeOfExcludingThis(mozilla::MallocSizeOf mallocSizeOf)
 {
     return frames.sizeOfExcludingThis(mallocSizeOf) +
            pcLocationMap.sizeOfExcludingThis(mallocSizeOf);
 }
 
+// Given that we have captured a stqck frame with the given principals and
+// source, return true if the requested `StackCapture` has been satisfied and
+// stack walking can halt. Return false otherwise (and stack walking and frame
+// capturing should continue).
+static inline bool
+captureIsSatisfied(JSContext* cx, JSPrincipals* principals, const JSAtom* source,
+                   JS::StackCapture& capture)
+{
+    class Matcher
+    {
+        JSContext* cx_;
+        JSPrincipals* framePrincipals_;
+        const JSAtom* frameSource_;
+
+      public:
+        Matcher(JSContext* cx, JSPrincipals* principals, const JSAtom* source)
+          : cx_(cx)
+          , framePrincipals_(principals)
+          , frameSource_(source)
+        { }
+
+        bool match(JS::FirstSubsumedFrame& target) {
+            auto subsumes = cx_->runtime()->securityCallbacks->subsumes;
+            return (!subsumes || subsumes(target.principals, framePrincipals_)) &&
+                   (!target.ignoreSelfHosted || frameSource_ != cx_->names().selfHosted);
+        }
+
+        bool match(JS::MaxFrames& target) {
+            return target.maxFrames == 1;
+        }
+
+        bool match(JS::AllFrames&) {
+            return false;
+        }
+    };
+
+    Matcher m(cx, principals, source);
+    return capture.match(m);
+}
+
 bool
 SavedStacks::insertFrames(JSContext* cx, FrameIter& iter, MutableHandleSavedFrame frame,
-                          unsigned maxFrameCount)
+                          JS::StackCapture&& capture)
 {
     // In order to lookup a cached SavedFrame object, we need to have its parent
     // SavedFrame, which means we need to walk the stack from oldest frame to
     // youngest. However, FrameIter walks the stack from youngest frame to
     // oldest. The solution is to append stack frames to a vector as we walk the
     // stack with FrameIter, and then do a second pass through that vector in
     // reverse order after the traversal has completed and get or create the
     // SavedFrame objects at that time.
@@ -1211,80 +1252,83 @@ SavedStacks::insertFrames(JSContext* cx,
         {
             AutoCompartment ac(cx, iter.compartment());
             if (!cx->compartment()->savedStacks().getLocation(cx, iter, &location))
                 return false;
         }
 
         // The bit set means that the next older parent (frame, pc) pair *must*
         // be in the cache.
-        if (maxFrameCount == 0)
+        if (capture.is<JS::AllFrames>())
             parentIsInCache = iter.hasCachedSavedFrame();
 
+        auto principals = iter.compartment()->principals();
         auto displayAtom = iter.isFunctionFrame() ? iter.functionDisplayAtom() : nullptr;
         if (!stackChain->emplaceBack(location.source(),
                                      location.line(),
                                      location.column(),
                                      displayAtom,
                                      nullptr,
                                      nullptr,
-                                     iter.compartment()->principals(),
+                                     principals,
                                      LiveSavedFrameCache::getFramePtr(iter),
                                      iter.pc(),
                                      &activation))
         {
             ReportOutOfMemory(cx);
             return false;
         }
 
-        ++iter;
-
-        if (maxFrameCount == 1) {
+        if (captureIsSatisfied(cx, principals, location.source(), capture)) {
             // The frame we just saved was the last one we were asked to save.
             // If we had an async stack, ensure we don't use any of its frames.
             asyncStack.set(nullptr);
             break;
         }
 
+        ++iter;
+
         if (parentIsInCache &&
             !iter.done() &&
             iter.hasCachedSavedFrame())
         {
             auto* cache = activation.getLiveSavedFrameCache(cx);
             if (!cache)
                 return false;
             cache->find(cx, iter, &cachedFrame);
             if (cachedFrame)
                 break;
         }
 
-        // If maxFrameCount is zero there's no limit on the number of frames.
-        if (maxFrameCount == 0)
-            continue;
-
-        maxFrameCount--;
+        if (capture.is<JS::MaxFrames>())
+            capture.as<JS::MaxFrames>().maxFrames--;
     }
 
     // Limit the depth of the async stack, if any, and ensure that the
     // SavedFrame instances we use are stored in the same compartment as the
     // rest of the synchronous stack chain.
     RootedSavedFrame parentFrame(cx, cachedFrame);
-    if (asyncStack && !adoptAsyncStack(cx, asyncStack, asyncCause, &parentFrame, maxFrameCount))
-        return false;
+    if (asyncStack && !capture.is<JS::FirstSubsumedFrame>()) {
+        unsigned maxAsyncFrames = capture.is<JS::MaxFrames>()
+            ? capture.as<JS::MaxFrames>().maxFrames
+            : ASYNC_STACK_MAX_FRAME_COUNT;
+        if (!adoptAsyncStack(cx, asyncStack, asyncCause, &parentFrame, maxAsyncFrames))
+            return false;
+    }
 
     // Iterate through |stackChain| in reverse order and get or create the
     // actual SavedFrame instances.
     for (size_t i = stackChain->length(); i != 0; i--) {
         SavedFrame::HandleLookup lookup = stackChain[i-1];
         lookup->parent = parentFrame;
         parentFrame.set(getOrCreateSavedFrame(cx, lookup));
         if (!parentFrame)
             return false;
 
-        if (maxFrameCount == 0 && lookup->framePtr && parentFrame != cachedFrame) {
+        if (capture.is<JS::AllFrames>() && lookup->framePtr && parentFrame != cachedFrame) {
             auto* cache = lookup->activation->getLiveSavedFrameCache(cx);
             if (!cache || !cache->insert(cx, *lookup->framePtr, lookup->pc, parentFrame))
                 return false;
         }
     }
 
     frame.set(parentFrame);
     return true;
--- a/js/src/vm/SavedStacks.h
+++ b/js/src/vm/SavedStacks.h
@@ -160,17 +160,17 @@ class SavedStacks {
         bernoulliSeeded(false),
         bernoulli(1.0, 0x59fdad7f6b4cc573, 0x91adf38db96a9354),
         creatingSavedFrame(false)
     { }
 
     MOZ_MUST_USE bool init();
     bool initialized() const { return frames.initialized(); }
     MOZ_MUST_USE bool saveCurrentStack(JSContext* cx, MutableHandleSavedFrame frame,
-                                       unsigned maxFrameCount = 0);
+                                       JS::StackCapture&& capture = JS::StackCapture(JS::AllFrames()));
     MOZ_MUST_USE bool copyAsyncStack(JSContext* cx, HandleObject asyncStack,
                                      HandleString asyncCause,
                                      MutableHandleSavedFrame adoptedStack,
                                      unsigned maxFrameCount = 0);
     void sweep();
     void trace(JSTracer* trc);
     uint32_t count();
     void clear();
@@ -216,17 +216,17 @@ class SavedStacks {
         ~AutoReentrancyGuard()
         {
             stacks.creatingSavedFrame = false;
         }
     };
 
     MOZ_MUST_USE bool insertFrames(JSContext* cx, FrameIter& iter,
                                    MutableHandleSavedFrame frame,
-                                   unsigned maxFrameCount = 0);
+                                   JS::StackCapture&& capture);
     MOZ_MUST_USE bool adoptAsyncStack(JSContext* cx, HandleSavedFrame asyncStack,
                                       HandleString asyncCause,
                                       MutableHandleSavedFrame adoptedStack,
                                       unsigned maxFrameCount);
     SavedFrame* getOrCreateSavedFrame(JSContext* cx, SavedFrame::HandleLookup lookup);
     SavedFrame* createFrameFromLookup(JSContext* cx, SavedFrame::HandleLookup lookup);
 
     // Cache for memoizing PCToLineNumber lookups.