Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions test/napi/napi-app/binding.gyp
Original file line number Diff line number Diff line change
Expand Up @@ -297,5 +297,16 @@
"NODE_API_EXPERIMENTAL_NOGC_ENV_OPT_OUT=1",
],
},
{
"target_name": "test_async_work_worker_terminate",
"sources": ["test_async_work_worker_terminate.c"],
"include_dirs": ["<!@(node -p \"require('node-addon-api').include\")"],
"libraries": [],
"dependencies": ["<!(node -p \"require('node-addon-api').gyp\")"],
"defines": [
"NAPI_DISABLE_CPP_EXCEPTIONS",
"NODE_API_EXPERIMENTAL_NOGC_ENV_OPT_OUT=1",
],
},
]
}
111 changes: 111 additions & 0 deletions test/napi/napi-app/test_async_work_worker_terminate.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
#include <node_api.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#ifdef _WIN32
#include <windows.h>
static void sleep_ms(unsigned ms) { Sleep(ms); }
#else
#include <unistd.h>
static void sleep_ms(unsigned ms) { usleep(ms * 1000); }
#endif

#define CHECK(env, call) \
do { \
napi_status s_ = (call); \
if (s_ != napi_ok) { \
napi_throw_error((env), NULL, #call " failed"); \
return NULL; \
} \
} while (0)

typedef struct {
napi_async_work work;
napi_ref buf_ref;
napi_ref cb_ref;
unsigned char *data;
size_t len;
unsigned sleep_ms;
} work_t;

static void exec_cb(napi_env env, void *arg) {
work_t *w = (work_t *)arg;
sleep_ms(w->sleep_ms);
// Touch the ArrayBuffer backing store. Before the fix, worker.terminate()
// could free it (via JSC VM teardown running the ArrayBuffer finalizer)
// while this callback is still running on the pool thread.
if (w->len > 0) {
volatile unsigned char first = w->data[0];
(void)first;
memset(w->data, 0xab, w->len);
}
}

static void done_cb(napi_env env, napi_status status, void *arg) {
work_t *w = (work_t *)arg;
napi_value cb = NULL, undef = NULL, argv[1];
if (w->cb_ref != NULL) {
napi_get_reference_value(env, w->cb_ref, &cb);
}
napi_get_undefined(env, &undef);
napi_create_int32(env, (int)status, &argv[0]);
if (cb != NULL) {
napi_call_function(env, undef, cb, 1, argv, NULL);
}
napi_delete_reference(env, w->buf_ref);
if (w->cb_ref != NULL) napi_delete_reference(env, w->cb_ref);
napi_delete_async_work(env, w->work);
free(w);
}

// queueWork(arrayBuffer, ms[, cb]) -> undefined
static napi_value queue_work(napi_env env, napi_callback_info info) {
size_t argc = 3;
napi_value argv[3];
CHECK(env, napi_get_cb_info(env, info, &argc, argv, NULL, NULL));
if (argc < 2) {
napi_throw_error(env, NULL, "expected (arrayBuffer, ms[, cb])");
return NULL;
}

work_t *w = (work_t *)calloc(1, sizeof(*w));

void *data = NULL;
size_t len = 0;
CHECK(env, napi_get_arraybuffer_info(env, argv[0], &data, &len));
w->data = (unsigned char *)data;
w->len = len;

int32_t ms = 0;
CHECK(env, napi_get_value_int32(env, argv[1], &ms));
w->sleep_ms = (unsigned)(ms < 0 ? 0 : ms);

CHECK(env, napi_create_reference(env, argv[0], 1, &w->buf_ref));
if (argc >= 3) {
napi_valuetype t;
CHECK(env, napi_typeof(env, argv[2], &t));
if (t == napi_function) {
CHECK(env, napi_create_reference(env, argv[2], 1, &w->cb_ref));
}
}

napi_value name;
CHECK(env, napi_create_string_utf8(env, "test_async_work_worker_terminate",
NAPI_AUTO_LENGTH, &name));
CHECK(env, napi_create_async_work(env, NULL, name, exec_cb, done_cb, w,
&w->work));
CHECK(env, napi_queue_async_work(env, w->work));

napi_value undef;
CHECK(env, napi_get_undefined(env, &undef));
return undef;
}

NAPI_MODULE_INIT() {
napi_value fn;
CHECK(env, napi_create_function(env, "queueWork", NAPI_AUTO_LENGTH,
queue_work, NULL, &fn));
CHECK(env, napi_set_named_property(env, exports, "queueWork", fn));
return exports;
}
60 changes: 60 additions & 0 deletions test/napi/napi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -550,6 +550,66 @@ describe.concurrent.skipIf(!canBuildNodeAddons())("napi", () => {
expect(output).toContain("success!");
expect(output).not.toContain("failure!");
});
// worker.terminate() while execute callbacks are still running on the
// thread pool must not free the worker's VirtualMachine (the pool-thread
// completion would post into a freed event loop) or its JSC heap (the
// ArrayBuffer backing store would be finalized while the addon is still
// writing it). VM teardown counts queued napi_async_work and waits for the
// pool to hand each one back before destroying the VM. Before that the
// subprocess died with a heap-use-after-free in the pool thread's
// completion post instead of printing PASS.
it("worker.terminate() with execute callbacks in flight waits for them and does not UAF", async () => {
const addon = join(__dirname, "napi-app/build/Debug/test_async_work_worker_terminate.node");
const workerSrc = /* js */ `
const { parentPort, workerData } = require("node:worker_threads");
const addon = require(workerData.addon);
const keep = [];
for (let i = 0; i < 4; i++) {
const ab = new ArrayBuffer(16 << 20);
keep.push(ab);
addon.queueWork(ab, 300 + i * 50, () => {});
}
parentPort.postMessage("up");
setInterval(() => {}, 1000);
`;
const script = /* js */ `
const { Worker } = require("node:worker_threads");
(async () => {
for (let r = 0; r < ${isASAN ? 3 : 5}; r++) {
const w = new Worker(process.env.WORKER_SRC, {
eval: true,
workerData: { addon: process.env.ADDON },
});
await new Promise((resolve, reject) => {
w.once("message", resolve);
w.once("error", reject);
w.once("exit", code => reject(new Error("worker exited before queueing work, code " + code)));
});
await w.terminate();
}
console.log("PASS");
})().catch(e => {
console.error(String(e));
process.exit(1);
});
`;
await using proc = spawn({
cmd: [bunExe(), "-e", script],
// LSan is off in the subprocess: a `-e` script that creates eval
// workers reports their source Blobs at exit regardless of the addon,
// and this test is about the crash.
env: {
...bunEnv,
ADDON: addon,
WORKER_SRC: workerSrc,
ASAN_OPTIONS: "detect_leaks=0:allow_user_segv_handler=1",
},
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect({ stdout: stdout.trim(), stderr, exitCode }).toEqual({ stdout: "PASS", stderr: "", exitCode: 0 });
}, 30_000);
});

describe("napi_threadsafe_function", () => {
Expand Down