Skip to content

Commit 3675059

Browse files
Gabriel Schulhofrvagg
Gabriel Schulhof
authored andcommittedFeb 28, 2019
n-api: guard against cond null dereference
A condition variable is only created by the thread-safe function if the queue size is set to something larger than zero. This adds null-checks around the condition variable and tests for the case where the queue size is zero. Fixes: nodejs/help#1387 PR-URL: #21871 Backport-PR-URL: #25002 Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com> Reviewed-By: Colin Ihrig <cjihrig@gmail.com> Reviewed-By: Michael Dawson <michael_dawson@ca.ibm.com>
1 parent c5a11dc commit 3675059

File tree

3 files changed

+104
-29
lines changed

3 files changed

+104
-29
lines changed
 

‎src/node_api.cc

+7-3
Original file line numberDiff line numberDiff line change
@@ -3683,7 +3683,7 @@ class TsFn: public node::AsyncResource {
36833683
if (thread_count == 0 || mode == napi_tsfn_abort) {
36843684
if (!is_closing) {
36853685
is_closing = (mode == napi_tsfn_abort);
3686-
if (is_closing) {
3686+
if (is_closing && max_queue_size > 0) {
36873687
cond->Signal(lock);
36883688
}
36893689
if (uv_async_send(&async) != 0) {
@@ -3772,7 +3772,9 @@ class TsFn: public node::AsyncResource {
37723772
if (size == 0) {
37733773
if (thread_count == 0) {
37743774
is_closing = true;
3775-
cond->Signal(lock);
3775+
if (max_queue_size > 0) {
3776+
cond->Signal(lock);
3777+
}
37763778
CloseHandlesAndMaybeDelete();
37773779
} else {
37783780
if (uv_idle_stop(&idle) != 0) {
@@ -3839,7 +3841,9 @@ class TsFn: public node::AsyncResource {
38393841
if (set_closing) {
38403842
node::Mutex::ScopedLock lock(this->mutex);
38413843
is_closing = true;
3842-
cond->Signal(lock);
3844+
if (max_queue_size > 0) {
3845+
cond->Signal(lock);
3846+
}
38433847
}
38443848
if (handles_closing) {
38453849
return;

‎test/addons-napi/test_threadsafe_function/binding.c

+34-5
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#include "../common.h"
1010

1111
#define ARRAY_LENGTH 10
12+
#define MAX_QUEUE_SIZE 2
1213

1314
static uv_thread_t uv_threads[2];
1415
static napi_threadsafe_function ts_fn;
@@ -18,6 +19,7 @@ typedef struct {
1819
napi_threadsafe_function_release_mode abort;
1920
bool start_secondary;
2021
napi_ref js_finalize_cb;
22+
uint32_t max_queue_size;
2123
} ts_fn_hint;
2224

2325
static ts_fn_hint ts_info;
@@ -71,6 +73,12 @@ static void data_source_thread(void* data) {
7173
for (index = ARRAY_LENGTH - 1; index > -1 && !queue_was_closing; index--) {
7274
status = napi_call_threadsafe_function(ts_fn, &ints[index],
7375
ts_fn_info->block_on_full);
76+
if (ts_fn_info->max_queue_size == 0) {
77+
// Let's make this thread really busy for 200 ms to give the main thread a
78+
// chance to abort.
79+
uint64_t start = uv_hrtime();
80+
for (; uv_hrtime() - start < 200000000;);
81+
}
7482
switch (status) {
7583
case napi_queue_full:
7684
queue_was_full = true;
@@ -167,8 +175,8 @@ static napi_value StartThreadInternal(napi_env env,
167175
napi_callback_info info,
168176
napi_threadsafe_function_call_js cb,
169177
bool block_on_full) {
170-
size_t argc = 3;
171-
napi_value argv[3];
178+
size_t argc = 4;
179+
napi_value argv[4];
172180

173181
ts_info.block_on_full =
174182
(block_on_full ? napi_tsfn_blocking : napi_tsfn_nonblocking);
@@ -178,8 +186,18 @@ static napi_value StartThreadInternal(napi_env env,
178186
napi_value async_name;
179187
NAPI_CALL(env, napi_create_string_utf8(env, "N-API Thread-safe Function Test",
180188
NAPI_AUTO_LENGTH, &async_name));
181-
NAPI_CALL(env, napi_create_threadsafe_function(env, argv[0], NULL, async_name,
182-
2, 2, uv_threads, join_the_threads, &ts_info, cb, &ts_fn));
189+
NAPI_CALL(env, napi_get_value_uint32(env, argv[3], &ts_info.max_queue_size));
190+
NAPI_CALL(env, napi_create_threadsafe_function(env,
191+
argv[0],
192+
NULL,
193+
async_name,
194+
ts_info.max_queue_size,
195+
2,
196+
uv_threads,
197+
join_the_threads,
198+
&ts_info,
199+
cb,
200+
&ts_fn));
183201
bool abort;
184202
NAPI_CALL(env, napi_get_value_bool(env, argv[1], &abort));
185203
ts_info.abort = abort ? napi_tsfn_abort : napi_tsfn_release;
@@ -224,8 +242,9 @@ static napi_value Init(napi_env env, napi_value exports) {
224242
for (index = 0; index < ARRAY_LENGTH; index++) {
225243
ints[index] = index;
226244
}
227-
napi_value js_array_length;
245+
napi_value js_array_length, js_max_queue_size;
228246
napi_create_uint32(env, ARRAY_LENGTH, &js_array_length);
247+
napi_create_uint32(env, MAX_QUEUE_SIZE, &js_max_queue_size);
229248

230249
napi_property_descriptor properties[] = {
231250
{
@@ -238,6 +257,16 @@ static napi_value Init(napi_env env, napi_value exports) {
238257
napi_enumerable,
239258
NULL
240259
},
260+
{
261+
"MAX_QUEUE_SIZE",
262+
NULL,
263+
NULL,
264+
NULL,
265+
NULL,
266+
js_max_queue_size,
267+
napi_enumerable,
268+
NULL
269+
},
241270
DECLARE_NAPI_PROPERTY("StartThread", StartThread),
242271
DECLARE_NAPI_PROPERTY("StartThreadNoNative", StartThreadNoNative),
243272
DECLARE_NAPI_PROPERTY("StartThreadNonblocking", StartThreadNonblocking),

‎test/addons-napi/test_threadsafe_function/test.js

+63-21
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ if (process.argv[2] === 'child') {
2525
if (callCount === 2) {
2626
binding.Unref();
2727
}
28-
}, false /* abort */, true /* launchSecondary */);
28+
}, false /* abort */, true /* launchSecondary */, +process.argv[3]);
2929

3030
// Release the thread-safe function from the main thread so that it may be
3131
// torn down via the environment cleanup handler.
@@ -37,6 +37,7 @@ function testWithJSMarshaller({
3737
threadStarter,
3838
quitAfter,
3939
abort,
40+
maxQueueSize,
4041
launchSecondary }) {
4142
return new Promise((resolve) => {
4243
const array = [];
@@ -49,7 +50,7 @@ function testWithJSMarshaller({
4950
}), !!abort);
5051
});
5152
}
52-
}, !!abort, !!launchSecondary);
53+
}, !!abort, !!launchSecondary, maxQueueSize);
5354
if (threadStarter === 'StartThreadNonblocking') {
5455
// Let's make this thread really busy for a short while to ensure that
5556
// the queue fills and the thread receives a napi_queue_full.
@@ -59,6 +60,24 @@ function testWithJSMarshaller({
5960
});
6061
}
6162

63+
function testUnref(queueSize) {
64+
return new Promise((resolve, reject) => {
65+
let output = '';
66+
const child = fork(__filename, ['child', queueSize], {
67+
stdio: [process.stdin, 'pipe', process.stderr, 'ipc']
68+
});
69+
child.on('close', (code) => {
70+
if (code === 0) {
71+
resolve(output.match(/\S+/g));
72+
} else {
73+
reject(new Error('Child process died with code ' + code));
74+
}
75+
});
76+
child.stdout.on('data', (data) => (output += data.toString()));
77+
})
78+
.then((result) => assert.strictEqual(result.indexOf(0), -1));
79+
}
80+
6281
new Promise(function testWithoutJSMarshaller(resolve) {
6382
let callCount = 0;
6483
binding.StartThreadNoNative(function testCallback() {
@@ -73,13 +92,23 @@ new Promise(function testWithoutJSMarshaller(resolve) {
7392
}), false);
7493
});
7594
}
76-
}, false /* abort */, false /* launchSecondary */);
95+
}, false /* abort */, false /* launchSecondary */, binding.MAX_QUEUE_SIZE);
7796
})
7897

7998
// Start the thread in blocking mode, and assert that all values are passed.
8099
// Quit after it's done.
81100
.then(() => testWithJSMarshaller({
82101
threadStarter: 'StartThread',
102+
maxQueueSize: binding.MAX_QUEUE_SIZE,
103+
quitAfter: binding.ARRAY_LENGTH
104+
}))
105+
.then((result) => assert.deepStrictEqual(result, expectedArray))
106+
107+
// Start the thread in blocking mode with an infinite queue, and assert that all
108+
// values are passed. Quit after it's done.
109+
.then(() => testWithJSMarshaller({
110+
threadStarter: 'StartThread',
111+
maxQueueSize: 0,
83112
quitAfter: binding.ARRAY_LENGTH
84113
}))
85114
.then((result) => assert.deepStrictEqual(result, expectedArray))
@@ -88,6 +117,7 @@ new Promise(function testWithoutJSMarshaller(resolve) {
88117
// Quit after it's done.
89118
.then(() => testWithJSMarshaller({
90119
threadStarter: 'StartThreadNonblocking',
120+
maxQueueSize: binding.MAX_QUEUE_SIZE,
91121
quitAfter: binding.ARRAY_LENGTH
92122
}))
93123
.then((result) => assert.deepStrictEqual(result, expectedArray))
@@ -96,6 +126,16 @@ new Promise(function testWithoutJSMarshaller(resolve) {
96126
// Quit early, but let the thread finish.
97127
.then(() => testWithJSMarshaller({
98128
threadStarter: 'StartThread',
129+
maxQueueSize: binding.MAX_QUEUE_SIZE,
130+
quitAfter: 1
131+
}))
132+
.then((result) => assert.deepStrictEqual(result, expectedArray))
133+
134+
// Start the thread in blocking mode with an infinite queue, and assert that all
135+
// values are passed. Quit early, but let the thread finish.
136+
.then(() => testWithJSMarshaller({
137+
threadStarter: 'StartThread',
138+
maxQueueSize: 0,
99139
quitAfter: 1
100140
}))
101141
.then((result) => assert.deepStrictEqual(result, expectedArray))
@@ -104,6 +144,7 @@ new Promise(function testWithoutJSMarshaller(resolve) {
104144
// Quit early, but let the thread finish.
105145
.then(() => testWithJSMarshaller({
106146
threadStarter: 'StartThreadNonblocking',
147+
maxQueueSize: binding.MAX_QUEUE_SIZE,
107148
quitAfter: 1
108149
}))
109150
.then((result) => assert.deepStrictEqual(result, expectedArray))
@@ -114,6 +155,7 @@ new Promise(function testWithoutJSMarshaller(resolve) {
114155
.then(() => testWithJSMarshaller({
115156
threadStarter: 'StartThread',
116157
quitAfter: 1,
158+
maxQueueSize: binding.MAX_QUEUE_SIZE,
117159
launchSecondary: true
118160
}))
119161
.then((result) => assert.deepStrictEqual(result, expectedArray))
@@ -124,15 +166,27 @@ new Promise(function testWithoutJSMarshaller(resolve) {
124166
.then(() => testWithJSMarshaller({
125167
threadStarter: 'StartThreadNonblocking',
126168
quitAfter: 1,
169+
maxQueueSize: binding.MAX_QUEUE_SIZE,
127170
launchSecondary: true
128171
}))
129172
.then((result) => assert.deepStrictEqual(result, expectedArray))
130173

131174
// Start the thread in blocking mode, and assert that it could not finish.
132-
// Quit early and aborting.
175+
// Quit early by aborting.
176+
.then(() => testWithJSMarshaller({
177+
threadStarter: 'StartThread',
178+
quitAfter: 1,
179+
maxQueueSize: binding.MAX_QUEUE_SIZE,
180+
abort: true
181+
}))
182+
.then((result) => assert.strictEqual(result.indexOf(0), -1))
183+
184+
// Start the thread in blocking mode with an infinite queue, and assert that it
185+
// could not finish. Quit early by aborting.
133186
.then(() => testWithJSMarshaller({
134187
threadStarter: 'StartThread',
135188
quitAfter: 1,
189+
maxQueueSize: 0,
136190
abort: true
137191
}))
138192
.then((result) => assert.strictEqual(result.indexOf(0), -1))
@@ -142,25 +196,13 @@ new Promise(function testWithoutJSMarshaller(resolve) {
142196
.then(() => testWithJSMarshaller({
143197
threadStarter: 'StartThreadNonblocking',
144198
quitAfter: 1,
199+
maxQueueSize: binding.MAX_QUEUE_SIZE,
145200
abort: true
146201
}))
147202
.then((result) => assert.strictEqual(result.indexOf(0), -1))
148203

149204
// Start a child process to test rapid teardown
150-
.then(() => {
151-
return new Promise((resolve, reject) => {
152-
let output = '';
153-
const child = fork(__filename, ['child'], {
154-
stdio: [process.stdin, 'pipe', process.stderr, 'ipc']
155-
});
156-
child.on('close', (code) => {
157-
if (code === 0) {
158-
resolve(output.match(/\S+/g));
159-
} else {
160-
reject(new Error('Child process died with code ' + code));
161-
}
162-
});
163-
child.stdout.on('data', (data) => (output += data.toString()));
164-
});
165-
})
166-
.then((result) => assert.strictEqual(result.indexOf(0), -1));
205+
.then(() => testUnref(binding.MAX_QUEUE_SIZE))
206+
207+
// Start a child process with an infinite queue to test rapid teardown
208+
.then(() => testUnref(0));

0 commit comments

Comments
 (0)
Please sign in to comment.