Skip to content

Commit 6f3bc0d

Browse files
jasnellMylesBorins
authored andcommitted
doc, test: document and test vm timeout escapes
Using `process.nextTick()` or `Promise`, it is possible to escape the `timeout` set when running code with `vm.runInContext()`, `vm.runInThisContext()`, and `vm.runInNewContext()`. This documents the issue and adds two known_issues tests. Refs: #3020 PR-URL: #23743 Refs: #3020 Reviewed-By: Luigi Pinca <[email protected]> Reviewed-By: Tiancheng "Timothy" Gu <[email protected]>
1 parent 3170cb4 commit 6f3bc0d

File tree

3 files changed

+112
-0
lines changed

3 files changed

+112
-0
lines changed

doc/api/vm.md

+32
Original file line numberDiff line numberDiff line change
@@ -944,6 +944,38 @@ within which it can operate. The process of creating the V8 Context and
944944
associating it with the `sandbox` object is what this document refers to as
945945
"contextifying" the `sandbox`.
946946

947+
## Timeout limitations when using process.nextTick(), and Promises
948+
949+
Because of the internal mechanics of how the `process.nextTick()` queue and
950+
the microtask queue that underlies Promises are implemented within V8 and
951+
Node.js, it is possible for code running within a context to "escape" the
952+
`timeout` set using `vm.runInContext()`, `vm.runInNewContext()`, and
953+
`vm.runInThisContext()`.
954+
955+
For example, the following code executed by `vm.runInNewContext()` with a
956+
timeout of 5 milliseconds schedules an infinite loop to run after a promise
957+
resolves. The scheduled loop is never interrupted by the timeout:
958+
959+
```js
960+
const vm = require('vm');
961+
962+
function loop() {
963+
while (1) console.log(Date.now());
964+
}
965+
966+
vm.runInNewContext(
967+
'Promise.resolve().then(loop);',
968+
{ loop, console },
969+
{ timeout: 5 }
970+
);
971+
```
972+
973+
This issue also occurs when the `loop()` call is scheduled using
974+
the `process.nextTick()` function.
975+
976+
This issue occurs because all contexts share the same microtask and nextTick
977+
queues.
978+
947979
[`Error`]: errors.html#errors_class_error
948980
[`URL`]: url.html#url_class_url
949981
[`eval()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
'use strict';
2+
3+
// https://github.com/nodejs/node/issues/3020
4+
// Promises, nextTick, and queueMicrotask allow code to escape the timeout
5+
// set for runInContext, runInNewContext, and runInThisContext
6+
7+
require('../common');
8+
const assert = require('assert');
9+
const vm = require('vm');
10+
11+
const NS_PER_MS = 1000000n;
12+
13+
const hrtime = process.hrtime.bigint;
14+
const nextTick = process.nextTick;
15+
16+
function loop() {
17+
const start = hrtime();
18+
while (1) {
19+
const current = hrtime();
20+
const span = (current - start) / NS_PER_MS;
21+
if (span >= 100n) {
22+
throw new Error(
23+
`escaped timeout at ${span} milliseconds!`);
24+
}
25+
}
26+
}
27+
28+
assert.throws(() => {
29+
vm.runInNewContext(
30+
'nextTick(loop); loop();',
31+
{
32+
hrtime,
33+
nextTick,
34+
loop
35+
},
36+
{ timeout: 5 }
37+
);
38+
}, {
39+
code: 'ERR_SCRIPT_EXECUTION_TIMEOUT',
40+
message: 'Script execution timed out after 5ms'
41+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
'use strict';
2+
3+
// https://github.com/nodejs/node/issues/3020
4+
// Promises, nextTick, and queueMicrotask allow code to escape the timeout
5+
// set for runInContext, runInNewContext, and runInThisContext
6+
7+
require('../common');
8+
const assert = require('assert');
9+
const vm = require('vm');
10+
11+
const NS_PER_MS = 1000000n;
12+
13+
const hrtime = process.hrtime.bigint;
14+
15+
function loop() {
16+
const start = hrtime();
17+
while (1) {
18+
const current = hrtime();
19+
const span = (current - start) / NS_PER_MS;
20+
if (span >= 100n) {
21+
throw new Error(
22+
`escaped timeout at ${span} milliseconds!`);
23+
}
24+
}
25+
}
26+
27+
assert.throws(() => {
28+
vm.runInNewContext(
29+
'Promise.resolve().then(loop); loop();',
30+
{
31+
hrtime,
32+
loop
33+
},
34+
{ timeout: 5 }
35+
);
36+
}, {
37+
code: 'ERR_SCRIPT_EXECUTION_TIMEOUT',
38+
message: 'Script execution timed out after 5ms'
39+
});

0 commit comments

Comments
 (0)