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: https://github.com/nodejs/node/issues/3020 PR-URL: https://github.com/nodejs/node/pull/34023 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Denys Otrishko <shishugi@gmail.com>
This commit is contained in:
parent
e68563e31c
commit
f63436d190
@ -188,6 +188,9 @@ overhead.
|
||||
<!-- YAML
|
||||
added: v0.3.1
|
||||
changes:
|
||||
- version: REPLACEME
|
||||
pr-url: https://github.com/nodejs/node/pull/34023
|
||||
description: The `microtaskMode` option is supported now.
|
||||
- version: v10.0.0
|
||||
pr-url: https://github.com/nodejs/node/pull/19016
|
||||
description: The `contextCodeGeneration` option is supported now.
|
||||
@ -225,6 +228,10 @@ changes:
|
||||
`EvalError`. **Default:** `true`.
|
||||
* `wasm` {boolean} If set to false any attempt to compile a WebAssembly
|
||||
module will throw a `WebAssembly.CompileError`. **Default:** `true`.
|
||||
* `microtaskMode` {string} If set to `afterEvaluate`, microtasks (tasks
|
||||
scheduled through `Promise`s any `async function`s) will be run immediately
|
||||
after the script has run. They are included in the `timeout` and
|
||||
`breakOnSigint` scopes in that case.
|
||||
* Returns: {any} the result of the very last statement executed in the script.
|
||||
|
||||
First contextifies the given `contextObject`, runs the compiled code contained
|
||||
@ -846,6 +853,9 @@ function with the given `params`.
|
||||
<!-- YAML
|
||||
added: v0.3.1
|
||||
changes:
|
||||
- version: REPLACEME
|
||||
pr-url: https://github.com/nodejs/node/pull/34023
|
||||
description: The `microtaskMode` option is supported now.
|
||||
- version: v10.0.0
|
||||
pr-url: https://github.com/nodejs/node/pull/19398
|
||||
description: The first argument can no longer be a function.
|
||||
@ -871,6 +881,10 @@ changes:
|
||||
`EvalError`. **Default:** `true`.
|
||||
* `wasm` {boolean} If set to false any attempt to compile a WebAssembly
|
||||
module will throw a `WebAssembly.CompileError`. **Default:** `true`.
|
||||
* `microtaskMode` {string} If set to `afterEvaluate`, microtasks (tasks
|
||||
scheduled through `Promise`s any `async function`s) will be run immediately
|
||||
after a script has run through [`script.runInContext()`][].
|
||||
They are included in the `timeout` and `breakOnSigint` scopes in that case.
|
||||
* Returns: {Object} contextified object.
|
||||
|
||||
If given a `contextObject`, the `vm.createContext()` method will [prepare
|
||||
@ -1002,6 +1016,9 @@ console.log(contextObject);
|
||||
<!-- YAML
|
||||
added: v0.3.1
|
||||
changes:
|
||||
- version: REPLACEME
|
||||
pr-url: https://github.com/nodejs/node/pull/34023
|
||||
description: The `microtaskMode` option is supported now.
|
||||
- version: v10.0.0
|
||||
pr-url: https://github.com/nodejs/node/pull/19016
|
||||
description: The `contextCodeGeneration` option is supported now.
|
||||
@ -1068,6 +1085,10 @@ changes:
|
||||
* Returns: {Module Namespace Object|vm.Module} Returning a `vm.Module` is
|
||||
recommended in order to take advantage of error tracking, and to avoid
|
||||
issues with namespaces that contain `then` function exports.
|
||||
* `microtaskMode` {string} If set to `afterEvaluate`, microtasks (tasks
|
||||
scheduled through `Promise`s any `async function`s) will be run immediately
|
||||
after the script has run. They are included in the `timeout` and
|
||||
`breakOnSigint` scopes in that case.
|
||||
* Returns: {any} the result of the very last statement executed in the script.
|
||||
|
||||
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
|
||||
associating it with the `contextObject` is what this document refers to as
|
||||
"contextifying" the object.
|
||||
|
||||
## Timeout limitations when using `process.nextTick()`, promises, and `queueMicrotask()`
|
||||
## Timeout interactions with asynchronous tasks and Promises
|
||||
|
||||
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()`.
|
||||
`Promise`s and `async function`s can schedule tasks run by the JavaScript
|
||||
engine asynchronously. By default, these tasks are run after all JavaScript
|
||||
functions on the current stack are done executing.
|
||||
This allows escaping the functionality of the `timeout` and
|
||||
`breakOnSigint` options.
|
||||
|
||||
For example, the following code executed by `vm.runInNewContext()` with a
|
||||
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:
|
||||
const vm = require('vm');
|
||||
|
||||
function loop() {
|
||||
console.log('entering loop');
|
||||
while (1) console.log(Date.now());
|
||||
}
|
||||
|
||||
vm.runInNewContext(
|
||||
'Promise.resolve().then(loop);',
|
||||
'Promise.resolve().then(() => loop());',
|
||||
{ loop, console },
|
||||
{ timeout: 5 }
|
||||
);
|
||||
// This prints *before* 'entering loop' (!)
|
||||
console.log('done executing');
|
||||
```
|
||||
|
||||
This issue also occurs when the `loop()` call is scheduled using
|
||||
the `process.nextTick()` and `queueMicrotask()` functions.
|
||||
This can be addressed by passing `microtaskMode: 'afterEvaluate'` to the code
|
||||
that creates the `Context`:
|
||||
|
||||
This issue occurs because all contexts share the same microtask and nextTick
|
||||
queues.
|
||||
```js
|
||||
const vm = require('vm');
|
||||
|
||||
function loop() {
|
||||
while (1) console.log(Date.now());
|
||||
}
|
||||
|
||||
vm.runInNewContext(
|
||||
'Promise.resolve().then(() => loop());',
|
||||
{ loop, console },
|
||||
{ timeout: 5, microtaskMode: 'afterEvaluate' }
|
||||
);
|
||||
```
|
||||
|
||||
In this case, the microtask scheduled through `promise.then()` will be run
|
||||
before returning from `vm.runInNewContext()`, and will be interrupted
|
||||
by the `timeout` functionality. This applies only to code running in a
|
||||
`vm.Context`, so e.g. [`vm.runInThisContext()`][] does not take this option.
|
||||
|
||||
Promise callbacks are entered into the microtask queue of the context in which
|
||||
they were created. For example, if `() => loop()` is replaced with just `loop`
|
||||
in the above example, then `loop` will be pushed into the global microtask
|
||||
queue, because it is a function from the outer (main) context, and thus will
|
||||
also be able to escape the timeout.
|
||||
|
||||
If asynchronous scheduling functions such as `process.nextTick()`,
|
||||
`queueMicrotask()`, `setTimeout()`, `setImmediate()`, etc. are made available
|
||||
inside a `vm.Context`, functions passed to them will be added to global queues,
|
||||
which are shared by all contexts. Therefore, callbacks passed to those functions
|
||||
are not controllable through the timeout either.
|
||||
|
||||
[`ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING`]: errors.html#ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING
|
||||
[`ERR_VM_MODULE_STATUS`]: errors.html#ERR_VM_MODULE_STATUS
|
||||
|
24
lib/vm.js
24
lib/vm.js
@ -29,6 +29,7 @@ const {
|
||||
|
||||
const {
|
||||
ContextifyScript,
|
||||
MicrotaskQueue,
|
||||
makeContext,
|
||||
isContext: _isContext,
|
||||
constants,
|
||||
@ -186,6 +187,7 @@ function getContextOptions(options) {
|
||||
name: options.contextName,
|
||||
origin: options.contextOrigin,
|
||||
codeGeneration: undefined,
|
||||
microtaskMode: options.microtaskMode,
|
||||
};
|
||||
if (contextOptions.name !== undefined)
|
||||
validateString(contextOptions.name, 'options.contextName');
|
||||
@ -201,6 +203,8 @@ function getContextOptions(options) {
|
||||
validateBoolean(wasm, 'options.contextCodeGeneration.wasm');
|
||||
contextOptions.codeGeneration = { strings, wasm };
|
||||
}
|
||||
if (options.microtaskMode !== undefined)
|
||||
validateString(options.microtaskMode, 'options.microtaskMode');
|
||||
return contextOptions;
|
||||
}
|
||||
|
||||
@ -222,7 +226,8 @@ function createContext(contextObject = {}, options = {}) {
|
||||
const {
|
||||
name = `VM Context ${defaultContextNameIndex++}`,
|
||||
origin,
|
||||
codeGeneration
|
||||
codeGeneration,
|
||||
microtaskMode
|
||||
} = options;
|
||||
|
||||
validateString(name, 'options.name');
|
||||
@ -239,7 +244,22 @@ function createContext(contextObject = {}, options = {}) {
|
||||
validateBoolean(wasm, 'options.codeGeneration.wasm');
|
||||
}
|
||||
|
||||
makeContext(contextObject, name, origin, strings, wasm);
|
||||
let microtaskQueue = null;
|
||||
if (microtaskMode !== undefined) {
|
||||
validateString(microtaskMode, 'options.microtaskMode');
|
||||
|
||||
if (microtaskMode === 'afterEvaluate') {
|
||||
microtaskQueue = new MicrotaskQueue();
|
||||
} else {
|
||||
throw new ERR_INVALID_ARG_VALUE(
|
||||
'options.microtaskQueue',
|
||||
microtaskQueue,
|
||||
'must be \'afterEvaluate\' or undefined'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
makeContext(contextObject, name, origin, strings, wasm, microtaskQueue);
|
||||
return contextObject;
|
||||
}
|
||||
|
||||
|
@ -433,6 +433,7 @@ constexpr size_t kFsStatsBufferLength =
|
||||
V(i18n_converter_template, v8::ObjectTemplate) \
|
||||
V(libuv_stream_wrap_ctor_template, v8::FunctionTemplate) \
|
||||
V(message_port_constructor_template, v8::FunctionTemplate) \
|
||||
V(microtask_queue_ctor_template, v8::FunctionTemplate) \
|
||||
V(pipe_constructor_template, v8::FunctionTemplate) \
|
||||
V(promise_wrap_template, v8::ObjectTemplate) \
|
||||
V(sab_lifetimepartner_constructor_template, v8::FunctionTemplate) \
|
||||
|
@ -35,6 +35,7 @@ using v8::IntegrityLevel;
|
||||
using v8::Isolate;
|
||||
using v8::Local;
|
||||
using v8::MaybeLocal;
|
||||
using v8::MicrotaskQueue;
|
||||
using v8::Module;
|
||||
using v8::Number;
|
||||
using v8::Object;
|
||||
@ -106,15 +107,15 @@ void ModuleWrap::New(const FunctionCallbackInfo<Value>& args) {
|
||||
Local<String> url = args[0].As<String>();
|
||||
|
||||
Local<Context> context;
|
||||
ContextifyContext* contextify_context = nullptr;
|
||||
if (args[1]->IsUndefined()) {
|
||||
context = that->CreationContext();
|
||||
} else {
|
||||
CHECK(args[1]->IsObject());
|
||||
ContextifyContext* sandbox =
|
||||
ContextifyContext::ContextFromContextifiedSandbox(
|
||||
env, args[1].As<Object>());
|
||||
CHECK_NOT_NULL(sandbox);
|
||||
context = sandbox->context();
|
||||
contextify_context = ContextifyContext::ContextFromContextifiedSandbox(
|
||||
env, args[1].As<Object>());
|
||||
CHECK_NOT_NULL(contextify_context);
|
||||
context = contextify_context->context();
|
||||
}
|
||||
|
||||
Local<Integer> line_offset;
|
||||
@ -224,6 +225,7 @@ void ModuleWrap::New(const FunctionCallbackInfo<Value>& args) {
|
||||
}
|
||||
|
||||
obj->context_.Reset(isolate, context);
|
||||
obj->contextify_context_ = contextify_context;
|
||||
|
||||
env->hash_to_module_map.emplace(module->GetIdentityHash(), obj);
|
||||
|
||||
@ -319,6 +321,11 @@ void ModuleWrap::Evaluate(const FunctionCallbackInfo<Value>& args) {
|
||||
Local<Context> context = obj->context_.Get(isolate);
|
||||
Local<Module> module = obj->module_.Get(isolate);
|
||||
|
||||
ContextifyContext* contextify_context = obj->contextify_context_;
|
||||
std::shared_ptr<MicrotaskQueue> microtask_queue;
|
||||
if (contextify_context != nullptr)
|
||||
microtask_queue = contextify_context->microtask_queue();
|
||||
|
||||
// module.evaluate(timeout, breakOnSigint)
|
||||
CHECK_EQ(args.Length(), 2);
|
||||
|
||||
@ -334,18 +341,24 @@ void ModuleWrap::Evaluate(const FunctionCallbackInfo<Value>& args) {
|
||||
bool timed_out = false;
|
||||
bool received_signal = false;
|
||||
MaybeLocal<Value> result;
|
||||
auto run = [&]() {
|
||||
MaybeLocal<Value> result = module->Evaluate(context);
|
||||
if (!result.IsEmpty() && microtask_queue)
|
||||
microtask_queue->PerformCheckpoint(isolate);
|
||||
return result;
|
||||
};
|
||||
if (break_on_sigint && timeout != -1) {
|
||||
Watchdog wd(isolate, timeout, &timed_out);
|
||||
SigintWatchdog swd(isolate, &received_signal);
|
||||
result = module->Evaluate(context);
|
||||
result = run();
|
||||
} else if (break_on_sigint) {
|
||||
SigintWatchdog swd(isolate, &received_signal);
|
||||
result = module->Evaluate(context);
|
||||
result = run();
|
||||
} else if (timeout != -1) {
|
||||
Watchdog wd(isolate, timeout, &timed_out);
|
||||
result = module->Evaluate(context);
|
||||
result = run();
|
||||
} else {
|
||||
result = module->Evaluate(context);
|
||||
result = run();
|
||||
}
|
||||
|
||||
if (result.IsEmpty()) {
|
||||
|
@ -12,6 +12,10 @@ namespace node {
|
||||
|
||||
class Environment;
|
||||
|
||||
namespace contextify {
|
||||
class ContextifyContext;
|
||||
}
|
||||
|
||||
namespace loader {
|
||||
|
||||
enum ScriptType : int {
|
||||
@ -82,12 +86,13 @@ class ModuleWrap : public BaseObject {
|
||||
static ModuleWrap* GetFromModule(node::Environment*, v8::Local<v8::Module>);
|
||||
|
||||
v8::Global<v8::Function> synthetic_evaluation_steps_;
|
||||
bool synthetic_ = false;
|
||||
v8::Global<v8::Module> module_;
|
||||
v8::Global<v8::String> url_;
|
||||
bool linked_ = false;
|
||||
std::unordered_map<std::string, v8::Global<v8::Promise>> resolve_cache_;
|
||||
v8::Global<v8::Context> context_;
|
||||
contextify::ContextifyContext* contextify_context_ = nullptr;
|
||||
bool synthetic_ = false;
|
||||
bool linked_ = false;
|
||||
uint32_t id_;
|
||||
};
|
||||
|
||||
|
@ -54,6 +54,8 @@ using v8::Maybe;
|
||||
using v8::MaybeLocal;
|
||||
using v8::MeasureMemoryExecution;
|
||||
using v8::MeasureMemoryMode;
|
||||
using v8::MicrotaskQueue;
|
||||
using v8::MicrotasksPolicy;
|
||||
using v8::Name;
|
||||
using v8::NamedPropertyHandlerConfiguration;
|
||||
using v8::Number;
|
||||
@ -108,7 +110,10 @@ Local<Name> Uint32ToName(Local<Context> context, uint32_t index) {
|
||||
|
||||
ContextifyContext::ContextifyContext(
|
||||
Environment* env,
|
||||
Local<Object> sandbox_obj, const ContextOptions& options) : env_(env) {
|
||||
Local<Object> sandbox_obj,
|
||||
const ContextOptions& options)
|
||||
: env_(env),
|
||||
microtask_queue_wrap_(options.microtask_queue_wrap) {
|
||||
MaybeLocal<Context> v8_context = CreateV8Context(env, sandbox_obj, options);
|
||||
|
||||
// Allocation failure, maximum call stack size reached, termination, etc.
|
||||
@ -188,7 +193,13 @@ MaybeLocal<Context> ContextifyContext::CreateV8Context(
|
||||
|
||||
object_template->SetHandler(config);
|
||||
object_template->SetHandler(indexed_config);
|
||||
Local<Context> ctx = Context::New(env->isolate(), nullptr, object_template);
|
||||
Local<Context> ctx = Context::New(
|
||||
env->isolate(),
|
||||
nullptr, // extensions
|
||||
object_template,
|
||||
{}, // global object
|
||||
{}, // deserialization callback
|
||||
microtask_queue() ? microtask_queue().get() : nullptr);
|
||||
if (ctx.IsEmpty()) return MaybeLocal<Context>();
|
||||
// Only partially initialize the context - the primordials are left out
|
||||
// and only initialized when necessary.
|
||||
@ -247,7 +258,7 @@ void ContextifyContext::Init(Environment* env, Local<Object> target) {
|
||||
void ContextifyContext::MakeContext(const FunctionCallbackInfo<Value>& args) {
|
||||
Environment* env = Environment::GetCurrent(args);
|
||||
|
||||
CHECK_EQ(args.Length(), 5);
|
||||
CHECK_EQ(args.Length(), 6);
|
||||
CHECK(args[0]->IsObject());
|
||||
Local<Object> sandbox = args[0].As<Object>();
|
||||
|
||||
@ -273,6 +284,13 @@ void ContextifyContext::MakeContext(const FunctionCallbackInfo<Value>& args) {
|
||||
CHECK(args[4]->IsBoolean());
|
||||
options.allow_code_gen_wasm = args[4].As<Boolean>();
|
||||
|
||||
if (args[5]->IsObject() &&
|
||||
!env->microtask_queue_ctor_template().IsEmpty() &&
|
||||
env->microtask_queue_ctor_template()->HasInstance(args[5])) {
|
||||
options.microtask_queue_wrap.reset(
|
||||
Unwrap<MicrotaskQueueWrap>(args[5].As<Object>()));
|
||||
}
|
||||
|
||||
TryCatchScope try_catch(env);
|
||||
auto context_ptr = std::make_unique<ContextifyContext>(env, sandbox, options);
|
||||
|
||||
@ -845,6 +863,7 @@ void ContextifyScript::RunInThisContext(
|
||||
display_errors,
|
||||
break_on_sigint,
|
||||
break_on_first_line,
|
||||
nullptr, // microtask_queue
|
||||
args);
|
||||
|
||||
TRACE_EVENT_NESTABLE_ASYNC_END0(
|
||||
@ -891,6 +910,7 @@ void ContextifyScript::RunInContext(const FunctionCallbackInfo<Value>& args) {
|
||||
display_errors,
|
||||
break_on_sigint,
|
||||
break_on_first_line,
|
||||
contextify_context->microtask_queue(),
|
||||
args);
|
||||
|
||||
TRACE_EVENT_NESTABLE_ASYNC_END0(
|
||||
@ -902,6 +922,7 @@ bool ContextifyScript::EvalMachine(Environment* env,
|
||||
const bool display_errors,
|
||||
const bool break_on_sigint,
|
||||
const bool break_on_first_line,
|
||||
std::shared_ptr<MicrotaskQueue> mtask_queue,
|
||||
const FunctionCallbackInfo<Value>& args) {
|
||||
if (!env->can_call_into_js())
|
||||
return false;
|
||||
@ -926,18 +947,24 @@ bool ContextifyScript::EvalMachine(Environment* env,
|
||||
MaybeLocal<Value> result;
|
||||
bool timed_out = false;
|
||||
bool received_signal = false;
|
||||
auto run = [&]() {
|
||||
MaybeLocal<Value> result = script->Run(env->context());
|
||||
if (!result.IsEmpty() && mtask_queue)
|
||||
mtask_queue->PerformCheckpoint(env->isolate());
|
||||
return result;
|
||||
};
|
||||
if (break_on_sigint && timeout != -1) {
|
||||
Watchdog wd(env->isolate(), timeout, &timed_out);
|
||||
SigintWatchdog swd(env->isolate(), &received_signal);
|
||||
result = script->Run(env->context());
|
||||
result = run();
|
||||
} else if (break_on_sigint) {
|
||||
SigintWatchdog swd(env->isolate(), &received_signal);
|
||||
result = script->Run(env->context());
|
||||
result = run();
|
||||
} else if (timeout != -1) {
|
||||
Watchdog wd(env->isolate(), timeout, &timed_out);
|
||||
result = script->Run(env->context());
|
||||
result = run();
|
||||
} else {
|
||||
result = script->Run(env->context());
|
||||
result = run();
|
||||
}
|
||||
|
||||
// Convert the termination exception into a regular exception.
|
||||
@ -1232,6 +1259,43 @@ static void MeasureMemory(const FunctionCallbackInfo<Value>& args) {
|
||||
args.GetReturnValue().Set(promise);
|
||||
}
|
||||
|
||||
MicrotaskQueueWrap::MicrotaskQueueWrap(Environment* env, Local<Object> obj)
|
||||
: BaseObject(env, obj),
|
||||
microtask_queue_(
|
||||
MicrotaskQueue::New(env->isolate(), MicrotasksPolicy::kExplicit)) {
|
||||
MakeWeak();
|
||||
}
|
||||
|
||||
const std::shared_ptr<MicrotaskQueue>&
|
||||
MicrotaskQueueWrap::microtask_queue() const {
|
||||
return microtask_queue_;
|
||||
}
|
||||
|
||||
void MicrotaskQueueWrap::New(const FunctionCallbackInfo<Value>& args) {
|
||||
CHECK(args.IsConstructCall());
|
||||
new MicrotaskQueueWrap(Environment::GetCurrent(args), args.This());
|
||||
}
|
||||
|
||||
void MicrotaskQueueWrap::Init(Environment* env, Local<Object> target) {
|
||||
HandleScope scope(env->isolate());
|
||||
Local<String> class_name =
|
||||
FIXED_ONE_BYTE_STRING(env->isolate(), "MicrotaskQueue");
|
||||
|
||||
Local<FunctionTemplate> tmpl = env->NewFunctionTemplate(New);
|
||||
tmpl->InstanceTemplate()->SetInternalFieldCount(
|
||||
ContextifyScript::kInternalFieldCount);
|
||||
tmpl->SetClassName(class_name);
|
||||
|
||||
if (target->Set(env->context(),
|
||||
class_name,
|
||||
tmpl->GetFunction(env->context()).ToLocalChecked())
|
||||
.IsNothing()) {
|
||||
return;
|
||||
}
|
||||
env->set_microtask_queue_ctor_template(tmpl);
|
||||
}
|
||||
|
||||
|
||||
void Initialize(Local<Object> target,
|
||||
Local<Value> unused,
|
||||
Local<Context> context,
|
||||
@ -1240,6 +1304,7 @@ void Initialize(Local<Object> target,
|
||||
Isolate* isolate = env->isolate();
|
||||
ContextifyContext::Init(env, target);
|
||||
ContextifyScript::Init(env, target);
|
||||
MicrotaskQueueWrap::Init(env, target);
|
||||
|
||||
env->SetMethod(target, "startSigintWatchdog", StartSigintWatchdog);
|
||||
env->SetMethod(target, "stopSigintWatchdog", StopSigintWatchdog);
|
||||
|
@ -10,11 +10,32 @@
|
||||
namespace node {
|
||||
namespace contextify {
|
||||
|
||||
class MicrotaskQueueWrap : public BaseObject {
|
||||
public:
|
||||
MicrotaskQueueWrap(Environment* env, v8::Local<v8::Object> obj);
|
||||
|
||||
const std::shared_ptr<v8::MicrotaskQueue>& microtask_queue() const;
|
||||
|
||||
static void Init(Environment* env, v8::Local<v8::Object> target);
|
||||
static void New(const v8::FunctionCallbackInfo<v8::Value>& args);
|
||||
|
||||
// This could have methods for running the microtask queue, if we ever decide
|
||||
// to make that fully customizable from userland.
|
||||
|
||||
SET_NO_MEMORY_INFO()
|
||||
SET_MEMORY_INFO_NAME(MicrotaskQueueWrap)
|
||||
SET_SELF_SIZE(MicrotaskQueueWrap)
|
||||
|
||||
private:
|
||||
std::shared_ptr<v8::MicrotaskQueue> microtask_queue_;
|
||||
};
|
||||
|
||||
struct ContextOptions {
|
||||
v8::Local<v8::String> name;
|
||||
v8::Local<v8::String> origin;
|
||||
v8::Local<v8::Boolean> allow_code_gen_strings;
|
||||
v8::Local<v8::Boolean> allow_code_gen_wasm;
|
||||
BaseObjectPtr<MicrotaskQueueWrap> microtask_queue_wrap;
|
||||
};
|
||||
|
||||
class ContextifyContext {
|
||||
@ -53,6 +74,11 @@ class ContextifyContext {
|
||||
context()->GetEmbedderData(ContextEmbedderIndex::kSandboxObject));
|
||||
}
|
||||
|
||||
inline std::shared_ptr<v8::MicrotaskQueue> microtask_queue() const {
|
||||
if (!microtask_queue_wrap_) return {};
|
||||
return microtask_queue_wrap_->microtask_queue();
|
||||
}
|
||||
|
||||
|
||||
template <typename T>
|
||||
static ContextifyContext* Get(const v8::PropertyCallbackInfo<T>& args);
|
||||
@ -102,6 +128,7 @@ class ContextifyContext {
|
||||
const v8::PropertyCallbackInfo<v8::Boolean>& args);
|
||||
Environment* const env_;
|
||||
v8::Global<v8::Context> context_;
|
||||
BaseObjectPtr<MicrotaskQueueWrap> microtask_queue_wrap_;
|
||||
};
|
||||
|
||||
class ContextifyScript : public BaseObject {
|
||||
@ -125,6 +152,7 @@ class ContextifyScript : public BaseObject {
|
||||
const bool display_errors,
|
||||
const bool break_on_sigint,
|
||||
const bool break_on_first_line,
|
||||
std::shared_ptr<v8::MicrotaskQueue> microtask_queue,
|
||||
const v8::FunctionCallbackInfo<v8::Value>& args);
|
||||
|
||||
inline uint32_t id() { return id_; }
|
||||
|
@ -14,9 +14,6 @@ test-vm-timeout-escape-queuemicrotask: SKIP
|
||||
[$system==win32]
|
||||
|
||||
[$system==linux]
|
||||
# https://github.com/nodejs/node/pull/23743
|
||||
# https://github.com/nodejs/node/issues/3020
|
||||
test-vm-timeout-escape-promise: PASS,FLAKY
|
||||
|
||||
[$system==macos]
|
||||
|
||||
|
@ -35,7 +35,7 @@ assert.throws(() => {
|
||||
queueMicrotask,
|
||||
loop
|
||||
},
|
||||
{ timeout }
|
||||
{ timeout, microtaskMode: 'afterScriptRun' }
|
||||
);
|
||||
}, {
|
||||
code: 'ERR_SCRIPT_EXECUTION_TIMEOUT',
|
||||
|
38
test/parallel/test-vm-timeout-escape-promise-2.js
Normal file
38
test/parallel/test-vm-timeout-escape-promise-2.js
Normal file
@ -0,0 +1,38 @@
|
||||
'use strict';
|
||||
// https://github.com/nodejs/node/issues/3020
|
||||
// Promises used to 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());',
|
||||
{
|
||||
hrtime,
|
||||
loop
|
||||
},
|
||||
{ timeout: 10, microtaskMode: 'afterEvaluate' }
|
||||
);
|
||||
}, {
|
||||
code: 'ERR_SCRIPT_EXECUTION_TIMEOUT',
|
||||
message: 'Script execution timed out after 10ms'
|
||||
});
|
42
test/parallel/test-vm-timeout-escape-promise-module-2.js
Normal file
42
test/parallel/test-vm-timeout-escape-promise-module-2.js
Normal file
@ -0,0 +1,42 @@
|
||||
// Flags: --experimental-vm-modules
|
||||
'use strict';
|
||||
|
||||
// https://github.com/nodejs/node/issues/3020
|
||||
// Promises used to allow code to escape the timeout
|
||||
// set for runInContext, runInNewContext, and runInThisContext.
|
||||
|
||||
const common = 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.rejects(async () => {
|
||||
const module = new vm.SourceTextModule(
|
||||
'Promise.resolve().then(() => loop());',
|
||||
{
|
||||
context: vm.createContext({
|
||||
hrtime,
|
||||
loop
|
||||
}, { microtaskMode: 'afterEvaluate' })
|
||||
});
|
||||
await module.link(common.mustNotCall());
|
||||
await module.evaluate({ timeout: 10 });
|
||||
}, {
|
||||
code: 'ERR_SCRIPT_EXECUTION_TIMEOUT',
|
||||
message: 'Script execution timed out after 10ms'
|
||||
});
|
42
test/parallel/test-vm-timeout-escape-promise-module.js
Normal file
42
test/parallel/test-vm-timeout-escape-promise-module.js
Normal file
@ -0,0 +1,42 @@
|
||||
// Flags: --experimental-vm-modules
|
||||
'use strict';
|
||||
|
||||
// https://github.com/nodejs/node/issues/3020
|
||||
// Promises used to allow code to escape the timeout
|
||||
// set for runInContext, runInNewContext, and runInThisContext.
|
||||
|
||||
const common = 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.rejects(async () => {
|
||||
const module = new vm.SourceTextModule(
|
||||
'Promise.resolve().then(() => loop()); loop();',
|
||||
{
|
||||
context: vm.createContext({
|
||||
hrtime,
|
||||
loop
|
||||
}, { microtaskMode: 'afterEvaluate' })
|
||||
});
|
||||
await module.link(common.mustNotCall());
|
||||
await module.evaluate({ timeout: 5 });
|
||||
}, {
|
||||
code: 'ERR_SCRIPT_EXECUTION_TIMEOUT',
|
||||
message: 'Script execution timed out after 5ms'
|
||||
});
|
@ -1,8 +1,8 @@
|
||||
'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
|
||||
// Promises used to allow code to escape the timeout
|
||||
// set for runInContext, runInNewContext, and runInThisContext.
|
||||
|
||||
require('../common');
|
||||
const assert = require('assert');
|
||||
@ -26,12 +26,12 @@ function loop() {
|
||||
|
||||
assert.throws(() => {
|
||||
vm.runInNewContext(
|
||||
'Promise.resolve().then(loop); loop();',
|
||||
'Promise.resolve().then(() => loop()); loop();',
|
||||
{
|
||||
hrtime,
|
||||
loop
|
||||
},
|
||||
{ timeout: 5 }
|
||||
{ timeout: 5, microtaskMode: 'afterEvaluate' }
|
||||
);
|
||||
}, {
|
||||
code: 'ERR_SCRIPT_EXECUTION_TIMEOUT',
|
Loading…
x
Reference in New Issue
Block a user