diff options
Diffstat (limited to 'js/src/jit-test/tests/saved-stacks')
37 files changed, 1185 insertions, 0 deletions
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 000000000..0dbafea98 --- /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 000000000..366712253 --- /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-max-frame-count.js b/js/src/jit-test/tests/saved-stacks/async-max-frame-count.js new file mode 100644 index 000000000..ebcbe13d3 --- /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(" + uneval(n) + ", " + uneval(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 000000000..09b8b5ce3 --- /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.toSource()); +high.eval(b.toSource()); +high.eval(c.toSource()); +high.eval(d.toSource()); +low .eval(e.toSource()); + +low .b = high.b; +high.e = low .e; + +low .eval(u.toSource()); +high.eval(v.toSource()); +high.eval(w.toSource()); +low .eval(x.toSource()); +low .eval(y.toSource()); +low .eval(z.toSource()); + +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.toSource()); +high.eval(checkVisibleStack.toSource()); + +// 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 000000000..6ab4546a7 --- /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 000000000..785c02494 --- /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 000000000..593c4b40b --- /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 000000000..95a63aafd --- /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 000000000..3162855a8 --- /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 000000000..aedd7726d --- /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 000000000..ca0dcaa31 --- /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 000000000..5b1319cd1 --- /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 000000000..1968f5850 --- /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 000000000..2d0a9919f --- /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/caching-and-ccws.js b/js/src/jit-test/tests/saved-stacks/caching-and-ccws.js new file mode 100644 index 000000000..b61e8c325 --- /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 000000000..a751d0e3c --- /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 000000000..6b126cb09 --- /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 000000000..4688fefe0 --- /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 000000000..41a0f9111 --- /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 000000000..cfe175758 --- /dev/null +++ b/js/src/jit-test/tests/saved-stacks/function-display-name.js @@ -0,0 +1,17 @@ +// Test the functionDisplayName of SavedFrame instances. + +function uno() { return dos(); } +const dos = () => tres.quattro(); +const 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 000000000..cf2646f47 --- /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 000000000..287899758 --- /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 000000000..be2e20739 --- /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 000000000..9a892f20b --- /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 000000000..17c375176 --- /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 000000000..4b12ad738 --- /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.js b/js/src/jit-test/tests/saved-stacks/oom-in-save-stack.js new file mode 100644 index 000000000..ba36df057 --- /dev/null +++ b/js/src/jit-test/tests/saved-stacks/oom-in-save-stack.js @@ -0,0 +1,4 @@ +if (!('oomTest' in this)) + quit(); +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 000000000..62640472d --- /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(" + uneval(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 000000000..24ffa82a8 --- /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(" + uneval(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 000000000..006b4477c --- /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 000000000..3a9b57800 --- /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 000000000..7ad1f6dc6 --- /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 000000000..b82ba1c04 --- /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 000000000..88f8ce200 --- /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 000000000..c6b4332dd --- /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 000000000..b2d125aa4 --- /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 000000000..2f867d8f3 --- /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); |