Aran is a JavaScript instrumenter for building dynamic analysis tools


Keywords
JavaScript, Instrumentation, Dynamic Analysis, Sandboxing, Monitoring, Tracing, Shadow Execution
License
MIT
Install
npm install aran@3.0.9

Documentation

Aran aran-logo

Aran is a npm module for instrumenting JavaScript code. To install, run npm install aran. Aran was designed as a generic infra-structure for building various development-time dynamic program analyses such as: objects and functions profiling, debugging, control-flow tracing, taint analysis and concolic testing. Aran can also be used at deployment-time but be mindful of performance overhead. For instance, Aran can be used to implement control access systems such as sandboxing.

Disclaimer: Aran is an academic research project, we are using it at our lab to support publications and run experiments. Although I spent a lot of time improving the quality of this software I do not claim it has reached industrial strength. Bugs may still remain and unforeseen behaviour may occur on large instrumented programs. In the near future, I will not add new features but will correct reported bugs.

Aran produces a flaky error when used with node 11.9.0, the LTS 10.15.1 is advised

Table of contents:

  1. Getting Started
  2. Demonstrators
  3. Limitations
  4. API
  5. Advice
  6. Builtins
  7. Predefined Values
  8. Known Heisenbugs
  9. Acknowledgements

Getting Started

npm install acorn aran astring
const Acorn = require("acorn");
const Aran = require("aran");
const Astring = require("astring");
let depth = "";
global.ADVICE = {
  apply: (f, t, xs, serial) => {
    console.log(depth + f.name + "(" + xs.join(", ") + ")");
    depth += ".";
    const x = Reflect.apply(f, t, xs);
    depth = depth.substring(1);
    console.log(depth + x);
    return x;
  }
};
const pointcut = (name, node) =>
  name === "apply" && node.type === "CallExpression";
const aran = Aran({namespace: "ADVICE"});
global.eval(Astring.generate(aran.setup()));
const estree1 = Acorn.parse(`
  const fac = (n) => n ? n * fac(n - 1) : 1;
  fac(6);
`);
const estree2 = aran.weave(estree1, pointcut);
global.eval(Astring.generate(estree2));
fac(6)
.fac(5)
..fac(4)
...fac(3)
....fac(2)
.....fac(1)
......fac(0)
......1
.....1
....2
...6
..24
.120
720

The code transformation performed by Aran essentially consists in inserting calls to functions named traps at ESTree nodes specified by the user. For instance, the expression x + y may be transformed into META.binary("+", x, y, 123). The last argument passed to traps is always a serial number which uniquely identifies the node which triggered the trap. The object that contains these trap functions is called the advice and the specification that characterises what trap should be triggered on each node is called the pointcut. The process of inserting trap calls based on a pointcut is called weaving. This terminology is borrowed from aspect-oriented programming.

weaving

When code weaving happens on the same process which evaluates the weaved code, it is called live weaving. Live weaving enables direct communication between an advice and its associated Aran instance. For instance, aran.nodes[serial] can be invoked by the advice to retrieve the line index of the node that triggered a trap. Another good reason for the advice to communicate with Aran arises when the target program performs dynamic code evaluation -- e.g. by calling the evil eval function.

Demonstrators

  • test/dead/apply/: The getting started example where weaving happens on the main thread and the evaluation of the weaved code on a webworker.
  • test/live/empty-estree.js: Empty advice, do nothing aside from instrumenting the argument of direct eval calls. Can be used to inspect how Aran normalise JavaScript.
  • test/live/empty-script.js: Same as empty-estree.js but uses the "script" format option instead.
  • test/live/forward-estree.js: Transparent implementation of all the traps. Can be used to inspect how Aran inserts traps.
  • test/live/forward-script.js: Same as forward-estree.js but uses the "script" format option instead.
  • test/live/logger.js: Same as forward-script.js but log the arguments and result of every trap.
  • test/live/shadow-value.js: Track program values across the value stack, the call stack and the environment but not the store. The identity of values is conserved by wrapping them inside regular objects.
  • test/live/shadow-state.js: Output the same log as shadow-value but by mirroring the value stack, the call stack and the environment rather than wrapping values. This analysis demonstrates the full capability of Aran and may serve documentation.
  • test/live/linvail-value.js Like shadow-value, this analysis wrap values into objects to conserve their identity. However this analysis can also track values through the store (a.k.a the objet graph) thanks to a separate npm module called linvail. Linvail uses ECMAScript proxies to implement an transitive access control system known as membrane.
  • test/live/linvail-taint.js Simple taint analysis using linvail which prevents information from meta variables __ARAN_SOURCE__ to flow to meta variables __ARAN_SINK__.

