-
Notifications
You must be signed in to change notification settings - Fork 30k
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
Promises allow vm.runInContext timeout to be escaped #3020
Comments
I added the original One possible way this could be implemented is if the
If the main loop ref count is equal to begin with, it will not block, the watchdog will destruct and everything will be fine. If the ref count is not equal, then that should indicate outstanding events have a ref on the loop and blocking should allow the watchdog to work as intended. A side effect from this change would be that code setting up an indefinite listener would always hit the timeout. You would only be able to execute code that completely and fully finishes executing, including any interaction(s) with the event loop. @bnoordhuis can you comment on the possibility of doing something like what I mentioned above in |
We can probably handle this by triggering microtask queue flush after |
All contexts share the same microtask queue (it's a per-isolate property) so I don't think this can be easily fixed, the promise callbacks can always enqueue new callbacks, ad infinitum. |
@bnoordhuis right, there is now way to flush only microtask added inside of the new context. didn't think of that. |
@bnoordhuis what I was thinking was that outstanding callbacks would ref the |
I don't think that could work. Contexts share the event loop. You could wait1 until the event loop's reference count drops below a certain threshold but that doesn't tell you to what contexts events were delivered, unless you add bookkeeping to every call into the VM. I don't think we want to go down that road and I'm not sure if it would even be enough. Microtasks, for example, are mostly outside of our control - they're driven by V8 - so there would still be loopholes. 1. In reality you can't right now because |
Closing, there's nothing that can be done about it |
If there is no reasonable way to fix this, then the documentation should have a notice about |
@ChALkeR oh, ok. saying that it's not reliable is not correct though, we should try to explain what it does and does not |
@nodejs/documentation |
we will put it on the |
We need to fix this. cc @chrisdickinson We can wrap .then calls for example which would still be than doing nothing. |
doesnt even need a while loop. throwing an exception in .then with no catch is enough. |
Wouldn't this work?
RunMicrotasks() will run until the job queue is 0, even if more jobs are scheduled or TerminateExecution() is called. In the TerminateExecution() case the jobs will just be canceled. Obviously if you had any resolved promises their callbacks will be triggered by the This seems to fix diff --git a/src/node_contextify.cc b/src/node_contextify.cc
index ff66ffdaaa..c19d8e242e 100644
--- a/src/node_contextify.cc
+++ b/src/node_contextify.cc
@@ -857,22 +857,27 @@ class ContextifyScript : public BaseObject {
Local<Value> result;
bool timed_out = false;
bool received_signal = false;
+ env->isolate()->RunMicrotasks();
if (break_on_sigint && timeout != -1) {
Watchdog wd(env->isolate(), timeout);
SigintWatchdog swd(env->isolate());
result = script->Run();
+ env->isolate()->RunMicrotasks();
timed_out = wd.HasTimedOut();
received_signal = swd.HasReceivedSignal();
} else if (break_on_sigint) {
SigintWatchdog swd(env->isolate());
result = script->Run();
+ env->isolate()->RunMicrotasks();
received_signal = swd.HasReceivedSignal();
} else if (timeout != -1) {
Watchdog wd(env->isolate(), timeout);
result = script->Run();
+ env->isolate()->RunMicrotasks();
timed_out = wd.HasTimedOut();
} else {
result = script->Run();
+ env->isolate()->RunMicrotasks();
}
if (try_catch->HasCaught()) {
@@ -896,6 +901,16 @@ class ContextifyScript : public BaseObject {
try_catch->ReThrow();
return false;
+ } else if (timed_out || received_signal) {
+ // `isolate->IsExecutionTerminating()` returns false which I suspect is a
+ // bug in v8, but `CancelTerminateExecution()` still works
+ env->isolate()->CancelTerminateExecution();
+ if (timed_out) {
+ env->ThrowError("Script execution timed out.");
+ } else {
+ env->ThrowError("Script execution interrupted.");
+ }
+ return false;
}
if (result.IsEmpty()) { |
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>
@devsnek Can you add a link to the bug report? |
Using `process.nextTick()` or `Promise`, 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 two 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>
Using `process.nextTick()` or `Promise`, 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 two 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>
Is this the right bug report? https://bugs.chromium.org/p/v8/issues/detail?id=8465 |
@Trott maybe? I also opened https://crbug.com/v8/8326 a bit ago. the feature was shimmed in 2caf079 |
Does that mean it may now be possible to fix things so that https://github.com/nodejs/node/blob/27dc74fdd0950d39d175145400a70174244870d9/test/known_issues/test-vm-timeout-escape-queuemicrotask.js is no longer a known_issue test and passes? |
@Trott it will still fail stopping at 5ms, but it should throw at 100ms instead of silently stopping. |
The issue itself is a |
Fwiw, I’m working on a fix for this, for the (The |
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: nodejs#3020
The timeout property on many of the vm module's functions isn't completely foolproof. In the following example, some sandboxed code executed by vm.runInNewContext with a timeout of 5ms schedules an infinite loop to run after a promise resolves, and then synchronously executes an infinite loop. The synchronous infinite loop is killed by the timeout, but then the scheduled loop fires and never ends.
Some output:
I'm not really sure what a good solution for this would be, if it's possible, and whether the vm module intends to stand up to this sort of thing. At the very least I think vm's docs should have a note that the timeout property works by best-effort or something and isn't completely foolproof, so no one thinks it's safe to try to use it to sandbox user code in a guaranteed timely manner.
The text was updated successfully, but these errors were encountered: