Skip to content

Commit f63436d

Browse files
addaleaxjasnell
authored andcommitted
vm: add run-after-evaluate microtask mode
This allows timeouts to apply to e.g. `Promise`s and `async function`s from code running inside of `vm.Context`s, by giving the Context its own microtasks queue. Fixes: #3020 PR-URL: #34023 Reviewed-By: James M Snell <[email protected]> Reviewed-By: Denys Otrishko <[email protected]>
1 parent e68563e commit f63436d

13 files changed

+342
-39
lines changed

doc/api/vm.md

+63-11
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,9 @@ overhead.
188188
<!-- YAML
189189
added: v0.3.1
190190
changes:
191+
- version: REPLACEME
192+
pr-url: https://github.com/nodejs/node/pull/34023
193+
description: The `microtaskMode` option is supported now.
191194
- version: v10.0.0
192195
pr-url: https://github.com/nodejs/node/pull/19016
193196
description: The `contextCodeGeneration` option is supported now.
@@ -225,6 +228,10 @@ changes:
225228
`EvalError`. **Default:** `true`.
226229
* `wasm` {boolean} If set to false any attempt to compile a WebAssembly
227230
module will throw a `WebAssembly.CompileError`. **Default:** `true`.
231+
* `microtaskMode` {string} If set to `afterEvaluate`, microtasks (tasks
232+
scheduled through `Promise`s any `async function`s) will be run immediately
233+
after the script has run. They are included in the `timeout` and
234+
`breakOnSigint` scopes in that case.
228235
* Returns: {any} the result of the very last statement executed in the script.
229236

230237
First contextifies the given `contextObject`, runs the compiled code contained
@@ -846,6 +853,9 @@ function with the given `params`.
846853
<!-- YAML
847854
added: v0.3.1
848855
changes:
856+
- version: REPLACEME
857+
pr-url: https://github.com/nodejs/node/pull/34023
858+
description: The `microtaskMode` option is supported now.
849859
- version: v10.0.0
850860
pr-url: https://github.com/nodejs/node/pull/19398
851861
description: The first argument can no longer be a function.
@@ -871,6 +881,10 @@ changes:
871881
`EvalError`. **Default:** `true`.
872882
* `wasm` {boolean} If set to false any attempt to compile a WebAssembly
873883
module will throw a `WebAssembly.CompileError`. **Default:** `true`.
884+
* `microtaskMode` {string} If set to `afterEvaluate`, microtasks (tasks
885+
scheduled through `Promise`s any `async function`s) will be run immediately
886+
after a script has run through [`script.runInContext()`][].
887+
They are included in the `timeout` and `breakOnSigint` scopes in that case.
874888
* Returns: {Object} contextified object.
875889