Limitations

  1. Aran performs a source-to-source code transformation fully compatible with most of ECMAScript2018. Known missing features are:
  2. There exists loopholes that will cause the target program to behave differentially when analysed. These bugs are often referred as Heisenbugs, and are discusses in Known Heisenbugs.
  3. Aran does not provide any facilities for instrumenting modularised JavaScript applications. To instrument server-side node applications and client-side browser applications we rely on a separate module called Otiluke.
  4. Aran does not offer an out-of-the-box interface for tracking primitive values through the object graph. This feature is crucial for data-centric analyses such as taint analysis and symbolic execution. In our research, we track primitive values through the object graph with linvail which implements a transitive access control system known as membrane.

API

  • aran = require("aran")({namespace, format, roots})

    Create a new Aran instance.

    • namespace :: string, default "_": The name of the variable holding the advice. It should not start by a dollar sign ($) and should not be be one of: arguments, eval, callee, error. This variable should be accessible from the instrumented code. For instance, this variable should be global if the instrumented code is evaluated globally. Aran performs identifier mangling in such a way that variables from the instrumented code never clash against this variable.
    • format :: "estree" | "script", default "estree": Defines the output format of aran.weave(root, pointcut, scope) and aran.setup().
      • "estree": Output an ESTree program. This option requires a generator like astring to obtain executable JavaScript code.
      • "script": Output a string which can directly be fed to eval. As Aran's instrumentation can be quite verbose, expressions are spanned in multiple lines.
    • roots :: array, default []: Each ESTree program node passed to aran.weave(root, pointcut, scope) will be stored in this array. If this array is not empty it should probably come from another Aran instance.

    The state of an Aran instance essentially consists in the node it instrumented. Aran instances can be serialised using the standard JSON.stringify function. For instance, in the code below, aran1 and aran2 are in the exact same state:

    const Aran = require("aran");
    const aran1 = Aran({...});
    const string = JSON.stringify(aran1);
    const options = JSON.parse(string);
    const aran2 = Aran(options);
  • output = aran.setup()

    Generate the setup code which should be executed before any instrumented code.

    • output :: estree.Program | string: The setup code whose format depends on the format option.

    The setup code should be evaluated in an environment where this points to the global object and where the advice variable is accessible. If the setup code is evaluated twice for the same advice, it will throw an error.

  • output = aran.weave(program, pointcut, serial)

    Normalise the input program and insert calls to trap functions at nodes specified by the pointcut.

    • program :: estree.Program: The ESTree program to instrument.
    • pointcut :: array | function: The specification that tells Aran where to insert trap calls. Two specification formats are supported:
      • array: An array containing the names of the traps to insert at every applicable cut point. For instance, the pointcut ["binary"] indicates Aran to insert the binary traps whenever applicable.
      • function: A function that tells whether to insert a given trap at a given node. For instance, the pointcut below results in Aran inserting a call to the binary trap at every update expression:
        const pointcut = (name, node) => name === "binary" && node.type === "UpdateExpression" ;
    • serial :: number | null, default null: If the given ESTree program is going to be evaluated inside a direct eval call within some previously instrumented code, the serial argument should be the serial number of that direct eval call. If the instrumented code is going to be evaluated globally, this argument should be null or undefined.
    • output :: estree.Program | string: The weaved code whose format depends on the format option.
  • result = aran.unary(operator, argument)

    Performs a unary operation as expected by the unary trap. This function can be implemented as easily as eval(operator+" argument") but we used a boring switch loop instead for performance reasons.

    • operator :: string
    • arguments :: *
    • result :: primitive
  • result = aran.binary(operator, left, right)

    Performs a binary operation as expected by the binary trap. This function can be implemented as easily as eval("left "+operator+" right") but we used a boring switch loop instead for performance reasons.

    • operator :: string
    • left :: *
    • right :: *
    • result :: primitive
  • aran.namespace

    The name of the variable holding the advice.

    {
      value: string,
      enumerable: true,
      configurable: false,
      writable: false
    }
  • aran.format

    The output format for aran.weave(estree, scope) and aran.setup(); either "estree" or "script".

    {
      value: string,
      enumerable: true,
      configurable: false,
      writable: false
    }
  • aran.roots

    An array of all the program nodes weaved by the Aran instance.

    {
      value: array,
      enumerable: true,
      configurable: false,
      writable: false
    }
  • aran.nodes

    An array indexing all the AST node visited by the Aran instance. This field is useful to retrieve a node from its serial number: aran.nodes[serial]. It is not enumerable to reduces the size of stringifying Aran instances. Each node in this array contains two additional properties: AranSerial and AranParentSerial which respectively refer to the serial number of the node and the serial number of its parent.

    {
      value: array,
      enumerable: false,
      configurable: false,
      writable: false
    }

