Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Instrumentation blocks creation of global vars with sloppy-mode indirect eval #458

Open
overlookmotel opened this issue Nov 19, 2022 · 1 comment
Labels
bug Something isn't working eval Issue related to `eval` instrumentation Relates to instrumentation

Comments

@overlookmotel
Copy link
Owner

overlookmotel commented Nov 19, 2022

Problem

(0, eval)('var x = 123;');
assert(global.x === 123);

In original code, assertion passes. In instrumented code, it fails.

Cause

This problem is similar to #457 (direct eval()).

Cause is that Livepack's instrumentation wraps the eval code in an arrow function to inject Livepack's internal vars.

(livepack_tracker, livepack_getScopeId) => eval('var x=123;');

The binding for var x is therefore created in the arrow function's scope, not global scope.

Possible solutions

The simple solution for #457 is not available here. Indirect eval is executed in global scope so only avenue for passing in Livepack's internal vars is wrapping in function (current solution which doesn't work) or via properties on global.

1. Global var

1.1. Static global var

At define a property on global as a function which returns livepack_tracker and livepack_getScopeId. That function can be a closure and the livepack_tracker instance for the file can be provided to the closure just before eval code is executed.

Property can have an obscure name like $$__livepack_getEvalVars so it's unlikely to clash with a property used by user code.

Property can be defined in livepack/init, so before any user code runs (in case user calls Object.freeze(global)).

Property key could also be a global symbol global[Symbol.for('$$__livepack_getEvalVars')] which makes it even more obscure.

This is pretty simple to implement, but has disadvantages:

  • Property name could (in unlikely case) clash with a var used by user code.
  • Property is visible to user via Object.getOwnPropertyDescriptors(global)

1.2. Dynamic global var

Prior to running eval code, define a getter property on global which removes itself as soon as it's accessed, so it's invisible to all user code.

A safe name for that property can be chosen after parsing the code to be executed, avoiding any vars used in it, and also avoiding any existing properties on global.

This solves the problems of above solution. Disadvantage:

  • Will not work if user code has called Object.preventExtensions(global).

1.3. Proxy on global

global itself cannot be converted into a proxy, but its prototype can.

const HIDDEN_VAR_NAME = '__livepackHidden',
  global = globalThis,
  globalProto = Object.getPrototypeOf(global),
  {hasOwn} = Object;

Object.setPrototypeOf(global, wrapGlobalProto(globalProto));

let hiddenVarName, hiddenValue;

function injectTempGlobal(value) {
  let varName = HIDDEN_VAR_NAME,
    number = 0;
  while (hasOwn(global, varName) || hasOwn(globalProto, varName)) {
    varName = `${HIDDEN_VAR_NAME}${number++}`;
  }

  hiddenValue = value;
  hiddenVarName = varName;
  return varName;
}

function resetTempGlobal() {
  hiddenVarName = undefined;
  hiddenValue = undefined;
}

function wrapGlobalProto(proto) {
  return new Proxy(proto, {
    has(target, prop) {
      if (hiddenVarName && prop === hiddenVarName) return true;
      return Reflect.has(target, prop);
    },
    get(target, prop, receiver) {
      if (hiddenVarName && prop === hiddenVarName) {
        const value = hiddenValue;
        resetTempGlobal();
        return value;
      }
      return Reflect.get(target, prop, receiver);
    }
  });
}

module.exports = { injectTempGlobal, resetTempGlobal };

injectTempGlobal() would be called just before executing the eval code. It creates a global var which disappears as soon as it's accessed. resetTempGlobal() can be used to delete the global var manually (if eval() call fails with a syntax error).

Object.setPrototypeOf, Reflect.setPrototypeOf and Object.prototype.__proto__ setter could also be shimmed to defeat user setting the prototype of global.

