Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
14 changes: 8 additions & 6 deletions src/bun.js/bindings/libuv/generate_uv_posix_stubs_constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -271,17 +271,19 @@
"uv_tcp_nodelay",
"uv_tcp_open",
"uv_tcp_simultaneous_accepts",
"uv_thread_create",
"uv_thread_create_ex",
"uv_thread_detach",
"uv_thread_equal",
// Defined in uv-posix-polyfills.c
// "uv_thread_create",
// "uv_thread_create_ex",
// "uv_thread_detach",
// "uv_thread_equal",
"uv_thread_getaffinity",
"uv_thread_getcpu",
"uv_thread_getname",
"uv_thread_getpriority",
"uv_thread_join",
"uv_thread_self",
// Defined in uv-posix-polyfills.c
// "uv_thread_join",
// "uv_thread_self",
"uv_thread_setaffinity",

Check warning on line 286 in src/bun.js/bindings/libuv/generate_uv_posix_stubs_constants.ts

View check run for this annotation

Claude / Claude Code Review

Stale uv_thread_detach entry in test_skipped after polyfill migration

Stale `uv_thread_detach` entry in `test_skipped` was not removed when the symbol was moved to polyfills. The entry is now dead code since `symbols_to_test = symbols.filter(s => \!test_skipped.includes(s))` can never match a symbol that isn't in `symbols`. Remove `"uv_thread_detach"` from `test_skipped` to keep the list consistent and eliminate a latent masking risk if the symbol is ever accidentally re-added to `symbols`.
"uv_thread_setname",
"uv_thread_setpriority",
"uv_timer_again",
Expand Down
76 changes: 76 additions & 0 deletions src/bun.js/bindings/uv-posix-polyfills.c
Original file line number Diff line number Diff line change
Expand Up @@ -138,4 +138,80 @@
abort();
}

// Copy-pasted from libuv (src/unix/thread.c)
UV_EXTERN uv_thread_t uv_thread_self(void)
{
return pthread_self();
}

// Copy-pasted from libuv (src/unix/thread.c)
UV_EXTERN int uv_thread_equal(const uv_thread_t* t1, const uv_thread_t* t2)
{
return pthread_equal(*t1, *t2);
}

// Copy-pasted from libuv (src/unix/thread.c)
UV_EXTERN int uv_thread_join(uv_thread_t* tid)
{
return UV__ERR(pthread_join(*tid, NULL));
}

// Copy-pasted from libuv (src/unix/thread.c)
UV_EXTERN int uv_thread_detach(uv_thread_t* tid)
{
return UV__ERR(pthread_detach(*tid));
}

// Copy-pasted from libuv (src/unix/thread.c), with stack size tuning omitted.
// The libuv version rounds a requested stack size up to a page boundary and
// clamps it to a minimum; here we fall through to pthread defaults when no
// size is requested and just honor the requested size otherwise.
UV_EXTERN int uv_thread_create_ex(uv_thread_t* tid,
const uv_thread_options_t* params,
uv_thread_cb entry,
void* arg)
{
int err;
pthread_attr_t* attr;
pthread_attr_t attr_storage;
size_t stack_size;

/* Used to squelch a -Wcast-function-type warning. */
union {
void (*in)(void*);
void* (*out)(void*);
} f;

stack_size = (params != NULL && (params->flags & UV_THREAD_HAS_STACK_SIZE))
? params->stack_size
: 0;

attr = NULL;
if (stack_size > 0) {
attr = &attr_storage;

if (pthread_attr_init(attr))
abort();

if (pthread_attr_setstacksize(attr, stack_size))
abort();
}

Check failure on line 198 in src/bun.js/bindings/uv-posix-polyfills.c

View check run for this annotation

Claude / Claude Code Review

uv_thread_create_ex aborts on pthread_attr_setstacksize failure

In uv_thread_create_ex, when UV_THREAD_HAS_STACK_SIZE is set and pthread_attr_setstacksize fails (e.g. EINVAL for a stack size below PTHREAD_STACK_MIN or not page-aligned), the polyfill calls abort() instead of returning UV__ERR(err) like the real libuv implementation. Any NAPI module that calls uv_thread_create_ex with a custom stack size that is too small or misaligned will crash the entire process rather than receiving a recoverable UV_EINVAL; fix by calling pthread_attr_destroy(attr) then re

f.in = entry;
err = pthread_create(tid, attr, f.out, arg);

if (attr != NULL)
pthread_attr_destroy(attr);

return UV__ERR(err);
}

// Copy-pasted from libuv (src/unix/thread.c)
UV_EXTERN int uv_thread_create(uv_thread_t* tid, uv_thread_cb entry, void* arg)
{
uv_thread_options_t params;
params.flags = UV_THREAD_NO_FLAGS;
return uv_thread_create_ex(tid, &params, entry, arg);
}

#endif
39 changes: 0 additions & 39 deletions src/bun.js/bindings/uv-posix-stubs.c
Original file line number Diff line number Diff line change
Expand Up @@ -1686,33 +1686,6 @@ UV_EXTERN int uv_tcp_simultaneous_accepts(uv_tcp_t* handle, int enable)
__builtin_unreachable();
}

