Skip to content

Commit

Permalink
vm: introduce vanilla contexts via vm.constants.DONT_CONTEXTIFY
Browse files Browse the repository at this point in the history
This implements a flavor of vm.createContext() and friends
that creates a context without contextifying its global object.
This is suitable when users want to freeze the context (impossible
when the global is contextified i.e. has interceptors installed)
or speed up the global access if they don't need the interceptor
behavior.

```js
const vm = require('node:vm');

const context = vm.createContext(vm.constants.DONT_CONTEXTIFY);

// In contexts with contextified global objects, this is false.
// In vanilla contexts this is true.
console.log(vm.runInContext('globalThis', context) === context);

// In contexts with contextified global objects, this would throw,
// but in vanilla contexts freezing the global object works.
vm.runInContext('Object.freeze(globalThis);', context);

// In contexts with contextified global objects, freezing throws
// and won't be effective. In vanilla contexts, freezing works
// and prevents scripts from accidentally leaking globals.
try {
  vm.runInContext('globalThis.foo = 1; foo;', context);
} catch(e) {
  console.log(e); // Uncaught ReferenceError: foo is not defined
}

console.log(context.Array);  // [Function: Array]
```

PR-URL: #54394
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
  • Loading branch information
joyeecheung authored and targos committed Oct 2, 2024
1 parent 10bea1c commit 2d90340
Show file tree
Hide file tree
Showing 7 changed files with 410 additions and 69 deletions.
166 changes: 143 additions & 23 deletions doc/api/vm.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,9 @@ overhead.
<!-- YAML
added: v0.3.1
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/54394
description: The `contextObject` argument now accepts `vm.constants.DONT_CONTEXTIFY`.
- version: v14.6.0
pr-url: https://github.com/nodejs/node/pull/34023
description: The `microtaskMode` option is supported now.
Expand All @@ -239,8 +242,9 @@ changes:
description: The `breakOnSigint` option is supported now.
-->

* `contextObject` {Object} An object that will be [contextified][]. If
`undefined`, a new object will be created.
* `contextObject` {Object|vm.constants.DONT\_CONTEXTIFY|undefined}
Either [`vm.constants.DONT_CONTEXTIFY`][] or an object that will be [contextified][].
If `undefined`, an empty contextified object will be created for backwards compatibility.
* `options` {Object}
* `displayErrors` {boolean} When `true`, if an [`Error`][] occurs
while compiling the `code`, the line of code causing the error is attached
Expand Down Expand Up @@ -274,9 +278,16 @@ changes:
`breakOnSigint` scopes in that case.
* Returns: {any} the result of the very last statement executed in the script.

First contextifies the given `contextObject`, runs the compiled code contained
by the `vm.Script` object within the created context, and returns the result.
Running code does not have access to local scope.
This method is a shortcut to `script.runInContext(vm.createContext(options), options)`.
It does several things at once:

1. Creates a new context.
2. If `contextObject` is an object, [contextifies][contextified] it with the new context.
If `contextObject` is undefined, creates a new object and [contextifies][contextified] it.
If `contextObject` is [`vm.constants.DONT_CONTEXTIFY`][], don't [contextify][contextified] anything.
3. Runs the compiled code contained by the `vm.Script` object within the created context. The code
does not have access to the scope in which this method is called.
4. Returns the result.

