Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

doc, test: document and test vm timeout escapes #23743

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions doc/api/vm.md
Original file line number Diff line number Diff line change
Expand Up @@ -962,6 +962,38 @@ within which it can operate. The process of creating the V8 Context and
associating it with the `sandbox` object is what this document refers to as
"contextifying" the `sandbox`.

## Timeout limitations when using process.nextTick(), Promises, and queueMicrotask()

Because of the internal mechanics of how the `process.nextTick()` queue and
the microtask queue that underlies Promises are implemented within V8 and
Node.js, it is possible for code running within a context to "escape" the
`timeout` set using `vm.runInContext()`, `vm.runInNewContext()`, and
`vm.runInThisContext()`.

For example, the following code executed by `vm.runInNewContext()` with a
timeout of 5 milliseconds schedules an infinite loop to run after a promise
resolves. The scheduled loop is never interrupted by the timeout:

```js
const vm = require('vm');

function loop() {
while (1) console.log(Date.now());
}

vm.runInNewContext(
'Promise.resolve().then(loop);',
{ loop, console },
{ timeout: 5 }
);
```

This issue also occurs when the `loop()` call is scheduled using
the `process.nextTick()` and `queueMicrotask()` functions.

This issue occurs because all contexts share the same microtask and nextTick
queues.

[`Error`]: errors.html#errors_class_error
[`ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING`]: errors.html#ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING
[`URL`]: url.html#url_class_url
Expand Down
3 changes: 3 additions & 0 deletions test/known_issues/known_issues.status
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ prefix known_issues
[$system==win32]

[$system==linux]
test-vm-timeout-escape-nexttick: PASS,FLAKY
test-vm-timeout-escape-promise: PASS,FLAKY
test-vm-timeout-escape-queuemicrotask: PASS,FLAKY

[$system==macos]

Expand Down
41 changes: 41 additions & 0 deletions test/known_issues/test-vm-timeout-escape-nexttick.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
'use strict';

// https://github.com/nodejs/node/issues/3020
// Promises, nextTick, and queueMicrotask allow code to escape the timeout
// set for runInContext, runInNewContext, and runInThisContext

require('../common');
const assert = require('assert');
const vm = require('vm');

const NS_PER_MS = 1000000n;

const hrtime = process.hrtime.bigint;
const nextTick = process.nextTick;

function loop() {
const start = hrtime();
while (1) {
const current = hrtime();
const span = (current - start) / NS_PER_MS;
if (span >= 100n) {
throw new Error(
`escaped timeout at ${span} milliseconds!`);
}
}
}

assert.throws(() => {
vm.runInNewContext(
'nextTick(loop); loop();',
{
hrtime,
nextTick,
loop
},
{ timeout: 5 }
);
}, {
code: 'ERR_SCRIPT_EXECUTION_TIMEOUT',
message: 'Script execution timed out after 5ms'
});
39 changes: 39 additions & 0 deletions test/known_issues/test-vm-timeout-escape-promise.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
'use strict';

// https://github.com/nodejs/node/issues/3020
// Promises, nextTick, and queueMicrotask allow code to escape the timeout
// set for runInContext, runInNewContext, and runInThisContext

require('../common');
const assert = require('assert');
const vm = require('vm');

const NS_PER_MS = 1000000n;

const hrtime = process.hrtime.bigint;

function loop() {
const start = hrtime();
while (1) {
const current = hrtime();
const span = (current - start) / NS_PER_MS;
if (span >= 100n) {
throw new Error(
`escaped timeout at ${span} milliseconds!`);
}
}
}

assert.throws(() => {
vm.runInNewContext(
'Promise.resolve().then(loop); loop();',
{
hrtime,
loop
},
{ timeout: 5 }
);
}, {
code: 'ERR_SCRIPT_EXECUTION_TIMEOUT',
message: 'Script execution timed out after 5ms'
});
40 changes: 40 additions & 0 deletions test/known_issues/test-vm-timeout-escape-queuemicrotask.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
'use strict';

// https://github.com/nodejs/node/issues/3020
// Promises, nextTick, and queueMicrotask allow code to escape the timeout
// set for runInContext, runInNewContext, and runInThisContext

require('../common');
const assert = require('assert');
const vm = require('vm');

const NS_PER_MS = 1000000n;

const hrtime = process.hrtime.bigint;

function loop() {
const start = hrtime();
while (1) {
const current = hrtime();
const span = (current - start) / NS_PER_MS;
if (span >= 100n) {
throw new Error(
`escaped timeout at ${span} milliseconds!`);
}
}
}

assert.throws(() => {
vm.runInNewContext(
'queueMicrotask(loop); loop();',
{
hrtime,
queueMicrotask,
loop
},
{ timeout: 5 }
);
}, {
code: 'ERR_SCRIPT_EXECUTION_TIMEOUT',
message: 'Script execution timed out after 5ms'
});