tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

capture-stack.js (10733B)


      1 // |jit-test| --setpref=experimental.error_capture_stack_trace;
      2 load(libdir + "asserts.js");
      3 
      4 if ('captureStackTrace' in Error) {
      5    assertEq(Error.captureStackTrace.length, 2);
      6 
      7    let x = Error.captureStackTrace({});
      8    assertEq(x, undefined);
      9 
     10    assertThrowsInstanceOf(() => Error.captureStackTrace(), TypeError);
     11    assertThrowsInstanceOf(() => Error.captureStackTrace(2), TypeError);
     12 
     13    Error.captureStackTrace({}, 2);
     14    Error.captureStackTrace({}, null);
     15    Error.captureStackTrace({}, {});
     16 
     17    function caller(f) {
     18        return f();
     19    }
     20 
     21    function fill() {
     22        let x = {}
     23        Error.captureStackTrace(x, caller);
     24        return x;
     25    }
     26 
     27    function test_elision() {
     28        let x = caller(fill);
     29        let { stack } = x;
     30        assertEq(stack.includes("caller"), false);
     31        assertEq(stack.includes("fill"), false);
     32 
     33 
     34        ({ stack } = caller(() => caller(fill)))
     35        print(stack);
     36        assertEq(stack.includes("caller"), true); // Only elide the first caller!
     37        assertEq(stack.includes("fill"), false);
     38    }
     39 
     40    test_elision();
     41 
     42    function nestedLambda(f) {
     43        (() => {
     44            (() => {
     45                (() => {
     46                    (() => {
     47                        f();
     48                    })();
     49                })();
     50            })();
     51        })();
     52    }
     53 
     54 
     55    // If we never see a matching frame when requesting a truncated
     56    // stack we should return the empty string
     57    function test_no_match() {
     58        let obj = {};
     59        // test_elision chosen arbitrarily as a function object which
     60        // doesn't exist in the call stack here.
     61        let capture = () => Error.captureStackTrace(obj, test_elision);
     62        nestedLambda(capture);
     63        assertEq(obj.stack, "");
     64    }
     65    test_no_match()
     66 
     67    function count_frames(str) {
     68        return str.split("\n").length
     69    }
     70 
     71    function test_nofilter() {
     72        let obj = {};
     73        let capture = () => Error.captureStackTrace(obj);
     74        nestedLambda(capture);
     75        assertEq(count_frames(obj.stack), 9);
     76    }
     77    test_nofilter();
     78 
     79    function test_in_eval() {
     80        let obj = eval(`
     81        let obj = {};
     82        let capture = () => Error.captureStackTrace(obj);
     83        nestedLambda(capture);
     84        obj
     85        `)
     86 
     87        // Same as above, with an eval frame added!
     88        assertEq(count_frames(obj.stack), 10);
     89    }
     90    test_in_eval();
     91 
     92    //
     93    // [[ErrorData]]
     94    //
     95    const stackGetter = Object.getOwnPropertyDescriptor(Error.prototype, 'stack').get;
     96    const getStack = function (obj) {
     97        return stackGetter.call(obj);
     98    };
     99 
    100    function test_uncensored() {
    101        let err = undefined;
    102        function create_err() {
    103            err = new Error;
    104            Error.captureStackTrace(err, test_uncensored);
    105        }
    106 
    107        nestedLambda(create_err);
    108 
    109        // Calling Error.captureStackTrace doesn't mess with the internal
    110        // [[ErrorData]] slot
    111        assertEq(count_frames(err.stack), 2);
    112        assertEq(count_frames(getStack(err)), 9)
    113    }
    114    test_uncensored()
    115 
    116    // In general, the stacks a non-caller version of Error.captureStackStrace
    117    // should match what Error gives you
    118    function compare_stacks() {
    119        function censor_column(str) {
    120            return str.replace(/:(\d+):\d+\n/g, ":$1:censored\n")
    121        }
    122 
    123        let obj = {};
    124        let err = (Error.captureStackTrace(obj), new Error)
    125        assertEq(censor_column(err.stack), censor_column(obj.stack));
    126    }
    127    compare_stacks();
    128    nestedLambda(compare_stacks)
    129 
    130    // New global
    131 
    132    function test_in_global(global) {
    133        global.evaluate(caller.toString());
    134        global.evaluate(fill.toString());
    135        global.evaluate(test_elision.toString());
    136        global.evaluate("test_elision()");
    137 
    138        global.evaluate(nestedLambda.toString())
    139        global.evaluate(test_no_match.toString());
    140        global.evaluate("test_no_match()");
    141 
    142 
    143        global.evaluate(compare_stacks.toString());
    144        global.evaluate(`
    145            compare_stacks();
    146            nestedLambda(compare_stacks)
    147        `)
    148    }
    149 
    150    let global = newGlobal();
    151    test_in_global(global);
    152 
    153    let global2 = newGlobal({ principal: 0 });
    154    test_in_global(global2)
    155 
    156    let global3 = newGlobal({ principal: 0xfffff });
    157    test_in_global(global3)
    158 
    159    // What if the caller is a proxy?
    160    const caller_proxy = new Proxy(caller, {
    161        apply: function (target, thisArg, arguments) {
    162            return target(...arguments);
    163        }
    164    });
    165 
    166    function fill_proxy() {
    167        let x = {}
    168        Error.captureStackTrace(x, caller_proxy);
    169        return x;
    170    }
    171 
    172    // Proxies don't count for elision.
    173    function test_proxy_elision() {
    174        let x = caller_proxy(fill_proxy);
    175        let { stack } = x;
    176        assertEq(stack.includes("caller"), true);
    177        assertEq(stack.includes("fill_proxy"), true);
    178    }
    179    test_proxy_elision();
    180 
    181    const trivial_proxy = new Proxy(caller, {});
    182    function fill_trivial() {
    183        let x = {}
    184        Error.captureStackTrace(x, trivial_proxy);
    185        return x;
    186    }
    187 
    188    // Elision doesn't work even on forwarding proxy
    189    function test_trivial_elision() {
    190        let x = caller(fill_trivial);
    191        let { stack } = x;
    192        assertEq(stack.includes("caller"), true);
    193        assertEq(stack.includes("fill"), true);
    194    }
    195    test_trivial_elision();
    196 
    197    // Elision happens through bind
    198    function test_bind_elision() {
    199        let b = caller.bind(undefined, fill);
    200        let { stack } = b();
    201        assertEq(stack.includes("caller"), false);
    202        assertEq(stack.includes("fill"), false);
    203    }
    204    test_bind_elision();
    205 
    206    // Cross Realm testing
    207 
    208    let nr = newGlobal({ newCompartment: true })
    209    nr.eval(`globalThis.x = {}`);
    210    Error.captureStackTrace(nr.x);
    211 
    212    // Test strict definition
    213    function test_strict_definition() {
    214        "use strict";
    215        assertThrowsInstanceOf(() => Error.captureStackTrace(Object.freeze({ stack: null })), TypeError);
    216    }
    217    test_strict_definition();
    218 
    219    function test_property_descriptor() {
    220        let o = {};
    221        Error.captureStackTrace(o);
    222        let desc = Object.getOwnPropertyDescriptor(o, "stack");
    223        assertEq(desc.configurable, true)
    224        assertEq(desc.writable, true)
    225        assertEq(desc.enumerable, false)
    226    }
    227    test_property_descriptor();
    228 
    229    function test_delete() {
    230        let o = {};
    231        Error.captureStackTrace(o);
    232        delete o.stack
    233        assertEq("stack" in o, false)
    234    }
    235    test_delete();
    236 
    237    // Principal testing: This is basic/shell-principals.js extended to support
    238    // and compare Error.captureStackTrace.
    239    //
    240    // Reminder:
    241    // >  In the shell, a principal is simply a 32-bit mask: P subsumes Q if the
    242    // >  set bits in P are a superset of those in Q. Thus, the principal 0 is
    243    // >  subsumed by everything, and the principal ~0 subsumes everything.
    244 
    245    // Given a string of letters |expected|, say "abc", assert that the stack
    246    // contains calls to a series of functions named by the next letter from
    247    // the string, say a, b, and then c. Younger frames appear earlier in
    248    // |expected| than older frames.
    249    let count = 0;
    250    function check(expected, stack) {
    251        print("check(" + JSON.stringify(expected) + ") against:\n" + stack);
    252        count++;
    253 
    254        // Extract only the function names from the stack trace. Omit the frames
    255        // for the top-level evaluation, if it is present.
    256        var split = stack.split(/(.)?@.*\n/).slice(0, -1);
    257        if (split[split.length - 1] === undefined)
    258            split = split.slice(0, -2);
    259 
    260        print(JSON.stringify(split));
    261        // Check the function names against the expected sequence.
    262        assertEq(split.length, expected.length * 2);
    263        for (var i = 0; i < expected.length; i++)
    264            assertEq(split[i * 2 + 1], expected[i]);
    265    }
    266 
    267    var low = newGlobal({ principal: 0 });
    268    var mid = newGlobal({ principal: 0xffff });
    269    var high = newGlobal({ principal: 0xfffff });
    270 
    271    eval('function a() { let o = {}; Error.captureStackTrace(o); check("a",    o.stack); b(); }');
    272    low.eval('function b() { let o = {}; Error.captureStackTrace(o); check("b",    o.stack); c(); }');
    273    mid.eval('function c() { let o = {}; Error.captureStackTrace(o); check("cba",  o.stack); d(); }');
    274    high.eval('function d() { let o = {}; Error.captureStackTrace(o); check("dcba", o.stack); e(); }');
    275 
    276    // Globals created with no explicit principals get 0xffff.
    277    eval('function e() { let o = {}; Error.captureStackTrace(o); check("ecba",     o.stack); f(); }');
    278 
    279    low.eval('function f() { let o = {}; Error.captureStackTrace(o); check("fb",       o.stack); g(); }');
    280    mid.eval('function g() { let o = {}; Error.captureStackTrace(o); check("gfecba",   o.stack); h(); }');
    281    high.eval('function h() { let o = {}; Error.captureStackTrace(o); check("hgfedcba", o.stack);      }');
    282 
    283    // Make everyone's functions visible to each other, as needed.
    284    b = low.b;
    285    low.c = mid.c;
    286    mid.d = high.d;
    287    high.e = e;
    288    f = low.f;
    289    low.g = mid.g;
    290    mid.h = high.h;
    291 
    292    low.check = mid.check = high.check = check;
    293 
    294    // Kick the whole process off.
    295    a();
    296 
    297    assertEq(count, 8);
    298 
    299    // Ensure filtering is based on caller realm not on target object.
    300    low.eval("low_target = {}");
    301    mid.eval("mid_target = {}");
    302    high.eval("high_target = {}");
    303 
    304    high.low_target = mid.low_target = low.low_target;
    305    high.mid_target = low.mid_target = mid.mid_target;
    306    mid.high_target = low.high_target = high.high_target;
    307 
    308    high.low_cst = mid.low_cst = low.low_cst = low.Error.captureStackTrace;
    309    high.mid_cst = low.mid_cst = mid.mid_cst = mid.Error.captureStackTrace;
    310    mid.high_cst = low.high_cst = high.high_cst = high.Error.captureStackTrace;
    311 
    312    for (let g of [low, mid, high]) {
    313        assertEq("low_target" in g, true);
    314        assertEq("mid_target" in g, true);
    315        assertEq("high_target" in g, true);
    316 
    317        assertEq("low_cst" in g, true);
    318        assertEq("mid_cst" in g, true);
    319        assertEq("high_cst" in g, true);
    320 
    321        // install caller function z -- single letter name for
    322        // check compat.
    323        g.eval("function z(f) { f() }")
    324    }
    325 
    326    low.eval("function q() { Error.captureStackTrace(low_target); }")
    327 
    328 
    329    high.q = low.q;
    330 
    331    // Caller function z is from high, but using low Error.captureStackTrace, so
    332    // z should be elided.
    333    high.eval("z(q)");
    334    check("q", low.low_target.stack);
    335 
    336    low.eval("function r() { high_cst(low_target) }")
    337    high.r = low.r;
    338 
    339    // Can see everything here using high cst and low target.
    340    high.eval("function t() { z(r) }");
    341    high.t();
    342    check("rzt", low.low_target.stack);
    343 
    344 
    345 }