summaryrefslogtreecommitdiffstats
path: root/js/src/jit-test/tests/saved-stacks
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
commit2aa4a82499d4becd2284cdb482213d541b8804dd (patch)
treeb80bf8bf13c3766139fbacc530efd0dd9d54394c /js/src/jit-test/tests/saved-stacks
parentInitial commit. (diff)
downloadfirefox-upstream.tar.xz
firefox-upstream.zip
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'js/src/jit-test/tests/saved-stacks')
-rw-r--r--js/src/jit-test/tests/saved-stacks/1438121-async-function.js119
-rw-r--r--js/src/jit-test/tests/saved-stacks/1438121-generator.js131
-rw-r--r--js/src/jit-test/tests/saved-stacks/SavedFrame-constructor.js4
-rw-r--r--js/src/jit-test/tests/saved-stacks/asm-frames.js37
-rw-r--r--js/src/jit-test/tests/saved-stacks/async-implicit.js52
-rw-r--r--js/src/jit-test/tests/saved-stacks/async-livecache.js43
-rw-r--r--js/src/jit-test/tests/saved-stacks/async-max-frame-count.js99
-rw-r--r--js/src/jit-test/tests/saved-stacks/async-principals.js247
-rw-r--r--js/src/jit-test/tests/saved-stacks/async.js24
-rw-r--r--js/src/jit-test/tests/saved-stacks/bug-1004479-savedStacks-with-string-parameter.js4
-rw-r--r--js/src/jit-test/tests/saved-stacks/bug-1006876-too-much-recursion.js10
-rw-r--r--js/src/jit-test/tests/saved-stacks/bug-1012646-strlen-crasher.js4
-rw-r--r--js/src/jit-test/tests/saved-stacks/bug-1031168-trace-sources.js9
-rw-r--r--js/src/jit-test/tests/saved-stacks/bug-1149495.js6
-rw-r--r--js/src/jit-test/tests/saved-stacks/bug-1225474.js6
-rw-r--r--js/src/jit-test/tests/saved-stacks/bug-1260712.js7
-rw-r--r--js/src/jit-test/tests/saved-stacks/bug-1289058.js13
-rw-r--r--js/src/jit-test/tests/saved-stacks/bug-1289073.js1
-rw-r--r--js/src/jit-test/tests/saved-stacks/bug-1445973-quick.js53
-rw-r--r--js/src/jit-test/tests/saved-stacks/bug-1451268.js23
-rw-r--r--js/src/jit-test/tests/saved-stacks/bug-1505387-dbg-eval-ion.js12
-rw-r--r--js/src/jit-test/tests/saved-stacks/bug-1509420.js9
-rw-r--r--js/src/jit-test/tests/saved-stacks/bug-1640034-dbg-eval-across-compartments.js22
-rw-r--r--js/src/jit-test/tests/saved-stacks/caching-and-ccws.js35
-rw-r--r--js/src/jit-test/tests/saved-stacks/caching-and-frame-count.js38
-rw-r--r--js/src/jit-test/tests/saved-stacks/capture-first-frame-with-principals.js92
-rw-r--r--js/src/jit-test/tests/saved-stacks/display-url.js26
-rw-r--r--js/src/jit-test/tests/saved-stacks/evals.js38
-rw-r--r--js/src/jit-test/tests/saved-stacks/function-display-name.js16
-rw-r--r--js/src/jit-test/tests/saved-stacks/gc-frame-cache.js87
-rw-r--r--js/src/jit-test/tests/saved-stacks/generators.js19
-rw-r--r--js/src/jit-test/tests/saved-stacks/get-set.js25
-rw-r--r--js/src/jit-test/tests/saved-stacks/getters-on-invalid-objects.js23
-rw-r--r--js/src/jit-test/tests/saved-stacks/max-frame-count.js42
-rw-r--r--js/src/jit-test/tests/saved-stacks/native-calls.js12
-rw-r--r--js/src/jit-test/tests/saved-stacks/oom-in-save-stack-02.js28
-rw-r--r--js/src/jit-test/tests/saved-stacks/oom-in-save-stack.js4
-rw-r--r--js/src/jit-test/tests/saved-stacks/principals-01.js70
-rw-r--r--js/src/jit-test/tests/saved-stacks/principals-02.js56
-rw-r--r--js/src/jit-test/tests/saved-stacks/principals-03.js23
-rw-r--r--js/src/jit-test/tests/saved-stacks/principals-04.js15
-rw-r--r--js/src/jit-test/tests/saved-stacks/proxy-handlers.js10
-rw-r--r--js/src/jit-test/tests/saved-stacks/same-stack.js12
-rw-r--r--js/src/jit-test/tests/saved-stacks/self-hosted.js26
-rw-r--r--js/src/jit-test/tests/saved-stacks/shared-parent-frames.js19
-rw-r--r--js/src/jit-test/tests/saved-stacks/stacks-are-frozen.js17
-rw-r--r--js/src/jit-test/tests/saved-stacks/stringify-with-self-hosted.js8
47 files changed, 1676 insertions, 0 deletions
diff --git a/js/src/jit-test/tests/saved-stacks/1438121-async-function.js b/js/src/jit-test/tests/saved-stacks/1438121-async-function.js
new file mode 100644
index 0000000000..87fc9bab0a
--- /dev/null
+++ b/js/src/jit-test/tests/saved-stacks/1438121-async-function.js
@@ -0,0 +1,119 @@
+const mainGlobal = this;
+const debuggerGlobal = newGlobal({newCompartment: true});
+
+function Memory({global}) {
+ this.dbg = new (debuggerGlobal.Debugger);
+ this.gDO = this.dbg.addDebuggee(global);
+}
+
+Memory.prototype = {
+ constructor: Memory,
+ attach() { return Promise.resolve('fake attach result'); },
+ detach() { return Promise.resolve('fake detach result'); },
+ startRecordingAllocations() {
+ this.dbg.memory.trackingAllocationSites = true;
+ return Promise.resolve('fake startRecordingAllocations result');
+ },
+ stopRecordingAllocations() {
+ this.dbg.memory.trackingAllocationSites = false;
+ return Promise.resolve('fake stopRecordingAllocations result');
+ },
+ getAllocations() {
+ return Promise.resolve({ allocations: this.dbg.memory.drainAllocationsLog() });
+ }
+};
+
+function ok(cond, msg) {
+ assertEq(!!cond, true, `ok(${JSON.stringify(cond)}, ${JSON.stringify(msg)})`);
+}
+
+const is = assertEq;
+
+function startServerAndGetSelectedTabMemory() {
+ let memory = new Memory({ global: mainGlobal });
+ return Promise.resolve({ memory, client: 'fake client' });
+}
+
+function destroyServerAndFinish() {
+ return Promise.resolve('fake destroyServerAndFinish result');
+}
+
+(async function body() {
+ let { memory, client } = await startServerAndGetSelectedTabMemory();
+ await memory.attach();
+
+ await memory.startRecordingAllocations();
+ ok(true, "Can start recording allocations");
+
+ // Allocate some objects.
+
+ let alloc1, alloc2, alloc3;
+
+ /* eslint-disable max-nested-callbacks */
+ (function outer() {
+ (function middle() {
+ (function inner() {
+ alloc1 = {}; alloc1.line = Error().lineNumber;
+ alloc2 = []; alloc2.line = Error().lineNumber;
+ // eslint-disable-next-line new-parens
+ alloc3 = new function () {}; alloc3.line = Error().lineNumber;
+ }());
+ }());
+ }());
+ /* eslint-enable max-nested-callbacks */
+
+ let response = await memory.getAllocations();
+
+ await memory.stopRecordingAllocations();
+ ok(true, "Can stop recording allocations");
+
+ // Filter out allocations by library and test code, and get only the
+ // allocations that occurred in our test case above.
+
+ function isTestAllocation(alloc) {
+ let frame = alloc.frame;
+ return frame
+ && frame.functionDisplayName === "inner"
+ && (frame.line === alloc1.line
+ || frame.line === alloc2.line
+ || frame.line === alloc3.line);
+ }
+
+ let testAllocations = response.allocations.filter(isTestAllocation);
+ ok(testAllocations.length >= 3,
+ "Should find our 3 test allocations (plus some allocations for the error "
+ + "objects used to get line numbers)");
+
+ // For each of the test case's allocations, ensure that the parent frame
+ // indices are correct. Also test that we did get an allocation at each
+ // line we expected (rather than a bunch on the first line and none on the
+ // others, etc).
+
+ let expectedLines = new Set([alloc1.line, alloc2.line, alloc3.line]);
+
+ for (let alloc of testAllocations) {
+ let innerFrame = alloc.frame;
+ ok(innerFrame, "Should get the inner frame");
+ is(innerFrame.functionDisplayName, "inner");
+ expectedLines.delete(innerFrame.line);
+
+ let middleFrame = innerFrame.parent;
+ ok(middleFrame, "Should get the middle frame");
+ is(middleFrame.functionDisplayName, "middle");
+
+ let outerFrame = middleFrame.parent;
+ ok(outerFrame, "Should get the outer frame");
+ is(outerFrame.functionDisplayName, "outer");
+
+ // Not going to test the rest of the frames because they are Task.jsm
+ // and promise frames and it gets gross. Plus, I wouldn't want this test
+ // to start failing if they changed their implementations in a way that
+ // added or removed stack frames here.
+ }
+
+ is(expectedLines.size, 0,
+ "Should have found all the expected lines");
+
+ await memory.detach();
+ destroyServerAndFinish(client);
+})().catch(e => { print("Error: " + e + "\nstack:\n" + e.stack); quit(1); });
diff --git a/js/src/jit-test/tests/saved-stacks/1438121-generator.js b/js/src/jit-test/tests/saved-stacks/1438121-generator.js
new file mode 100644
index 0000000000..4e3a88122c
--- /dev/null
+++ b/js/src/jit-test/tests/saved-stacks/1438121-generator.js
@@ -0,0 +1,131 @@
+const mainGlobal = this;
+const debuggerGlobal = newGlobal({newCompartment: true});
+
+function Memory({global}) {
+ this.dbg = new (debuggerGlobal.Debugger);
+ this.gDO = this.dbg.addDebuggee(global);
+}
+
+Memory.prototype = {
+ constructor: Memory,
+ attach() { return Promise.resolve('fake attach result'); },
+ detach() { return Promise.resolve('fake detach result'); },
+ startRecordingAllocations() {
+ this.dbg.memory.trackingAllocationSites = true;
+ return Promise.resolve('fake startRecordingAllocations result');
+ },
+ stopRecordingAllocations() {
+ this.dbg.memory.trackingAllocationSites = false;
+ return Promise.resolve('fake stopRecordingAllocations result');
+ },
+ getAllocations() {
+ return Promise.resolve({ allocations: this.dbg.memory.drainAllocationsLog() });
+ }
+};
+
+function ok(cond, msg) {
+ assertEq(!!cond, true, `ok(${JSON.stringify(cond)}, ${JSON.stringify(msg)})`);
+}
+
+const is = assertEq;
+
+function startServerAndGetSelectedTabMemory() {
+ let memory = new Memory({ global: mainGlobal });
+ return Promise.resolve({ memory, client: 'fake client' });
+}
+
+function destroyServerAndFinish() {
+ return Promise.resolve('fake destroyServerAndFinish result');
+}
+
+function* body() {
+ let { memory, client } = yield startServerAndGetSelectedTabMemory();
+ yield memory.attach();
+
+ yield memory.startRecordingAllocations();
+ ok(true, "Can start recording allocations");
+
+ // Allocate some objects.
+
+ let alloc1, alloc2, alloc3;
+
+ /* eslint-disable max-nested-callbacks */
+ (function outer() {
+ (function middle() {
+ (function inner() {
+ alloc1 = {}; alloc1.line = Error().lineNumber;
+ alloc2 = []; alloc2.line = Error().lineNumber;
+ // eslint-disable-next-line new-parens
+ alloc3 = new function () {}; alloc3.line = Error().lineNumber;
+ }());
+ }());
+ }());
+ /* eslint-enable max-nested-callbacks */
+
+ let response = yield memory.getAllocations();
+
+ yield memory.stopRecordingAllocations();
+ ok(true, "Can stop recording allocations");
+
+ // Filter out allocations by library and test code, and get only the
+ // allocations that occurred in our test case above.
+
+ function isTestAllocation(alloc) {
+ let frame = alloc.frame;
+ return frame
+ && frame.functionDisplayName === "inner"
+ && (frame.line === alloc1.line
+ || frame.line === alloc2.line
+ || frame.line === alloc3.line);
+ }
+
+ let testAllocations = response.allocations.filter(isTestAllocation);
+ ok(testAllocations.length >= 3,
+ "Should find our 3 test allocations (plus some allocations for the error "
+ + "objects used to get line numbers)");
+
+ // For each of the test case's allocations, ensure that the parent frame
+ // indices are correct. Also test that we did get an allocation at each
+ // line we expected (rather than a bunch on the first line and none on the
+ // others, etc).
+
+ let expectedLines = new Set([alloc1.line, alloc2.line, alloc3.line]);
+
+ for (let alloc of testAllocations) {
+ let innerFrame = alloc.frame;
+ ok(innerFrame, "Should get the inner frame");
+ is(innerFrame.functionDisplayName, "inner");
+ expectedLines.delete(innerFrame.line);
+
+ let middleFrame = innerFrame.parent;
+ ok(middleFrame, "Should get the middle frame");
+ is(middleFrame.functionDisplayName, "middle");
+
+ let outerFrame = middleFrame.parent;
+ ok(outerFrame, "Should get the outer frame");
+ is(outerFrame.functionDisplayName, "outer");
+
+ // Not going to test the rest of the frames because they are Task.jsm
+ // and promise frames and it gets gross. Plus, I wouldn't want this test
+ // to start failing if they changed their implementations in a way that
+ // added or removed stack frames here.
+ }
+
+ is(expectedLines.size, 0,
+ "Should have found all the expected lines");
+
+ yield memory.detach();
+ destroyServerAndFinish(client);
+}
+
+const generator = body();
+loop(generator.next());
+
+function loop({ value: promise, done }) {
+ if (done)
+ return;
+ promise
+ .catch(e => loop(generator.throw(e)))
+ .then(v => { loop(generator.next(v)); })
+ .catch(e => { print(`Error: ${e}\nstack:\n${e.stack}`); });
+}
diff --git a/js/src/jit-test/tests/saved-stacks/SavedFrame-constructor.js b/js/src/jit-test/tests/saved-stacks/SavedFrame-constructor.js
new file mode 100644
index 0000000000..0dbafea986
--- /dev/null
+++ b/js/src/jit-test/tests/saved-stacks/SavedFrame-constructor.js
@@ -0,0 +1,4 @@
+// The SavedFrame constructor shouldn't have been exposed to JS on the global.
+
+saveStack();
+assertEq(typeof SavedFrame, "undefined");
diff --git a/js/src/jit-test/tests/saved-stacks/asm-frames.js b/js/src/jit-test/tests/saved-stacks/asm-frames.js
new file mode 100644
index 0000000000..3667122532
--- /dev/null
+++ b/js/src/jit-test/tests/saved-stacks/asm-frames.js
@@ -0,0 +1,37 @@
+function AsmModule(stdlib, foreign, heap) {
+ "use asm";
+ var ffi = foreign.t;
+
+ function doTest() {
+ ffi();
+ }
+
+ function test() {
+ doTest();
+ }
+
+ return { test: test };
+}
+
+let stack;
+
+function tester() {
+ stack = saveStack();
+}
+
+const buf = new ArrayBuffer(1024*8);
+const module = AsmModule(this, { t: tester }, buf);
+module.test();
+
+print(stack);
+assertEq(stack.functionDisplayName, "tester");
+
+assertEq(stack.parent.functionDisplayName, "doTest");
+assertEq(stack.parent.line, 6);
+
+assertEq(stack.parent.parent.functionDisplayName, "test");
+assertEq(stack.parent.parent.line, 10);
+
+assertEq(stack.parent.parent.parent.line, 24);
+
+assertEq(stack.parent.parent.parent.parent, null);
diff --git a/js/src/jit-test/tests/saved-stacks/async-implicit.js b/js/src/jit-test/tests/saved-stacks/async-implicit.js
new file mode 100644
index 0000000000..d0b9328906
--- /dev/null
+++ b/js/src/jit-test/tests/saved-stacks/async-implicit.js
@@ -0,0 +1,52 @@
+// Test AutoSetAsyncStackForNewCalls's IMPLICIT kind.
+
+// Given a SavedFrame stack, return a string listing the frame's function names
+// and their async causes, if any.
+function stackFunctions(stack) {
+ const frames = [];
+ for (; stack; stack = stack.parent || stack.asyncParent) {
+ if (!stack.functionDisplayName) {
+ frames.push('(top level)');
+ } else if (stack.asyncCause) {
+ frames.push(`${stack.asyncCause}*${stack.functionDisplayName}`);
+ } else {
+ frames.push(stack.functionDisplayName);
+ }
+ }
+ return frames.join(', ');
+}
+
+let fakeStack = (function fake1() {
+ function fake2() {
+ return saveStack();
+ }
+ return fake2();
+})();
+
+function bindAndExpect(options, expected) {
+ function bindee() {
+ assertEq(stackFunctions(saveStack()), expected);
+ }
+
+ return bindToAsyncStack(bindee, options);
+}
+
+function caller(f) {
+ return f();
+}
+
+// An explicit async stack always overrides the actual callers of the bindee.
+// An implicit async stack never overrides callers; it is only attached when
+// the stack is otherwise empty.
+caller(bindAndExpect({ stack: fakeStack, cause: 'ano', explicit: false },
+ "bindee, caller, (top level)"));
+
+caller(bindAndExpect({ stack: fakeStack, cause: 'hi', explicit: true },
+ "bindee, hi*fake2, fake1, (top level)"));
+
+enqueueJob(bindAndExpect({ stack: fakeStack, cause: 'mita', explicit: false },
+ "bindee, mita*fake2, fake1, (top level)"));
+
+enqueueJob(bindAndExpect({ stack: fakeStack, cause: 'hana', explicit: true },
+ "bindee, hana*fake2, fake1, (top level)"));
+
diff --git a/js/src/jit-test/tests/saved-stacks/async-livecache.js b/js/src/jit-test/tests/saved-stacks/async-livecache.js
new file mode 100644
index 0000000000..1034b5fc49
--- /dev/null
+++ b/js/src/jit-test/tests/saved-stacks/async-livecache.js
@@ -0,0 +1,43 @@
+// Async stacks should not supplant LiveSavedFrameCache hits.
+
+top();
+
+// An ordinary function, to give the frame a convenient name.
+function top() {
+ // Perform an async call. F will run in an activation that has an async stack
+ // supplied.
+ f().catch(catchError);
+}
+
+async function f() {
+ // Perform an ordinary call. Its parent frame will be a LiveSavedFrameCache
+ // hit.
+ g();
+}
+
+function g() {
+ // Populate the LiveSavedFrameCache.
+ saveStack();
+
+ // Capturing the stack again should find f (if not g) in the cache. The async
+ // stack supplied below the call to f should not supplant f's own frame.
+ let frame = saveStack();
+
+ assertEq(frame.functionDisplayName, 'g');
+ assertEq(parent(frame).functionDisplayName, 'f');
+ assertEq(parent(parent(frame)).functionDisplayName, 'top');
+}
+
+// Return the parent of |frame|, skipping self-hosted code and following async
+// parent links.
+function parent(frame) {
+ do {
+ frame = frame.parent || frame.asyncParent;
+ } while (frame.source.match(/self-hosted/));
+ return frame;
+}
+
+function catchError(e) {
+ print(`${e}\n${e.stack}`);
+ quit(1)
+}
diff --git a/js/src/jit-test/tests/saved-stacks/async-max-frame-count.js b/js/src/jit-test/tests/saved-stacks/async-max-frame-count.js
new file mode 100644
index 0000000000..bada5b1ac9
--- /dev/null
+++ b/js/src/jit-test/tests/saved-stacks/async-max-frame-count.js
@@ -0,0 +1,99 @@
+// Test that async stacks are limited on recursion.
+
+const defaultAsyncStackLimit = 60;
+
+function recur(n, limit) {
+ if (n > 0) {
+ return callFunctionWithAsyncStack(function recur() {return recur(n - 1, limit)},
+ saveStack(limit), "Recurse");
+ }
+ return saveStack(limit);
+}
+
+function checkRecursion(n, limit) {
+ print("checkRecursion(" + String(n) + ", " + String(limit) + ")");
+
+ try {
+ var stack = recur(n, limit);
+ } catch (e) {
+ // Some platforms, like ASAN builds, can end up overrecursing. Tolerate
+ // these failures.
+ assertEq(/too much recursion/.test("" + e), true);
+ return;
+ }
+
+ // Async stacks are limited even if we didn't ask for a limit. There is a
+ // default limit on frames attached on top of any synchronous frames, and
+ // every time the limit is reached when capturing, half of the frames are
+ // truncated from the old end of the async stack.
+ if (limit == 0) {
+ // Always add one synchronous frame that is the last call to `recur`.
+ if (n + 1 < defaultAsyncStackLimit) {
+ limit = defaultAsyncStackLimit + 1;
+ } else {
+ limit = n + 2 - (defaultAsyncStackLimit / 2);
+ }
+ }
+
+ // The first `n` or `limit` frames should have `recur` as their `asyncParent`.
+ for (var i = 0; i < Math.min(n, limit); i++) {
+ assertEq(stack.functionDisplayName, "recur");
+ assertEq(stack.parent, null);
+ stack = stack.asyncParent;
+ }
+
+ // This frame should be the first call to `recur`.
+ if (limit > n) {
+ assertEq(stack.functionDisplayName, "recur");
+ assertEq(stack.asyncParent, null);
+ stack = stack.parent;
+ } else {
+ assertEq(stack, null);
+ }
+
+ // This frame should be the call to `checkRecursion`.
+ if (limit > n + 1) {
+ assertEq(stack.functionDisplayName, "checkRecursion");
+ assertEq(stack.asyncParent, null);
+ stack = stack.parent;
+ } else {
+ assertEq(stack, null);
+ }
+
+ // We should be at the top frame, which is the test script itself.
+ if (limit > n + 2) {
+ assertEq(stack.functionDisplayName, null);
+ assertEq(stack.asyncParent, null);
+ assertEq(stack.parent, null);
+ } else {
+ assertEq(stack, null);
+ }
+}
+
+// Check capturing with no limit, which should still apply a default limit.
+checkRecursion(0, 0);
+checkRecursion(1, 0);
+checkRecursion(2, 0);
+checkRecursion(defaultAsyncStackLimit - 10, 0);
+checkRecursion(defaultAsyncStackLimit, 0);
+checkRecursion(defaultAsyncStackLimit + 10, 0);
+
+// Limit of 1 frame.
+checkRecursion(0, 1);
+checkRecursion(1, 1);
+checkRecursion(2, 1);
+
+// Limit of 2 frames.
+checkRecursion(0, 2);
+checkRecursion(1, 2);
+checkRecursion(2, 2);
+
+// Limit of 3 frames.
+checkRecursion(0, 3);
+checkRecursion(1, 3);
+checkRecursion(2, 3);
+
+// Limit higher than the default limit.
+checkRecursion(defaultAsyncStackLimit + 10, defaultAsyncStackLimit + 10);
+checkRecursion(defaultAsyncStackLimit + 11, defaultAsyncStackLimit + 10);
+checkRecursion(defaultAsyncStackLimit + 12, defaultAsyncStackLimit + 10);
diff --git a/js/src/jit-test/tests/saved-stacks/async-principals.js b/js/src/jit-test/tests/saved-stacks/async-principals.js
new file mode 100644
index 0000000000..4a253057e8
--- /dev/null
+++ b/js/src/jit-test/tests/saved-stacks/async-principals.js
@@ -0,0 +1,247 @@
+// Test cases when a SavedFrame or one of its ancestors have a principal that is
+// not subsumed by the caller's principal, when async parents are present.
+
+function checkVisibleStack(stackFrame, expectedFrames) {
+ // We check toString separately from properties like asyncCause to check that
+ // it walks the physical SavedFrame chain consistently with the properties.
+ var stringFrames = stackFrame.toString().split("\n");
+
+ while (expectedFrames.length) {
+ let expectedFrame = expectedFrames.shift();
+ let stringFrame = stringFrames.shift();
+
+ // Check the frame properties.
+ assertEq(stackFrame.functionDisplayName, expectedFrame.name);
+ assertEq(stackFrame.asyncCause, expectedFrame.asyncCause);
+
+ // Check the stringified version.
+ let expectedStart =
+ (expectedFrame.asyncCause ? expectedFrame.asyncCause + "*" : "") +
+ expectedFrame.name;
+ assertEq(stringFrame.replace(/@.*$/, ""), expectedStart);
+
+ // If the next frame has an asyncCause, it should be an asyncParent.
+ if (expectedFrames.length && expectedFrames[0].asyncCause) {
+ assertEq(stackFrame.parent, null);
+ stackFrame = stackFrame.asyncParent;
+ } else {
+ assertEq(stackFrame.asyncParent, null);
+ stackFrame = stackFrame.parent;
+ }
+ }
+}
+
+var low = newGlobal({ principal: 0 });
+var high = newGlobal({ principal: 0xfffff });
+
+// Test with synchronous cross-compartment calls.
+//
+// With arrows representing child-to-parent links, and fat arrows representing
+// child-to-async-parent links, create a SavedFrame stack like this:
+//
+// low.e -> high.d => high.c => high.b -> low.a
+//
+// This stack captured in function `e` would be seen in its complete version if
+// accessed by `high`'s compartment, while in `low`'s compartment it would look
+// like this:
+//
+// low.e => low.a
+//
+// The asyncCause seen on `low.a` above should not leak information about the
+// real asyncCause on `high.c` and `high.d`.
+//
+// The stack captured in function `d` would be seen in its complete version if
+// accessed by `high`'s compartment, while in `low`'s compartment it would look
+// like this:
+//
+// low.a
+
+// We'll move these functions into the right globals below before invoking them.
+function a() {
+ b();
+}
+function b() {
+ callFunctionWithAsyncStack(c, saveStack(), "BtoC");
+}
+function c() {
+ callFunctionWithAsyncStack(d, saveStack(), "CtoD");
+}
+function d() {
+ let stackD = saveStack();
+
+ print("high.checkVisibleStack(stackD)");
+ checkVisibleStack(stackD, [
+ { name: "d", asyncCause: null },
+ { name: "c", asyncCause: "CtoD" },
+ { name: "b", asyncCause: "BtoC" },
+ { name: "a", asyncCause: null },
+ ]);
+
+ let stackE = e(saveStack(0, low));
+
+ print("high.checkVisibleStack(stackE)");
+ checkVisibleStack(stackE, [
+ { name: "e", asyncCause: null },
+ { name: "d", asyncCause: null },
+ { name: "c", asyncCause: "CtoD" },
+ { name: "b", asyncCause: "BtoC" },
+ { name: "a", asyncCause: null },
+ ]);
+}
+function e(stackD) {
+ print("low.checkVisibleStack(stackD)");
+ checkVisibleStack(stackD, [
+ { name: "a", asyncCause: "Async" },
+ ]);
+
+ let stackE = saveStack();
+
+ print("low.checkVisibleStack(stackE)");
+ checkVisibleStack(stackE, [
+ { name: "e", asyncCause: null },
+ { name: "a", asyncCause: "Async" },
+ ]);
+
+ return saveStack(0, high);
+}
+
+// Test with asynchronous cross-compartment calls and shared frames.
+//
+// With arrows representing child-to-parent links, and fat arrows representing
+// child-to-async-parent links, create a SavedFrame stack like this:
+//
+// low.x => high.v => low.u
+// low.y -> high.v => low.u
+// low.z => high.w -> low.u
+//
+// This stack captured in functions `x`, `y`, and `z` would be seen in its
+// complete version if accessed by `high`'s compartment, while in `low`'s
+// compartment it would look like this:
+//
+// low.x => low.u
+// low.y => low.u
+// low.z => low.u
+//
+// The stack captured in function `v` would be seen in its complete version if
+// accessed by `high`'s compartment, while in `low`'s compartment it would look
+// like this:
+//
+// low.u
+
+// We'll move these functions into the right globals below before invoking them.
+function u() {
+ callFunctionWithAsyncStack(v, saveStack(), "UtoV");
+ w();
+}
+function v() {
+ let stackV = saveStack();
+ print("high.checkVisibleStack(stackV)");
+ checkVisibleStack(stackV, [
+ { name: "v", asyncCause: null },
+ { name: "u", asyncCause: "UtoV" },
+ ]);
+
+ let stack = saveStack(0, low);
+ function xToCall() { return x(stack);};
+
+ let stackX = callFunctionWithAsyncStack(xToCall, saveStack(), "VtoX");
+
+ print("high.checkVisibleStack(stackX)");
+ checkVisibleStack(stackX, [
+ { name: "x", asyncCause: null },
+ { name: "xToCall", asyncCause: null },
+ { name: "v", asyncCause: "VtoX" },
+ { name: "u", asyncCause: "UtoV" },
+ ]);
+
+ let stackY = y();
+
+ print("high.checkVisibleStack(stackY)");
+ checkVisibleStack(stackY, [
+ { name: "y", asyncCause: null },
+ { name: "v", asyncCause: null },
+ { name: "u", asyncCause: "UtoV" },
+ ]);
+}
+function w() {
+ let stackZ = callFunctionWithAsyncStack(z, saveStack(), "WtoZ");
+
+ print("high.checkVisibleStack(stackZ)");
+ checkVisibleStack(stackZ, [
+ { name: "z", asyncCause: null },
+ { name: "w", asyncCause: "WtoZ" },
+ { name: "u", asyncCause: null },
+ ]);
+}
+function x(stackV) {
+ print("low.checkVisibleStack(stackV)");
+ checkVisibleStack(stackV, [
+ { name: "u", asyncCause: "UtoV" },
+ ]);
+
+ let stackX = saveStack();
+
+ print("low.checkVisibleStack(stackX)");
+ checkVisibleStack(stackX, [
+ { name: "x", asyncCause: null },
+ { name: "u", asyncCause: "UtoV" },
+ ]);
+
+ return saveStack(0, high);
+}
+
+function y() {
+ let stackY = saveStack();
+
+ print("low.checkVisibleStack(stackY)");
+ checkVisibleStack(stackY, [
+ { name: "y", asyncCause: null },
+ { name: "u", asyncCause: "UtoV" },
+ ]);
+
+ return saveStack(0, high);
+}
+function z() {
+ let stackZ = saveStack();
+
+ print("low.checkVisibleStack(stackZ)");
+ checkVisibleStack(stackZ, [
+ { name: "z", asyncCause: null },
+ { name: "u", asyncCause: "Async" },
+ ]);
+
+ return saveStack(0, high);
+}
+
+// Split the functions in their respective globals.
+low .eval(a.toString());
+high.eval(b.toString());
+high.eval(c.toString());
+high.eval(d.toString());
+low .eval(e.toString());
+
+low .b = high.b;
+high.e = low .e;
+
+low .eval(u.toString());
+high.eval(v.toString());
+high.eval(w.toString());
+low .eval(x.toString());
+low .eval(y.toString());
+low .eval(z.toString());
+
+low .v = high.v;
+low .w = high.w;
+high.x = low .x;
+high.y = low .y;
+high.z = low .z;
+
+low .high = high;
+high.low = low;
+
+low .eval(checkVisibleStack.toString());
+high.eval(checkVisibleStack.toString());
+
+// Execute the tests.
+low.a();
+low.u();
diff --git a/js/src/jit-test/tests/saved-stacks/async.js b/js/src/jit-test/tests/saved-stacks/async.js
new file mode 100644
index 0000000000..6ab4546a7c
--- /dev/null
+++ b/js/src/jit-test/tests/saved-stacks/async.js
@@ -0,0 +1,24 @@
+// Test calling a function using a previously captured stack as an async stack.
+
+function getAsyncStack() {
+ return saveStack();
+}
+
+// asyncCause may contain non-ASCII characters.
+let testAsyncCause = "Tes" + String.fromCharCode(355) + "String";
+
+callFunctionWithAsyncStack(function asyncCallback() {
+ let stack = saveStack();
+
+ assertEq(stack.functionDisplayName, "asyncCallback");
+ assertEq(stack.parent, null);
+ assertEq(stack.asyncCause, null);
+
+ assertEq(stack.asyncParent.functionDisplayName, "getAsyncStack");
+ assertEq(stack.asyncParent.asyncCause, testAsyncCause);
+ assertEq(stack.asyncParent.asyncParent, null);
+
+ assertEq(stack.asyncParent.parent.asyncCause, null);
+ assertEq(stack.asyncParent.parent.asyncParent, null);
+ assertEq(stack.asyncParent.parent.parent, null);
+}, getAsyncStack(), testAsyncCause);
diff --git a/js/src/jit-test/tests/saved-stacks/bug-1004479-savedStacks-with-string-parameter.js b/js/src/jit-test/tests/saved-stacks/bug-1004479-savedStacks-with-string-parameter.js
new file mode 100644
index 0000000000..785c024945
--- /dev/null
+++ b/js/src/jit-test/tests/saved-stacks/bug-1004479-savedStacks-with-string-parameter.js
@@ -0,0 +1,4 @@
+// This test case was found by the fuzzer and crashed the js shell.
+
+Object.preventExtensions(this);
+saveStack();
diff --git a/js/src/jit-test/tests/saved-stacks/bug-1006876-too-much-recursion.js b/js/src/jit-test/tests/saved-stacks/bug-1006876-too-much-recursion.js
new file mode 100644
index 0000000000..593c4b40bf
--- /dev/null
+++ b/js/src/jit-test/tests/saved-stacks/bug-1006876-too-much-recursion.js
@@ -0,0 +1,10 @@
+// |jit-test| exitstatus: 3
+
+// This test case was found by the fuzzer and crashed the js shell. It should
+// throw a "too much recursion" error, but was crashing instead.
+
+enableTrackAllocations();
+function f() {
+ f();
+}
+f();
diff --git a/js/src/jit-test/tests/saved-stacks/bug-1012646-strlen-crasher.js b/js/src/jit-test/tests/saved-stacks/bug-1012646-strlen-crasher.js
new file mode 100644
index 0000000000..95a63aafde
--- /dev/null
+++ b/js/src/jit-test/tests/saved-stacks/bug-1012646-strlen-crasher.js
@@ -0,0 +1,4 @@
+// |jit-test| exitstatus: 3
+
+enableTrackAllocations();
+evaluate("throw Error();", {fileName: null});
diff --git a/js/src/jit-test/tests/saved-stacks/bug-1031168-trace-sources.js b/js/src/jit-test/tests/saved-stacks/bug-1031168-trace-sources.js
new file mode 100644
index 0000000000..3162855a8c
--- /dev/null
+++ b/js/src/jit-test/tests/saved-stacks/bug-1031168-trace-sources.js
@@ -0,0 +1,9 @@
+loadFile("\
+saveStack();\
+gcPreserveCode = function() {};\
+gc();\
+saveStack() == 3\
+");
+function loadFile(lfVarx) {
+ evaluate(lfVarx);
+}
diff --git a/js/src/jit-test/tests/saved-stacks/bug-1149495.js b/js/src/jit-test/tests/saved-stacks/bug-1149495.js
new file mode 100644
index 0000000000..aedd7726d6
--- /dev/null
+++ b/js/src/jit-test/tests/saved-stacks/bug-1149495.js
@@ -0,0 +1,6 @@
+try {
+ offThreadCompileScript('Error()', { lineNumber: (4294967295)});
+ runOffThreadScript().stack;
+} catch (e) {
+ // Ignore "Error: Can't use offThreadCompileScript with --no-threads"
+}
diff --git a/js/src/jit-test/tests/saved-stacks/bug-1225474.js b/js/src/jit-test/tests/saved-stacks/bug-1225474.js
new file mode 100644
index 0000000000..ca0dcaa31c
--- /dev/null
+++ b/js/src/jit-test/tests/saved-stacks/bug-1225474.js
@@ -0,0 +1,6 @@
+// setSavedStacksRNGState shouldn't crash regardless of the seed value passed.
+
+setSavedStacksRNGState(0);
+setSavedStacksRNGState({});
+setSavedStacksRNGState(false);
+setSavedStacksRNGState(NaN);
diff --git a/js/src/jit-test/tests/saved-stacks/bug-1260712.js b/js/src/jit-test/tests/saved-stacks/bug-1260712.js
new file mode 100644
index 0000000000..5b1319cd17
--- /dev/null
+++ b/js/src/jit-test/tests/saved-stacks/bug-1260712.js
@@ -0,0 +1,7 @@
+low = high = newGlobal({
+ principal: 5
+})
+high.low = low
+high.eval("function a() { return saveStack(1, low) }")
+set = eval("high.a()")
+serialize(set)
diff --git a/js/src/jit-test/tests/saved-stacks/bug-1289058.js b/js/src/jit-test/tests/saved-stacks/bug-1289058.js
new file mode 100644
index 0000000000..1968f58500
--- /dev/null
+++ b/js/src/jit-test/tests/saved-stacks/bug-1289058.js
@@ -0,0 +1,13 @@
+const g1 = newGlobal({});
+const g2 = newGlobal(newGlobal);
+g1.g2obj = g2.eval("new Object");
+g1.evaluate(`
+ const global = this;
+ function capture(shouldIgnoreSelfHosted = true) {
+ return captureFirstSubsumedFrame(global.g2obj, shouldIgnoreSelfHosted);
+ }
+ (function iife1() {
+ const captureTrueStack = capture(true);
+ }());
+`, {
+});
diff --git a/js/src/jit-test/tests/saved-stacks/bug-1289073.js b/js/src/jit-test/tests/saved-stacks/bug-1289073.js
new file mode 100644
index 0000000000..2d0a9919f6
--- /dev/null
+++ b/js/src/jit-test/tests/saved-stacks/bug-1289073.js
@@ -0,0 +1 @@
+saveStack(0.2);
diff --git a/js/src/jit-test/tests/saved-stacks/bug-1445973-quick.js b/js/src/jit-test/tests/saved-stacks/bug-1445973-quick.js
new file mode 100644
index 0000000000..e95315f71f
--- /dev/null
+++ b/js/src/jit-test/tests/saved-stacks/bug-1445973-quick.js
@@ -0,0 +1,53 @@
+// |jit-test| --no-baseline; skip-if: !('oomTest' in this)
+//
+// For background, see the comments for LiveSavedFrameCache in js/src/vm/Stack.h.
+//
+// The cache would like to assert that, assuming the cache hasn't been
+// completely flushed due to a compartment mismatch, if a stack frame's
+// hasCachedSavedFrame bit is set, then that frame does indeed have an entry in
+// the cache.
+//
+// When LiveSavedFrameCache::find finds an entry whose frame address matches,
+// but whose pc does not match, it removes that entry from the cache. Usually, a
+// fresh entry for that frame will be pushed into the cache in short order as we
+// rebuild the SavedFrame chain, but if the creation of the SavedFrame fails due
+// to OOM, then we are left with no cache entry for that frame.
+//
+// The fix for 1445973 is simply to clear the frame's bit when we remove the
+// cache entry for a pc mismatch. Previously the code neglected to do this, but
+// usually got away with it because the cache would be re-populated. OOM fuzzing
+// interrupted the code at the proper place and revealed the crash, but did so
+// with a test that took 90s to run. This test runs in a fraction of a second.
+
+function f() {
+ // Ensure that we will try to allocate fresh SavedFrame objects.
+ clearSavedFrames();
+
+ // Ensure that all frames have their hasCachedSavedFrame bits set.
+ saveStack();
+
+ try {
+ // Capture the stack again. The entry for this frame will be removed due to
+ // a pc mismatch. The OOM must occur here, causing the cache not to be
+ // repopulated.
+ saveStack();
+ } catch (e) { }
+
+ // Capture the stack a third time. This will see that f's frame has its bit
+ // set, even though it has no entry in the cache.
+ saveStack();
+}
+
+// This ensures that there is a frame below f's in the same Activation, so that
+// the assertion doesn't get skipped because the LiveSavedFrameCache is entirely
+// empty, to handle caches flushed by compartment mismatches.
+function g() { f(); }
+
+// Call all the code once, to ensure that everything has been delazified. When
+// different calls to g perform different amounts of allocation, oomTest's
+// simple strategy for choosing which allocation should fail can neglect to hit
+// the SavedFrame creation. This is also why we disable the baseline compiler in
+// the test metaline.
+g();
+
+oomTest(g);
diff --git a/js/src/jit-test/tests/saved-stacks/bug-1451268.js b/js/src/jit-test/tests/saved-stacks/bug-1451268.js
new file mode 100644
index 0000000000..a4b7b03d92
--- /dev/null
+++ b/js/src/jit-test/tests/saved-stacks/bug-1451268.js
@@ -0,0 +1,23 @@
+// |jit-test| --no-threads; --ion-eager
+// Walking into Rematerialized frames under ordinary frames with their
+// hasCachedSavedFrame bits set shouldn't cause an assertion.
+
+enableTrackAllocations();
+var g = newGlobal({ newCompartment: true });
+var dbg = new Debugger;
+g.toggle = function toggle(x, d) {
+ if (d) {
+ dbg.addDebuggee(g);
+ var frame = dbg.getNewestFrame().older;
+ }
+};
+g.eval("" + function f(x, d) {
+ g(x, d);
+});
+g.eval("" + function g(x, d) {
+ toggle(x, d);
+});
+g.eval("(" + function test() {
+ for (var i = 0; i < 5; i++) f(42, false);
+ f(42, true);
+} + ")();");
diff --git a/js/src/jit-test/tests/saved-stacks/bug-1505387-dbg-eval-ion.js b/js/src/jit-test/tests/saved-stacks/bug-1505387-dbg-eval-ion.js
new file mode 100644
index 0000000000..64029d8cb0
--- /dev/null
+++ b/js/src/jit-test/tests/saved-stacks/bug-1505387-dbg-eval-ion.js
@@ -0,0 +1,12 @@
+// |jit-test| --ion-eager; --no-threads;
+// This test ensures that debugger eval on an ion frame is able to correctly
+// follow the debugger eval frame link to its parent frame.
+
+var dbgGlobal = newGlobal({ newCompartment: true });
+var dbg = new dbgGlobal.Debugger(globalThis);
+
+for (var i = 0; i < 1; i++) {
+ (() => {
+ dbg.getNewestFrame().older.eval("saveStack()");
+ })();
+}
diff --git a/js/src/jit-test/tests/saved-stacks/bug-1509420.js b/js/src/jit-test/tests/saved-stacks/bug-1509420.js
new file mode 100644
index 0000000000..87cd0b7f21
--- /dev/null
+++ b/js/src/jit-test/tests/saved-stacks/bug-1509420.js
@@ -0,0 +1,9 @@
+// bindtoAsyncStack shouldn't choke on CCWs of functions.
+
+var g = newGlobal();
+g.evaluate("function h() {}");
+bindToAsyncStack(g.h, { stack: saveStack() })();
+
+bindToAsyncStack(new Proxy(() => {}, { apply: () => {} }),
+ { stack: saveStack() })
+();
diff --git a/js/src/jit-test/tests/saved-stacks/bug-1640034-dbg-eval-across-compartments.js b/js/src/jit-test/tests/saved-stacks/bug-1640034-dbg-eval-across-compartments.js
new file mode 100644
index 0000000000..73c4dc9621
--- /dev/null
+++ b/js/src/jit-test/tests/saved-stacks/bug-1640034-dbg-eval-across-compartments.js
@@ -0,0 +1,22 @@
+// This test ensures that capturing a stack works when the Debugger.Frame
+// being used for an eval has an async stack on the activation between
+// the two debuggee frames.
+
+const global = newGlobal({ newCompartment: true });
+const dbg = new Debugger(global);
+dbg.onDebuggerStatement = function() {
+ const frame = dbg.getNewestFrame();
+
+ // Capturing this stack inside the debugger compartment will populate the
+ // LiveSavedFrameCache with a SavedFrame in the debugger compartment.
+ const opts = {
+ stack: saveStack(),
+ };
+
+ bindToAsyncStack(function() {
+ // This captured stack needs to properly invalidate the LiveSavedFrameCache
+ // for the frame and create a new one inside the debuggee compartment.
+ frame.eval(`saveStack()`);
+ }, opts)();
+};
+global.eval(`debugger;`);
diff --git a/js/src/jit-test/tests/saved-stacks/caching-and-ccws.js b/js/src/jit-test/tests/saved-stacks/caching-and-ccws.js
new file mode 100644
index 0000000000..b61e8c3252
--- /dev/null
+++ b/js/src/jit-test/tests/saved-stacks/caching-and-ccws.js
@@ -0,0 +1,35 @@
+// Test that the SavedFrame caching doesn't get messed up in the presence of
+// cross-compartment calls.
+
+const funcSource = "function call(f) { return f(); }";
+
+const g1 = newGlobal();
+const g2 = newGlobal();
+
+g1.eval(funcSource);
+g2.eval(funcSource);
+eval(funcSource);
+
+function doSaveStack() {
+ return saveStack();
+}
+
+const captureStacksAcrossCompartmens = () =>
+ [this, g1, g2].map(g => g.call(doSaveStack));
+
+(function f0() {
+ const stacks = [];
+
+ for (var i = 0; i < 2; i++)
+ stacks.push(...captureStacksAcrossCompartmens());
+
+ const [s1, s2, s3, s4, s5, s6] = stacks;
+
+ assertEq(s1 != s2, true);
+ assertEq(s2 != s3, true);
+ assertEq(s3 != s1, true);
+
+ assertEq(s1, s4);
+ assertEq(s2, s5);
+ assertEq(s3, s6);
+}());
diff --git a/js/src/jit-test/tests/saved-stacks/caching-and-frame-count.js b/js/src/jit-test/tests/saved-stacks/caching-and-frame-count.js
new file mode 100644
index 0000000000..a751d0e3c6
--- /dev/null
+++ b/js/src/jit-test/tests/saved-stacks/caching-and-frame-count.js
@@ -0,0 +1,38 @@
+// Test that the SavedFrame caching doesn't mess up counts. Specifically, that
+// if we capture only the first n frames of a stack, we don't cache that stack
+// and return it for when someone else captures another stack and asks for more
+// frames than that last time.
+
+function stackLength(stack) {
+ return stack === null
+ ? 0
+ : 1 + stackLength(stack.parent);
+}
+
+(function f0() {
+ (function f1() {
+ (function f2() {
+ (function f3() {
+ (function f4() {
+ (function f5() {
+ (function f6() {
+ (function f7() {
+ (function f8() {
+ (function f9() {
+ const s1 = saveStack(3);
+ const s2 = saveStack(5);
+ const s3 = saveStack(0 /* no limit */);
+
+ assertEq(stackLength(s1), 3);
+ assertEq(stackLength(s2), 5);
+ assertEq(stackLength(s3), 11);
+ }());
+ }());
+ }());
+ }());
+ }());
+ }());
+ }());
+ }());
+ }());
+}());
diff --git a/js/src/jit-test/tests/saved-stacks/capture-first-frame-with-principals.js b/js/src/jit-test/tests/saved-stacks/capture-first-frame-with-principals.js
new file mode 100644
index 0000000000..6b126cb098
--- /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"
+});
diff --git a/js/src/jit-test/tests/saved-stacks/display-url.js b/js/src/jit-test/tests/saved-stacks/display-url.js
new file mode 100644
index 0000000000..4688fefe01
--- /dev/null
+++ b/js/src/jit-test/tests/saved-stacks/display-url.js
@@ -0,0 +1,26 @@
+eval(`
+ function a() {
+ return b();
+ }
+ //# sourceURL=source-a.js
+`);
+
+eval(`
+ function b() {
+ return c();
+ }
+ //# sourceURL=source-b.js
+`);
+
+eval(`
+ function c() {
+ return saveStack();
+ }
+ //# sourceURL=source-c.js
+`);
+
+let stack = a();
+print(stack);
+assertEq(stack.source, "source-c.js");
+assertEq(stack.parent.source, "source-b.js");
+assertEq(stack.parent.parent.source, "source-a.js");
diff --git a/js/src/jit-test/tests/saved-stacks/evals.js b/js/src/jit-test/tests/saved-stacks/evals.js
new file mode 100644
index 0000000000..41a0f9111c
--- /dev/null
+++ b/js/src/jit-test/tests/saved-stacks/evals.js
@@ -0,0 +1,38 @@
+// Test that we can save stacks with direct and indirect eval calls.
+
+const directEval = (function iife() {
+ return eval("(" + function evalFrame() {
+ return saveStack();
+ } + "())");
+}());
+
+assertEq(directEval.source.includes("> eval"), true);
+assertEq(directEval.functionDisplayName, "evalFrame");
+
+assertEq(directEval.parent.source.includes("> eval"), true);
+
+assertEq(directEval.parent.parent.source.includes("> eval"), false);
+assertEq(directEval.parent.parent.functionDisplayName, "iife");
+
+assertEq(directEval.parent.parent.parent.source.includes("> eval"), false);
+
+assertEq(directEval.parent.parent.parent.parent, null);
+
+const lave = eval;
+const indirectEval = (function iife() {
+ return lave("(" + function evalFrame() {
+ return saveStack();
+ } + "())");
+}());
+
+assertEq(indirectEval.source.includes("> eval"), true);
+assertEq(indirectEval.functionDisplayName, "evalFrame");
+
+assertEq(indirectEval.parent.source.includes("> eval"), true);
+
+assertEq(indirectEval.parent.parent.source.includes("> eval"), false);
+assertEq(indirectEval.parent.parent.functionDisplayName, "iife");
+
+assertEq(indirectEval.parent.parent.parent.source.includes("> eval"), false);
+
+assertEq(indirectEval.parent.parent.parent.parent, null);
diff --git a/js/src/jit-test/tests/saved-stacks/function-display-name.js b/js/src/jit-test/tests/saved-stacks/function-display-name.js
new file mode 100644
index 0000000000..f10b7de6bd
--- /dev/null
+++ b/js/src/jit-test/tests/saved-stacks/function-display-name.js
@@ -0,0 +1,16 @@
+// Test the functionDisplayName of SavedFrame instances.
+
+function uno() { return dos(); }
+const dos = () => tres.quattro();
+let tres = {};
+tres.quattro = () => saveStack()
+
+const frame = uno();
+
+assertEq(frame.functionDisplayName, "tres.quattro");
+assertEq(frame.parent.functionDisplayName, "dos");
+assertEq(frame.parent.parent.functionDisplayName, "uno");
+assertEq(frame.parent.parent.parent.functionDisplayName, null);
+
+assertEq(frame.parent.parent.parent.parent, null);
+
diff --git a/js/src/jit-test/tests/saved-stacks/gc-frame-cache.js b/js/src/jit-test/tests/saved-stacks/gc-frame-cache.js
new file mode 100644
index 0000000000..cf2646f471
--- /dev/null
+++ b/js/src/jit-test/tests/saved-stacks/gc-frame-cache.js
@@ -0,0 +1,87 @@
+// Test that SavedFrame instances get removed from the SavedStacks frames cache
+// after a GC.
+
+const FUZZ_FACTOR = 3;
+
+function isAboutEq(actual, expected) {
+ return Math.abs(actual - expected) <= FUZZ_FACTOR;
+}
+
+var stacks = [];
+
+(function () {
+ // Use an IIFE here so that we don't keep these saved stacks alive in the
+ // frame cache when we test that they all go away at the end of the test.
+
+ var startCount = getSavedFrameCount();
+ print("startCount = " + startCount);
+
+ stacks.push(saveStack());
+ stacks.push(saveStack());
+ stacks.push(saveStack());
+ stacks.push(saveStack());
+ stacks.push(saveStack());
+ stacks.push(saveStack());
+ stacks.push(saveStack());
+ stacks.push(saveStack());
+ stacks.push(saveStack());
+ stacks.push(saveStack());
+ stacks.push(saveStack());
+ stacks.push(saveStack());
+ stacks.push(saveStack());
+ stacks.push(saveStack());
+ stacks.push(saveStack());
+ stacks.push(saveStack());
+ stacks.push(saveStack());
+ stacks.push(saveStack());
+ stacks.push(saveStack());
+ stacks.push(saveStack());
+ stacks.push(saveStack());
+ stacks.push(saveStack());
+ stacks.push(saveStack());
+ stacks.push(saveStack());
+ stacks.push(saveStack());
+ stacks.push(saveStack());
+ stacks.push(saveStack());
+ stacks.push(saveStack());
+ stacks.push(saveStack());
+ stacks.push(saveStack());
+ stacks.push(saveStack());
+ stacks.push(saveStack());
+ stacks.push(saveStack());
+ stacks.push(saveStack());
+ stacks.push(saveStack());
+ stacks.push(saveStack());
+ stacks.push(saveStack());
+ stacks.push(saveStack());
+ stacks.push(saveStack());
+ stacks.push(saveStack());
+ stacks.push(saveStack());
+ stacks.push(saveStack());
+ stacks.push(saveStack());
+ stacks.push(saveStack());
+ stacks.push(saveStack());
+ stacks.push(saveStack());
+ stacks.push(saveStack());
+ stacks.push(saveStack());
+ stacks.push(saveStack());
+ stacks.push(saveStack());
+ stacks.push(saveStack());
+
+ gc();
+
+ var endCount = getSavedFrameCount();
+ print("endCount = " + endCount);
+
+ assertEq(isAboutEq(endCount - startCount, 50), true);
+}());
+
+while (stacks.length) {
+ stacks.pop();
+}
+gc();
+
+stacks = null;
+gc();
+
+assertEq(isAboutEq(getSavedFrameCount(), 0), true);
diff --git a/js/src/jit-test/tests/saved-stacks/generators.js b/js/src/jit-test/tests/saved-stacks/generators.js
new file mode 100644
index 0000000000..2878997580
--- /dev/null
+++ b/js/src/jit-test/tests/saved-stacks/generators.js
@@ -0,0 +1,19 @@
+// Test that we can save stacks which have generator frames.
+
+const { value: frame } = (function iife1() {
+ return (function* generator() {
+ yield (function iife2() {
+ return saveStack();
+ }());
+ }()).next();
+}());
+
+// Bug 1102498 - toString does not include self-hosted frames, which can appear
+// depending on GC timing. This may end up changing in the future, see
+// bug 1103155.
+
+var lines = frame.toString().split("\n");
+assertEq(lines[0].startsWith("iife2@"), true);
+assertEq(lines[1].startsWith("generator@"), true);
+assertEq(lines[2].startsWith("iife1@"), true);
+assertEq(lines[3].startsWith("@"), true);
diff --git a/js/src/jit-test/tests/saved-stacks/get-set.js b/js/src/jit-test/tests/saved-stacks/get-set.js
new file mode 100644
index 0000000000..be2e207399
--- /dev/null
+++ b/js/src/jit-test/tests/saved-stacks/get-set.js
@@ -0,0 +1,25 @@
+// Test that we can save stacks with getter and setter function frames.
+
+function assertStackLengthEq(stack, expectedLength) {
+ let actual = 0;
+ while (stack) {
+ actual++;
+ stack = stack.parent;
+ }
+ assertEq(actual, expectedLength);
+}
+
+const get = { get s() { return saveStack(); } }.s;
+assertStackLengthEq(get, 2);
+
+let set;
+try {
+ ({
+ set s(v) {
+ throw saveStack();
+ }
+ }).s = 1;
+} catch (s) {
+ set = s;
+}
+assertStackLengthEq(set, 2);
diff --git a/js/src/jit-test/tests/saved-stacks/getters-on-invalid-objects.js b/js/src/jit-test/tests/saved-stacks/getters-on-invalid-objects.js
new file mode 100644
index 0000000000..9a892f20bf
--- /dev/null
+++ b/js/src/jit-test/tests/saved-stacks/getters-on-invalid-objects.js
@@ -0,0 +1,23 @@
+// Test that you can't call the SavedFrame constructor and can only use
+// SavedFrame's getters on SavedFrame instances.
+
+load(libdir + "asserts.js");
+
+let proto = Object.getPrototypeOf(saveStack());
+
+// Can't create new SavedFrame instances by hand.
+print("Testing constructor");
+assertThrowsInstanceOf(() => {
+ new proto.constructor();
+}, TypeError);
+
+for (let p of ["source", "line", "column", "functionDisplayName", "parent"]) {
+ print("Testing getter: " + p);
+ // The getters shouldn't work on the prototype.
+ assertThrowsInstanceOf(() => proto[p], TypeError);
+
+ // Nor should they work on random objects.
+ let o = {};
+ Object.defineProperty(o, p, Object.getOwnPropertyDescriptor(proto, p));
+ assertThrowsInstanceOf(() => o[p], TypeError);
+}
diff --git a/js/src/jit-test/tests/saved-stacks/max-frame-count.js b/js/src/jit-test/tests/saved-stacks/max-frame-count.js
new file mode 100644
index 0000000000..17c3751765
--- /dev/null
+++ b/js/src/jit-test/tests/saved-stacks/max-frame-count.js
@@ -0,0 +1,42 @@
+// Test that we can capture only the N newest frames.
+// This is the maxFrameCount argument to JS::CaptureCurrentStack.
+
+load(libdir + 'asserts.js');
+
+function recur(n, limit) {
+ if (n > 0)
+ return recur(n - 1, limit);
+ return saveStack(limit);
+}
+
+// Negative values are rejected.
+assertThrowsInstanceOf(() => saveStack(-1), TypeError);
+
+// Zero means 'no limit'.
+assertEq(saveStack(0).parent, null);
+assertEq(recur(0, 0).parent !== null, true);
+assertEq(recur(0, 0).parent.parent, null);
+assertEq(recur(1, 0).parent.parent.parent, null);
+assertEq(recur(2, 0).parent.parent.parent.parent, null);
+assertEq(recur(3, 0).parent.parent.parent.parent.parent, null);
+
+// limit of 1
+assertEq(saveStack(1).parent, null);
+assertEq(recur(0, 1).parent, null);
+assertEq(recur(0, 1).parent, null);
+assertEq(recur(1, 1).parent, null);
+assertEq(recur(2, 1).parent, null);
+
+// limit of 2
+assertEq(saveStack(2).parent, null);
+assertEq(recur(0, 2).parent !== null, true);
+assertEq(recur(0, 2).parent.parent, null);
+assertEq(recur(1, 2).parent.parent, null);
+assertEq(recur(2, 2).parent.parent, null);
+
+// limit of 3
+assertEq(saveStack(3).parent, null);
+assertEq(recur(0, 3).parent !== null, true);
+assertEq(recur(0, 3).parent.parent, null);
+assertEq(recur(1, 3).parent.parent.parent, null);
+assertEq(recur(2, 3).parent.parent.parent, null);
diff --git a/js/src/jit-test/tests/saved-stacks/native-calls.js b/js/src/jit-test/tests/saved-stacks/native-calls.js
new file mode 100644
index 0000000000..4b12ad7383
--- /dev/null
+++ b/js/src/jit-test/tests/saved-stacks/native-calls.js
@@ -0,0 +1,12 @@
+// Test that we can save stacks with native code on the stack.
+
+// Unlike Array.prototype.map, Array.prototype.filter is not self-hosted.
+const filter = (function iife() {
+ try {
+ callFunctionFromNativeFrame(n => { throw saveStack() });
+ } catch (s) {
+ return s;
+ }
+}());
+
+assertEq(filter.parent.functionDisplayName, "iife");
diff --git a/js/src/jit-test/tests/saved-stacks/oom-in-save-stack-02.js b/js/src/jit-test/tests/saved-stacks/oom-in-save-stack-02.js
new file mode 100644
index 0000000000..ed24db8f0f
--- /dev/null
+++ b/js/src/jit-test/tests/saved-stacks/oom-in-save-stack-02.js
@@ -0,0 +1,28 @@
+// |jit-test| --no-ion; --no-baseline; --no-blinterp; skip-if: !('oomAtAllocation' in this)
+// This shouldn't assert (bug 1516514).
+//
+// Disabled for ion and baseline because those introduce OOMs at some point that
+// we don't seem to be able to catch, and they're not relevant to the bug.
+
+let g = newGlobal();
+
+function oomTest() {
+ let done = false;
+ for (let j = 1; !done; j++) {
+ saveStack();
+
+ oomAtAllocation(j);
+
+ try {
+ g.saveStack();
+ } catch {}
+
+ done = !resetOOMFailure();
+
+ try {
+ g.saveStack();
+ } catch {}
+ }
+}
+
+oomTest();
diff --git a/js/src/jit-test/tests/saved-stacks/oom-in-save-stack.js b/js/src/jit-test/tests/saved-stacks/oom-in-save-stack.js
new file mode 100644
index 0000000000..c96bbc5c6b
--- /dev/null
+++ b/js/src/jit-test/tests/saved-stacks/oom-in-save-stack.js
@@ -0,0 +1,4 @@
+// |jit-test| skip-if: !('oomTest' in this)
+
+let s = saveStack();
+oomTest(() => { saveStack(); });
diff --git a/js/src/jit-test/tests/saved-stacks/principals-01.js b/js/src/jit-test/tests/saved-stacks/principals-01.js
new file mode 100644
index 0000000000..85b46feb87
--- /dev/null
+++ b/js/src/jit-test/tests/saved-stacks/principals-01.js
@@ -0,0 +1,70 @@
+// Test that SavedFrame.prototype.parent gives the next older frame whose
+// principals are subsumed by the caller's principals.
+
+// 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.
+function check(expected, stack) {
+ print("check(" + JSON.stringify(expected) + ") against:\n" + stack);
+ count++;
+
+ while (stack.length && expected.length) {
+ assertEq(stack.shift(), expected[0]);
+ expected = expected.slice(1);
+ }
+
+ if (expected.length > 0) {
+ throw new Error("Missing frames for: " + expected);
+ }
+ if (stack.length > 0 && !stack.every(s => s === null)) {
+ throw new Error("Unexpected extra frame(s):\n" + stack);
+ }
+}
+
+// Go from a SavedFrame linked list to an array of function display names.
+function extract(stack) {
+ const results = [];
+ while (stack) {
+ results.push(stack.functionDisplayName);
+ stack = stack.parent;
+ }
+ return results;
+}
+
+const low = newGlobal({ principal: 0 });
+const mid = newGlobal({ principal: 0xffff });
+const high = newGlobal({ principal: 0xfffff });
+
+var count = 0;
+
+ eval('function a() { check("a", extract(saveStack())); b(); }');
+low .eval('function b() { check("b", extract(saveStack())); c(); }');
+mid .eval('function c() { check("cba", extract(saveStack())); d(); }');
+high.eval('function d() { check("dcba", extract(saveStack())); e(); }');
+ eval('function e() { check("ecba", extract(saveStack())); f(); }');
+low .eval('function f() { check("fb", extract(saveStack())); g(); }');
+mid .eval('function g() { check("gfecba", extract(saveStack())); h(); }');
+high.eval('function h() { check("hgfedcba", extract(saveStack())); }');
+
+// 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;
+
+// They each must have their own extract so that CCWs don't mess up the
+// principals when we ask for the parent property.
+low. eval("" + extract);
+mid. eval("" + extract);
+high.eval("" + extract);
+
+// Kick the whole process off.
+a();
+
+assertEq(count, 8);
diff --git a/js/src/jit-test/tests/saved-stacks/principals-02.js b/js/src/jit-test/tests/saved-stacks/principals-02.js
new file mode 100644
index 0000000000..8253ce4566
--- /dev/null
+++ b/js/src/jit-test/tests/saved-stacks/principals-02.js
@@ -0,0 +1,56 @@
+// Test that SavedFrame.prototype.toString only shows frames whose principal is
+// subsumed by the caller's principal.
+
+var count = 0;
+
+// 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.
+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.
+ const frames = stack
+ .split("\n")
+ .filter(f => f.match(/^.@/))
+ .map(f => f.replace(/@.*$/g, ""));
+
+ // Check the function names against the expected sequence.
+ assertEq(frames.length, expected.length);
+ for (var i = 0; i < expected.length; i++) {
+ assertEq(frames[i], expected[i]);
+ }
+}
+
+var low = newGlobal({ principal: 0 });
+var mid = newGlobal({ principal: 0xffff });
+var high = newGlobal({ principal: 0xfffff });
+
+ eval('function a() { check("a", saveStack().toString()); b(); }');
+low .eval('function b() { check("b", saveStack().toString()); c(); }');
+mid .eval('function c() { check("cba", saveStack().toString()); d(); }');
+high.eval('function d() { check("dcba", saveStack().toString()); e(); }');
+ eval('function e() { check("ecba", saveStack().toString()); f(); }');
+low .eval('function f() { check("fb", saveStack().toString()); g(); }');
+mid .eval('function g() { check("gfecba", saveStack().toString()); h(); }');
+high.eval('function h() { check("hgfedcba", saveStack().toString()); }');
+
+// 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);
+
diff --git a/js/src/jit-test/tests/saved-stacks/principals-03.js b/js/src/jit-test/tests/saved-stacks/principals-03.js
new file mode 100644
index 0000000000..006b4477cd
--- /dev/null
+++ b/js/src/jit-test/tests/saved-stacks/principals-03.js
@@ -0,0 +1,23 @@
+// With arrows representing child-to-parent links, create a SavedFrame stack
+// like this:
+//
+// high.a -> low.b
+//
+// in `low`'s compartment and give `low` a reference to this stack. Assert the
+// stack's youngest frame's properties doesn't leak information about `high.a`
+// that `low` shouldn't have access to, and instead returns information about
+// `low.b`.
+
+var low = newGlobal({ principal: 0 });
+var high = newGlobal({ principal: 0xfffff });
+
+low.high = high;
+high.low = low;
+
+high.eval("function a() { return saveStack(0, low); }");
+low.eval("function b() { return high.a(); }")
+
+var stack = low.b();
+
+assertEq(stack.functionDisplayName, "b");
+assertEq(stack.parent, null);
diff --git a/js/src/jit-test/tests/saved-stacks/principals-04.js b/js/src/jit-test/tests/saved-stacks/principals-04.js
new file mode 100644
index 0000000000..3a9b578005
--- /dev/null
+++ b/js/src/jit-test/tests/saved-stacks/principals-04.js
@@ -0,0 +1,15 @@
+// Test what happens when a compartment gets a SavedFrame that it doesn't have
+// the principals to access any of its frames.
+
+var low = newGlobal({ principal: 0 });
+var high = newGlobal({ principal: 0xfffff });
+
+low.high = high;
+high.low = low;
+
+high.eval("function a() { return saveStack(1, low); }");
+var stack = low.eval("high.a();")
+
+assertEq(stack.functionDisplayName, null);
+assertEq(stack.parent, null);
+assertEq(stack.toString(), "");
diff --git a/js/src/jit-test/tests/saved-stacks/proxy-handlers.js b/js/src/jit-test/tests/saved-stacks/proxy-handlers.js
new file mode 100644
index 0000000000..7ad1f6dc68
--- /dev/null
+++ b/js/src/jit-test/tests/saved-stacks/proxy-handlers.js
@@ -0,0 +1,10 @@
+// Test that we can save stacks with proxy handler frames.
+
+const stack = (function iife() {
+ return (new Proxy({}, {
+ get: function get(t, n, r) { return saveStack(); }
+ })).stack;
+}());
+
+assertEq(stack.functionDisplayName, "get");
+assertEq(stack.parent.functionDisplayName, "iife");
diff --git a/js/src/jit-test/tests/saved-stacks/same-stack.js b/js/src/jit-test/tests/saved-stacks/same-stack.js
new file mode 100644
index 0000000000..b82ba1c04a
--- /dev/null
+++ b/js/src/jit-test/tests/saved-stacks/same-stack.js
@@ -0,0 +1,12 @@
+// Test that the same saved stack is only ever allocated once.
+
+const stacks = [];
+
+for (let i = 0; i < 10; i++) {
+ stacks.push(saveStack());
+}
+
+const s = stacks.pop();
+for (let stack of stacks) {
+ assertEq(s, stack);
+}
diff --git a/js/src/jit-test/tests/saved-stacks/self-hosted.js b/js/src/jit-test/tests/saved-stacks/self-hosted.js
new file mode 100644
index 0000000000..88f8ce2007
--- /dev/null
+++ b/js/src/jit-test/tests/saved-stacks/self-hosted.js
@@ -0,0 +1,26 @@
+// Test that we can save stacks with self-hosted function frames in them.
+
+const map = (function () {
+ return [3].map(n => saveStack()).pop();
+}());
+
+assertEq(map.parent.functionDisplayName, "map");
+assertEq(map.parent.source, "self-hosted");
+
+const reduce = (function () {
+ return [3].reduce(() => saveStack(), 3);
+}());
+
+assertEq(reduce.parent.functionDisplayName, "reduce");
+assertEq(reduce.parent.source, "self-hosted");
+
+const forEach = (function () {
+ try {
+ [3].forEach(n => { throw saveStack() });
+ } catch (s) {
+ return s;
+ }
+}());
+
+assertEq(forEach.parent.functionDisplayName, "forEach");
+assertEq(forEach.parent.source, "self-hosted");
diff --git a/js/src/jit-test/tests/saved-stacks/shared-parent-frames.js b/js/src/jit-test/tests/saved-stacks/shared-parent-frames.js
new file mode 100644
index 0000000000..c6b4332dd9
--- /dev/null
+++ b/js/src/jit-test/tests/saved-stacks/shared-parent-frames.js
@@ -0,0 +1,19 @@
+// Test that parent frames are shared when the older portions of two stacks are
+// the same.
+
+let f1, f2;
+
+function dos() {
+ f1 = saveStack();
+ f2 = saveStack();
+}
+
+(function uno() {
+ dos();
+}());
+
+
+// Different youngest frames.
+assertEq(f1 == f2, false);
+// However the parents should be the same.
+assertEq(f1.parent, f2.parent);
diff --git a/js/src/jit-test/tests/saved-stacks/stacks-are-frozen.js b/js/src/jit-test/tests/saved-stacks/stacks-are-frozen.js
new file mode 100644
index 0000000000..b2d125aa42
--- /dev/null
+++ b/js/src/jit-test/tests/saved-stacks/stacks-are-frozen.js
@@ -0,0 +1,17 @@
+// Test that SavedFrame instances are frozen and can't be messed with.
+
+// Strict mode so that mutating frozen objects doesn't silently fail.
+"use strict";
+
+const s = saveStack();
+
+load(libdir + 'asserts.js');
+
+assertThrowsInstanceOf(() => s.source = "fake.url",
+ TypeError);
+
+assertThrowsInstanceOf(() => {
+ Object.defineProperty(s.__proto__, "line", {
+ get: () => 0
+ })
+}, TypeError);
diff --git a/js/src/jit-test/tests/saved-stacks/stringify-with-self-hosted.js b/js/src/jit-test/tests/saved-stacks/stringify-with-self-hosted.js
new file mode 100644
index 0000000000..2f867d8f3d
--- /dev/null
+++ b/js/src/jit-test/tests/saved-stacks/stringify-with-self-hosted.js
@@ -0,0 +1,8 @@
+// Test that stringify'ing a saved frame with self-hosted parent frames doesn't
+// include the self-hosted parent frame in the output.
+
+const map = (function () {
+ return [3].map(n => saveStack()).pop();
+}());
+
+assertEq(map.toString().includes("@self-hosted:"), false);