Skip to content

Commit 3e87824

Browse files
committed
vm: introduce vanilla contexts via vm.constants.VANILLA_CONTEXT
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 } ```
1 parent 6051826 commit 3e87824

File tree

6 files changed

+367
-62
lines changed

6 files changed

+367
-62
lines changed

doc/api/vm.md

+107-17
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,9 @@ overhead.
229229
<!-- YAML
230230
added: v0.3.1
231231
changes:
232+
- version: REPLACEME
233+
pr-url: https://github.com/nodejs/node/pull/54394
234+
description: The `contextObject` argument now accepts `vm.constants.VANILLA_CONTEXT`.
232235
- version: v14.6.0
233236
pr-url: https://github.com/nodejs/node/pull/34023
234237
description: The `microtaskMode` option is supported now.
@@ -240,8 +243,9 @@ changes:
240243
description: The `breakOnSigint` option is supported now.
241244
-->
242245

243-
* `contextObject` {Object} An object that will be [contextified][]. If
244-
`undefined`, a new object will be created.
246+
* `contextObject` {Object|vm.constants.VANILLA\_CONTEXT|undefined}
247+
Either [`vm.constants.VANILLA_CONTEXT`][] or an object that will be [contextified][].
248+
If `undefined`, an empty contextified object will be created for backwards compatibility.
245249
* `options` {Object}
246250
* `displayErrors` {boolean} When `true`, if an [`Error`][] occurs
247251
while compiling the `code`, the line of code causing the error is attached
@@ -275,7 +279,9 @@ changes:
275279
`breakOnSigint` scopes in that case.
276280
* Returns: {any} the result of the very last statement executed in the script.
277281

278-
First contextifies the given `contextObject`, runs the compiled code contained
282+
First contextifies the given `contextObject` (or
283+
creates a new `contextObject` if passed as `undefined`), or creates a new
284+
vanilla context (if it's `vm.constants.VANILLA_CONTEXT`), runs the compiled code contained
279285
by the `vm.Script` object within the created context, and returns the result.
280286
Running code does not have access to local scope.
281287

@@ -295,6 +301,12 @@ contexts.forEach((context) => {
295301

296302
console.log(contexts);
297303
// Prints: [{ globalVar: 'set' }, { globalVar: 'set' }, { globalVar: 'set' }]
304+
305+
// This would throw if the context is created from a contextified object.
306+
// vm.constants.VANILLA_CONTEXT allows creating vanilla contexts with ordinary
307+
// global objects that can be freezed.
308+
const freezeScript = new vm.Script('Object.freeze(globalThis); globalThis;');
309+
const freezedContext = freezeScript.runInNewContext(vm.constants.VANILLA_CONTEXT);
298310
```
299311

300312
### `script.runInThisContext([options])`
@@ -1072,6 +1084,10 @@ For detailed information, see
10721084
<!-- YAML
10731085
added: v0.3.1
10741086
changes:
1087+
- version:
1088+
- REPLACEME
1089+
pr-url: https://github.com/nodejs/node/pull/54394
1090+
description: The `contextObject` argument now accepts `vm.constants.VANILLA_CONTEXT`.
10751091
- version:
10761092
- v21.7.0
10771093
- v20.12.0
@@ -1094,7 +1110,9 @@ changes:
10941110
description: The `codeGeneration` option is supported now.
10951111
-->
10961112

1097-
* `contextObject` {Object}
1113+
* `contextObject` {Object|vm.constants.VANILLA\_CONTEXT|undefined}
1114+
Either [`vm.constants.VANILLA_CONTEXT`][] or an object that will be [contextified][].
1115+
If `undefined`, an empty contextified object will be created for backwards compatibility.
10981116
* `options` {Object}
10991117
* `name` {string} Human-readable name of the newly created context.
11001118
**Default:** `'VM Context i'`, where `i` is an ascending numerical index of
@@ -1124,10 +1142,10 @@ changes:
11241142
[Support of dynamic `import()` in compilation APIs][].
11251143
* Returns: {Object} contextified object.
11261144