Advice

In Aran, an advice is a collection of functions that will be called during the evaluation of weaved code. These functions are called traps. They are independently optional and they all receive as last argument a number which is the index of the ESTree node that triggered the them. Serial numbers can be seen as program counters.

Traps Categorisation:

There exists 26 traps which we categorise based on their transparent implementation:

  • Informers (7): Do nothing.
  • Modifiers (15): Return their first argument.
  • Combiners (4): Compute a new value:
    • unary = (operator, argument, serial) => eval(operator+" argument");
    • binary = (operator, left, right, serial) => eval("left "+operator+" right");
    • apply = (closure, context, arguments, serial) => Reflect.apply(closure, context, arguments);
    • construct = (constructor, arguments, serial) => Reflect.construct(constructor, arguments);

Traps Insertion:

Name Original Instrumented
Informers
arrival function () { ... } ... function callee () { ... META.arrival(callee, new.target, this, arguments, @serial); ... } ...
break break l; META.break("l", @serial); break l;
continue continue l; META.continue("l", @serial); continue l;
debugger debugger; META.debugger(@serial); debugger;
enter l: { let x; ... } l : { META.enter("block", ["l"], ["x"], @serial); ... }
leave { ... } { ... META.leave(@serial); }
program ... (program) program(META.builtins.global, @serial); ...
Modifiers
abrupt function () { ... } ... function callee () { ... try { ... } catch (error) { throw META.abrupt(error, @serial); } ... } ...
argument function () { ... } ... function callee () { ... META.argument(arguments.length, "length", @serial) ... } ...
builtin [x, y] META.builtin(META.builtins["Array.of"], "Array.of", @serial)($x, $y)
closure function () { ... } META.closure(... function callee () { ... } ..., @serial)
drop (x, y) (META.drop($x, @serial), $y)
error try { ... } catch (e) { ... } try { ... } catch (error) { ... META.error(error, @serial) ... }
eval eval(x) ... eval(META.eval($x, @serial)) ...
failure ... (program) try { ... } catch (error) { throw META.failure(error, @serial); }
primitive "foo" META.primitive("foo", @serial)
read x META.read($x, "x", @serial)
return return x; return META.return($x, @serial);
success x; (program) META.success($x, @serial);
test x ? y : z META.test($x, @serial) ? $y : $z
throw throw e; throw META.throw($e, @serial);
write x = y; $x = META.write($y, "x", @serial);
Combiners
apply f(x, y) META.apply($f, undefined, [$x, $y], @serial)
binary x + y META.binary("+", $x, $y, @serial)
construct new F(x, y) META.construct($F, [$x, $y], @serial)
unary !x META.unary("!", $x, @serial)

Traps Signature:

Name arguments[0] arguments[1] arguments[2] arguments[3] arguments[4]
Informers
arrival callee :: function new.target :: function this :: value arguments :: [value] serial :: number
break label :: string | null serial :: number
continue label :: string | null serial :: number
debugger serial :: number
enter tag :: "program" | "block" | "then" | "else" | "loop" | "try" | "catch" | "finally" | "switch" labels :: [string] variables :: [string] serial :: number
leave serial :: number
program global :: object serial :: number
Modifiers
abrupt error :: value serial :: number
argument produced :: value index :: number | "new.target" | "this" | "length" serial :: number
builtin produced :: value name :: string serial :: number
closure produced :: function serial :: number
drop consumed :: value serial :: number
error produced :: value serial :: number
failure error :: value serial :: number
primitive produced :: undefined | null | boolean | number | string serial :: number
read produced :: value variable :: string serial :: number
return consumed :: value serial :: number
success consumed :: value serial :: number
test consumed :: value serial :: number
throw consumed :: value serial :: number
write consumed :: value variable :: string serial :: number
Combiners
apply function :: value this :: value arguments :: [value] serial :: number
binary operator :: "==" | "!=" | "===" | "!==" | "<" | "<=" | ">" | ">=" | "<<" | ">>" | ">>>" | "+" | "-" | "*" | "/" | "%" | "|" | "^" | "&" | "in" | "instanceof" | ".." left :: value right :: value serial :: number
construct constructor :: value arguments :: [value] serial :: number
unary operator :: "-" | "+" | "!" | "~" | "typeof" | "void" argument :: value serial :: number

