From 2c17d3b90e3884463663f2867a68ad2e6af5b686 Mon Sep 17 00:00:00 2001 From: Matthew Gaudet Date: Thu, 16 Jan 2025 15:31:28 +0000 Subject: [PATCH] Bug 1886820 - Add experimental support for Error.captureStackTrace r=jandem Differential Revision: https://phabricator.services.mozilla.com/D230327 --- js/public/Stack.h | 5 +- .../tests/errors/capture-stack-jit.js | 41 +++ js/src/jit-test/tests/errors/capture-stack.js | 344 ++++++++++++++++++ js/src/jsapi.cpp | 6 +- js/src/jsexn.cpp | 4 - js/src/jsexn.h | 4 + js/src/vm/CommonPropertyNames.h | 1 + js/src/vm/ErrorObject.cpp | 84 +++++ js/src/vm/JSObject.cpp | 6 + js/src/vm/SavedStacks.cpp | 39 +- js/src/vm/SavedStacks.h | 6 +- 11 files changed, 520 insertions(+), 20 deletions(-) create mode 100644 js/src/jit-test/tests/errors/capture-stack-jit.js create mode 100644 js/src/jit-test/tests/errors/capture-stack.js diff --git a/js/public/Stack.h b/js/public/Stack.h index 76fe482fd9d6..bf9c845615ce 100644 --- a/js/public/Stack.h +++ b/js/public/Stack.h @@ -19,7 +19,7 @@ #include "js/NativeStackLimits.h" #include "js/Principals.h" // JSPrincipals, JS_HoldPrincipals, JS_DropPrincipals -#include "js/TypeDecls.h" // JSContext, Handle*, MutableHandle* +#include "js/RootingAPI.h" /** * Set the size of the native stack that should not be exceed. To disable @@ -142,7 +142,8 @@ using StackCapture = mozilla::Variant; */ extern JS_PUBLIC_API bool CaptureCurrentStack( JSContext* cx, MutableHandleObject stackp, - StackCapture&& capture = StackCapture(AllFrames())); + StackCapture&& capture = StackCapture(AllFrames()), + HandleObject startAfter = nullptr); /** * Returns true if capturing stack trace data to associate with an asynchronous diff --git a/js/src/jit-test/tests/errors/capture-stack-jit.js b/js/src/jit-test/tests/errors/capture-stack-jit.js new file mode 100644 index 000000000000..28c382331913 --- /dev/null +++ b/js/src/jit-test/tests/errors/capture-stack-jit.js @@ -0,0 +1,41 @@ +// |jit-test| --setpref=experimental.error_capture_stack_trace; --no-threads; --fast-warmup; +load(libdir + "asserts.js"); + +function caller(f) { + return f(); +} + + +function fill() { + let x = {} + Error.captureStackTrace(x, caller); + return x; +} + +let iter = 1500 +for (let i = 0; i < iter; i++) { + // Make sure caller is an IonFrame. + caller(fill); +} + + +function test_jit_elision() { + let x = caller(fill); + let { stack } = x; + print(stack); + assertEq(stack.includes("caller"), false); + assertEq(stack.includes("fill"), false); +} + +function test_jit_elision2() { + ({ stack } = caller(() => { + let x = caller(fill); + return x; + })); + assertEq(stack.includes("caller"), true); // Only elide the first caller! + assertEq(stack.includes("fill"), false); +} + +test_jit_elision(); +test_jit_elision2(); + diff --git a/js/src/jit-test/tests/errors/capture-stack.js b/js/src/jit-test/tests/errors/capture-stack.js new file mode 100644 index 000000000000..8b7724be581d --- /dev/null +++ b/js/src/jit-test/tests/errors/capture-stack.js @@ -0,0 +1,344 @@ +// |jit-test| --setpref=experimental.error_capture_stack_trace; +load(libdir + "asserts.js"); + +assertEq('captureStackTrace' in Error, true); +assertEq(Error.captureStackTrace.length, 2); + +let x = Error.captureStackTrace({}); +assertEq(x, undefined); + +assertThrowsInstanceOf(() => Error.captureStackTrace(), TypeError); +assertThrowsInstanceOf(() => Error.captureStackTrace(2), TypeError); + +Error.captureStackTrace({}, 2); +Error.captureStackTrace({}, null); +Error.captureStackTrace({}, {}); + +function caller(f) { + return f(); +} + +function fill() { + let x = {} + Error.captureStackTrace(x, caller); + return x; +} + +function test_elision() { + let x = caller(fill); + let { stack } = x; + assertEq(stack.includes("caller"), false); + assertEq(stack.includes("fill"), false); + + + ({ stack } = caller(() => caller(fill))) + print(stack); + assertEq(stack.includes("caller"), true); // Only elide the first caller! + assertEq(stack.includes("fill"), false); +} + +test_elision(); + +function nestedLambda(f) { + (() => { + (() => { + (() => { + (() => { + f(); + })(); + })(); + })(); + })(); +} + + +// If we never see a matching frame when requesting a truncated +// stack we should return the empty string +function test_no_match() { + let obj = {}; + // test_elision chosen arbitrarily as a function object which + // doesn't exist in the call stack here. + let capture = () => Error.captureStackTrace(obj, test_elision); + nestedLambda(capture); + assertEq(obj.stack, ""); +} +test_no_match() + +function count_frames(str) { + return str.split("\n").length +} + +function test_nofilter() { + let obj = {}; + let capture = () => Error.captureStackTrace(obj); + nestedLambda(capture); + assertEq(count_frames(obj.stack), 9); +} +test_nofilter(); + +function test_in_eval() { + let obj = eval(` + let obj = {}; + let capture = () => Error.captureStackTrace(obj); + nestedLambda(capture); + obj + `) + + // Same as above, with an eval frame added! + assertEq(count_frames(obj.stack), 10); +} +test_in_eval(); + +// +// [[ErrorData]] +// +const stackGetter = Object.getOwnPropertyDescriptor(Error.prototype, 'stack').get; +const getStack = function (obj) { + return stackGetter.call(obj); +}; + +function test_uncensored() { + let err = undefined; + function create_err() { + err = new Error; + Error.captureStackTrace(err, test_uncensored); + } + + nestedLambda(create_err); + + // Calling Error.captureStackTrace doesn't mess with the internal + // [[ErrorData]] slot + assertEq(count_frames(err.stack), 2); + assertEq(count_frames(getStack(err)), 9) +} +test_uncensored() + +// In general, the stacks a non-caller version of Error.captureStackStrace +// should match what Error gives you +function compare_stacks() { + function censor_column(str) { + return str.replace(/:(\d+):\d+\n/g, ":$1:censored\n") + } + + let obj = {}; + let err = (Error.captureStackTrace(obj), new Error) + assertEq(censor_column(err.stack), censor_column(obj.stack)); +} +compare_stacks(); +nestedLambda(compare_stacks) + +// New global + +function test_in_global(global) { + global.evaluate(caller.toString()); + global.evaluate(fill.toString()); + global.evaluate(test_elision.toString()); + global.evaluate("test_elision()"); + + global.evaluate(nestedLambda.toString()) + global.evaluate(test_no_match.toString()); + global.evaluate("test_no_match()"); + + + global.evaluate(compare_stacks.toString()); + global.evaluate(` + compare_stacks(); + nestedLambda(compare_stacks) + `) +} + +let global = newGlobal(); +test_in_global(global); + +let global2 = newGlobal({ principal: 0 }); +test_in_global(global2) + +let global3 = newGlobal({ principal: 0xfffff }); +test_in_global(global3) + +// What if the caller is a proxy? +const caller_proxy = new Proxy(caller, { + apply: function (target, thisArg, arguments) { + return target(...arguments); + } +}); + +function fill_proxy() { + let x = {} + Error.captureStackTrace(x, caller_proxy); + return x; +} + +// Proxies don't count for elision. +function test_proxy_elision() { + let x = caller_proxy(fill_proxy); + let { stack } = x; + assertEq(stack.includes("caller"), true); + assertEq(stack.includes("fill_proxy"), true); +} +test_proxy_elision(); + +const trivial_proxy = new Proxy(caller, {}); +function fill_trivial() { + let x = {} + Error.captureStackTrace(x, trivial_proxy); + return x; +} + +// Elision doesn't work even on forwarding proxy +function test_trivial_elision() { + let x = caller(fill_trivial); + let { stack } = x; + assertEq(stack.includes("caller"), true); + assertEq(stack.includes("fill"), true); +} +test_trivial_elision(); + +// Elision happens through bind +function test_bind_elision() { + let b = caller.bind(undefined, fill); + let { stack } = b(); + assertEq(stack.includes("caller"), false); + assertEq(stack.includes("fill"), false); +} +test_bind_elision(); + +// Cross Realm testing + +let nr = newGlobal({ newCompartment: true }) +nr.eval(`globalThis.x = {}`); +Error.captureStackTrace(nr.x); + +// Test strict definition +function test_strict_definition() { + "use strict"; + assertThrowsInstanceOf(() => Error.captureStackTrace(Object.freeze({ stack: null })), TypeError); +} +test_strict_definition(); + +function test_property_descriptor() { + let o = {}; + Error.captureStackTrace(o); + let desc = Object.getOwnPropertyDescriptor(o, "stack"); + assertEq(desc.configurable, true) + assertEq(desc.writable, true) + assertEq(desc.enumerable, false) +} +test_property_descriptor(); + +function test_delete() { + let o = {}; + Error.captureStackTrace(o); + delete o.stack + assertEq("stack" in o, false) +} +test_delete(); + +// Principal testing: This is basic/shell-principals.js extended to support +// and compare Error.captureStackTrace. +// +// Reminder: +// > In the shell, a principal is simply a 32-bit mask: P subsumes Q if the +// > set bits in P are a superset of those in Q. Thus, the principal 0 is +// > subsumed by everything, and the principal ~0 subsumes everything. + +// Given a string of letters |expected|, say "abc", assert that the stack +// contains calls to a series of functions named by the next letter from +// the string, say a, b, and then c. Younger frames appear earlier in +// |expected| than older frames. +let count = 0; +function check(expected, stack) { + print("check(" + JSON.stringify(expected) + ") against:\n" + stack); + count++; + + // Extract only the function names from the stack trace. Omit the frames + // for the top-level evaluation, if it is present. + var split = stack.split(/(.)?@.*\n/).slice(0, -1); + if (split[split.length - 1] === undefined) + split = split.slice(0, -2); + + print(JSON.stringify(split)); + // Check the function names against the expected sequence. + assertEq(split.length, expected.length * 2); + for (var i = 0; i < expected.length; i++) + assertEq(split[i * 2 + 1], expected[i]); +} + +var low = newGlobal({ principal: 0 }); +var mid = newGlobal({ principal: 0xffff }); +var high = newGlobal({ principal: 0xfffff }); + +eval('function a() { let o = {}; Error.captureStackTrace(o); check("a", o.stack); b(); }'); +low.eval('function b() { let o = {}; Error.captureStackTrace(o); check("b", o.stack); c(); }'); +mid.eval('function c() { let o = {}; Error.captureStackTrace(o); check("cba", o.stack); d(); }'); +high.eval('function d() { let o = {}; Error.captureStackTrace(o); check("dcba", o.stack); e(); }'); + +// Globals created with no explicit principals get 0xffff. +eval('function e() { let o = {}; Error.captureStackTrace(o); check("ecba", o.stack); f(); }'); + +low.eval('function f() { let o = {}; Error.captureStackTrace(o); check("fb", o.stack); g(); }'); +mid.eval('function g() { let o = {}; Error.captureStackTrace(o); check("gfecba", o.stack); h(); }'); +high.eval('function h() { let o = {}; Error.captureStackTrace(o); check("hgfedcba", o.stack); }'); + +// Make everyone's functions visible to each other, as needed. +b = low.b; +low.c = mid.c; +mid.d = high.d; +high.e = e; +f = low.f; +low.g = mid.g; +mid.h = high.h; + +low.check = mid.check = high.check = check; + +// Kick the whole process off. +a(); + +assertEq(count, 8); + +// Ensure filtering is based on caller realm not on target object. +low.eval("low_target = {}"); +mid.eval("mid_target = {}"); +high.eval("high_target = {}"); + +high.low_target = mid.low_target = low.low_target; +high.mid_target = low.mid_target = mid.mid_target; +mid.high_target = low.high_target = high.high_target; + +high.low_cst = mid.low_cst = low.low_cst = low.Error.captureStackTrace; +high.mid_cst = low.mid_cst = mid.mid_cst = mid.Error.captureStackTrace; +mid.high_cst = low.high_cst = high.high_cst = high.Error.captureStackTrace; + +for (let g of [low, mid, high]) { + assertEq("low_target" in g, true); + assertEq("mid_target" in g, true); + assertEq("high_target" in g, true); + + assertEq("low_cst" in g, true); + assertEq("mid_cst" in g, true); + assertEq("high_cst" in g, true); + + // install caller function z -- single letter name for + // check compat. + g.eval("function z(f) { f() }") +} + +low.eval("function q() { Error.captureStackTrace(low_target); }") + + +high.q = low.q; + +// Caller function z is from high, but using low Error.captureStackTrace, so +// z should be elided. +high.eval("z(q)"); +check("q", low.low_target.stack); + +low.eval("function r() { high_cst(low_target) }") +high.r = low.r; + +// Can see everything here using high cst and low target. +high.eval("function t() { z(r) }"); +high.t(); +check("rzt", low.low_target.stack); + + diff --git a/js/src/jsapi.cpp b/js/src/jsapi.cpp index ccb78b1d8799..fe0a548e252c 100644 --- a/js/src/jsapi.cpp +++ b/js/src/jsapi.cpp @@ -4931,14 +4931,16 @@ JS::FirstSubsumedFrame::FirstSubsumedFrame( JS_PUBLIC_API bool JS::CaptureCurrentStack( JSContext* cx, JS::MutableHandleObject stackp, - JS::StackCapture&& capture /* = JS::StackCapture(JS::AllFrames()) */) { + JS::StackCapture&& capture /* = JS::StackCapture(JS::AllFrames()) */, + JS::HandleObject startAt /* = nullptr*/) { AssertHeapIsIdle(); CHECK_THREAD(cx); MOZ_RELEASE_ASSERT(cx->realm()); Realm* realm = cx->realm(); Rooted frame(cx); - if (!realm->savedStacks().saveCurrentStack(cx, &frame, std::move(capture))) { + if (!realm->savedStacks().saveCurrentStack(cx, &frame, std::move(capture), + startAt)) { return false; } stackp.set(frame.get()); diff --git a/js/src/jsexn.cpp b/js/src/jsexn.cpp index a1e261f6b267..ac42ea9b11c1 100644 --- a/js/src/jsexn.cpp +++ b/js/src/jsexn.cpp @@ -221,10 +221,6 @@ struct SuppressErrorsGuard { ~SuppressErrorsGuard() { JS::SetWarningReporter(cx, prevReporter); } }; -// 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; - bool js::CaptureStack(JSContext* cx, MutableHandleObject stack) { return CaptureCurrentStack( cx, stack, JS::StackCapture(JS::MaxFrames(MAX_REPORTED_STACK_DEPTH))); diff --git a/js/src/jsexn.h b/js/src/jsexn.h index 62acd51fcc31..d348cd75026a 100644 --- a/js/src/jsexn.h +++ b/js/src/jsexn.h @@ -36,6 +36,10 @@ UniquePtr CopyErrorNote(JSContext* cx, UniquePtr CopyErrorReport(JSContext* cx, JSErrorReport* report); +// 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; + bool CaptureStack(JSContext* cx, MutableHandleObject stack); JSString* ComputeStackString(JSContext* cx); diff --git a/js/src/vm/CommonPropertyNames.h b/js/src/vm/CommonPropertyNames.h index b444a8cbd0fb..1443c51332b7 100644 --- a/js/src/vm/CommonPropertyNames.h +++ b/js/src/vm/CommonPropertyNames.h @@ -83,6 +83,7 @@ MACRO_(callee, "callee") \ MACRO_(caller, "caller") \ MACRO_(callFunction, "callFunction") \ + MACRO_(captureStackTrace, "captureStackTrace") \ MACRO_(cancel, "cancel") \ MACRO_(case_, "case") \ MACRO_(caseFirst, "caseFirst") \ diff --git a/js/src/vm/ErrorObject.cpp b/js/src/vm/ErrorObject.cpp index dc4198638a4c..b544b114713c 100644 --- a/js/src/vm/ErrorObject.cpp +++ b/js/src/vm/ErrorObject.cpp @@ -55,6 +55,7 @@ #include "vm/JSContext-inl.h" #include "vm/JSObject-inl.h" #include "vm/ObjectOperations-inl.h" +#include "vm/Realm-inl.h" #include "vm/SavedStacks-inl.h" #include "vm/Shape-inl.h" @@ -96,11 +97,13 @@ static const JSFunctionSpec error_methods[] = { #ifdef NIGHTLY_BUILD static bool exn_isError(JSContext* cx, unsigned argc, Value* vp); +static bool exn_captureStackTrace(JSContext* cx, unsigned argc, Value* vp); #endif static const JSFunctionSpec error_static_methods[] = { #ifdef NIGHTLY_BUILD JS_FN("isError", exn_isError, 1, 0), + JS_FN("captureStackTrace", exn_captureStackTrace, 2, 0), #endif JS_FS_END, }; @@ -969,4 +972,85 @@ static bool exn_isError(JSContext* cx, unsigned argc, Value* vp) { return true; } +// The below is the "documentation" from https://v8.dev/docs/stack-trace-api +// +// ## Stack trace collection for custom exceptions +// +// The stack trace mechanism used for built-in errors is implemented using a +// general stack trace collection API that is also available to user scripts. +// The function +// +// Error.captureStackTrace(error, constructorOpt) +// +// adds a stack property to the given error object that yields the stack trace +// at the time captureStackTrace was called. Stack traces collected through +// Error.captureStackTrace are immediately collected, formatted, and attached +// to the given error object. +// +// The optional constructorOpt parameter allows you to pass in a function +// value. When collecting the stack trace all frames above the topmost call to +// this function, including that call, are left out of the stack trace. This +// can be useful to hide implementation details that won’t be useful to the +// user. The usual way of defining a custom error that captures a stack trace +// would be: +// +// function MyError() { +// Error.captureStackTrace(this, MyError); +// // Any other initialization goes here. +// } +// +// Passing in MyError as a second argument means that the constructor call to +// MyError won’t show up in the stack trace. + +static bool exn_captureStackTrace(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + const char* callerName = "Error.captureStackTrace"; + + if (!args.requireAtLeast(cx, callerName, 1)) { + return false; + } + + Rooted obj(cx, + RequireObjectArg(cx, "`target`", callerName, args[0])); + if (!obj) { + return false; + } + + Rooted caller(cx, nullptr); + if (args.length() > 1 && args[1].isObject() && + args[1].toObject().isCallable()) { + caller = CheckedUnwrapStatic(&args[1].toObject()); + if (!caller) { + ReportAccessDenied(cx); + return false; + } + } + + RootedObject stack(cx); + if (!CaptureCurrentStack( + cx, &stack, JS::StackCapture(JS::MaxFrames(MAX_REPORTED_STACK_DEPTH)), + caller)) { + return false; + } + + RootedString stackString(cx); + + // Do frame filtering based on the current realm, to filter out any + // chrome frames which could exist on the stack. + JSPrincipals* principals = cx->realm()->principals(); + if (!BuildStackString(cx, principals, stack, &stackString)) { + return false; + } + + // V8 installs a non-enumerable, configurable getter-setter on the object. + // JSC installs a non-enumerable, configurable, writable value on the + // object. We are following JSC here, not V8. + RootedValue string(cx, StringValue(stackString)); + if (!DefineDataProperty(cx, obj, cx->names().stack, string, 0)) { + return false; + } + + args.rval().setUndefined(); + return true; +} #endif diff --git a/js/src/vm/JSObject.cpp b/js/src/vm/JSObject.cpp index 70547aa7f52c..b3ab5acadf7f 100644 --- a/js/src/vm/JSObject.cpp +++ b/js/src/vm/JSObject.cpp @@ -2345,6 +2345,12 @@ JS_PUBLIC_API bool js::ShouldIgnorePropertyDefinition(JSContext* cx, } #endif + if (key == JSProto_Function && + !JS::Prefs::experimental_error_capture_stack_trace() && + id == NameToId(cx->names().captureStackTrace)) { + return true; + } + if (key == JSProto_JSON && !JS::Prefs::experimental_json_parse_with_source() && (id == NameToId(cx->names().isRawJSON) || diff --git a/js/src/vm/SavedStacks.cpp b/js/src/vm/SavedStacks.cpp index c419e3c5f05e..5ef9b5312c72 100644 --- a/js/src/vm/SavedStacks.cpp +++ b/js/src/vm/SavedStacks.cpp @@ -1324,7 +1324,8 @@ bool SavedFrame::toStringMethod(JSContext* cx, unsigned argc, Value* vp) { bool SavedStacks::saveCurrentStack( JSContext* cx, MutableHandle frame, - JS::StackCapture&& capture /* = JS::StackCapture(JS::AllFrames()) */) { + JS::StackCapture&& capture /* = JS::StackCapture(JS::AllFrames()) */, + HandleObject startAt /* nullptr */) { MOZ_RELEASE_ASSERT(cx->realm()); MOZ_DIAGNOSTIC_ASSERT(&cx->realm()->savedStacks() == this); @@ -1335,7 +1336,7 @@ bool SavedStacks::saveCurrentStack( } AutoGeckoProfilerEntry labelFrame(cx, "js::SavedStacks::saveCurrentStack"); - return insertFrames(cx, frame, std::move(capture)); + return insertFrames(cx, frame, std::move(capture), startAt); } bool SavedStacks::copyAsyncStack(JSContext* cx, HandleObject asyncStack, @@ -1411,7 +1412,10 @@ static inline bool captureIsSatisfied(JSContext* cx, JSPrincipals* principals, } bool SavedStacks::insertFrames(JSContext* cx, MutableHandle frame, - JS::StackCapture&& capture) { + JS::StackCapture&& capture, + HandleObject startAtObj) { + MOZ_ASSERT_IF(startAtObj, startAtObj->isCallable()); + // In order to look up 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 @@ -1465,6 +1469,11 @@ bool SavedStacks::insertFrames(JSContext* cx, MutableHandle frame, // targets and ensure that we don't stop before they have all been reached. Vector unreachedEvalTargets(cx); + Rooted startAt(cx, startAtObj && startAtObj->is() + ? &startAtObj->as() + : nullptr); + bool seenStartAt = !startAt; + while (!iter.done()) { Activation& activation = *iter.activation(); Maybe framePtr = @@ -1541,19 +1550,29 @@ bool SavedStacks::insertFrames(JSContext* cx, MutableHandle frame, auto principals = iter.realm()->principals(); MOZ_ASSERT_IF(framePtr && !iter.isWasm(), iter.pc()); - if (!stackChain.emplaceBack(location.source(), location.sourceId(), - location.line(), location.column(), displayAtom, - nullptr, // asyncCause - nullptr, // parent (not known yet) - principals, iter.mutedErrors(), framePtr, - iter.pc(), &activation)) { - return false; + // If we haven't yet seen the start, then don't add anything to the stack + // chain. + if (seenStartAt) { + if (!stackChain.emplaceBack(location.source(), location.sourceId(), + location.line(), location.column(), + displayAtom, + nullptr, // asyncCause + nullptr, // parent (not known yet) + principals, iter.mutedErrors(), framePtr, + iter.pc(), &activation)) { + return false; + } } if (captureIsSatisfied(cx, principals, location.source(), capture)) { break; } + if (!seenStartAt && iter.isFunctionFrame() && + iter.matchCallee(cx, startAt)) { + seenStartAt = true; + } + ++iter; framePtr = LiveSavedFrameCache::FramePtr::create(iter); diff --git a/js/src/vm/SavedStacks.h b/js/src/vm/SavedStacks.h index cd00a040038a..b314f1f37280 100644 --- a/js/src/vm/SavedStacks.h +++ b/js/src/vm/SavedStacks.h @@ -167,7 +167,8 @@ class SavedStacks { [[nodiscard]] bool saveCurrentStack( JSContext* cx, MutableHandle frame, - JS::StackCapture&& capture = JS::StackCapture(JS::AllFrames())); + JS::StackCapture&& capture = JS::StackCapture(JS::AllFrames()), + HandleObject startAt = nullptr); [[nodiscard]] bool copyAsyncStack( JSContext* cx, HandleObject asyncStack, HandleString asyncCause, MutableHandle adoptedStack, @@ -218,7 +219,8 @@ class SavedStacks { [[nodiscard]] bool insertFrames(JSContext* cx, MutableHandle frame, - JS::StackCapture&& capture); + JS::StackCapture&& capture, + HandleObject startAt); [[nodiscard]] bool adoptAsyncStack( JSContext* cx, MutableHandle asyncStack, Handle asyncCause, const mozilla::Maybe& maxFrameCount);