Skip to content

Commit c6b07f2

Browse files
committed
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.
1 parent c212e3e commit c6b07f2

File tree

4 files changed

+86
-73
lines changed

4 files changed

+86
-73
lines changed

src/node_contextify.cc

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -958,35 +958,33 @@ class ContextifyScript : public BaseObject {
958958
bool timed_out = false;
959959
bool received_signal = false;
960960
if (break_on_sigint && timeout != -1) {
961-
Watchdog wd(env->isolate(), timeout);
962-
SigintWatchdog swd(env->isolate());
961+
Watchdog wd(env->isolate(), timeout, &timed_out);
962+
SigintWatchdog swd(env->isolate(), &received_signal);
963963
result = script->Run();
964-
timed_out = wd.HasTimedOut();
965-
received_signal = swd.HasReceivedSignal();
966964
} else if (break_on_sigint) {
967-
SigintWatchdog swd(env->isolate());
965+
SigintWatchdog swd(env->isolate(), &received_signal);
968966
result = script->Run();
969-
received_signal = swd.HasReceivedSignal();
970967
} else if (timeout != -1) {
971-
Watchdog wd(env->isolate(), timeout);
968+
Watchdog wd(env->isolate(), timeout, &timed_out);
972969
result = script->Run();
973-
timed_out = wd.HasTimedOut();
974970
} else {
975971
result = script->Run();
976972
}
977973

978-
if (try_catch->HasCaught()) {
979-
if (try_catch->HasTerminated())
980-
env->isolate()->CancelTerminateExecution();
981-
974+
if (timed_out || received_signal) {
982975
// It is possible that execution was terminated by another timeout in
983976
// which this timeout is nested, so check whether one of the watchdogs
984977
// from this invocation is responsible for termination.
985978
if (timed_out) {
986979
env->ThrowError("Script execution timed out.");
987980
} else if (received_signal) {
988981
env->ThrowError("Script execution interrupted.");
989-
} else if (display_errors) {
982+
}
983+
env->isolate()->CancelTerminateExecution();
984+
}
985+
986+
if (try_catch->HasCaught()) {
987+
if (!timed_out && !received_signal && display_errors) {
990988
// We should decorate non-termination exceptions
991989
DecorateErrorStack(env, *try_catch);
992990
}

src/node_watchdog.cc

Lines changed: 11 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@
2727

2828
namespace node {
2929

30-
Watchdog::Watchdog(v8::Isolate* isolate, uint64_t ms) : isolate_(isolate),
31-
timed_out_(false),
32-
destroyed_(false) {
30+
Watchdog::Watchdog(v8::Isolate* isolate, uint64_t ms, bool* timed_out)
31+
: isolate_(isolate), timed_out_(timed_out) {
32+
3333
int rc;
3434
loop_ = new uv_loop_t;
3535
CHECK(loop_);
@@ -54,20 +54,6 @@ Watchdog::Watchdog(v8::Isolate* isolate, uint64_t ms) : isolate_(isolate),
5454

5555

5656
Watchdog::~Watchdog() {
57-
Destroy();
58-
}
59-
60-
61-
void Watchdog::Dispose() {
62-
Destroy();
63-
}
64-
65-
66-
void Watchdog::Destroy() {
67-
if (destroyed_) {
68-
return;
69-
}
70-
7157
uv_async_send(&async_);
7258
uv_thread_join(&thread_);
7359

@@ -80,8 +66,6 @@ void Watchdog::Destroy() {
8066
CHECK_EQ(0, rc);
8167
delete loop_;
8268
loop_ = nullptr;
83-
84-
destroyed_ = true;
8569
}
8670

8771

@@ -93,7 +77,7 @@ void Watchdog::Run(void* arg) {
9377
uv_run(wd->loop_, UV_RUN_DEFAULT);
9478

9579
// Loop ref count reaches zero when both handles are closed.
96-
// Close the timer handle on this side and let Destroy() close async_
80+
// Close the timer handle on this side and let ~Watchdog() close async_
9781
uv_close(reinterpret_cast<uv_handle_t*>(&wd->timer_), nullptr);
9882
}
9983

@@ -106,45 +90,30 @@ void Watchdog::Async(uv_async_t* async) {
10690

10791
void Watchdog::Timer(uv_timer_t* timer) {
10892
Watchdog* w = ContainerOf(&Watchdog::timer_, timer);
109-
w->timed_out_ = true;
110-
uv_stop(w->loop_);
93+
*w->timed_out_ = true;
11194
w->isolate()->TerminateExecution();
95+
uv_stop(w->loop_);
11296
}
11397

11498

115-
SigintWatchdog::~SigintWatchdog() {
116-
Destroy();
117-
}
118-
119-
120-
void SigintWatchdog::Dispose() {
121-
Destroy();
122-
}
123-
124-
125-
SigintWatchdog::SigintWatchdog(v8::Isolate* isolate)
126-
: isolate_(isolate), received_signal_(false), destroyed_(false) {
99+
SigintWatchdog::SigintWatchdog(
100+
v8::Isolate* isolate, bool* received_signal)
101+
: isolate_(isolate), received_signal_(received_signal) {
127102
// Register this watchdog with the global SIGINT/Ctrl+C listener.
128103
SigintWatchdogHelper::GetInstance()->Register(this);
129104
// Start the helper thread, if that has not already happened.
130105
SigintWatchdogHelper::GetInstance()->Start();
131106
}
132107

133108

134-
void SigintWatchdog::Destroy() {
135-
if (destroyed_) {
136-
return;
137-
}
138-
139-
destroyed_ = true;
140-
109+
SigintWatchdog::~SigintWatchdog() {
141110
SigintWatchdogHelper::GetInstance()->Unregister(this);
142111
SigintWatchdogHelper::GetInstance()->Stop();
143112
}
144113

145114

146115
void SigintWatchdog::HandleSigint() {
147-
received_signal_ = true;
116+
*received_signal_ = true;
148117
isolate_->TerminateExecution();
149118
}
150119

src/node_watchdog.h

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -37,16 +37,14 @@ namespace node {
3737

3838
class Watchdog {
3939
public:
40-
explicit Watchdog(v8::Isolate* isolate, uint64_t ms);
40+
explicit Watchdog(
41+
v8::Isolate* isolate,
42+
uint64_t ms,
43+
bool* timed_out = NULL);
4144
~Watchdog();
42-
43-
void Dispose();
44-
4545
v8::Isolate* isolate() { return isolate_; }
46-
bool HasTimedOut() { return timed_out_; }
47-
private:
48-
void Destroy();
4946

47+
private:
5048
static void Run(void* arg);
5149
static void Async(uv_async_t* async);
5250
static void Timer(uv_timer_t* timer);
@@ -56,27 +54,21 @@ class Watchdog {
5654
uv_loop_t* loop_;
5755
uv_async_t async_;
5856
uv_timer_t timer_;
59-
bool timed_out_;
60-
bool destroyed_;
57+
bool* timed_out_;
6158
};
6259

6360
class SigintWatchdog {
6461
public:
65-
explicit SigintWatchdog(v8::Isolate* isolate);
62+
explicit SigintWatchdog(
63+
v8::Isolate* isolate,
64+
bool* received_signal = NULL);
6665
~SigintWatchdog();
67-
68-
void Dispose();
69-
7066
v8::Isolate* isolate() { return isolate_; }
71-
bool HasReceivedSignal() { return received_signal_; }
7267
void HandleSigint();
7368

7469
private:
75-
void Destroy();
76-
7770
v8::Isolate* isolate_;
78-
bool received_signal_;
79-
bool destroyed_;
71+
bool* received_signal_;
8072
};
8173

8274
class SigintWatchdogHelper {

test/pummel/test-vm-race.js

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Copyright Joyent, Inc. and other Node contributors.
2+
//
3+
// Permission is hereby granted, free of charge, to any person obtaining a
4+
// copy of this software and associated documentation files (the
5+
// "Software"), to deal in the Software without restriction, including
6+
// without limitation the rights to use, copy, modify, merge, publish,
7+
// distribute, sublicense, and/or sell copies of the Software, and to permit
8+
// persons to whom the Software is furnished to do so, subject to the
9+
// following conditions:
10+
//
11+
// The above copyright notice and this permission notice shall be included
12+
// in all copies or substantial portions of the Software.
13+
//
14+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
15+
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16+
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
17+
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
18+
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
19+
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
20+
// USE OR OTHER DEALINGS IN THE SOFTWARE.
21+
22+
'use strict';
23+
require('../common');
24+
const vm = require('vm');
25+
26+
// We're testing a race condition so we just have to spin this in a loop
27+
// for a little while and see if it breaks. The condition being tested
28+
// is an `isolate->TerminateExecution()` reaching the main JS stack from
29+
// the timeout watchdog.
30+
const sandbox = { timeout: 5 };
31+
const context = vm.createContext(sandbox);
32+
const script = new vm.Script(
33+
'var d = Date.now() + timeout;while (d > Date.now());'
34+
);
35+
const immediate = setImmediate(function() {
36+
throw new Error('Detected vm race condition!');
37+
});
38+
39+
// When this condition was first discovered this test would fail in 50ms
40+
// or so. A better, but still incorrect implementation would fail after
41+
// 100 seconds or so. If you're messing with vm timeouts you might
42+
// consider increasing this timeout to hammer out races.
43+
const giveUp = Date.now() + 5000;
44+
do {
45+
// The loop adjusts the timeout up or down trying to hit the race
46+
try {
47+
script.runInContext(context, { timeout: 5 });
48+
++sandbox.timeout;
49+
} catch (err) {
50+
--sandbox.timeout;
51+
}
52+
} while (Date.now() < giveUp);
53+
54+
clearImmediate(immediate);

0 commit comments

Comments
 (0)