UV_EXTERN int uv_thread_create(uv_thread_t* tid, uv_thread_cb entry, void* arg)
{
__bun_throw_not_implemented("uv_thread_create");
__builtin_unreachable();
}

UV_EXTERN int uv_thread_create_ex(uv_thread_t* tid,
const uv_thread_options_t* params,
uv_thread_cb entry,
void* arg)
{
__bun_throw_not_implemented("uv_thread_create_ex");
__builtin_unreachable();
}

UV_EXTERN int uv_thread_detach(uv_thread_t* tid)
{
__bun_throw_not_implemented("uv_thread_detach");
__builtin_unreachable();
}

UV_EXTERN int uv_thread_equal(const uv_thread_t* t1, const uv_thread_t* t2)
{
__bun_throw_not_implemented("uv_thread_equal");
__builtin_unreachable();
}

UV_EXTERN int uv_thread_getaffinity(uv_thread_t* tid,
char* cpumask,
size_t mask_size)
Expand All @@ -1739,18 +1712,6 @@ UV_EXTERN int uv_thread_getpriority(uv_thread_t tid, int* priority)
__builtin_unreachable();
}

UV_EXTERN int uv_thread_join(uv_thread_t* tid)
{
__bun_throw_not_implemented("uv_thread_join");
__builtin_unreachable();
}

UV_EXTERN uv_thread_t uv_thread_self(void)
{
__bun_throw_not_implemented("uv_thread_self");
__builtin_unreachable();
}

UV_EXTERN int uv_thread_setaffinity(uv_thread_t* tid,
char* cpumask,
char* oldmask,
Expand Down
11 changes: 11 additions & 0 deletions test/napi/napi-app/binding.gyp
Original file line number Diff line number Diff line change
Expand Up @@ -232,5 +232,16 @@
"NAPI_VERSION_EXPERIMENTAL=1",
],
},
{
"target_name": "uv_thread_addon",
"sources": ["uv_thread_addon.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",
],
},
]
}
73 changes: 73 additions & 0 deletions test/napi/napi-app/uv_thread_addon.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// Regression test for https://github.com/oven-sh/bun/issues/29260:
// NAPI modules (e.g. ffi-napi) call uv_thread_self at module init. This
// addon exercises uv_thread_self + uv_thread_equal + uv_thread_create +
// uv_thread_join + uv_thread_detach + uv_thread_create_ex. If any of them
// is not polyfilled, Bun panics with "unsupported uv function".

#include <node_api.h>
#include <stdio.h>
#include <uv.h>

static void thread_entry(void *arg) {
int *counter = (int *)arg;
*counter = 42;
}

// For the detach sub-test: the caller returns before this runs, so we
// must NOT touch anything from the caller's stack. No-op instead.
static void thread_entry_detach(void *arg) { (void)arg; }

static napi_value fail(napi_env env, const char *msg) {
napi_throw_error(env, NULL, msg);
return NULL;
}

NAPI_MODULE_INIT(/* napi_env env, napi_value exports */) {
// uv_thread_self + uv_thread_equal: self must equal self.
uv_thread_t self1 = uv_thread_self();
uv_thread_t self2 = uv_thread_self();
if (!uv_thread_equal(&self1, &self2)) {
return fail(env, "uv_thread_equal(self, self) returned false");
}

// uv_thread_create + uv_thread_join: spawn, join, verify the thread ran.
int counter = 0;
uv_thread_t tid;
if (uv_thread_create(&tid, thread_entry, &counter) != 0) {
return fail(env, "uv_thread_create failed");
}
if (uv_thread_join(&tid) != 0) {
return fail(env, "uv_thread_join failed");
}
if (counter != 42) {
return fail(env, "uv_thread_create: thread did not run");
}

// uv_thread_create_ex: no flags → default pthread stack size.
counter = 0;
uv_thread_options_t opts;
opts.flags = UV_THREAD_NO_FLAGS;
if (uv_thread_create_ex(&tid, &opts, thread_entry, &counter) != 0) {
return fail(env, "uv_thread_create_ex failed");
}
if (uv_thread_join(&tid) != 0) {
return fail(env, "uv_thread_join (after _ex) failed");
}
if (counter != 42) {
return fail(env, "uv_thread_create_ex: thread did not run");
}

// uv_thread_detach: spawn, detach, the thread cleans up on its own.
if (uv_thread_create(&tid, thread_entry_detach, NULL) != 0) {
return fail(env, "uv_thread_create (detach) failed");
}
if (uv_thread_detach(&tid) != 0) {
return fail(env, "uv_thread_detach failed");
}

napi_value result;
if (napi_get_boolean(env, true, &result) != napi_ok) {
return fail(env, "napi_get_boolean failed");
}
return result;
}
68 changes: 63 additions & 5 deletions test/napi/uv-stub-stuff/good_plugin.c
Original file line number Diff line number Diff line change
@@ -1,17 +1,75 @@
#include <node_api.h>
#include <sys/types.h>