The following example compiles code that sets a global variable, then executes
the code multiple times in different contexts. The globals are set on and
Expand All @@ -294,6 +305,12 @@ contexts.forEach((context) => {

console.log(contexts);
// Prints: [{ globalVar: 'set' }, { globalVar: 'set' }, { globalVar: 'set' }]

// This would throw if the context is created from a contextified object.
// vm.constants.DONT_CONTEXTIFY allows creating contexts with ordinary
// global objects that can be frozen.
const freezeScript = new vm.Script('Object.freeze(globalThis); globalThis;');
const frozenContext = freezeScript.runInNewContext(vm.constants.DONT_CONTEXTIFY);
```

### `script.runInThisContext([options])`
Expand Down Expand Up @@ -1063,6 +1080,10 @@ For detailed information, see
<!-- YAML
added: v0.3.1
changes:
- version:
- REPLACEME
pr-url: https://github.com/nodejs/node/pull/54394
description: The `contextObject` argument now accepts `vm.constants.DONT_CONTEXTIFY`.
- version:
- v20.12.0
pr-url: https://github.com/nodejs/node/pull/51244
Expand All @@ -1083,7 +1104,9 @@ changes:
description: The `codeGeneration` option is supported now.
-->

* `contextObject` {Object}
* `contextObject` {Object|vm.constants.DONT\_CONTEXTIFY|undefined}
Either [`vm.constants.DONT_CONTEXTIFY`][] or an object that will be [contextified][].
If `undefined`, an empty contextified object will be created for backwards compatibility.
* `options` {Object}
* `name` {string} Human-readable name of the newly created context.
**Default:** `'VM Context i'`, where `i` is an ascending numerical index of
Expand Down Expand Up @@ -1113,10 +1136,10 @@ changes:
[Support of dynamic `import()` in compilation APIs][].
* Returns: {Object} contextified object.

If given a `contextObject`, the `vm.createContext()` method will [prepare that
If the given `contextObject` is an object, the `vm.createContext()` method will [prepare that
object][contextified] and return a reference to it so that it can be used in
calls to [`vm.runInContext()`][] or [`script.runInContext()`][]. Inside such
scripts, the `contextObject` will be the global object, retaining all of its
scripts, the global object will be wrapped by the `contextObject`, retaining all of its
existing properties but also having the built-in objects and functions any
standard [global object][] has. Outside of scripts run by the vm module, global
variables will remain unchanged.
Expand All @@ -1141,6 +1164,11 @@ console.log(global.globalVar);
If `contextObject` is omitted (or passed explicitly as `undefined`), a new,
empty [contextified][] object will be returned.

When the global object in the newly created context is [contextified][], it has some quirks
compared to ordinary global objects. For example, it cannot be frozen. To create a context
without the contextifying quirks, pass [`vm.constants.DONT_CONTEXTIFY`][] as the `contextObject`
argument. See the documentation of [`vm.constants.DONT_CONTEXTIFY`][] for details.

The `vm.createContext()` method is primarily useful for creating a single
context that can be used to run multiple scripts. For instance, if emulating a
web browser, the method can be used to create a single context representing a
Expand All @@ -1160,7 +1188,8 @@ added: v0.11.7
* Returns: {boolean}
Returns `true` if the given `object` object has been [contextified][] using
[`vm.createContext()`][].
[`vm.createContext()`][], or if it's the global object of a context created
using [`vm.constants.DONT_CONTEXTIFY`][].

## `vm.measureMemory([options])`

Expand Down Expand Up @@ -1320,6 +1349,10 @@ console.log(contextObject);
<!-- YAML
added: v0.3.1
changes:
- version:
- REPLACEME
pr-url: https://github.com/nodejs/node/pull/54394
description: The `contextObject` argument now accepts `vm.constants.DONT_CONTEXTIFY`.
- version:
- v20.12.0
pr-url: https://github.com/nodejs/node/pull/51244
Expand All @@ -1343,8 +1376,9 @@ changes:
-->
* `code` {string} The JavaScript code to compile and run.
* `contextObject` {Object} An object that will be [contextified][]. If
`undefined`, a new object will be created.
* `contextObject` {Object|vm.constants.DONT\_CONTEXTIFY|undefined}
Either [`vm.constants.DONT_CONTEXTIFY`][] or an object that will be [contextified][].
If `undefined`, an empty contextified object will be created for backwards compatibility.
* `options` {Object|string}
* `filename` {string} Specifies the filename used in stack traces produced
by this script. **Default:** `'evalmachine.<anonymous>'`.
Expand Down Expand Up @@ -1394,13 +1428,21 @@ changes:
`breakOnSigint` scopes in that case.
* Returns: {any} the result of the very last statement executed in the script.

The `vm.runInNewContext()` first contextifies the given `contextObject` (or
creates a new `contextObject` if passed as `undefined`), compiles the `code`,
runs it within the created context, then returns the result. Running code
does not have access to the local scope.
This method is a shortcut to
`(new vm.Script(code, options)).runInContext(vm.createContext(options), options)`.
If `options` is a string, then it specifies the filename.

It does several things at once:

1. Creates a new context.
2. If `contextObject` is an object, [contextifies][contextified] it with the new context.
If `contextObject` is undefined, creates a new object and [contextifies][contextified] it.
If `contextObject` is [`vm.constants.DONT_CONTEXTIFY`][], don't [contextify][contextified] anything.
3. Compiles the code as a`vm.Script`
4. Runs the compield code within the created context. The code does not have access to the scope in
which this method is called.
5. Returns the result.
The following example compiles and executes code that increments a global
variable and sets a new one. These globals are contained in the `contextObject`.
Expand All @@ -1415,6 +1457,11 @@ const contextObject = {
vm.runInNewContext('count += 1; name = "kitty"', contextObject);
console.log(contextObject);
// Prints: { animal: 'cat', count: 3, name: 'kitty' }
// This would throw if the context is created from a contextified object.
// vm.constants.DONT_CONTEXTIFY allows creating contexts with ordinary global objects that
// can be frozen.
const frozenContext = vm.runInNewContext('Object.freeze(globalThis); globalThis;', vm.constants.DONT_CONTEXTIFY);
```
## `vm.runInThisContext(code[, options])`
Expand Down Expand Up @@ -1541,13 +1588,85 @@ According to the [V8 Embedder's Guide][]:
> JavaScript applications to run in a single instance of V8. You must explicitly
> specify the context in which you want any JavaScript code to be run.
When the method `vm.createContext()` is called, the `contextObject` argument
(or a newly-created object if `contextObject` is `undefined`) is associated
internally with a new instance of a V8 Context. This V8 Context provides the
`code` run using the `node:vm` module's methods with an isolated global
environment within which it can operate. The process of creating the V8 Context
and associating it with the `contextObject` is what this document refers to as
"contextifying" the object.
When the method `vm.createContext()` is called with an object, the `contextObject` argument
will be used to wrap the global object of a new instance of a V8 Context
(if `contextObject` is `undefined`, a new object will be created from the current context
before its contextified). This V8 Context provides the `code` run using the `node:vm`
module's methods with an isolated global environment within which it can operate.
The process of creating the V8 Context and associating it with the `contextObject`
in the outer context is what this document refers to as "contextifying" the object.

The contextifying would introduce some quirks to the `globalThis` value in the context.
For example, it cannot be frozen, and it is not reference equal to the `contextObject`
in the outer context.

```js
const vm = require('node:vm');
// An undefined `contextObject` option makes the global object contextified.
const context = vm.createContext();
console.log(vm.runInContext('globalThis', context) === context); // false
// A contextified global object cannot be frozen.
try {
vm.runInContext('Object.freeze(globalThis);', context);
} catch (e) {
console.log(e); // TypeError: Cannot freeze
}
console.log(vm.runInContext('globalThis.foo = 1; foo;', context)); // 1
```

To create a context with an ordinary global object and get access to a global proxy in
the outer context with fewer quirks, specify `vm.constants.DONT_CONTEXTIFY` as the
`contextObject` argument.

### `vm.constants.DONT_CONTEXTIFY`

This constant, when used as the `contextObject` argument in vm APIs, instructs Node.js to create
a context without wrapping its global object with another object in a Node.js-specific manner.
As a result, the `globalThis` value inside the new context would behave more closely to an ordinary
one.

```js
const vm = require('node:vm');
// Use vm.constants.DONT_CONTEXTIFY to freeze the global object.
const context = vm.createContext(vm.constants.DONT_CONTEXTIFY);
vm.runInContext('Object.freeze(globalThis);', context);
try {
vm.runInContext('bar = 1; bar;', context);
} catch (e) {
console.log(e); // Uncaught ReferenceError: bar is not defined
}
```

When `vm.constants.DONT_CONTEXTIFY` is used as the `contextObject` argument to [`vm.createContext()`][],
the returned object is a proxy-like object to the global object in the newly created context with
fewer Node.js-specific quirks. It is reference equal to the `globalThis` value in the new context,
can be modified from outside the context, and can be used to access built-ins in the new context directly.

```js
const vm = require('node:vm');
const context = vm.createContext(vm.constants.DONT_CONTEXTIFY);
// Returned object is reference equal to globalThis in the new context.
console.log(vm.runInContext('globalThis', context) === context); // true
// Can be used to access globals in the new context directly.
console.log(context.Array); // [Function: Array]
vm.runInContext('foo = 1;', context);
console.log(context.foo); // 1
context.bar = 1;
console.log(vm.runInContext('bar;', context)); // 1
// Can be frozen and it affects the inner context.
Object.freeze(context);
try {
vm.runInContext('baz = 1; baz;', context);
} catch (e) {
console.log(e); // Uncaught ReferenceError: baz is not defined
}
```

## Timeout interactions with asynchronous tasks and Promises

Expand Down Expand Up @@ -1837,6 +1956,7 @@ const { Script, SyntheticModule } = require('node:vm');
[`script.runInThisContext()`]: #scriptruninthiscontextoptions
[`url.origin`]: url.md#urlorigin
[`vm.compileFunction()`]: #vmcompilefunctioncode-params-options
[`vm.constants.DONT_CONTEXTIFY`]: #vmconstantsdont_contextify
[`vm.createContext()`]: #vmcreatecontextcontextobject-options
[`vm.runInContext()`]: #vmrunincontextcode-contextifiedobject-options
[`vm.runInThisContext()`]: #vmruninthiscontextcode-options
Expand Down
10 changes: 6 additions & 4 deletions lib/vm.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ const {
} = require('internal/vm');
const {
vm_dynamic_import_main_context_default,
vm_context_no_contextify,
} = internalBinding('symbols');
const kParsingContext = Symbol('script parsing context');

Expand Down Expand Up @@ -222,7 +223,7 @@ function getContextOptions(options) {

let defaultContextNameIndex = 1;
function createContext(contextObject = {}, options = kEmptyObject) {
if (isContext(contextObject)) {
if (contextObject !== vm_context_no_contextify && isContext(contextObject)) {
return contextObject;
}

Expand Down Expand Up @@ -258,10 +259,10 @@ function createContext(contextObject = {}, options = kEmptyObject) {
const hostDefinedOptionId =
getHostDefinedOptionId(importModuleDynamically, name);

makeContext(contextObject, name, origin, strings, wasm, microtaskQueue, hostDefinedOptionId);
const result = makeContext(contextObject, name, origin, strings, wasm, microtaskQueue, hostDefinedOptionId);
// Register the context scope callback after the context was initialized.
registerImportModuleDynamically(contextObject, importModuleDynamically);
return contextObject;
registerImportModuleDynamically(result, importModuleDynamically);
return result;
}

function createScript(code, options) {
Expand Down Expand Up @@ -394,6 +395,7 @@ function measureMemory(options = kEmptyObject) {
const vmConstants = {
__proto__: null,
USE_MAIN_CONTEXT_DEFAULT_LOADER: vm_dynamic_import_main_context_default,
DONT_CONTEXTIFY: vm_context_no_contextify,
};

ObjectFreeze(vmConstants);
Expand Down
1 change: 1 addition & 0 deletions src/env_properties.h
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
V(resource_symbol, "resource_symbol") \
V(trigger_async_id_symbol, "trigger_async_id_symbol") \
V(source_text_module_default_hdo, "source_text_module_default_hdo") \
V(vm_context_no_contextify, "vm_context_no_contextify") \
V(vm_dynamic_import_default_internal, "vm_dynamic_import_default_internal") \
V(vm_dynamic_import_main_context_default, \
"vm_dynamic_import_main_context_default") \
Expand Down
Loading

0 comments on commit 2d90340

Please sign in to comment.