Traps Comments:

Some groups of traps requires additional explanations:

  • enter, write, read, leave: These (only) four traps describe the runtime interaction with the environment. We discuss how below:

    1. In the normalisation process, Aran often inserts new variables called token. Tokens appear to be numbers from traps perspective whereas variables present in the original code appear as strings.
    2. Aran only declares let variables:
      • var declarations at the top-level scope are normalised into property definition on the global object.
      • var declarations inside functions are hoisted and normalised into let declarations.
      • const declarations are normalised into let declarations and a static type error is throws upon rewrite attempt.
    3. Aran hoists its let declarations at the top of lexical blocks. This makes the temporal deadzone disappear. To restore the behaviour of the temporal deadzone, a token is associated to each variable. At runtime, these tokens will refer to a boolean value indicating whether their associated variable has already been initialised or not. Before accessing a variable in a dynamic portion of the deadzone, a runtime time check on its associated token is inserted. In many cases, the temporal deadzone of a variable can be statically determined and its associated token is entirely removed from the instrumented code. Not that eval kills this optimisation because we have to assume that any reachable variable may be accessed.
    // Original //
    const a = () => x;
    let x = "foo";
    a();
    a = "bar";
    // (pseudo) Instrumented //
    let $a, $x, $1;
    META.enter("program", [], ["a", "x", 123]);
    $1 = META.write(false, 1);
    $a = () => (
      META.read($1, 1) ?
      META.read($x, "x", 123) :
      ((() => { throw new TypeError("x is not defined") }) ()));
    $x = META.write("foo", "x");
    $1 = META.write(true, 1);
    $a();
    throw new TypeError("Assignment to a constant variable");
    META.leave();
  • program, success, failure: These traps are inserted into programs that will not be evaluated inside a direct eval call. The first trap invoked by a program is always program(@serial) and the last trap is either success($1, @serial) or failure(error, @serial).

    // Original //
    "foo";
    // Instrumented //
    META.program();
    let $1 = undefined;
    try {
      $1 = "foo";
      META.success($1);
    } catch (error) {
      throw META.failure(error);
    }
  • arrival, argument, return, abrupt: When applying an instrumented closure, these traps are invoked in the following order:

    1. arrival(callee, new.target, this, arguments, @serial): Beware: callee refers to the function given as parameter to the closure trap and not its return value.
    2. argument(new.target, "new.target", @serial): For arrows, this trap is used to check that it was not called as a constructor. For functions, this trap is used to initialise a sanitised version of the new.target variable. For functions, if its result is not undefined, it will be used to initialise a sanitised version of the this variable.
    3. argument(this, "this", @serial): This trap is invoked only if the previous trap returned undefined. For arrows, the result of this trap is discarded. For functions, this trap is used to initialise a sanitised version of the this variable.
    4. argument(arguments.length, "length", @serial): The value returned by this trap is used to define how many times the next trap should be called.
    5. argument(arguments[argindex], argindex, @serial): This trap is used to initialise parameters. For functions reading the arguments variable it is also used to initialise the arguments object. The variable argindex is counter from 0 to the value returned by the previous trap.
    6. return(<EXPR>, @serial) or abrupt(error, @serial): If the closure normally terminated, the return trap is invoked with its result. Else, the abrupt trap is called with the error that caused it to abruptly terminate. The serial number of the return trap may either points to a return statement in the original code or the closure if it terminated without hitting a return statement.
    // Original //
    const f = function (x) => {
      console.log("Square = " x * x);
    }
    // (pseudo) Instrumented //
    let $f;
    $f = function callee () {
      try {
        let $x, $0newtarget, $this, $1, $2;
        META.arrival(callee, new.target, this, arguments, @serial);
        $0newtarget = META.argument(new.target, "new.target", @serial);
        if ($0newtarget)
          $this = Object.create($0newtarget.prototype);
        else
          $this = META.argument(this, "this", @serial);
        $1 = META.argument(arguments.length, "length", @serial);
        $x = 0 < $1 ? META.argument(arguments[0], 0, @serial) : undefined;
        $2 = 1;
        while ($2 < $1) {
          META.argument(arguments[$2], $2, @serial);
          $2 = $2 + 1;
        }
        console.log("Square = " $x * $x);
        return META.return(undefined, @serial);
      } catch (error) {
        throw META.abrupt(error, @serial);
      }
    };
  • enter, break, continue, leave: These traps describe runtime label jumps:

    // Original //
    l: m: while (true) {
      continue l;
    }
    // Instrumented //
    l: m: while (true) {
      META.enter("loop", ["l", "m"], [], @serial1);
      META.continue("l", @serial2);
      continue l;
      META.leave(@serial1);
    }
  • builtin: In the normalisation process, Aran often uses pre-existing values from the global object (a.k.a: builtins, a.k.a: primordials). To render the instrumented code resilient to modification of the global object it is important to store these builtin values upfront. This is performed during the setup phase.

    // Original //
    /abc/g
    // (pseudo) Instrumented //
    new META.builtin(META.builtins.RegExp, "RegExp")("abc", "g");