#include <signal.h>
#include <stdio.h>
#include <unistd.h>
#include <uv.h>

typedef pid_t uv_pid_t;
uv_pid_t uv_os_getpid();
static void thread_entry(void *arg) {
int *counter = (int *)arg;
*counter = 42;
}

// For the detach sub-test: the caller returns before this runs, so we
// must NOT touch the caller's stack. No-op instead.
static void thread_entry_detach(void *arg) { (void)arg; }

napi_value Init(napi_env env, napi_value exports) {
uv_pid_t pid = uv_os_getpid();
printf("%d\n", pid);

// uv_thread_self + uv_thread_equal: self must equal self.
uv_thread_t self1 = uv_thread_self();
uv_thread_t self2 = uv_thread_self();
if (!uv_thread_equal(&self1, &self2)) {
printf("FAIL: uv_thread_equal(self, self)\n");
return NULL;
}

// uv_thread_create + uv_thread_join: spawn a thread, join, verify it ran.
int counter = 0;
uv_thread_t tid;
if (uv_thread_create(&tid, thread_entry, &counter) != 0) {
printf("FAIL: uv_thread_create\n");
return NULL;
}
if (uv_thread_join(&tid) != 0) {
printf("FAIL: uv_thread_join\n");
return NULL;
}
if (counter != 42) {
printf("FAIL: thread did not run (counter=%d)\n", counter);
return NULL;
}

// uv_thread_create_ex: no flags → default pthread stack size.
counter = 0;
uv_thread_options_t opts;
opts.flags = UV_THREAD_NO_FLAGS;
if (uv_thread_create_ex(&tid, &opts, thread_entry, &counter) != 0) {
printf("FAIL: uv_thread_create_ex\n");
return NULL;
}
if (uv_thread_join(&tid) != 0) {
printf("FAIL: uv_thread_join (ex)\n");
return NULL;
}
if (counter != 42) {
printf("FAIL: uv_thread_create_ex thread did not run (counter=%d)\n",
counter);
return NULL;
}

// uv_thread_detach: spawn, detach. The thread runs a no-op so it's
// safe for the caller to return before the thread starts.
if (uv_thread_create(&tid, thread_entry_detach, NULL) != 0) {
printf("FAIL: uv_thread_create (detach)\n");
return NULL;
}
if (uv_thread_detach(&tid) != 0) {
printf("FAIL: uv_thread_detach\n");
return NULL;
}

printf("THREAD_OK\n");
return NULL;
}

Expand Down
40 changes: 0 additions & 40 deletions test/napi/uv-stub-stuff/plugin.c
Original file line number Diff line number Diff line change
Expand Up @@ -2040,33 +2040,6 @@ napi_value call_uv_func(napi_env env, napi_callback_info info) {
return NULL;
}

if (strcmp(buffer, "uv_thread_create") == 0) {
uv_thread_t *arg0 = {0};
uv_thread_cb arg1 = NULL;
void *arg2 = {0};

uv_thread_create(arg0, arg1, arg2);
return NULL;
}

if (strcmp(buffer, "uv_thread_create_ex") == 0) {
uv_thread_t *arg0 = {0};
const uv_thread_options_t *arg1 = {0};
uv_thread_cb arg2 = NULL;
void *arg3 = {0};

uv_thread_create_ex(arg0, arg1, arg2, arg3);
return NULL;
}

if (strcmp(buffer, "uv_thread_equal") == 0) {
const uv_thread_t *arg0 = {0};
const uv_thread_t *arg1 = {0};

uv_thread_equal(arg0, arg1);
return NULL;
}

if (strcmp(buffer, "uv_thread_getaffinity") == 0) {
uv_thread_t *arg0 = {0};
char *arg1 = {0};
Expand All @@ -2082,19 +2055,6 @@ napi_value call_uv_func(napi_env env, napi_callback_info info) {
return NULL;
}

if (strcmp(buffer, "uv_thread_join") == 0) {
uv_thread_t *arg0 = {0};

uv_thread_join(arg0);
return NULL;
}

if (strcmp(buffer, "uv_thread_self") == 0) {

uv_thread_self();
return NULL;
}

if (strcmp(buffer, "uv_thread_setaffinity") == 0) {
uv_thread_t *arg0 = {0};
char *arg1 = {0};
Expand Down
6 changes: 5 additions & 1 deletion test/napi/uv_stub.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,11 @@ describe.if(!isWindows)("uv stubs", () => {

test("should not crash when calling supported uv functions", async () => {
const { stdout, exitCode } = await Bun.$`${bunExe()} run nocrash.ts`.cwd(tempdir).throws(false).quiet();
const out = stdout.toString();
expect(out).toContain("HI!");
// good_plugin.c exercises uv_thread_self/_equal/_join/_detach/_create/_create_ex
// — regression for #29260 (uv_thread_self panic loading ffi-napi).
expect(out).toContain("THREAD_OK");
expect(exitCode).toBe(0);
expect(stdout.toString()).toContain("HI!");
});
});
Loading
Loading