Skip to content

Commit 2d90340

Browse files
joyeecheungtargos
authored andcommitted
vm: introduce vanilla contexts via vm.constants.DONT_CONTEXTIFY
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 <[email protected]> Reviewed-By: Chengzhong Wu <[email protected]>
1 parent 10bea1c commit 2d90340

File tree

7 files changed

+410
-69
lines changed

7 files changed

+410
-69
lines changed

doc/api/vm.md

+143-23
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,9 @@ overhead.
228228
<!-- YAML
229229
added: v0.3.1
230230
changes:
231+
- version: REPLACEME
232+
pr-url: https://github.com/nodejs/node/pull/54394
233+
description: The `contextObject` argument now accepts `vm.constants.DONT_CONTEXTIFY`.
231234
- version: v14.6.0
232235
pr-url: https://github.com/nodejs/node/pull/34023
233236
description: The `microtaskMode` option is supported now.
@@ -239,8 +242,9 @@ changes:
239242
description: The `breakOnSigint` option is supported now.
240243
-->
241244

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

277-
First contextifies the given `contextObject`, runs the compiled code contained
278-
by the `vm.Script` object within the created context, and returns the result.
279-
Running code does not have access to local scope.
281+
This method is a shortcut to `script.runInContext(vm.createContext(options), options)`.
282+
It does several things at once:
283+
284+
1. Creates a new context.
285+
2. If `contextObject` is an object, [contextifies][contextified] it with the new context.
286+
If `contextObject` is undefined, creates a new object and [contextifies][contextified] it.
287+
If `contextObject` is [`vm.constants.DONT_CONTEXTIFY`][], don't [contextify][contextified] anything.
288+
3. Runs the compiled code contained by the `vm.Script` object within the created context. The code
289+
does not have access to the scope in which this method is called.
290+
4. Returns the result.
280291

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

