vm: fix race condition with timeout param

This fixes a race condition in the watchdog timer used for vm timeouts.
The condition would terminate the main stack's execution instead of the
code running under the sandbox.

PR-URL: https://github.com/nodejs/node/pull/13074
Reviewed-By: Anna Henningsen <anna@addaleax.net>
This commit is contained in:
Marcel Laverdet 2017-05-18 17:12:41 -05:00 committed by Anna Henningsen
parent 3c5bfba28b
commit e018c33111
No known key found for this signature in database
GPG Key ID: D8B9F5AEAE84E4CF
4 changed files with 63 additions and 73 deletions

View File

@ -958,27 +958,20 @@ class ContextifyScript : public BaseObject {
bool timed_out = false; bool timed_out = false;
bool received_signal = false; bool received_signal = false;
if (break_on_sigint && timeout != -1) { if (break_on_sigint && timeout != -1) {
Watchdog wd(env->isolate(), timeout); Watchdog wd(env->isolate(), timeout, &timed_out);
SigintWatchdog swd(env->isolate()); SigintWatchdog swd(env->isolate(), &received_signal);
result = script->Run(); result = script->Run();
timed_out = wd.HasTimedOut();
received_signal = swd.HasReceivedSignal();
} else if (break_on_sigint) { } else if (break_on_sigint) {
SigintWatchdog swd(env->isolate()); SigintWatchdog swd(env->isolate(), &received_signal);
result = script->Run(); result = script->Run();
received_signal = swd.HasReceivedSignal();
} else if (timeout != -1) { } else if (timeout != -1) {
Watchdog wd(env->isolate(), timeout); Watchdog wd(env->isolate(), timeout, &timed_out);
result = script->Run(); result = script->Run();
timed_out = wd.HasTimedOut();
} else { } else {
result = script->Run(); result = script->Run();
} }
if (try_catch->HasCaught()) { if (timed_out || received_signal) {
if (try_catch->HasTerminated())
env->isolate()->CancelTerminateExecution();
// It is possible that execution was terminated by another timeout in // It is possible that execution was terminated by another timeout in
// which this timeout is nested, so check whether one of the watchdogs // which this timeout is nested, so check whether one of the watchdogs
// from this invocation is responsible for termination. // from this invocation is responsible for termination.
@ -986,7 +979,12 @@ class ContextifyScript : public BaseObject {
env->ThrowError("Script execution timed out."); env->ThrowError("Script execution timed out.");
} else if (received_signal) { } else if (received_signal) {
env->ThrowError("Script execution interrupted."); env->ThrowError("Script execution interrupted.");
} else if (display_errors) { }
env->isolate()->CancelTerminateExecution();
}
if (try_catch->HasCaught()) {
if (!timed_out && !received_signal && display_errors) {
// We should decorate non-termination exceptions // We should decorate non-termination exceptions
DecorateErrorStack(env, *try_catch); DecorateErrorStack(env, *try_catch);
} }

View File

@ -27,9 +27,9 @@
namespace node { namespace node {
Watchdog::Watchdog(v8::Isolate* isolate, uint64_t ms) : isolate_(isolate), Watchdog::Watchdog(v8::Isolate* isolate, uint64_t ms, bool* timed_out)
timed_out_(false), : isolate_(isolate), timed_out_(timed_out) {
destroyed_(false) {
int rc; int rc;
loop_ = new uv_loop_t; loop_ = new uv_loop_t;
CHECK(loop_); CHECK(loop_);
@ -54,20 +54,6 @@ Watchdog::Watchdog(v8::Isolate* isolate, uint64_t ms) : isolate_(isolate),
Watchdog::~Watchdog() { Watchdog::~Watchdog() {
Destroy();
}
void Watchdog::Dispose() {
Destroy();
}
void Watchdog::Destroy() {
if (destroyed_) {
return;
}
uv_async_send(&async_); uv_async_send(&async_);
uv_thread_join(&thread_); uv_thread_join(&thread_);
@ -80,8 +66,6 @@ void Watchdog::Destroy() {
CHECK_EQ(0, rc); CHECK_EQ(0, rc);
delete loop_; delete loop_;
loop_ = nullptr; loop_ = nullptr;
destroyed_ = true;
} }
@ -93,7 +77,7 @@ void Watchdog::Run(void* arg) {
uv_run(wd->loop_, UV_RUN_DEFAULT); uv_run(wd->loop_, UV_RUN_DEFAULT);
// Loop ref count reaches zero when both handles are closed. // Loop ref count reaches zero when both handles are closed.
// Close the timer handle on this side and let Destroy() close async_ // Close the timer handle on this side and let ~Watchdog() close async_
uv_close(reinterpret_cast<uv_handle_t*>(&wd->timer_), nullptr); uv_close(reinterpret_cast<uv_handle_t*>(&wd->timer_), nullptr);
} }
@ -106,24 +90,15 @@ void Watchdog::Async(uv_async_t* async) {
void Watchdog::Timer(uv_timer_t* timer) { void Watchdog::Timer(uv_timer_t* timer) {
Watchdog* w = ContainerOf(&Watchdog::timer_, timer); Watchdog* w = ContainerOf(&Watchdog::timer_, timer);
w->timed_out_ = true; *w->timed_out_ = true;
uv_stop(w->loop_);
w->isolate()->TerminateExecution(); w->isolate()->TerminateExecution();
uv_stop(w->loop_);
} }
SigintWatchdog::~SigintWatchdog() { SigintWatchdog::SigintWatchdog(
Destroy(); v8::Isolate* isolate, bool* received_signal)
} : isolate_(isolate), received_signal_(received_signal) {
void SigintWatchdog::Dispose() {
Destroy();
}
SigintWatchdog::SigintWatchdog(v8::Isolate* isolate)
: isolate_(isolate), received_signal_(false), destroyed_(false) {
// Register this watchdog with the global SIGINT/Ctrl+C listener. // Register this watchdog with the global SIGINT/Ctrl+C listener.
SigintWatchdogHelper::GetInstance()->Register(this); SigintWatchdogHelper::GetInstance()->Register(this);
// Start the helper thread, if that has not already happened. // Start the helper thread, if that has not already happened.
@ -131,20 +106,14 @@ SigintWatchdog::SigintWatchdog(v8::Isolate* isolate)
} }
void SigintWatchdog::Destroy() { SigintWatchdog::~SigintWatchdog() {
if (destroyed_) {
return;
}
destroyed_ = true;
SigintWatchdogHelper::GetInstance()->Unregister(this); SigintWatchdogHelper::GetInstance()->Unregister(this);
SigintWatchdogHelper::GetInstance()->Stop(); SigintWatchdogHelper::GetInstance()->Stop();
} }
void SigintWatchdog::HandleSigint() { void SigintWatchdog::HandleSigint() {
received_signal_ = true; *received_signal_ = true;
isolate_->TerminateExecution(); isolate_->TerminateExecution();
} }

View File

@ -37,16 +37,13 @@ namespace node {
class Watchdog { class Watchdog {
public: public:
explicit Watchdog(v8::Isolate* isolate, uint64_t ms); explicit Watchdog(v8::Isolate* isolate,
uint64_t ms,
bool* timed_out = nullptr);
~Watchdog(); ~Watchdog();
void Dispose();
v8::Isolate* isolate() { return isolate_; } v8::Isolate* isolate() { return isolate_; }
bool HasTimedOut() { return timed_out_; }
private:
void Destroy();
private:
static void Run(void* arg); static void Run(void* arg);
static void Async(uv_async_t* async); static void Async(uv_async_t* async);
static void Timer(uv_timer_t* timer); static void Timer(uv_timer_t* timer);
@ -56,27 +53,20 @@ class Watchdog {
uv_loop_t* loop_; uv_loop_t* loop_;
uv_async_t async_; uv_async_t async_;
uv_timer_t timer_; uv_timer_t timer_;
bool timed_out_; bool* timed_out_;
bool destroyed_;
}; };
class SigintWatchdog { class SigintWatchdog {
public: public:
explicit SigintWatchdog(v8::Isolate* isolate); explicit SigintWatchdog(v8::Isolate* isolate,
bool* received_signal = nullptr);
~SigintWatchdog(); ~SigintWatchdog();
void Dispose();
v8::Isolate* isolate() { return isolate_; } v8::Isolate* isolate() { return isolate_; }
bool HasReceivedSignal() { return received_signal_; }
void HandleSigint(); void HandleSigint();
private: private:
void Destroy();
v8::Isolate* isolate_; v8::Isolate* isolate_;
bool received_signal_; bool* received_signal_;
bool destroyed_;
}; };
class SigintWatchdogHelper { class SigintWatchdogHelper {

View File

@ -0,0 +1,33 @@
'use strict';
require('../common');
const vm = require('vm');
// We're testing a race condition so we just have to spin this in a loop
// for a little while and see if it breaks. The condition being tested
// is an `isolate->TerminateExecution()` reaching the main JS stack from
// the timeout watchdog.
const sandbox = { timeout: 5 };
const context = vm.createContext(sandbox);
const script = new vm.Script(
'var d = Date.now() + timeout;while (d > Date.now());'
);
const immediate = setImmediate(function() {
throw new Error('Detected vm race condition!');
});
// When this condition was first discovered this test would fail in 50ms
// or so. A better, but still incorrect implementation would fail after
// 100 seconds or so. If you're messing with vm timeouts you might
// consider increasing this timeout to hammer out races.
const giveUp = Date.now() + 5000;
do {
// The loop adjusts the timeout up or down trying to hit the race
try {
script.runInContext(context, { timeout: 5 });
++sandbox.timeout;
} catch (err) {
--sandbox.timeout;
}
} while (Date.now() < giveUp);
clearImmediate(immediate);