Builtins

Below is a list of the all the builtins stored by the setup code along with their utility:

  • global: For declaring/writing/reading global variables and for assigning the initial value of this.
  • eval: For detecting whether a syntactic direct eval call actually resolves to a direct eval call at runtime.
  • RegExp: For normalising literal regular expressions.
  • ReferenceError: Reference errors are thrown when a variable is not defined or is in the temporal deadzone.
  • TypeError: Type errors are thrown at many places, e.g: when Reflect.set returns false in strict mode.
  • Reflect.get: For normalising member expressions.
  • Reflect.set: For normalising assignments on member expressions and building various objects.
  • Reflect.has: Called when variable lookups hit a with statement.
  • Reflect.deleteProperty: For normalising delete unary operations on member expressions.
  • Reflect.apply: For normalising call expressions containing spread elements.
  • Reflect.construct: For normalising new expressions containing spread elements.
  • Reflect.getOwnPropertyDescriptor: For knowing whether a variable is globally defined.
  • Reflect.defineProperty: For building various objects (e.g.: object literals, the arguments object, functions, functions prototype).
  • Reflect.getPrototypeOf: For knowing whether a variable is defined in the global object and for collecting the enumerable keys of an object as enumerated by the for .. in statement.
  • Reflect.setPrototypeOf: For building various objects (e.g.: object literals, the arguments object, functions prototype).
  • Object: Often called before calling functions of the Reflect object to prevent type errors.
  • Object.prototype: The [[Prototype]] field of various objects (e.g.: object literals, the arguments object, functions prototype).
  • Object.create: For building various objects.
  • Object.freeze: For building the first argument passed to the tag of template expressions and its raw property.
  • Object.keys: For normalising for ... in statements.
  • Array.of: For normalising array expressions and building the first argument passed to the tag of template expressions and its raw property.
  • Array.prototype.push: For building the rest elements of array patterns, object patterns and argument patterns.
  • Array.prototype.concat: For normalising expressions with spread elements (array expressions, call expressions, new expressions).
  • Array.prototype.values: The @@iterator field of arguments objects.
  • Array.prototype.includes: For building the rest value of object patterns.
  • Symbol.unscopables: Used when variable lookups hit a with statement.
  • Symbol.iterator: For normalising the iteration protocol.
  • Reflect.getOwnPropertyDescriptor(Function.prototype,'arguments').get: The callee fields of arguments objects in strict mode.
  • Reflect.getOwnPropertyDescriptor(Function.prototype,'arguments').set: The callee fields of arguments objects in strict mode.

Predefined Values

Rather than defining a closure whenever statements are needed in an expression context, Aran defines several functions at the beginning of programs. This help reduce the size of the instrumented code. The other value that Aran predefines is the completion value of the program. These predefined values are always assigned to the same token in the order listed below:

  1. HelperThrowTypeError(message): Throws a type error; called when:
    • Assigning to a constant variable.
    • Reading/Assigning a property on null or undefined.
    • Calling an arrow as a constructor.
    • In strict mode, deleting a member expression and failing.
    • In strict mode, assigning a member expression and failing.
    • Passing null or undefined a with statement.
  2. HelperThrowReferenceError(message): Throws a reference error; called when:
    • Accessing a local variable in its temporal dead zone.
    • Reading a non existing global variable.
    • In Strict mode, writing to non existing global variable.
  3. boolean = HelperIsGlobal(name): Indicates whether an identifier is globally defined (non-enumerable properties also count). A call to this helper are inserted whenever a variable lookup reach the global scope in Aran's static scope analysis.
  4. array = HelperIteratorRest(iterator): Pushes the remaining elements of an iterator into an array. A call to this helper is inserted to compute the value of an array pattern's rest element.
  5. target = HelperObjectRest(source, keys): Create an object target that contains the own enumerable properties of source safe the ones listed in keys. A call to this helper is inserted to computed the value of an object pattern's rest element.
  6. HelperObjectAssign(target, source): Similar to Object.assign but uses Reflect.defineProperty rather than Reflect.set on the target object. A call to this helper is inserted whenever an object expression contain a spread element.
  7. Completion: The completion value of the program, initially: undefined.

