Skip to content

Commit 8b3aacc

Browse files
laverdetMylesBorins
authored andcommitted
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. Backport-PR-URL: #14373 PR-URL: #13074 Reviewed-By: Anna Henningsen <[email protected]>
1 parent 6e60c83 commit 8b3aacc

File tree

4 files changed

+63
-73
lines changed

4 files changed

+63
-73
lines changed

src/node_contextify.cc

+11-13
Original file line numberDiff line numberDiff line change
@@ -906,35 +906,33 @@ class ContextifyScript : public BaseObject {
906906
bool timed_out = false;
907907
bool received_signal = false;
908908
if (break_on_sigint && timeout != -1) {
909-
Watchdog wd(env->isolate(), timeout);
910-
SigintWatchdog swd(env->isolate());
909+
Watchdog wd(env->isolate(), timeout, &timed_out);
910+
SigintWatchdog swd(env->isolate(), &received_signal);
911911
result = script->Run();
912-
timed_out = wd.HasTimedOut();
913-
received_signal = swd.HasReceivedSignal();
914912
} else if (break_on_sigint) {
915-
SigintWatchdog swd(env->isolate());
913+
SigintWatchdog swd(env->isolate(), &received_signal);
916914
result = script->Run();
917-
received_signal = swd.HasReceivedSignal();
918915
} else if (timeout != -1) {
919-
Watchdog wd(env->isolate(), timeout);
916+
Watchdog wd(env->isolate(), timeout, &timed_out);
920917
result = script->Run();
921-
timed_out = wd.HasTimedOut();
922918
} else {
923919
result = script->Run();
924920
}
925921

926-
if (try_catch->HasCaught()) {
927-
if (try_catch->HasTerminated())
928-
env->isolate()->CancelTerminateExecution();
929-
922+
if (timed_out || received_signal) {
930923
// It is possible that execution was terminated by another timeout in
931924
// which this timeout is nested, so check whether one of the watchdogs
932925
// from this invocation is responsible for termination.
933926
if (timed_out) {
934927
env->ThrowError("Script execution timed out.");
935928
} else if (received_signal) {
936929
env->ThrowError("Script execution interrupted.");
937-
} else if (display_errors) {
930+
}
931+
env->isolate()->CancelTerminateExecution();
932+
}
933+
934+
if (try_catch->HasCaught()) {
935+
if (!timed_out && !received_signal && display_errors) {
938936
// We should decorate non-termination exceptions
939937
DecorateErrorStack(env, *try_catch);
940938
}

src/node_watchdog.cc

+11-42
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66

77
namespace node {
88

9-
Watchdog::Watchdog(v8::Isolate* isolate, uint64_t ms) : isolate_(isolate),
10-
timed_out_(false),
11-
destroyed_(false) {
9+
Watchdog::Watchdog(v8::Isolate* isolate, uint64_t ms, bool* timed_out)
10+
: isolate_(isolate), timed_out_(timed_out) {
11+
1212
int rc;
1313
loop_ = new uv_loop_t;
1414
CHECK(loop_);
@@ -33,20 +33,6 @@ Watchdog::Watchdog(v8::Isolate* isolate, uint64_t ms) : isolate_(isolate),
3333

3434

3535
Watchdog::~Watchdog() {
36-
Destroy();
37-
}
38-
39-
40-
void Watchdog::Dispose() {
41-
Destroy();
42-
}
43-
44-
45-
void Watchdog::Destroy() {
46-
if (destroyed_) {
47-
return;
48-
}
49-
5036
uv_async_send(&async_);
5137
uv_thread_join(&thread_);
5238

@@ -59,8 +45,6 @@ void Watchdog::Destroy() {
5945
CHECK_EQ(0, rc);
6046
delete loop_;
6147
loop_ = nullptr;
62-
63-
destroyed_ = true;
6448
}
6549

6650

@@ -72,7 +56,7 @@ void Watchdog::Run(void* arg) {
7256
uv_run(wd->loop_, UV_RUN_DEFAULT);
7357

7458
// Loop ref count reaches zero when both handles are closed.
75-
// Close the timer handle on this side and let Destroy() close async_
59+
// Close the timer handle on this side and let ~Watchdog() close async_
7660
uv_close(reinterpret_cast<uv_handle_t*>(&wd->timer_), nullptr);
7761
}
7862

@@ -85,45 +69,30 @@ void Watchdog::Async(uv_async_t* async) {
8569

8670
void Watchdog::Timer(uv_timer_t* timer) {
8771
Watchdog* w = ContainerOf(&Watchdog::timer_, timer);
88-
w->timed_out_ = true;
89-
uv_stop(w->loop_);
72+
*w->timed_out_ = true;
9073
w->isolate()->TerminateExecution();
74+
uv_stop(w->loop_);
9175
}
9276

9377

94-
SigintWatchdog::~SigintWatchdog() {
95-
Destroy();
96-
}
97-
98-
99-
void SigintWatchdog::Dispose() {
100-
Destroy();
101-
}
102-
103-
104-
SigintWatchdog::SigintWatchdog(v8::Isolate* isolate)
105-
: isolate_(isolate), received_signal_(false), destroyed_(false) {
78+
SigintWatchdog::SigintWatchdog(
79+
v8::Isolate* isolate, bool* received_signal)
80+
: isolate_(isolate), received_signal_(received_signal) {
10681
// Register this watchdog with the global SIGINT/Ctrl+C listener.
10782
SigintWatchdogHelper::GetInstance()->Register(this);
10883
// Start the helper thread, if that has not already happened.
10984
SigintWatchdogHelper::GetInstance()->Start();
11085
}
11186

11287

113-
void SigintWatchdog::Destroy() {
114-
if (destroyed_) {
115-
return;
116-
}
117-
118-
destroyed_ = true;
119-
88+
SigintWatchdog::~SigintWatchdog() {
12089
SigintWatchdogHelper::GetInstance()->Unregister(this);
12190
SigintWatchdogHelper::GetInstance()->Stop();
12291
}
12392

12493

12594
void SigintWatchdog::HandleSigint() {
126-
received_signal_ = true;
95+
*received_signal_ = true;
12796
isolate_->TerminateExecution();
12897
}
12998

src/node_watchdog.h

+8-18
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,13 @@ namespace node {
1616

1717
class Watchdog {
1818
public:
19-
explicit Watchdog(v8::Isolate* isolate, uint64_t ms);
19+
explicit Watchdog(v8::Isolate* isolate,
20+
uint64_t ms,
21+
bool* timed_out = nullptr);
2022
~Watchdog();
21-
22-
void Dispose();
23-
2423
v8::Isolate* isolate() { return isolate_; }
25-
bool HasTimedOut() { return timed_out_; }
26-
private:
27-
void Destroy();
2824

25+
private:
2926
static void Run(void* arg);
3027
static void Async(uv_async_t* async);
3128
static void Timer(uv_timer_t* timer);
@@ -35,27 +32,20 @@ class Watchdog {
3532
uv_loop_t* loop_;
3633
uv_async_t async_;
3734
uv_timer_t timer_;
38-
bool timed_out_;
39-
bool destroyed_;
35+
bool* timed_out_;
4036
};
4137

4238
class SigintWatchdog {
4339
public:
44-
explicit SigintWatchdog(v8::Isolate* isolate);
40+
explicit SigintWatchdog(v8::Isolate* isolate,
41+
bool* received_signal = nullptr);
4542
~SigintWatchdog();
46-
47-
void Dispose();
48-
4943
v8::Isolate* isolate() { return isolate_; }
50-
bool HasReceivedSignal() { return received_signal_; }
5144
void HandleSigint();
5245

5346
private:
54-
void Destroy();
55-
5647
v8::Isolate* isolate_;
57-
bool received_signal_;
58-
bool destroyed_;
48+
bool* received_signal_;
5949
};
6050

6151
class SigintWatchdogHelper {

test/pummel/test-vm-race.js

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
'use strict';
2+
require('../common');
3+
const vm = require('vm');
4+
5+
// We're testing a race condition so we just have to spin this in a loop
6+
// for a little while and see if it breaks. The condition being tested
7+
// is an `isolate->TerminateExecution()` reaching the main JS stack from
8+
// the timeout watchdog.
9+
const sandbox = { timeout: 5 };
10+
const context = vm.createContext(sandbox);
11+
const script = new vm.Script(
12+
'var d = Date.now() + timeout;while (d > Date.now());'
13+
);
14+
const immediate = setImmediate(function() {
15+
throw new Error('Detected vm race condition!');
16+
});
17+
18+
// When this condition was first discovered this test would fail in 50ms
19+
// or so. A better, but still incorrect implementation would fail after
20+
// 100 seconds or so. If you're messing with vm timeouts you might
21+
// consider increasing this timeout to hammer out races.
22+
const giveUp = Date.now() + 5000;
23+
do {
24+
// The loop adjusts the timeout up or down trying to hit the race
25+
try {
26+
script.runInContext(context, { timeout: 5 });
27+
++sandbox.timeout;
28+
} catch (err) {
29+
--sandbox.timeout;
30+
}
31+
} while (Date.now() < giveUp);
32+
33+
clearImmediate(immediate);

0 commit comments

Comments
 (0)