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

Promises allow vm.runInContext timeout to be escaped #3020

Closed
Macil opened this issue Sep 23, 2015 · 52 comments
Closed

Promises allow vm.runInContext timeout to be escaped #3020

Macil opened this issue Sep 23, 2015 · 52 comments
Labels
v8 engine Issues and PRs related to the V8 dependency. vm Issues and PRs related to the vm subsystem.

Comments

@Macil
Copy link

Macil commented Sep 23, 2015

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.

vm.runInNewContext(
  'Promise.resolve().then(()=>{while(1)console.log("foo", Date.now());}); while(1)console.log(Date.now())',
  {console:{log(){console.log.apply(console,arguments);}}},
  {timeout:5}
);

Some output:

1442966735705
...
1442966735710
1442966735710
1442966735710
1442966735710
1442966735710
Error: Script execution timed out.
    at Error (native)
    at ContextifyScript.Script.runInNewContext (vm.js:18:15)
    at Object.exports.runInNewContext (vm.js:49:17)
    at repl:1:4
    at REPLServer.defaultEval (repl.js:164:27)
    at bound (domain.js:250:14)
    at REPLServer.runBound [as eval] (domain.js:263:12)
    at REPLServer.<anonymous> (repl.js:392:12)
    at emitOne (events.js:82:20)
    at REPLServer.emit (events.js:169:7)
> foo 1442966735715
foo 1442966735716
foo 1442966735716
foo 1442966735716
foo 1442966735716
... [continues forever]

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.

@ChALkeR ChALkeR added the vm Issues and PRs related to the vm subsystem. label Sep 23, 2015
@Fishrock123 Fishrock123 added the v8 engine Issues and PRs related to the V8 dependency. label Sep 23, 2015
@apaprocki
Copy link
Contributor

I added the original vm timeout capability. This gets pretty nasty.. the watchdog timer can only cover the execution time of the actual script passed in. If that script, during its execution, interacts with the Node/uv event queue and queues up more asynchronous callbacks (process.nextTick, setTimeout, setInterval) then those will execute later and the watchdog object will have already been destructed if the script executed in less time than the timeout.

