NeilFraser / JS-Interpreter

A sandboxed JavaScript interpreter in JavaScript.
Apache License 2.0
1.96k stars 352 forks source link

IPC between sandbox and parent process (Node.js) #269

Open Rylxnd opened 3 months ago

Rylxnd commented 3 months ago

I'm attempting to implement a sort of IPC system (a bridge to put it bluntly) between the sandboxed code running within the interpreter and the process that is running the interpreter which is a Node.js process.

The reason behind me wanting to do this is because we are implementing a sort system where users ca create custom actions on our site. And when doing these actions they can create things such as tickets and whatnot. We have chosen to go with a more advanced approach using Blockly that will then generate into JavaScript code. For the custom blocks that do these special 'actions' we decided we are going to define a global object with some native functions on it that will be used.

But here's the issue, we need a way to communicate between the sandbox running the generated code and our parent process that is running the sandbox in node. I won't get into the fine details of how the communication works, but just the fact that I need it to happen.

I read through the documentation and previous Issues here on GitHub, but unfortunately didn't find anything helpful regarding what I'm trying to achieve. If this isn't possible with this package, please let me know. As well if you are aware of any other package that can achieve what I am looking for.

Thanks!

brownstein commented 3 months ago

I've built this for a game I'm working on.

As long as you only need to call out from the interpreter to normal JS, trick is to define functions in the global interpreter scope with setProperty wrapped in createNativeFunction of createAsyncFunction.

Calling back into the interpreter is more difficult, as the only tool we have available is createTask_, which is undocumented and probably meant to be internal-only.

If anyone here knows how to manually manipulate the call stack to add arbitrary function calls, this is a good place to have the native -> pseudo invocation discussion.

brownstein commented 3 months ago

Figured out how to manually append to the stack for pseudo function execution from native code - here's a code snippet that assumes Promise has already been polyfilled, centered around returning a Promise that the native caller can resolve:

  let resolveFunc: InterpreterFunction | undefined;
  let rejectFunc: InterpreterFunction | undefined;
  const bindingVar = `__${Math.random()}__`.replaceAll(/[\.\-]/g, "");
  function binder (
    resolve: InterpreterFunction,
    reject: InterpreterFunction
  ) {
    resolveFunc = resolve;
    rejectFunc = reject;
  };

  const currentScope = interpreter.stateStack.at(-1)?.scope;
  if (!currentScope) throw new Error("Current scope not defined.");

  const promiseIdentifier = interpreter.newNode() as Acorn.Identifier;
  promiseIdentifier.type = "Identifier";
  promiseIdentifier.start = 0;
  promiseIdentifier.end = 0;
  promiseIdentifier.name = "Promise";

  const binderIdentifier = interpreter.newNode() as Acorn.Identifier;
  binderIdentifier.type = "Identifier";
  binderIdentifier.start = 0;
  binderIdentifier.end = 0;
  binderIdentifier.name = bindingVar;

  const newPromiseExpression = interpreter.newNode() as Acorn.NewExpression;
  newPromiseExpression.type = "NewExpression";
  newPromiseExpression.start = 0;
  newPromiseExpression.end = 0;
  newPromiseExpression.callee = promiseIdentifier
  newPromiseExpression.arguments = [binderIdentifier];

  const newPromiseExScope = interpreter.createScope(newPromiseExpression, interpreter.getGlobalScope());
  interpreter.setProperty(newPromiseExScope.object, bindingVar, interpreter.createNativeFunction(binder));
  const newPromiseExState = new Interpreter.State(newPromiseExpression, newPromiseExScope);

  // Pop the current native function execution state from the stack.
  interpreter.stateStack.pop();
  // Push the new execution state we prepared onto the stack.
  interpreter.stateStack.push(newPromiseExState);

  // Build resolve and reject functions that can be called from native code.
  const resolve = (value: InterpreterPseudoValue | void) => {
    if (!resolveFunc) return;
    const expressionNode = interpreter.newNode() as Acorn.CallExpression;
    expressionNode.type = "CallExpression";
    const task = new Interpreter.Task(
      resolveFunc,
      value === undefined ? [] : [value],
      currentScope,
      expressionNode,
      -1
    );
    interpreter.scheduleTask_(task, 0);
  };
  const reject = (value: InterpreterPseudoValue | void) => {
    if (!rejectFunc) return;
    const expressionNode = interpreter.newNode() as Acorn.CallExpression;
    expressionNode.type = "CallExpression";
    const task = new Interpreter.Task(
      rejectFunc,
      value === undefined ? [] : [value],
      currentScope,
      expressionNode,
      -1
    );
    interpreter.scheduleTask_(task, 0);
  };

This works well as long as you don't need the native code to get a return value from the executed pseudo code.