const {getPrototypeOf: objectGetPrototypeOf, setPrototypeOf: objectSetPrototypeOf} = Object;
const {getPrototypeOf: shimmedObjectGetPrototypeOf, setPrototypeOf: shimmedObjectSetPrototypeOf} = {
  getPrototypeOf(obj) {
    if (obj === global) return globalProto;
    return objectGetPrototypeOf(obj);
  },
  setPrototypeOf(obj, proto) {
    if (obj !== global || typeof proto !== 'object') return objectSetPrototypeOf(obj, proto);

    const wrappedProto = wrapGlobalProto(proto === null ? Object.create(null) : proto);
    objectSetPrototypeOf(obj, wrappedProto);
    globalProto = proto;
    return obj;
  }
};
Object.getPrototypeOf = shimmedObjectGetPrototypeOf;
Object.setPrototypeOf = shimmedObjectSetPrototypeOf;

const {getPrototypeOf: refectGetPrototypeOf, setPrototypeOf: reflectSetPrototypeOf} = Reflect;
Object.assign(Reflect, {
  getPrototypeOf(obj) {
    if (obj === global) return globalProto;
    return refectGetPrototypeOf(obj);
  },
  setPrototypeOf(obj, proto) {
    if (obj !== global || typeof proto !== 'object') return reflectSetPrototypeOf(obj, proto);

    const wrappedProto = wrapGlobalProto(proto === null ? Object.create(null) : proto);
    const success = reflectSetPrototypeOf(obj, wrappedProto);
    if (success) globalProto = proto;
    return success;
  }
});

const {set, get} = Object.getOwnPropertyDescriptor({
  get __proto__() {
    return shimmedObjectGetPrototypeOf(this);
  },
  set __proto__(proto) {
    shimmedObjectSetPrototypeOf(this, proto);
  }
}, '__proto__');
Object.defineProperty(Object.prototype, '__proto__', {get, set});

This approach will address all above problems, but is very complicated.

It still could be defeated by user if they use vm module to get access to an unshimmed Object.setPrototypeOf, but that'd be pretty bizarre.

Putting a Proxy in the prototype chain of global could be bad for performance, and this setup code needs to be run before any user code executes, regardless of whether that code uses eval() anywhere or not. It could be a high price to pay to handle an uncommon case.

2. Use VM

Use vm module.

Run eval code with:

const injectedGlobal = {
  __proto__: global,
  global,
  __livepackHidden: [livepack_tracker, livepack_getScopeId]
};
vm.runInNewContext(code, injectedGlobal)

Something of this sort should work, but:

  1. It changes stack traces
  2. Pretty heavy machinery for a simple task

3. Transform eval-ed code

Transform the eval-ed code so that it can be wrapped in a function but still maintain correct behavior.

Top-level var and function declarations can be identified during instrumentation and moved outside of the wrapper function.

var x = 1; var y = 2; would become:

var x, y;
(livepack_tracker, livepack_getScopeId) => eval('x = 1; y = 2;');

function f() {} would become:

var f;
(livepack_tracker, livepack_getScopeId) => eval(`
  global.f = f;
  function f() {}
`);

It gets much more complicated with functions in nested blocks which are hoisted e.g. if (q) { function f() {} }.

Assuming can get the implementation right, this has advantages:

  1. It'd be the most correct implementation.
  2. No temp vars which can potentially be accessed by user code.
  3. Avoids monkeying around shimming the global object.

However, implementation could be quite fiendish to handle all the edge cases with hoisted function declarations.

@overlookmotel overlookmotel added bug Something isn't working eval Issue related to `eval` instrumentation Relates to instrumentation labels Nov 19, 2022
@overlookmotel
Copy link
Owner Author

#137 (comment) has the beginnings of a solution, I think.

global.eval is going to be a getter which dynamically alters what it returns. This mechanism could be re-used to smuggle livepack_tracker etc into the eval-ed code as a property of global.eval e.g. global.eval.internalVars.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working eval Issue related to `eval` instrumentation Relates to instrumentation
Projects
None yet
Development

No branches or pull requests

1 participant