Skip to content

Commit db80eac

Browse files
joyeecheungRafaelGSS
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 a4bebf8 commit db80eac

File tree

7 files changed

+410
-69
lines changed

7 files changed

+410
-69
lines changed

doc/api/vm.md

Lines changed: 143 additions & 23 deletions
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.DONT_CONTEXTIFY`.
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.DONT\_CONTEXTIFY|undefined}
247+
Either [`vm.constants.DONT_CONTEXTIFY`][] 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,9 +279,16 @@ 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
279-
by the `vm.Script` object within the created context, and returns the result.
280-
Running code does not have access to local scope.
282+
This method is a shortcut to `script.runInContext(vm.createContext(options), options)`.
283+
It does several things at once:
284+
285+
1. Creates a new context.
286+
2. If `contextObject` is an object, [contextifies][contextified] it with the new context.
287+
If `contextObject` is undefined, creates a new object and [contextifies][contextified] it.
288+
If `contextObject` is [`vm.constants.DONT_CONTEXTIFY`][], don't [contextify][contextified] anything.
289+
3. Runs the compiled code contained by the `vm.Script` object within the created context. The code
290+
does not have access to the scope in which this method is called.
291+
4. Returns the result.
281292

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

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

300317
### `script.runInThisContext([options])`
@@ -1072,6 +1089,10 @@ For detailed information, see
10721089
<!-- YAML
10731090
added: v0.3.1
10741091
changes:
1092+
- version:
1093+
- REPLACEME
1094+
pr-url: https://github.com/nodejs/node/pull/54394
1095+
description: The `contextObject` argument now accepts `vm.constants.DONT_CONTEXTIFY`.
10751096
- version:
10761097
- v21.7.0
10771098
- v20.12.0
@@ -1094,7 +1115,9 @@ changes:
10941115
description: The `codeGeneration` option is supported now.
10951116
-->
10961117

1097-
* `contextObject` {Object}
1118+
* `contextObject` {Object|vm.constants.DONT\_CONTEXTIFY|undefined}
1119+
Either [`vm.constants.DONT_CONTEXTIFY`][] or an object that will be [contextified][].
1120+
If `undefined`, an empty contextified object will be created for backwards compatibility.
10981121
* `options` {Object}
10991122
* `name` {string} Human-readable name of the newly created context.
11001123
**Default:** `'VM Context i'`, where `i` is an ascending numerical index of
@@ -1124,10 +1147,10 @@ changes:
11241147
[Support of dynamic `import()` in compilation APIs][].
11251148
* Returns: {Object} contextified object.
11261149

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

1178+
When the global object in the newly created context is [contextified][], it has some quirks
1179+
compared to ordinary global objects. For example, it cannot be frozen. To create a context
1180+
without the contextifying quirks, pass [`vm.constants.DONT_CONTEXTIFY`][] as the `contextObject`
1181+
argument. See the documentation of [`vm.constants.DONT_CONTEXTIFY`][] for details.
1182+
11551183
The `vm.createContext()` method is primarily useful for creating a single
11561184
context that can be used to run multiple scripts. For instance, if emulating a
11571185
web browser, the method can be used to create a single context representing a
@@ -1171,7 +1199,8 @@ added: v0.11.7
11711199
* Returns: {boolean}
11721200
11731201
Returns `true` if the given `object` object has been [contextified][] using
1174-
[`vm.createContext()`][].
1202+
[`vm.createContext()`][], or if it's the global object of a context created
1203+
using [`vm.constants.DONT_CONTEXTIFY`][].
11751204

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

@@ -1332,6 +1361,10 @@ console.log(contextObject);
13321361
<!-- YAML
13331362
added: v0.3.1
13341363
changes:
1364+
- version:
1365+
- REPLACEME
1366+
pr-url: https://github.com/nodejs/node/pull/54394
1367+
description: The `contextObject` argument now accepts `vm.constants.DONT_CONTEXTIFY`.
13351368
- version:
13361369
- v21.7.0
13371370
- v20.12.0
@@ -1356,8 +1389,9 @@ changes:
13561389
-->
13571390
13581391
* `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.
1392+
* `contextObject` {Object|vm.constants.DONT\_CONTEXTIFY|undefined}
1393+
Either [`vm.constants.DONT_CONTEXTIFY`][] or an object that will be [contextified][].
1394+
If `undefined`, an empty contextified object will be created for backwards compatibility.
13611395
* `options` {Object|string}
13621396
* `filename` {string} Specifies the filename used in stack traces produced
13631397
by this script. **Default:** `'evalmachine.<anonymous>'`.
@@ -1407,13 +1441,21 @@ changes:
14071441
`breakOnSigint` scopes in that case.
14081442
* Returns: {any} the result of the very last statement executed in the script.
14091443

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

