Skip to content

Commit 1851cf4

Browse files
jasnelltargos
authored andcommitted
doc, test: document and test vm timeout escapes
Using `process.nextTick()`, `Promise`, or `queueMicrotask()`, 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 three known_issues tests. Refs: #3020 PR-URL: #23743 Refs: #3020 Reviewed-By: Luigi Pinca <luigipinca@gmail.com> Reviewed-By: Tiancheng "Timothy" Gu <timothygu99@gmail.com>
1 parent 97496f0 commit 1851cf4

4 files changed

+152
-0
lines changed

doc/api/vm.md

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

965+
## Timeout limitations when using process.nextTick(), Promises, and queueMicrotask()
966+
967+
Because of the internal mechanics of how the `process.nextTick()` queue and
968+
the microtask queue that underlies Promises are implemented within V8 and
969+
Node.js, it is possible for code running within a context to "escape" the
970+
`timeout` set using `vm.runInContext()`, `vm.runInNewContext()`, and
971+
`vm.runInThisContext()`.
972+
973+
For example, the following code executed by `vm.runInNewContext()` with a
974+
timeout of 5 milliseconds schedules an infinite loop to run after a promise
975+
resolves. The scheduled loop is never interrupted by the timeout:
976+
977+
```js
978+
const vm = require('vm');
979+
980+
function loop() {
981+
while (1) console.log(Date.now());
982+
}
983+
984+
vm.runInNewContext(
985+
'Promise.resolve().then(loop);',
986+
{ loop, console },
987+
{ timeout: 5 }
988+
);
989+
```
990+
991+
This issue also occurs when the `loop()` call is scheduled using
992+
the `process.nextTick()` and `queueMicrotask()` functions.
993+
994+
This issue occurs because all contexts share the same microtask and nextTick
995+
queues.
996+
965997
[`Error`]: errors.html#errors_class_error
966998
[`ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING`]: errors.html#ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING
967999
[`URL`]: url.html#url_class_url
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+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
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+
'queueMicrotask(loop); loop();',
30+
{
31+
hrtime,
32+
queueMicrotask,
33+
loop
34+
},
35+
{ timeout: 5 }
36+
);
37+
}, {
38+
code: 'ERR_SCRIPT_EXECUTION_TIMEOUT',
39+
message: 'Script execution timed out after 5ms'
40+
});

0 commit comments

Comments
 (0)