One possible way this could be implemented is if the libuv could support something like mentioned in the comments interposed into the actual vm code:

    if (timeout != -1) {
      // query and save uv main loop reference count
      Watchdog wd(env, timeout);
      result = script->Run();
      // keep spinning uv event loop until reference count equals saved value (soft blocking)
    } else {

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 uv? I'm just throwing out that idea, but there may be other reasons why that is not possible. The only other way I could think of is very nasty and involves injecting wrappers for any functions that can add to the event queue and force a check against some pre-computed timestamp.

@vkurchatkin
Copy link
Contributor

We can probably handle this by triggering microtask queue flush after vm invocation, though it uses C++ APIs so I'm not sure it can be interrupted properly.

@bnoordhuis
Copy link
Member

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.

@vkurchatkin
Copy link
Contributor

@bnoordhuis right, there is now way to flush only microtask added inside of the new context. didn't think of that.

@apaprocki
Copy link
Contributor

@bnoordhuis what I was thinking was that outstanding callbacks would ref the uv event loop, no? So if a way was added to block until ref count reached a desired value, then it would allow the main thread to block, thus allowing the watchdog to do its thing. If the code kept enqueuing callbacks and the count never reached the initial value then it would rightfully timeout, but behavior outside of the vm timeout case would not be affected in any way.

@bnoordhuis
Copy link
Member

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 uv_run() is not re-entrant.

@vkurchatkin
Copy link
Contributor

Closing, there's nothing that can be done about it

@vkurchatkin vkurchatkin added the wontfix Issues that will not be fixed. label Jan 30, 2016
@ChALkeR
Copy link
Member

ChALkeR commented Feb 3, 2016

If there is no reasonable way to fix this, then the documentation should have a notice about timeout not beeing reliable in some cases (and about the shared microtask queue, perhaps).

@ChALkeR ChALkeR reopened this Feb 3, 2016
@ChALkeR ChALkeR added the doc Issues and PRs related to the documentations. label Feb 3, 2016
@vkurchatkin
Copy link
Contributor

@ChALkeR oh, ok. saying that it's not reliable is not correct though, we should try to explain what it does and does not

@jasnell
Copy link
Member

jasnell commented Apr 4, 2016

@nodejs/documentation

@Knighton910
Copy link

we will put it on the docs todo list 📄

@benjamingr
Copy link
Member

We need to fix this. cc @chrisdickinson

We can wrap .then calls for example which would still be than doing nothing.

jasnell added a commit to jasnell/node that referenced this issue Aug 9, 2016
@sam-github sam-github added doc Issues and PRs related to the documentations. and removed doc Issues and PRs related to the documentations. docs-requested labels Dec 1, 2016
@jeacott1
Copy link

doesnt even need a while loop. throwing an exception in .then with no catch is enough.

@laverdet
Copy link
Contributor

laverdet commented May 9, 2017

Wouldn't this work?

  1. Flush microtasks
  2. Set up watchdogs
  3. Run user script
  4. Run microtasks again
  5. Disable watchdogs

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 vm library, I'm not sure how bad of a side effect that would be considered.

This seems to fix timeout for runaway promises:

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()) {

targos pushed a commit that referenced this issue Nov 1, 2018
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>
targos pushed a commit that referenced this issue Nov 1, 2018
These are known issues that can be flaky on certain platforms
because they rely entirely on timing differences.

PR-URL: #23743
Refs: #3020
Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
Reviewed-By: Tiancheng "Timothy" Gu <timothygu99@gmail.com>
@Trott
Copy link
Member

Trott commented Nov 28, 2018

edit: so it looks like i made a miscalculation. emitting an error just throws, but its still inside the microtask. this leads to what i would consider a bug in v8, where uncaught exceptions in microtasks don't trigger our fatal exception handler. i've opened a bug report.

@devsnek Can you add a link to the bug report?

codebytere pushed a commit that referenced this issue Dec 13, 2018
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>
codebytere pushed a commit that referenced this issue Dec 13, 2018
These are known issues that can be flaky on certain platforms
because they rely entirely on timing differences.

PR-URL: #23743
Refs: #3020
Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
Reviewed-By: Tiancheng "Timothy" Gu <timothygu99@gmail.com>
MylesBorins pushed a commit that referenced this issue Dec 26, 2018
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>
MylesBorins pushed a commit that referenced this issue Dec 26, 2018
These are known issues that can be flaky on certain platforms
because they rely entirely on timing differences.

PR-URL: #23743
Refs: #3020
Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
Reviewed-By: Tiancheng "Timothy" Gu <timothygu99@gmail.com>
@Trott
Copy link
Member

Trott commented Jan 13, 2019

edit: so it looks like i made a miscalculation. emitting an error just throws, but its still inside the microtask. this leads to what i would consider a bug in v8, where uncaught exceptions in microtasks don't trigger our fatal exception handler. i've opened a bug report.

@devsnek Can you add a link to the bug report?

Is this the right bug report? https://bugs.chromium.org/p/v8/issues/detail?id=8465

@devsnek
Copy link
Member

devsnek commented Jan 13, 2019

@Trott maybe? I also opened https://crbug.com/v8/8326 a bit ago.

the feature was shimmed in 2caf079

@Trott
Copy link
Member

Trott commented Jan 13, 2019

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?

@devsnek
Copy link
Member

devsnek commented Jan 13, 2019

@Trott it will still fail stopping at 5ms, but it should throw at 100ms instead of silently stopping.

@sam-github sam-github added flaky-test Issues and PRs related to the tests with unstable failures on the CI. linux Issues and PRs related to the Linux platform. labels Jun 7, 2019
@lundibundi
Copy link
Member

The issue itself is a wontfix as noted before and was documented by 5e5a945.
Feel free to reopen if there is anything else to do here.

@addaleax
Copy link
Member

Fwiw, I’m working on a fix for this, for the Promise case, so I’ll reopen this.

(The queueMicrotask() and nextTick() examples are not really fixable, but I wouldn’t consider them bugs or actual issues either, just misunderstandings about what those functions do.)

@addaleax addaleax reopened this Jun 22, 2020
addaleax added a commit to addaleax/node that referenced this issue Jun 22, 2020
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
@addaleax addaleax removed flaky-test Issues and PRs related to the tests with unstable failures on the CI. doc Issues and PRs related to the documentations. linux Issues and PRs related to the Linux platform. labels Jun 26, 2020
MylesBorins pushed a commit that referenced this issue Jul 14, 2020
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 <jasnell@gmail.com>
Reviewed-By: Denys Otrishko <shishugi@gmail.com>
MylesBorins pushed a commit that referenced this issue Jul 16, 2020
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 <jasnell@gmail.com>
Reviewed-By: Denys Otrishko <shishugi@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
v8 engine Issues and PRs related to the V8 dependency. vm Issues and PRs related to the vm subsystem.
Projects
None yet
Development

Successfully merging a pull request may close this issue.