Skip to content

Commit 45816c5

Browse files
Gabriel Schulhoftargos
Gabriel Schulhof
authored andcommitted
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 Reviewed-By: Benjamin Gruenbaum <[email protected]> Reviewed-By: Colin Ihrig <[email protected]> Reviewed-By: Michael Dawson <[email protected]>
1 parent 5e1ceaa commit 45816c5

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
@@ -3782,7 +3782,7 @@ class TsFn: public node::AsyncResource {
37823782
if (thread_count == 0 || mode == napi_tsfn_abort) {
37833783
if (!is_closing) {
37843784
is_closing = (mode == napi_tsfn_abort);
3785-
if (is_closing) {
3785+
if (is_closing && max_queue_size > 0) {
37863786
cond->Signal(lock);
37873787
}
37883788
if (uv_async_send(&async) != 0) {
@@ -3872,7 +3872,9 @@ class TsFn: public node::AsyncResource {
38723872
if (size == 0) {
38733873
if (thread_count == 0) {
38743874
is_closing = true;
3875-
cond->Signal(lock);
3875+
if (max_queue_size > 0) {
3876+
cond->Signal(lock);
3877+
}
38763878
CloseHandlesAndMaybeDelete();
38773879
} else {
38783880
if (uv_idle_stop(&idle) != 0) {
@@ -3939,7 +3941,9 @@ class TsFn: public node::AsyncResource {
39393941
if (set_closing) {
39403942
node::Mutex::ScopedLock lock(this->mutex);
39413943
is_closing = true;
3942-
cond->Signal(lock);
3944+
if (max_queue_size > 0) {
3945+
cond->Signal(lock);
3946+
}
39433947
}
39443948
if (handles_closing) {
39453949
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
@@ -23,7 +23,7 @@ if (process.argv[2] === 'child') {
2323
if (callCount === 2) {
2424
binding.Unref();
2525
}
26-
}, false /* abort */, true /* launchSecondary */);
26+
}, false /* abort */, true /* launchSecondary */, +process.argv[3]);
2727

2828
// Release the thread-safe function from the main thread so that it may be
2929
// torn down via the environment cleanup handler.
@@ -35,6 +35,7 @@ function testWithJSMarshaller({
3535
threadStarter,
3636
quitAfter,
3737
abort,
38+
maxQueueSize,
3839
launchSecondary }) {
3940
return new Promise((resolve) => {
4041
const array = [];
@@ -47,7 +48,7 @@ function testWithJSMarshaller({
4748
}), !!abort);
4849
});
4950
}
50-
}, !!abort, !!launchSecondary);
51+
}, !!abort, !!launchSecondary, maxQueueSize);
5152
if (threadStarter === 'StartThreadNonblocking') {
5253
// Let's make this thread really busy for a short while to ensure that
5354
// the queue fills and the thread receives a napi_queue_full.
@@ -57,6 +58,24 @@ function testWithJSMarshaller({
5758
});
5859
}
5960

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

7796
// Start the thread in blocking mode, and assert that all values are passed.
7897
// Quit after it's done.
7998
.then(() => testWithJSMarshaller({
8099
threadStarter: 'StartThread',
100+
maxQueueSize: binding.MAX_QUEUE_SIZE,
101+
quitAfter: binding.ARRAY_LENGTH
102+
}))
103+
.then((result) => assert.deepStrictEqual(result, expectedArray))
104+
105+
// Start the thread in blocking mode with an infinite queue, and assert that all
106+
// values are passed. Quit after it's done.
107+
.then(() => testWithJSMarshaller({
108+
threadStarter: 'StartThread',
109+
maxQueueSize: 0,
81110
quitAfter: binding.ARRAY_LENGTH
82111
}))
83112
.then((result) => assert.deepStrictEqual(result, expectedArray))
@@ -86,6 +115,7 @@ new Promise(function testWithoutJSMarshaller(resolve) {
86115
// Quit after it's done.
87116
.then(() => testWithJSMarshaller({
88117
threadStarter: 'StartThreadNonblocking',
118+
maxQueueSize: binding.MAX_QUEUE_SIZE,
89119
quitAfter: binding.ARRAY_LENGTH
90120
}))
91121
.then((result) => assert.deepStrictEqual(result, expectedArray))
@@ -94,6 +124,16 @@ new Promise(function testWithoutJSMarshaller(resolve) {
94124
// Quit early, but let the thread finish.
95125
.then(() => testWithJSMarshaller({
96126
threadStarter: 'StartThread',
127+
maxQueueSize: binding.MAX_QUEUE_SIZE,
128+
quitAfter: 1
129+
}))
130+
.then((result) => assert.deepStrictEqual(result, expectedArray))
131+
132+
// Start the thread in blocking mode with an infinite queue, and assert that all
133+
// values are passed. Quit early, but let the thread finish.
134+
.then(() => testWithJSMarshaller({
135+
threadStarter: 'StartThread',
136+
maxQueueSize: 0,
97137
quitAfter: 1
98138
}))
99139
.then((result) => assert.deepStrictEqual(result, expectedArray))
@@ -102,6 +142,7 @@ new Promise(function testWithoutJSMarshaller(resolve) {
102142
// Quit early, but let the thread finish.
103143
.then(() => testWithJSMarshaller({
104144
threadStarter: 'StartThreadNonblocking',
145+
maxQueueSize: binding.MAX_QUEUE_SIZE,
105146
quitAfter: 1
106147
}))
107148
.then((result) => assert.deepStrictEqual(result, expectedArray))
@@ -112,6 +153,7 @@ new Promise(function testWithoutJSMarshaller(resolve) {
112153
.then(() => testWithJSMarshaller({
113154
threadStarter: 'StartThread',
114155
quitAfter: 1,
156+
maxQueueSize: binding.MAX_QUEUE_SIZE,
115157
launchSecondary: true
116158
}))
117159
.then((result) => assert.deepStrictEqual(result, expectedArray))
@@ -122,15 +164,27 @@ new Promise(function testWithoutJSMarshaller(resolve) {
122164
.then(() => testWithJSMarshaller({
123165
threadStarter: 'StartThreadNonblocking',
124166
quitAfter: 1,
167+
maxQueueSize: binding.MAX_QUEUE_SIZE,
125168
launchSecondary: true
126169
}))
127170
.then((result) => assert.deepStrictEqual(result, expectedArray))
128171

129172
// Start the thread in blocking mode, and assert that it could not finish.
130-
// Quit early and aborting.
173+
// Quit early by aborting.
174+
.then(() => testWithJSMarshaller({
175+
threadStarter: 'StartThread',
176+
quitAfter: 1,
177+
maxQueueSize: binding.MAX_QUEUE_SIZE,
178+
abort: true
179+
}))
180+
.then((result) => assert.strictEqual(result.indexOf(0), -1))
181+
182+
// Start the thread in blocking mode with an infinite queue, and assert that it
183+
// could not finish. Quit early by aborting.
131184
.then(() => testWithJSMarshaller({
132185
threadStarter: 'StartThread',
133186
quitAfter: 1,
187+
maxQueueSize: 0,
134188
abort: true
135189
}))
136190
.then((result) => assert.strictEqual(result.indexOf(0), -1))
@@ -140,25 +194,13 @@ new Promise(function testWithoutJSMarshaller(resolve) {
140194
.then(() => testWithJSMarshaller({
141195
threadStarter: 'StartThreadNonblocking',
142196
quitAfter: 1,
197+
maxQueueSize: binding.MAX_QUEUE_SIZE,
143198
abort: true
144199
}))
145200
.then((result) => assert.strictEqual(result.indexOf(0), -1))
146201

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

0 commit comments

Comments
 (0)