Skip to content

Commit

Permalink
vm: introduce vanilla contexts via vm.constants.VANILLA_CONTEXT
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 the interceptors. This is suitable
when users want to freeze the context (impossible when the global
has interceptors installed) or speed up the global access if
they don't need the interceptor behavior.

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

// Use vm.constants.VANILLA_CONTEXT to create a vanilla context.
const context = vm.createContext(vm.constants.VANILLA_CONTEXT);

// 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
}
```
  • Loading branch information
joyeecheung committed Aug 15, 2024
1 parent 6051826 commit 055c9c9
Show file tree
Hide file tree
Showing 6 changed files with 367 additions and 62 deletions.
124 changes: 107 additions & 17 deletions doc/api/vm.md
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,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.VANILLA_CONTEXT`.
- version: v14.6.0
pr-url: https://github.com/nodejs/node/pull/34023
description: The `microtaskMode` option is supported now.
Expand All @@ -240,8 +243,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.VANILLA\_CONTEXT|undefined}
Either [`vm.constants.VANILLA_CONTEXT`][] 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 @@ -275,7 +279,9 @@ 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
First contextifies the given `contextObject` (or
creates a new `contextObject` if passed as `undefined`), or creates a new
vanilla context (if it's `vm.constants.VANILLA_CONTEXT`), 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.

Expand All @@ -295,6 +301,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.VANILLA_CONTEXT allows creating vanilla contexts with ordinary
// global objects that can be freezed.
const freezeScript = new vm.Script('Object.freeze(globalThis); globalThis;');
const freezedContext = freezeScript.runInNewContext(vm.constants.VANILLA_CONTEXT);
```

### `script.runInThisContext([options])`
Expand Down Expand Up @@ -1072,6 +1084,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.VANILLA_CONTEXT`.
- version:
- v21.7.0
- v20.12.0
Expand All @@ -1094,7 +1110,9 @@ changes:
description: The `codeGeneration` option is supported now.
-->

* `contextObject` {Object}
* `contextObject` {Object|vm.constants.VANILLA\_CONTEXT|undefined}
Either [`vm.constants.VANILLA_CONTEXT`][] 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 @@ -1124,10 +1142,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 @@ -1152,6 +1170,12 @@ console.log(global.globalVar);
If `contextObject` is omitted (or passed explicitly as `undefined`), a new,
empty [contextified][] object will be returned.

The global object wrapped as a [contextified][] object has some particularities compared to
normal global objects. For example, it cannot be freezed. To create a vanilla context with an
ordinary global object, pass [`vm.constants.VANILLA_CONTEXT`][] as the `contextObject` argument.
The returned object will be the global object of the newly created context itself, without a
contextified wrapper in between. See documentation of [`vm.constants.VANILLA_CONTEXT`][] 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 @@ -1171,7 +1195,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.VANILLA_CONTEXT`][].

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

Expand Down Expand Up @@ -1332,6 +1357,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.VANILLA_CONTEXT`.
- version:
- v21.7.0
- v20.12.0
Expand All @@ -1356,8 +1385,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.VANILLA\_CONTEXT|undefined}
Either [`vm.constants.VANILLA_CONTEXT`][] 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 @@ -1408,7 +1438,8 @@ changes:
* 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`,
creates a new `contextObject` if passed as `undefined`), or creates a new
vanilla context (if it's `vm.constants.VANILLA_CONTEXT`), compiles the `code`,
runs it within the created context, then returns the result. Running code
does not have access to the local scope.
Expand All @@ -1428,6 +1459,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.VANILLA_CONTEXT allows creating vanilla contexts with ordinary
// global objects that can be freezed.
const freezedContext = vm.runInNewContext('Object.freeze(globalThis); globalThis;', vm.constants.VANILLA_CONTEXT);
```
## `vm.runInThisContext(code[, options])`
Expand Down Expand Up @@ -1555,13 +1591,66 @@ 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`
is what this document refers to as "contextifying" the object.

The contextifying would make the global object within the V8 context behave differently
from a normal global object. For example, it is not reference equal to the `globalThis`
value in the context, and it cannot be freezed.

```js
const vm = require('node:vm');
// An undefined `contextObject` option makes the global object contextified.
let context = vm.createContext();
console.log(vm.runInContext('globalThis', context) === context); // false
// A contextified global object cannot be freezed.
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 vanilla context with an ordinary global object, specify `vm.constants.VANILLA_CONTEXT`
as the `contextObject` argument.

### `vm.constants.VANILLA_CONTEXT`

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 the outer context. As a result,
the global object inside the new context would behave more closely to an ordinary global object.
For example, it can be freezed to prevent unintentional leakage to the global scope.

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

When used as the `contextObject` argument to [`vm.createContext()`][], the returned object
is the global object of the context.
The global object can also be modified from outside the context.

```js
const context = vm.createContext(vm.constants.VANILLA_CONTEXT);
console.log(vm.runInContext('globalThis', context) === context); // true
context.bar = 1;
console.log(vm.runInContext('bar;', context)); // 1
```

## Timeout interactions with asynchronous tasks and Promises

Expand Down Expand Up @@ -1851,6 +1940,7 @@ const { Script, SyntheticModule } = require('node:vm');
[`script.runInThisContext()`]: #scriptruninthiscontextoptions
[`url.origin`]: url.md#urlorigin
[`vm.compileFunction()`]: #vmcompilefunctioncode-params-options
[`vm.constants.VANILLA_CONTEXT`]: #vmconstantsvanillacontext
[`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_vanilla,
} = 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_vanilla && 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,
VANILLA_CONTEXT: vm_context_vanilla,
};

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 @@ -57,6 +57,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_vanilla, "vm_context_vanilla") \
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 055c9c9

Please sign in to comment.