tor-browser

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

strict-eval-bindings.js (7269B)


      1 // |jit-test| skip-if: !('disassemble' in this)
      2 // Strict direct eval supports static binding of identifiers.
      3 
      4 "use strict";
      5 
      6 // Check that a script contains a particular bytecode sequence.
      7 //
      8 // `actual` is the output of the `disassemble()` shell builtin.
      9 // `expected` is a semicolon-separated string of opcodes.
     10 //    Can include regular expression syntax, e.g. "GetLocal .* x$"
     11 //    to match a GetLocal instruction with ` x` at the end of the line.
     12 // `message` is a string to include in the error message if the test fails.
     13 //
     14 function assertBytecode(actual, expected, message) {
     15  // Grab the opcode name and everything after to the end of the line.  This
     16  // intentionally includes the expression stack, as that is what makes the
     17  // `GetLocal .* y$` trick work. The disassemble() output is like this:
     18  //
     19  //     00016:  10 GetLocal 0                      # x y
     20  //
     21  let actualOps =
     22      actual.split('\n')
     23      .map(s => /^\d{5}: +\d+ +(.*)$/.exec(s)?.[1])
     24      .filter(x => x !== undefined);
     25 
     26  // Turn the expectations into regular expressions.
     27  let expectedOps =
     28      expected.split(';')
     29      .map(s => {
     30        s = s.trim();
     31        // If the op is a single word, like `Dup`, add `\b` to rule out
     32        // similarly named ops like `Dup2`.
     33        if (/^\w+$/.test(s)) {
     34          s += "\\b";
     35        }
     36        return new RegExp("^" + s);
     37      });
     38 
     39  // The condition on this for-loop is saying, "continue as long as the range
     40  // [i..i+expectedOps.length] is entirely within in the actualOps array".
     41  // Hence the rare use of `<=` in a for-loop!
     42  for (let i = 0; i + expectedOps.length <= actualOps.length; i++) {
     43    if (expectedOps.every((expectRegExp, j) => expectRegExp.test(actualOps[i + j]))) {
     44      // Found a complete match.
     45      return;
     46    }
     47  }
     48  throw new Error(`Assertion failed: ${message}\nexpected ${uneval(expected)}, got:\n${actual}`);
     49 }
     50 
     51 
     52 // --- Tests
     53 
     54 var bytecode;
     55 
     56 // `var`s in strict eval code are statically bound as locals.
     57 eval(`
     58  var pet = "ostrich";
     59  bytecode = disassemble();
     60  pet
     61 `);
     62 assertEq(globalThis.hasOwnProperty('pet'), false);
     63 assertBytecode(bytecode, 'String "ostrich"; SetLocal; Pop',
     64               "`pet` is stored in a stack local");
     65 assertBytecode(bytecode, "GetLocal; SetRval; RetRval",
     66               "`pet` is loaded from the local at the end of the eval code");
     67 
     68 // Same for top-level `function`s.
     69 eval(`
     70  function banana() { return "potassium"; }
     71  bytecode = disassemble();
     72 `);
     73 assertEq(globalThis.hasOwnProperty('banana'), false);
     74 assertBytecode(bytecode, 'Lambda .* banana; SetLocal; Pop',
     75               "`banana` is stored in a stack local");
     76 
     77 // Same for let/const.
     78 eval(`
     79  let a = "ushiko-san";
     80  const b = "umao-san";
     81  bytecode = disassemble();
     82  [a, b]
     83 `);
     84 assertBytecode(bytecode, 'String "ushiko-san"; InitLexical; Pop',
     85               "`let a` is stored in a stack local");
     86 assertBytecode(bytecode, 'String "umao-san"; InitLexical; Pop',
     87               "`const b` is stored in a stack local");
     88 assertBytecode(bytecode, 'GetLocal .* a$; InitElemArray; GetLocal .* b$; InitElemArray',
     89               "lexical variables are loaded from stack locals");
     90 
     91 // Same for arguments and locals in functions declared in strict eval code.
     92 let g = eval(`
     93  function f(a) {
     94    let x = 'x';
     95    function g(b) {
     96      let y = "wye";
     97      return [f, a, x, g, b, y];
     98    }
     99    return g;
    100  }
    101  f();
    102 `);
    103 bytecode = disassemble(g);
    104 assertBytecode(bytecode, 'GetAliasedVar "f"',
    105               "closed-over eval-scope `function` is accessed via aliased op");
    106 assertBytecode(bytecode, 'GetAliasedVar "a"',
    107               "closed-over argument is accessed via aliased op");
    108 assertBytecode(bytecode, 'GetAliasedVar "x"',
    109               "closed-over local `let` variable is accessed via aliased op");
    110 assertBytecode(bytecode, 'GetAliasedVar "g"',
    111               "closed-over local `function` is accessed via aliased op");
    112 assertBytecode(bytecode, 'GetArg .* b$',
    113               "non-closed-over arguments are optimized");
    114 assertBytecode(bytecode, 'GetLocal .* y$',
    115               "non-closed-over locals are optimized");
    116 
    117 // Closed-over bindings declared in strict eval code are statically bound.
    118 var fac = eval(`
    119  bytecode = disassemble();
    120  function fac(x) { return x <= 1 ? 1 : x * fac(x - 1); }
    121  fac
    122 `);
    123 assertBytecode(bytecode, 'SetAliasedVar "fac"',
    124               "strict eval code accesses closed-over top-level function using aliased ops");
    125 assertBytecode(disassemble(fac), 'GetAliasedVar "fac"',
    126               "function in strict eval accesses itself using aliased ops");
    127 
    128 // References to `this` in an enclosing method are statically bound.
    129 let obj = {
    130  m(s) { return eval(s); }
    131 };
    132 let result = obj.m(`
    133  bytecode = disassemble();
    134  this;
    135 `);
    136 assertEq(result, obj);
    137 assertBytecode(bytecode, 'GetAliasedVar ".this"',
    138               "strict eval in a method can access `this` using aliased ops");
    139 
    140 // Same for `arguments`.
    141 function fn_with_args() {
    142  return eval(`
    143    bytecode = disassemble();
    144    arguments[0];
    145  `);
    146 }
    147 assertEq(fn_with_args(117), 117);
    148 assertBytecode(bytecode, 'GetAliasedVar "arguments"',
    149               "strict eval in a function can access `arguments` using aliased ops");
    150 
    151 // The frontend can emit GName ops in strict eval.
    152 result = eval(`
    153  bytecode = disassemble();
    154  fn_with_args;
    155 `);
    156 assertEq(result, fn_with_args);
    157 assertBytecode(bytecode, 'GetGName "fn_with_args"',
    158               "strict eval code can optimize access to globals");
    159 
    160 // Even within a function.
    161 function test_globals_in_function() {
    162  result = eval(`
    163    bytecode = disassemble();
    164    fn_with_args;
    165  `);
    166  assertEq(result, fn_with_args);
    167  assertBytecode(bytecode, 'GetGName "fn_with_args"',
    168                 "strict eval code in a function can optimize access to globals");
    169 }
    170 test_globals_in_function();
    171 
    172 // Nested eval is no obstacle.
    173 {
    174  let outer = "outer";
    175  const f = function (code, a, b) {
    176    return eval(code);
    177  };
    178  let result = f(`
    179    eval("bytecode = disassemble();\\n" +
    180         "outer += a + b;\\n");
    181  `, 3, 4);
    182  assertEq(outer, "outer7");
    183  assertBytecode(bytecode, 'GetAliasedVar "outer"',
    184                 "access to outer bindings is optimized even through nested strict evals");
    185  assertBytecode(bytecode, 'GetAliasedVar "a"',
    186                 "access to outer bindings is optimized even through nested strict evals");
    187  assertBytecode(bytecode, 'SetAliasedVar "outer"',
    188                 "assignment to outer bindings is optimized even through nested strict evals");
    189 }
    190 
    191 // Assignment to an outer const is handled correctly.
    192 {
    193  const doNotSetMe = "i already have a value, thx";
    194  let f = eval(`() => { doNotSetMe = 34; }`);
    195  assertBytecode(disassemble(f), 'ThrowSetConst "doNotSetMe"',
    196                 "assignment to outer const in strict eval code emits ThrowSetConst");
    197 }
    198 
    199 // OK, there are other scopes but let's just do one more: the
    200 // computed-property-name scope.
    201 {
    202  let stashed;
    203  (class C {
    204    [(
    205      eval(`
    206        var secret = () => C;
    207        stashed = () => secret;
    208      `),
    209      "method"
    210    )]() {
    211      return "ok";
    212    }
    213  });
    214 
    215  bytecode = disassemble(stashed());
    216  assertBytecode(bytecode, 'GetAliasedVar "C"',
    217                 "access to class name uses aliased ops");
    218  let C = stashed()();
    219  assertEq(new C().method(), "ok");
    220 }