1448+
It does several things at once:
1449+
1450+
1. Creates a new context.
1451+
2. If `contextObject` is an object, [contextifies][contextified] it with the new context.
1452+
If `contextObject` is undefined, creates a new object and [contextifies][contextified] it.
1453+
If `contextObject` is [`vm.constants.DONT_CONTEXTIFY`][], don't [contextify][contextified] anything.
1454+
3. Compiles the code as a`vm.Script`
1455+
4. Runs the compield code within the created context. The code does not have access to the scope in
1456+
which this method is called.
1457+
5. Returns the result.
1458+
14171459
The following example compiles and executes code that increments a global
14181460
variable and sets a new one. These globals are contained in the `contextObject`.
14191461
@@ -1428,6 +1470,11 @@ const contextObject = {
14281470
vm.runInNewContext('count += 1; name = "kitty"', contextObject);
14291471
console.log(contextObject);
14301472
// Prints: { animal: 'cat', count: 3, name: 'kitty' }
1473+
1474+
// This would throw if the context is created from a contextified object.
1475+
// vm.constants.DONT_CONTEXTIFY allows creating contexts with ordinary global objects that
1476+
// can be frozen.
1477+
const frozenContext = vm.runInNewContext('Object.freeze(globalThis); globalThis;', vm.constants.DONT_CONTEXTIFY);
14311478
```
14321479
14331480
## `vm.runInThisContext(code[, options])`
@@ -1555,13 +1602,85 @@ According to the [V8 Embedder's Guide][]:
15551602
> JavaScript applications to run in a single instance of V8. You must explicitly
15561603
> specify the context in which you want any JavaScript code to be run.
15571604
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.
1605+
When the method `vm.createContext()` is called with an object, the `contextObject` argument
1606+
will be used to wrap the global object of a new instance of a V8 Context
1607+
(if `contextObject` is `undefined`, a new object will be created from the current context
1608+
before its contextified). This V8 Context provides the `code` run using the `node:vm`
1609+
module's methods with an isolated global environment within which it can operate.
1610+
The process of creating the V8 Context and associating it with the `contextObject`
1611+
in the outer context is what this document refers to as "contextifying" the object.
1612+
1613+
The contextifying would introduce some quirks to the `globalThis` value in the context.
1614+
For example, it cannot be frozen, and it is not reference equal to the `contextObject`
1615+
in the outer context.
1616+
1617+
```js
1618+
const vm = require('node:vm');
1619+
1620+
// An undefined `contextObject` option makes the global object contextified.
1621+
let context = vm.createContext();
1622+
console.log(vm.runInContext('globalThis', context) === context); // false
1623+
// A contextified global object cannot be frozen.
1624+
try {
1625+
vm.runInContext('Object.freeze(globalThis);', context);
1626+
} catch(e) {
1627+
console.log(e); // TypeError: Cannot freeze
1628+
}
1629+
console.log(vm.runInContext('globalThis.foo = 1; foo;', context)); // 1
1630+
```
1631+
1632+
To create a context with an ordinary global object and get access to a global proxy in
1633+
the outer context with fewer quirks, specify `vm.constants.DONT_CONTEXTIFY` as the
1634+
`contextObject` argument.
1635+
1636+
### `vm.constants.DONT_CONTEXTIFY`
1637+
1638+
This constant, when used as the `contextObject` argument in vm APIs, instructs Node.js to create
1639+
a context without wrapping its global object with another object in a Node.js-specific manner.
1640+
As a result, the `globalThis` value inside the new context would behave more closely to an ordinary
1641+
one.
1642+
1643+
```js
1644+
const vm = require('node:vm');
1645+
1646+
// Use vm.constants.DONT_CONTEXTIFY to freeze the global object.
1647+
const context = vm.createContext(vm.constants.DONT_CONTEXTIFY);
1648+
vm.runInContext('Object.freeze(globalThis);', context);
1649+
try {
1650+
vm.runInContext('bar = 1; bar;', context);
1651+
} catch(e) {
1652+
console.log(e); // Uncaught ReferenceError: bar is not defined
1653+
}
1654+
```
1655+
1656+
When `vm.constants.DONT_CONTEXTIFY` is used as the `contextObject` argument to [`vm.createContext()`][],
1657+
the returned object is a proxy-like object to the global object in the newly created context with
1658+
fewer Node.js-specific quirks. It is reference equal to the `globalThis` value in the new context,
1659+
can be modified from outside the context, and can be used to access built-ins in the new context directly.
1660+
1661+
```js
1662+
const vm = require('node:vm');
1663+
1664+
const context = vm.createContext(vm.constants.DONT_CONTEXTIFY);
1665+
1666+
// Returned object is reference equal to globalThis in the new context.
1667+
console.log(vm.runInContext('globalThis', context) === context); // true
1668+
1669+
// Can be used to access globals in the new context directly.
1670+
console.log(context.Array); // [Function: Array]
1671+
vm.runInContext('foo = 1;', context);
1672+
console.log(context.foo); // 1
1673+
context.bar = 1;
1674+
console.log(vm.runInContext('bar;', context)); // 1
1675+
1676+
// Can be frozen and it affects the inner context.
1677+
Object.freeze(context);
1678+
try {
1679+
vm.runInContext('baz = 1; baz;', context);
1680+
} catch(e) {
1681+
console.log(e); // Uncaught ReferenceError: baz is not defined
1682+
}
1683+
```
15651684

15661685
## Timeout interactions with asynchronous tasks and Promises
15671686

@@ -1851,6 +1970,7 @@ const { Script, SyntheticModule } = require('node:vm');
18511970
[`script.runInThisContext()`]: #scriptruninthiscontextoptions
18521971
[`url.origin`]: url.md#urlorigin
18531972
[`vm.compileFunction()`]: #vmcompilefunctioncode-params-options
1973+
[`vm.constants.DONT_CONTEXTIFY`]: #vmconstantsdont_contextify
18541974
[`vm.createContext()`]: #vmcreatecontextcontextobject-options
18551975
[`vm.runInContext()`]: #vmrunincontextcode-contextifiedobject-options
18561976
[`vm.runInThisContext()`]: #vmruninthiscontextcode-options

lib/vm.js

Lines changed: 6 additions & 4 deletions
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

Lines changed: 1 addition & 0 deletions
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_no_contextify, "vm_context_no_contextify") \
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)