Skip to content

Commit 49e3fcd

Browse files
apaprockibnoordhuis
authored andcommitted
vm: fix race condition in watchdog cleanup
Previous code was calling uv_loop_delete() directly on a running loop, which led to race condition aborts/segfaults within libuv. This change changes the watchdog thread to call uv_run() with UV_RUN_ONCE so that the call exits after either the timer times out or uv_async_send() is called from the main thread in Watchdog::Destroy(). The timer/async handles are then closed and uv_run() with UV_RUN_DEFAULT is called so that libuv has a chance to cleanup before the thread exits. The main thread meanwhile calls uv_thread_join() and then uv_loop_delete() to complete the cleanup.
1 parent 7ce5a31 commit 49e3fcd

3 files changed

Lines changed: 48 additions & 18 deletions

File tree

src/node_watchdog.cc

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,31 +20,31 @@
2020
// USE OR OTHER DEALINGS IN THE SOFTWARE.
2121

2222
#include "node_watchdog.h"
23+
#include <assert.h>
2324

2425
namespace node {
2526

2627
using v8::V8;
2728

2829

2930
Watchdog::Watchdog(uint64_t ms)
30-
: timer_started_(false)
31-
, thread_created_(false)
31+
: thread_created_(false)
3232
, destroyed_(false) {
3333

3434
loop_ = uv_loop_new();
3535
if (!loop_)
3636
return;
3737

38-
int rc = uv_timer_init(loop_, &timer_);
39-
if (rc) {
40-
return;
41-
}
38+
int rc = uv_async_init(loop_, &async_, &Watchdog::Async);
39+
assert(rc == 0);
40+
41+
rc = uv_timer_init(loop_, &timer_);
42+
assert(rc == 0);
4243

4344
rc = uv_timer_start(&timer_, &Watchdog::Timer, ms, 0);
4445
if (rc) {
4546
return;
4647
}
47-
timer_started_ = true;
4848

4949
rc = uv_thread_create(&thread_, &Watchdog::Run, this);
5050
if (rc) {
@@ -69,28 +69,38 @@ void Watchdog::Destroy() {
6969
return;
7070
}
7171

72-
if (timer_started_) {
73-
uv_timer_stop(&timer_);
72+
if (thread_created_) {
73+
uv_async_send(&async_);
74+
uv_thread_join(&thread_);
7475
}
7576

7677
if (loop_) {
7778
uv_loop_delete(loop_);
7879
}
7980

80-
if (thread_created_) {
81-
uv_thread_join(&thread_);
82-
}
83-
8481
destroyed_ = true;
8582
}
8683

8784

8885
void Watchdog::Run(void* arg) {
8986
Watchdog* wd = static_cast<Watchdog*>(arg);
87+
88+
// UV_RUN_ONCE so async_ or timer_ wakeup exits uv_run() call.
89+
uv_run(wd->loop_, UV_RUN_ONCE);
90+
91+
// Loop ref count reaches zero when both handles are closed.
92+
uv_close(reinterpret_cast<uv_handle_t*>(&wd->async_), NULL);
93+
uv_close(reinterpret_cast<uv_handle_t*>(&wd->timer_), NULL);
94+
95+
// UV_RUN_DEFAULT so that libuv has a chance to clean up.
9096
uv_run(wd->loop_, UV_RUN_DEFAULT);
9197
}
9298

9399

100+
void Watchdog::Async(uv_async_t* async, int status) {
101+
}
102+
103+
94104
void Watchdog::Timer(uv_timer_t* timer, int status) {
95105
V8::TerminateExecution();
96106
}

src/node_watchdog.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,13 @@ class Watchdog {
3838
void Destroy();
3939

4040
static void Run(void* arg);
41+
static void Async(uv_async_t* async, int status);
4142
static void Timer(uv_timer_t* timer, int status);
4243

4344
uv_thread_t thread_;
4445
uv_loop_t* loop_;
46+
uv_async_t async_;
4547
uv_timer_t timer_;
46-
bool timer_started_;
4748
bool thread_created_;
4849
bool destroyed_;
4950
};

test/simple/test-vm-run-timeout.js

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,34 @@ var common = require('../common');
2323
var assert = require('assert');
2424
var vm = require('vm');
2525

26+
// Test 1: Timeout of 100ms executing endless loop
2627
assert.throws(function() {
2728
vm.runInThisContext('while(true) {}', '', 100);
2829
});
2930

31+
// Test 2: Timeout must be >= 0ms
3032
assert.throws(function() {
3133
vm.runInThisContext('', '', -1);
3234
});
3335

34-
assert.doesNotThrow(function() {
35-
vm.runInThisContext('', '', 0);
36-
vm.runInThisContext('', '', 100);
37-
});
36+
// Test 3: Timeout of 0ms
37+
vm.runInThisContext('', '', 0);
38+
39+
// Test 4: Timeout of 1000ms, script finishes first
40+
vm.runInThisContext('', '', 1000);
41+
42+
// Test 5: Nested vm timeouts, inner timeout propagates out
43+
try {
44+
var context = {
45+
log: console.log,
46+
runInVM: function(timeout) {
47+
vm.runInNewContext('while(true) {}', context, '', timeout);
48+
}
49+
};
50+
vm.runInNewContext('runInVM(10)', context, '', 100);
51+
throw new Error('Test 5 failed');
52+
} catch (e) {
53+
if (-1 === e.message.search(/Script execution timed out./)) {
54+
throw e;
55+
}
56+
}

0 commit comments

Comments
 (0)