diff --git a/doc/api/vm.md b/doc/api/vm.md index 5bf09de37940fb..842ae3b81a01de 100644 --- a/doc/api/vm.md +++ b/doc/api/vm.md @@ -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 diff --git a/test/known_issues/known_issues.status b/test/known_issues/known_issues.status index e21913e232c03f..5a1ab289280d23 100644 --- a/test/known_issues/known_issues.status +++ b/test/known_issues/known_issues.status @@ -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] diff --git a/test/known_issues/test-vm-timeout-escape-nexttick.js b/test/known_issues/test-vm-timeout-escape-nexttick.js new file mode 100644 index 00000000000000..8afe2fb8cebb15 --- /dev/null +++ b/test/known_issues/test-vm-timeout-escape-nexttick.js @@ -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' +}); diff --git a/test/known_issues/test-vm-timeout-escape-promise.js b/test/known_issues/test-vm-timeout-escape-promise.js new file mode 100644 index 00000000000000..4452c83cd182e3 --- /dev/null +++ b/test/known_issues/test-vm-timeout-escape-promise.js @@ -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' +}); diff --git a/test/known_issues/test-vm-timeout-escape-queuemicrotask.js b/test/known_issues/test-vm-timeout-escape-queuemicrotask.js new file mode 100644 index 00000000000000..8de33bc24ddcdc --- /dev/null +++ b/test/known_issues/test-vm-timeout-escape-queuemicrotask.js @@ -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' +});