1127-
If given a `contextObject`, the `vm.createContext()` method will [prepare that
1145+
If the given `contextObject` is an object, the `vm.createContext()` method will [prepare that
11281146
object][contextified] and return a reference to it so that it can be used in
11291147
calls to [`vm.runInContext()`][] or [`script.runInContext()`][]. Inside such
1130-
scripts, the `contextObject` will be the global object, retaining all of its
1148+
scripts, the global object will be wrapped by the `contextObject`, retaining all of its
11311149
existing properties but also having the built-in objects and functions any
11321150
standard [global object][] has. Outside of scripts run by the vm module, global
11331151
variables will remain unchanged.
@@ -1152,6 +1170,12 @@ console.log(global.globalVar);
11521170
If `contextObject` is omitted (or passed explicitly as `undefined`), a new,
11531171
empty [contextified][] object will be returned.
11541172

1173+
The global object wrapped as a [contextified][] object has some particularities compared to
1174+
normal global objects. For example, it cannot be freezed. To create a vanilla context with an
1175+
ordinary global object, pass [`vm.constants.VANILLA_CONTEXT`][] as the `contextObject` argument.
1176+
The returned object will be the global object of the newly created context itself, without a
1177+
contextified wrapper in between. See documentation of [`vm.constants.VANILLA_CONTEXT`][] for details.
1178+
11551179
The `vm.createContext()` method is primarily useful for creating a single
11561180
context that can be used to run multiple scripts. For instance, if emulating a
11571181
web browser, the method can be used to create a single context representing a
@@ -1171,7 +1195,8 @@ added: v0.11.7
11711195
* Returns: {boolean}
11721196
11731197
Returns `true` if the given `object` object has been [contextified][] using
1174-
[`vm.createContext()`][].
1198+
[`vm.createContext()`][], or if it's the global object of a context created
1199+
using [`vm.constants.VANILLA_CONTEXT`][].
11751200

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

@@ -1332,6 +1357,10 @@ console.log(contextObject);
13321357
<!-- YAML
13331358
added: v0.3.1
13341359
changes:
1360+
- version:
1361+
- REPLACEME
1362+
pr-url: https://github.com/nodejs/node/pull/54394
1363+
description: The `contextObject` argument now accepts `vm.constants.VANILLA_CONTEXT`.
13351364
- version:
13361365
- v21.7.0
13371366
- v20.12.0
@@ -1356,8 +1385,9 @@ changes:
13561385
-->
13571386
13581387
* `code` {string} The JavaScript code to compile and run.
1359-
* `contextObject` {Object} An object that will be [contextified][]. If
1360-
`undefined`, a new object will be created.
1388+
* `contextObject` {Object|vm.constants.VANILLA\_CONTEXT|undefined}
1389+
Either [`vm.constants.VANILLA_CONTEXT`][] or an object that will be [contextified][].
1390+
If `undefined`, an empty contextified object will be created for backwards compatibility.
13611391
* `options` {Object|string}
13621392
* `filename` {string} Specifies the filename used in stack traces produced
13631393
by this script. **Default:** `'evalmachine.<anonymous>'`.
@@ -1408,7 +1438,8 @@ changes:
14081438
* Returns: {any} the result of the very last statement executed in the script.
14091439

14101440
The `vm.runInNewContext()` first contextifies the given `contextObject` (or
1411-
creates a new `contextObject` if passed as `undefined`), compiles the `code`,
1441+
creates a new `contextObject` if passed as `undefined`), or creates a new
1442+
vanilla context (if it's `vm.constants.VANILLA_CONTEXT`), compiles the `code`,
14121443
runs it within the created context, then returns the result. Running code
14131444
does not have access to the local scope.
14141445
@@ -1428,6 +1459,11 @@ const contextObject = {
14281459
vm.runInNewContext('count += 1; name = "kitty"', contextObject);
14291460
console.log(contextObject);
14301461
// Prints: { animal: 'cat', count: 3, name: 'kitty' }
1462+
1463+
// This would throw if the context is created from a contextified object.
1464+
// vm.constants.VANILLA_CONTEXT allows creating vanilla contexts with ordinary
1465+
// global objects that can be freezed.
1466+
const freezedContext = vm.runInNewContext('Object.freeze(globalThis); globalThis;', vm.constants.VANILLA_CONTEXT);
14311467
```
14321468
14331469
## `vm.runInThisContext(code[, options])`
@@ -1555,13 +1591,66 @@ According to the [V8 Embedder's Guide][]:
15551591
> JavaScript applications to run in a single instance of V8. You must explicitly
15561592
> specify the context in which you want any JavaScript code to be run.
15571593
1558-
When the method `vm.createContext()` is called, the `contextObject` argument
1559-
(or a newly-created object if `contextObject` is `undefined`) is associated
1560-
internally with a new instance of a V8 Context. This V8 Context provides the
1561-
`code` run using the `node:vm` module's methods with an isolated global
1562-
environment within which it can operate. The process of creating the V8 Context
1563-
and associating it with the `contextObject` is what this document refers to as
1564-
"contextifying" the object.
1594+
When the method `vm.createContext()` is called with an object, the `contextObject` argument
1595+
will be used to wrap the global object of a new instance of a V8 Context
1596+
(if `contextObject` is `undefined`, a new object will be created from the current context
1597+
before its contextified). This V8 Context provides the `code` run using the `node:vm`
1598+
module's methods with an isolated global environment within which it can operate.
1599+
The process of creating the V8 Context and associating it with the `contextObject`
1600+
is what this document refers to as "contextifying" the object.
1601+
1602+
The contextifying would make the global object within the V8 context behave differently
1603+
from a normal global object. For example, it is not reference equal to the `globalThis`
1604+
value in the context, and it cannot be freezed.
1605+
1606+
```js
1607+
const vm = require('node:vm');
1608+
1609+
// An undefined `contextObject` option makes the global object contextified.
1610+
let context = vm.createContext();
1611+
console.log(vm.runInContext('globalThis', context) === context); // false
1612+
// A contextified global object cannot be freezed.
1613+
try {
1614+
vm.runInContext('Object.freeze(globalThis);', context);
1615+
} catch(e) {
1616+
console.log(e); // TypeError: Cannot freeze
1617+
}
1618+
console.log(vm.runInContext('globalThis.foo = 1; foo;', context)); // 1
1619+
```
1620+
1621+
To create a vanilla context with an ordinary global object, specify `vm.constants.VANILLA_CONTEXT`
1622+
as the `contextObject` argument.
1623+
1624+
### `vm.constants.VANILLA_CONTEXT`
1625+
1626+
This constant, when used as the `contextObject` argument in vm APIs, instructs Node.js to create
1627+
a context without wrapping its global object with another object in the outer context. As a result,
1628+
the global object inside the new context would behave more closely to an ordinary global object.
1629+
For example, it can be freezed to prevent unintentional leakage to the global scope.
1630+
1631+
```js
1632+
const vm = require('node:vm');
1633+
1634+
// Use vm.constants.VANILLA_CONTEXT to freeze the global object.
1635+
const freezedContext = vm.createContext(vm.constants.VANILLA_CONTEXT);
1636+
vm.runInContext('Object.freeze(globalThis);', freezedContext);
1637+
try {
1638+
vm.runInContext('globalThis.foo = 1; foo;', freezedContext);
1639+
} catch(e) {
1640+
console.log(e); // Uncaught ReferenceError: foo is not defined
1641+
}
1642+
```
1643+
1644+
When used as the `contextObject` argument to [`vm.createContext()`][], the returned object
1645+
is the global object of the context.
1646+
The global object can also be modified from outside the context.
1647+
1648+
```js
1649+
const context = vm.createContext(vm.constants.VANILLA_CONTEXT);
1650+
console.log(vm.runInContext('globalThis', context) === context); // true
1651+
context.bar = 1;
1652+
console.log(vm.runInContext('bar;', context)); // 1
1653+
```
15651654

15661655
## Timeout interactions with asynchronous tasks and Promises
15671656

@@ -1851,6 +1940,7 @@ const { Script, SyntheticModule } = require('node:vm');
18511940
[`script.runInThisContext()`]: #scriptruninthiscontextoptions
18521941
[`url.origin`]: url.md#urlorigin
18531942
[`vm.compileFunction()`]: #vmcompilefunctioncode-params-options
1943+
[`vm.constants.VANILLA_CONTEXT`]: #vmconstantsvanillacontext
18541944
[`vm.createContext()`]: #vmcreatecontextcontextobject-options
18551945
[`vm.runInContext()`]: #vmrunincontextcode-contextifiedobject-options
18561946
[`vm.runInThisContext()`]: #vmruninthiscontextcode-options

lib/vm.js

+6-4
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ const {
6565
} = require('internal/vm');
6666
const {
6767
vm_dynamic_import_main_context_default,
68+
vm_context_vanilla,
6869
} = internalBinding('symbols');
6970
const kParsingContext = Symbol('script parsing context');
7071

@@ -222,7 +223,7 @@ function getContextOptions(options) {
222223

223224
let defaultContextNameIndex = 1;
224225
function createContext(contextObject = {}, options = kEmptyObject) {
225-
if (isContext(contextObject)) {
226+
if (contextObject !== vm_context_vanilla && isContext(contextObject)) {
226227
return contextObject;
227228
}
228229

@@ -258,10 +259,10 @@ function createContext(contextObject = {}, options = kEmptyObject) {
258259
const hostDefinedOptionId =
259260
getHostDefinedOptionId(importModuleDynamically, name);
260261

261-
makeContext(contextObject, name, origin, strings, wasm, microtaskQueue, hostDefinedOptionId);
262+
const result = makeContext(contextObject, name, origin, strings, wasm, microtaskQueue, hostDefinedOptionId);
262263
// Register the context scope callback after the context was initialized.
263-
registerImportModuleDynamically(contextObject, importModuleDynamically);
264-
return contextObject;
264+
registerImportModuleDynamically(result, importModuleDynamically);
265+
return result;
265266
}
266267

267268
function createScript(code, options) {
@@ -394,6 +395,7 @@ function measureMemory(options = kEmptyObject) {
394395
const vmConstants = {
395396
__proto__: null,
396397
USE_MAIN_CONTEXT_DEFAULT_LOADER: vm_dynamic_import_main_context_default,
398+
VANILLA_CONTEXT: vm_context_vanilla,
397399
};
398400

399401
ObjectFreeze(vmConstants);

src/env_properties.h

+1
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
V(resource_symbol, "resource_symbol") \
5858
V(trigger_async_id_symbol, "trigger_async_id_symbol") \
5959
V(source_text_module_default_hdo, "source_text_module_default_hdo") \
60+
V(vm_context_vanilla, "vm_context_vanilla") \
6061
V(vm_dynamic_import_default_internal, "vm_dynamic_import_default_internal") \
6162
V(vm_dynamic_import_main_context_default, \
6263
"vm_dynamic_import_main_context_default") \

0 commit comments

Comments
 (0)