laverdet / isolated-vm

Secure & isolated JS environments for nodejs
ISC License
2.11k stars 148 forks source link

How to use ExternalCopy with non-primitives? #491

Open errol59 opened 3 weeks ago

errol59 commented 3 weeks ago

nodeJS version: 20.10.0 (running inside Docker) isolated-vm version: 5.0.1

I'm trying to pass non-primitive values (object literals and arrays) to an isolate, but the docs are pretty unclear and sometimes even contradictory about how this can be done.

Below is a small reproducible piece of code that I'm working with.

const value = 123;

const isolate = new Isolate({ memoryLimit: 16 });
const context = await isolate.createContext();
const reference = context.global;
await reference.set('global', reference.derefInto());
await reference.set('passedValue', new ExternalCopy(value).copyInto());

console.log(await context.evalClosure('return passedValue;'))
> 123

This code works. But the moment I change the contents of the variable value to [123], the following error gets thrown:

A non-transferable value was passed

When I navigate to the section of the ExternalCopy class inside the docs, the following is stated:

Primitive values can be copied exactly as they are. Date objects will be copied as Dates. ArrayBuffers, TypedArrays, and DataViews will be copied in an efficient format. SharedArrayBuffers will simply copy a reference to the existing memory and when copied into another isolate the new SharedArrayBuffer will point to the same underlying data. After passing a SharedArrayBuffer to ExternalCopy for the first time isolated-vm will take over management of the underlying memory block, so a "copied" SharedArrayBuffer can outlive the isolate that created the memory originally.

All other objects will be copied in seralized form using the structured clone algorithm.

In a pretty old answer I found the following statement:

All instances of ExternalCopy are transferable no matter what.

Besides that, the docs also contain an example in the examples section where a basic log function is created that a new isolate can use. A function is not a primitive, but apparently it works. But then if you scroll just a little bit more down you will be met with the FAQ section with the only frequently asked question:

"How do I pass a [module, function, object, library] into an isolate?" Answer: You don't! [...]

???

So what am I misunderstanding here?

errol59 commented 3 weeks ago

Okay, after some testing I managed to resolve my problem.

The error A non-transferable value was passed led me astray. I was thinking that values passed into the isolate were triggering this error. For me that was not the case. Apparently this error is (also?) thrown the moment when NON-primitive values are passed OUT of the executed code.

Simply deep copying the result resolves this issue. In my case I only needed to add this to the evalClosure method:

await context.evalClosure('return passedValue;', undefined, { result: { copy: true } })

laverdet commented 3 weeks ago

Yes you need to "transfer" both in and out.