diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /js/src/jit-test/tests/saved-stacks | |
parent | Initial commit. (diff) | |
download | firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'js/src/jit-test/tests/saved-stacks')
49 files changed, 1713 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..996f606f13 --- /dev/null +++ b/js/src/jit-test/tests/saved-stacks/bug-1149495.js @@ -0,0 +1,7 @@ +try { + offThreadCompileToStencil('Error()', { lineNumber: (4294967295)}); + var stencil = finishOffThreadStencil(); + evalStencil(stencil).stack; +} catch (e) { + // Ignore "Error: Can't use offThreadCompileToStencil 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/bug-1744495.js b/js/src/jit-test/tests/saved-stacks/bug-1744495.js new file mode 100644 index 0000000000..4ce56f4235 --- /dev/null +++ b/js/src/jit-test/tests/saved-stacks/bug-1744495.js @@ -0,0 +1,18 @@ +// |jit-test| --fast-warmup; --more-compartments + +enableTrackAllocations() +e = function(a) { + b = newGlobal() + c = new b.Debugger + return function(d, code) { + c.addDebuggee(a) + c.getNewestFrame().older.older.eval(code) + } +}(this) +for (var i = 0; i < 50; i++) f() +function g() { + e(1, "a.push0") +} +function f() { + g() +} diff --git a/js/src/jit-test/tests/saved-stacks/bug1813533.js b/js/src/jit-test/tests/saved-stacks/bug1813533.js new file mode 100644 index 0000000000..e16587413c --- /dev/null +++ b/js/src/jit-test/tests/saved-stacks/bug1813533.js @@ -0,0 +1,22 @@ +async function foo() { + // Suspend this function until later. + await undefined; + + // Capture the stack using JS::AllFrames. + saveStack(); + + // Capture the stack using JS::FirstSubsumedFrame. + var g = newGlobal({principal: {}}); + captureFirstSubsumedFrame(g); +} + +// Invoke foo. This will capture the stack and mark the bit +// on the top-level script, but we clear it below. +foo(); + +// Tier up to blinterp, clearing the hasCachedSavedFrame bit +// on the top-level script. +for (var i = 0; i < 20; i++) {} + +// Continue execution of foo. +drainJobQueue(); 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..de40e564ae --- /dev/null +++ b/js/src/jit-test/tests/saved-stacks/caching-and-frame-count.js @@ -0,0 +1,40 @@ +// |jit-test| skip-if: getBuildConfiguration()['wasi'] +// +// 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..640b129567 --- /dev/null +++ b/js/src/jit-test/tests/saved-stacks/capture-first-frame-with-principals.js @@ -0,0 +1,86 @@ +// 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; + + 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) <- Array.prototype.map <- capture (g1) + + (function iife2() { + const trueStack = [true].map(capture)[0]; + dumpStack("trueStack", trueStack); + assertEq(getOldestFrame(trueStack).functionDisplayName, "iife2"); + assertEq(getOldestFrame(trueStack).source, "script2.js"); + + const falseStack = [false].map(capture)[0]; + dumpStack("falseStack", falseStack); + assertEq(getOldestFrame(falseStack).functionDisplayName !== "iife2", true); + assertEq(getOldestFrame(falseStack).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); |