Known Heisenbugs

When dynamically analysing a program, it is implicitly assumed that the analysis will conserve the program's behaviour. If this is not the case, the analysis might draw erroneous conclusions. Behavioural divergences caused by an analysis over the target program are called Heisenbugs. It is very easy to write an analysis that is not transparent, for instance advice.primitive = () => "foo"; will drastically alter the behaviour of the program under analysis. However, Aran introduce Heisenbugs by itself as well:

  • Performance Overhead: A program being dynamically analysed will necessarily perform slower. Events might interleave in a different order or malicious code might detect the overhead. Unfortunately there seem to be no general solution to this problem.
  • Code Reification: Whenever the target program can obtain a representation of itself (a.k.a. code reification), the original code should be returned rather than its instrumented version. In the code below, the assertion should pass but it does not after instrumentation:
    function f () {}
    assert(f.toString() === "function f () {}");
    Solving this issue should probably involve (i) receiving indentation information along with the program AST (ii) maintaining at runtime a mapping from closures to their non-instrumented source code (iii) inserting runtime checks at every function call to transform the result. Not only this would make Aran's API uglier, it would also not completely solve the problem as Function.prototype.toString could be called on instrumented closures in non-instrumented code areas.
  • Dynamic Scoping: Normally, in non-strict mode, direct eval calls should be able to declare variables to their calling environment. This is not the case for instrumented code because Aran performs a static scope analysis. To make JavaScript scope purely static, Aran simply transforms the top level var declarations into let declarations for programs being evaluated inside a direct eval call. In the code below, the assertion should pass but it does not after instrumentation:
    function f () {
      eval("var x = 'foo';");
      assert(typeof x === "string");
    }
    f();
    This could be solved but it would render the scope analysis much more complex.
  • Aliasing Arguments' Properties: In non strict mode, arguments objects are linked to their closure's environment. Modifications of the arguments object will be reflected to the binding of their corresponding parameter. This link is broken by Aran's instrumentation because instrumented closures don't have parameters and use the arguments object instead. In the code below, both assertions should pass but they are both failing after instrumentation:
    function f (x) {
      arguments[0] = "bar";
      assert(x === "bar");
      x = "qux";
      assert(arguments[0] === "qux");
    }
    f("foo");
    The link between the arguments object and the environment could be re-established by using proxies and modifying the scope analysis performed by Aran.
  • Computed Methods' Name: Normally, in an object literal, methods names should be assigned to their property key even if it is computed. This is not the case for instrumented code because Aran uses a simple static analysis to assign function's names. In the code below, the assertion should pass but it fails after instrumentation:
    var x = "foo"
    var o = {
      [x]: function () {}
    };
    assert(o.foo.name === "foo");
    This issue could be solved in Aran.
  • Arrow's Prototype: Normally, arrows should not define a prototype property. This is not the case for instrumented code because Aran transforms arrows into function which have a non-configurable prototype property. In the code below, both assertion should pass but the second one fails after instrumentation
    var a = () => {};
    assert(a.prototype === undefined); // Success
    assert(Reflect.getOwnPropertyDescriptor(a, "prototype") === undefined);
    This issue could be solved by not transforming arrows to functions.
  • Constructing Arrow Proxies: Normally, a proxy wrapping an arrow function should never call its construct handler. This is not the case for instrumented code because Aran transforms arrows into functions that throw a type error when the new.target is defined.
    const a = () => {};
    const p = new Proxy(a, {
      construct: () => {
        throw new Error("This should never happen");
      }
    });
    new p();
    This issue could be solved by not transforming arrows to functions.

Acknowledgments

I'm Laurent Christophe a phd student at the Vrij Universiteit of Brussel. I'm working at the SOFT language lab, my promoters are Coen De Roover and Wolfgang De Meuter. I'm currently being employed on the Tearless project.

vub soft tearless