876890
If given a `contextObject`, the `vm.createContext()` method will [prepare
@@ -1002,6 +1016,9 @@ console.log(contextObject);
10021016
<!-- YAML
10031017
added: v0.3.1
10041018
changes:
1019+
- version: REPLACEME
1020+
pr-url: https://github.com/nodejs/node/pull/34023
1021+
description: The `microtaskMode` option is supported now.
10051022
- version: v10.0.0
10061023
pr-url: https://github.com/nodejs/node/pull/19016
10071024
description: The `contextCodeGeneration` option is supported now.
@@ -1068,6 +1085,10 @@ changes:
10681085
* Returns: {Module Namespace Object|vm.Module} Returning a `vm.Module` is
10691086
recommended in order to take advantage of error tracking, and to avoid
10701087
issues with namespaces that contain `then` function exports.
1088+
* `microtaskMode` {string} If set to `afterEvaluate`, microtasks (tasks
1089+
scheduled through `Promise`s any `async function`s) will be run immediately
1090+
after the script has run. They are included in the `timeout` and
1091+
`breakOnSigint` scopes in that case.
10711092
* Returns: {any} the result of the very last statement executed in the script.
10721093
10731094
The `vm.runInNewContext()` first contextifies the given `contextObject` (or
@@ -1224,13 +1245,13 @@ within which it can operate. The process of creating the V8 Context and
12241245
associating it with the `contextObject` is what this document refers to as
12251246
"contextifying" the object.
12261247

1227-
## Timeout limitations when using `process.nextTick()`, promises, and `queueMicrotask()`
1248+
## Timeout interactions with asynchronous tasks and Promises
12281249

1229-
Because of the internal mechanics of how the `process.nextTick()` queue and
1230-
the microtask queue that underlies Promises are implemented within V8 and
1231-
Node.js, it is possible for code running within a context to "escape" the
1232-
`timeout` set using `vm.runInContext()`, `vm.runInNewContext()`, and
1233-
`vm.runInThisContext()`.
1250+
`Promise`s and `async function`s can schedule tasks run by the JavaScript
1251+
engine asynchronously. By default, these tasks are run after all JavaScript
1252+
functions on the current stack are done executing.
1253+
This allows escaping the functionality of the `timeout` and
1254+
`breakOnSigint` options.
12341255

12351256
For example, the following code executed by `vm.runInNewContext()` with a
12361257
timeout of 5 milliseconds schedules an infinite loop to run after a promise
@@ -1240,21 +1261,52 @@ resolves. The scheduled loop is never interrupted by the timeout:
12401261
const vm = require('vm');
12411262
12421263
function loop() {
1264+
console.log('entering loop');
12431265
while (1) console.log(Date.now());
12441266
}
12451267
12461268
vm.runInNewContext(
1247-
'Promise.resolve().then(loop);',
1269+
'Promise.resolve().then(() => loop());',
12481270
{ loop, console },
12491271
{ timeout: 5 }
12501272
);
1273+
// This prints *before* 'entering loop' (!)
1274+
console.log('done executing');
12511275
```
12521276

1253-
This issue also occurs when the `loop()` call is scheduled using
1254-
the `process.nextTick()` and `queueMicrotask()` functions.
1277+
This can be addressed by passing `microtaskMode: 'afterEvaluate'` to the code
1278+
that creates the `Context`:
12551279

1256-
This issue occurs because all contexts share the same microtask and nextTick
1257-
queues.
1280+
```js
1281+
const vm = require('vm');
1282+
1283+
function loop() {
1284+
while (1) console.log(Date.now());
1285+
}
1286+
1287+
vm.runInNewContext(
1288+
'Promise.resolve().then(() => loop());',
1289+
{ loop, console },
1290+
{ timeout: 5, microtaskMode: 'afterEvaluate' }
1291+
);
1292+
```
1293+
1294+
In this case, the microtask scheduled through `promise.then()` will be run
1295+
before returning from `vm.runInNewContext()`, and will be interrupted
1296+
by the `timeout` functionality. This applies only to code running in a
1297+
`vm.Context`, so e.g. [`vm.runInThisContext()`][] does not take this option.
1298+
1299+
Promise callbacks are entered into the microtask queue of the context in which
1300+
they were created. For example, if `() => loop()` is replaced with just `loop`
1301+
in the above example, then `loop` will be pushed into the global microtask
1302+
queue, because it is a function from the outer (main) context, and thus will
1303+
also be able to escape the timeout.
1304+
1305+
If asynchronous scheduling functions such as `process.nextTick()`,
1306+
`queueMicrotask()`, `setTimeout()`, `setImmediate()`, etc. are made available
1307+
inside a `vm.Context`, functions passed to them will be added to global queues,
1308+
which are shared by all contexts. Therefore, callbacks passed to those functions
1309+
are not controllable through the timeout either.
12581310

12591311
[`ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING`]: errors.html#ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING
12601312
[`ERR_VM_MODULE_STATUS`]: errors.html#ERR_VM_MODULE_STATUS

lib/vm.js

+22-2
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ const {
2929

3030
const {
3131
ContextifyScript,
32+
MicrotaskQueue,
3233
makeContext,
3334
isContext: _isContext,
3435
constants,
@@ -186,6 +187,7 @@ function getContextOptions(options) {
186187
name: options.contextName,
187188
origin: options.contextOrigin,
188189
codeGeneration: undefined,
190+
microtaskMode: options.microtaskMode,
189191
};
190192
if (contextOptions.name !== undefined)
191193
validateString(contextOptions.name, 'options.contextName');
@@ -201,6 +203,8 @@ function getContextOptions(options) {
201203
validateBoolean(wasm, 'options.contextCodeGeneration.wasm');
202204
contextOptions.codeGeneration = { strings, wasm };
203205
}
206+
if (options.microtaskMode !== undefined)
207+
validateString(options.microtaskMode, 'options.microtaskMode');
204208
return contextOptions;
205209
}
206210

@@ -222,7 +226,8 @@ function createContext(contextObject = {}, options = {}) {
222226
const {
223227
name = `VM Context ${defaultContextNameIndex++}`,
224228
origin,
225-
codeGeneration
229+
codeGeneration,
230+
microtaskMode
226231
} = options;
227232

228233
validateString(name, 'options.name');
@@ -239,7 +244,22 @@ function createContext(contextObject = {}, options = {}) {
239244
validateBoolean(wasm, 'options.codeGeneration.wasm');
240245
}
241246

242-
makeContext(contextObject, name, origin, strings, wasm);
247+
let microtaskQueue = null;
248+
if (microtaskMode !== undefined) {
249+
validateString(microtaskMode, 'options.microtaskMode');
250+
251+
if (microtaskMode === 'afterEvaluate') {
252+
microtaskQueue = new MicrotaskQueue();
253+
} else {
254+
throw new ERR_INVALID_ARG_VALUE(
255+
'options.microtaskQueue',
256+
microtaskQueue,
257+
'must be \'afterEvaluate\' or undefined'
258+
);
259+
}
260+
}
261+
262+
makeContext(contextObject, name, origin, strings, wasm, microtaskQueue);
243263
return contextObject;
244264
}
245265

src/env.h

+1
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,7 @@ constexpr size_t kFsStatsBufferLength =
433433
V(i18n_converter_template, v8::ObjectTemplate) \
434434
V(libuv_stream_wrap_ctor_template, v8::FunctionTemplate) \
435435
V(message_port_constructor_template, v8::FunctionTemplate) \
436+
V(microtask_queue_ctor_template, v8::FunctionTemplate) \
436437
V(pipe_constructor_template, v8::FunctionTemplate) \
437438
V(promise_wrap_template, v8::ObjectTemplate) \
438439
V(sab_lifetimepartner_constructor_template, v8::FunctionTemplate) \

src/module_wrap.cc

+22-9
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ using v8::IntegrityLevel;
3535
using v8::Isolate;
3636
using v8::Local;
3737
using v8::MaybeLocal;
38+
using v8::MicrotaskQueue;
3839
using v8::Module;
3940
using v8::Number;
4041
using v8::Object;
@@ -106,15 +107,15 @@ void ModuleWrap::New(const FunctionCallbackInfo<Value>& args) {
106107
Local<String> url = args[0].As<String>();
107108

108109
Local<Context> context;
110+
ContextifyContext* contextify_context = nullptr;
109111
if (args[1]->IsUndefined()) {
110112
context = that->CreationContext();
111113
} else {
112114
CHECK(args[1]->IsObject());
113-
ContextifyContext* sandbox =
114-
ContextifyContext::ContextFromContextifiedSandbox(
115-
env, args[1].As<Object>());
116-
CHECK_NOT_NULL(sandbox);
117-
context = sandbox->context();
115+
contextify_context = ContextifyContext::ContextFromContextifiedSandbox(
116+
env, args[1].As<Object>());
117+
CHECK_NOT_NULL(contextify_context);
118+
context = contextify_context->context();
118119
}
119120

120121
Local<Integer> line_offset;
@@ -224,6 +225,7 @@ void ModuleWrap::New(const FunctionCallbackInfo<Value>& args) {
224225
}
225226

226227
obj->context_.Reset(isolate, context);
228+
obj->contextify_context_ = contextify_context;
227229

228230
env->hash_to_module_map.emplace(module->GetIdentityHash(), obj);
229231

@@ -319,6 +321,11 @@ void ModuleWrap::Evaluate(const FunctionCallbackInfo<Value>& args) {
319321
Local<Context> context = obj->context_.Get(isolate);
320322
Local<Module> module = obj->module_.Get(isolate);
321323

324+
ContextifyContext* contextify_context = obj->contextify_context_;
325+
std::shared_ptr<MicrotaskQueue> microtask_queue;
326+
if (contextify_context != nullptr)
327+
microtask_queue = contextify_context->microtask_queue();
328+
322329
// module.evaluate(timeout, breakOnSigint)
323330
CHECK_EQ(args.Length(), 2);
324331

@@ -334,18 +341,24 @@ void ModuleWrap::Evaluate(const FunctionCallbackInfo<Value>& args) {
334341
bool timed_out = false;
335342
bool received_signal = false;
336343
MaybeLocal<Value> result;
344+
auto run = [&]() {
345+
MaybeLocal<Value> result = module->Evaluate(context);
346+
if (!result.IsEmpty() && microtask_queue)
347+
microtask_queue->PerformCheckpoint(isolate);
348+
return result;
349+
};
337350
if (break_on_sigint && timeout != -1) {
338351
Watchdog wd(isolate, timeout, &timed_out);
339352
SigintWatchdog swd(isolate, &received_signal);
340-
result = module->Evaluate(context);
353+
result = run();
341354
} else if (break_on_sigint) {
342355
SigintWatchdog swd(isolate, &received_signal);
343-
result = module->Evaluate(context);
356+
result = run();
344357
} else if (timeout != -1) {
345358
Watchdog wd(isolate, timeout, &timed_out);
346-
result = module->Evaluate(context);
359+
result = run();
347360
} else {
348-
result = module->Evaluate(context);
361+
result = run();
349362
}
350363

351364
if (result.IsEmpty()) {

src/module_wrap.h

+7-2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ namespace node {
1212

1313
class Environment;
1414

15+
namespace contextify {
16+
class ContextifyContext;
17+
}
18+
1519
namespace loader {
1620

1721
enum ScriptType : int {
@@ -82,12 +86,13 @@ class ModuleWrap : public BaseObject {
8286
static ModuleWrap* GetFromModule(node::Environment*, v8::Local<v8::Module>);
8387

8488
v8::Global<v8::Function> synthetic_evaluation_steps_;
85-
bool synthetic_ = false;
8689
v8::Global<v8::Module> module_;
8790
v8::Global<v8::String> url_;
88-
bool linked_ = false;
8991
std::unordered_map<std::string, v8::Global<v8::Promise>> resolve_cache_;
9092
v8::Global<v8::Context> context_;
93+
contextify::ContextifyContext* contextify_context_ = nullptr;
94+
bool synthetic_ = false;
95+
bool linked_ = false;
9196
uint32_t id_;
9297
};
9398

0 commit comments

Comments
 (0)