295306
console.log(contexts);
296307
// Prints: [{ globalVar: 'set' }, { globalVar: 'set' }, { globalVar: 'set' }]
308+
309+
// This would throw if the context is created from a contextified object.
310+
// vm.constants.DONT_CONTEXTIFY allows creating contexts with ordinary
311+
// global objects that can be frozen.
312+
const freezeScript = new vm.Script('Object.freeze(globalThis); globalThis;');
313+
const frozenContext = freezeScript.runInNewContext(vm.constants.DONT_CONTEXTIFY);
297314
```
298315

299316
### `script.runInThisContext([options])`
@@ -1063,6 +1080,10 @@ For detailed information, see
10631080
<!-- YAML
10641081
added: v0.3.1
10651082
changes:
1083+
- version:
1084+
- REPLACEME
1085+
pr-url: https://github.com/nodejs/node/pull/54394
1086+
description: The `contextObject` argument now accepts `vm.constants.DONT_CONTEXTIFY`.
10661087
- version:
10671088
- v20.12.0
10681089
pr-url: https://github.com/nodejs/node/pull/51244
@@ -1083,7 +1104,9 @@ changes:
10831104
description: The `codeGeneration` option is supported now.
10841105
-->
10851106

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

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

1167+
When the global object in the newly created context is [contextified][], it has some quirks
1168+
compared to ordinary global objects. For example, it cannot be frozen. To create a context
1169+
without the contextifying quirks, pass [`vm.constants.DONT_CONTEXTIFY`][] as the `contextObject`
1170+
argument. See the documentation of [`vm.constants.DONT_CONTEXTIFY`][] for details.
1171+
11441172
The `vm.createContext()` method is primarily useful for creating a single
11451173
context that can be used to run multiple scripts. For instance, if emulating a
11461174
web browser, the method can be used to create a single context representing a
@@ -1160,7 +1188,8 @@ added: v0.11.7
11601188
* Returns: {boolean}
11611189
11621190
Returns `true` if the given `object` object has been [contextified][] using
1163-
[`vm.createContext()`][].
1191+
[`vm.createContext()`][], or if it's the global object of a context created
1192+
using [`vm.constants.DONT_CONTEXTIFY`][].
11641193

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

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

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

1435+
It does several things at once:
1436+
1437+
1. Creates a new context.
1438+
2. If `contextObject` is an object, [contextifies][contextified] it with the new context.
1439+
If `contextObject` is undefined, creates a new object and [contextifies][contextified] it.
1440+
If `contextObject` is [`vm.constants.DONT_CONTEXTIFY`][], don't [contextify][contextified] anything.
1441+
3. Compiles the code as a`vm.Script`
1442+
4. Runs the compield code within the created context. The code does not have access to the scope in
1443+
which this method is called.
1444+
5. Returns the result.
1445+
14041446
The following example compiles and executes code that increments a global
14051447
variable and sets a new one. These globals are contained in the `contextObject`.
14061448
@@ -1415,6 +1457,11 @@ const contextObject = {
14151457
vm.runInNewContext('count += 1; name = "kitty"', contextObject);
14161458
console.log(contextObject);
14171459
// Prints: { animal: 'cat', count: 3, name: 'kitty' }
1460+
1461+
// This would throw if the context is created from a contextified object.
1462+
// vm.constants.DONT_CONTEXTIFY allows creating contexts with ordinary global objects that
1463+
// can be frozen.
1464+
const frozenContext = vm.runInNewContext('Object.freeze(globalThis); globalThis;', vm.constants.DONT_CONTEXTIFY);
14181465
```
14191466
14201467
## `vm.runInThisContext(code[, options])`
@@ -1541,13 +1588,85 @@ According to the [V8 Embedder's Guide][]:
15411588
> JavaScript applications to run in a single instance of V8. You must explicitly
15421589
> specify the context in which you want any JavaScript code to be run.
15431590
1544-
When the method `vm.createContext()` is called, the `contextObject` argument
1545-
(or a newly-created object if `contextObject` is `undefined`) is associated
1546-
internally with a new instance of a V8 Context. This V8 Context provides the
1547-
`code` run using the `node:vm` module's methods with an isolated global
1548-
environment within which it can operate. The process of creating the V8 Context
1549-
and associating it with the `contextObject` is what this document refers to as
1550-
"contextifying" the object.
1591+
When the method `vm.createContext()` is called with an object, the `contextObject` argument
1592+
will be used to wrap the global object of a new instance of a V8 Context
1593+
(if `contextObject` is `undefined`, a new object will be created from the current context
1594+
before its contextified). This V8 Context provides the `code` run using the `node:vm`
1595+
module's methods with an isolated global environment within which it can operate.
1596+
The process of creating the V8 Context and associating it with the `contextObject`
1597+
in the outer context is what this document refers to as "contextifying" the object.
1598+
1599+
The contextifying would introduce some quirks to the `globalThis` value in the context.
1600+
For example, it cannot be frozen, and it is not reference equal to the `contextObject`
1601+
in the outer context.
1602+
1603+
```js
1604+
const vm = require('node:vm');
1605+
1606+
// An undefined `contextObject` option makes the global object contextified.
1607+
const context = vm.createContext();
1608+
console.log(vm.runInContext('globalThis', context) === context); // false
1609+
// A contextified global object cannot be frozen.
1610+
try {
1611+
vm.runInContext('Object.freeze(globalThis);', context);
1612+
} catch (e) {
1613+
console.log(e); // TypeError: Cannot freeze
1614+
}
1615+
console.log(vm.runInContext('globalThis.foo = 1; foo;', context)); // 1
1616+
```
1617+
1618+
To create a context with an ordinary global object and get access to a global proxy in
1619+
the outer context with fewer quirks, specify `vm.constants.DONT_CONTEXTIFY` as the
1620+
`contextObject` argument.
1621+
1622+
### `vm.constants.DONT_CONTEXTIFY`
1623+
1624+
This constant, when used as the `contextObject` argument in vm APIs, instructs Node.js to create
1625+
a context without wrapping its global object with another object in a Node.js-specific manner.
1626+
As a result, the `globalThis` value inside the new context would behave more closely to an ordinary
1627+
one.
1628+
1629+
```js
1630+
const vm = require('node:vm');
1631+
1632+
// Use vm.constants.DONT_CONTEXTIFY to freeze the global object.
1633+
const context = vm.createContext(vm.constants.DONT_CONTEXTIFY);
1634+
vm.runInContext('Object.freeze(globalThis);', context);
1635+
try {
1636+
vm.runInContext('bar = 1; bar;', context);
1637+
} catch (e) {
1638+
console.log(e); // Uncaught ReferenceError: bar is not defined
1639+
}
1640+
```
1641+
1642+
When `vm.constants.DONT_CONTEXTIFY` is used as the `contextObject` argument to [`vm.createContext()`][],
1643+
the returned object is a proxy-like object to the global object in the newly created context with
1644+
fewer Node.js-specific quirks. It is reference equal to the `globalThis` value in the new context,
1645+
can be modified from outside the context, and can be used to access built-ins in the new context directly.
1646+
1647+
```js
1648+
const vm = require('node:vm');
1649+
1650+
const context = vm.createContext(vm.constants.DONT_CONTEXTIFY);
1651+
1652+
// Returned object is reference equal to globalThis in the new context.
1653+
console.log(vm.runInContext('globalThis', context) === context); // true
1654+
1655+
// Can be used to access globals in the new context directly.
1656+
console.log(context.Array); // [Function: Array]
1657+
vm.runInContext('foo = 1;', context);
1658+
console.log(context.foo); // 1
1659+
context.bar = 1;
1660+
console.log(vm.runInContext('bar;', context)); // 1
1661+
1662+
// Can be frozen and it affects the inner context.
1663+
Object.freeze(context);
1664+
try {
1665+
vm.runInContext('baz = 1; baz;', context);
1666+
} catch (e) {
1667+
console.log(e); // Uncaught ReferenceError: baz is not defined
1668+
}
1669+
```
15511670

15521671
## Timeout interactions with asynchronous tasks and Promises
15531672

@@ -1837,6 +1956,7 @@ const { Script, SyntheticModule } = require('node:vm');
18371956
[`script.runInThisContext()`]: #scriptruninthiscontextoptions
18381957
[`url.origin`]: url.md#urlorigin
18391958
[`vm.compileFunction()`]: #vmcompilefunctioncode-params-options
1959+
[`vm.constants.DONT_CONTEXTIFY`]: #vmconstantsdont_contextify
18401960
[`vm.createContext()`]: #vmcreatecontextcontextobject-options
18411961
[`vm.runInContext()`]: #vmrunincontextcode-contextifiedobject-options
18421962
[`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_no_contextify,
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_no_contextify && 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+
DONT_CONTEXTIFY: vm_context_no_contextify,
397399
};
398400

399401
ObjectFreeze(vmConstants);

src/env_properties.h

+1
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
V(resource_symbol, "resource_symbol") \
5353
V(trigger_async_id_symbol, "trigger_async_id_symbol") \
5454
V(source_text_module_default_hdo, "source_text_module_default_hdo") \
55+
V(vm_context_no_contextify, "vm_context_no_contextify") \
5556
V(vm_dynamic_import_default_internal, "vm_dynamic_import_default_internal") \
5657
V(vm_dynamic_import_main_context_default, \
5758
"vm_dynamic_import_main_context_default") \

0 commit comments

Comments
 (0)