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 346254 0916f44729ff5048a81117573c627089946b0c1a
parent 346253 4baf0f3644aae5a52847a2f2edbe33034847d02f
child 346255 3ad6f81739fb5ee9cfed230439415e1ce995f57e
push id6389
push userraliiev@mozilla.com
push dateMon, 19 Sep 2016 13:38:22 +0000
treeherdermozilla-beta@01d67bfe6c81 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbz, jimb
bugs1280818
milestone50.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 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.