diff --git a/src/bun.js/bindings/libuv/generate_uv_posix_stubs_constants.ts b/src/bun.js/bindings/libuv/generate_uv_posix_stubs_constants.ts index b28f652ed91..64341dc5169 100644 --- a/src/bun.js/bindings/libuv/generate_uv_posix_stubs_constants.ts +++ b/src/bun.js/bindings/libuv/generate_uv_posix_stubs_constants.ts @@ -1,6 +1,5 @@ export const test_skipped = [ "uv_getrusage_thread", - "uv_thread_detach", "uv_thread_getname", "uv_thread_getpriority", "uv_thread_setname", @@ -271,16 +270,18 @@ export const symbols = [ "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", "uv_thread_setname", "uv_thread_setpriority", diff --git a/src/bun.js/bindings/uv-posix-polyfills.c b/src/bun.js/bindings/uv-posix-polyfills.c index b36cd5c6e4e..34dd0dcc785 100644 --- a/src/bun.js/bindings/uv-posix-polyfills.c +++ b/src/bun.js/bindings/uv-posix-polyfills.c @@ -2,9 +2,11 @@ #if OS(LINUX) || OS(DARWIN) +#include #include -#include #include +#include +#include // libuv does the annoying thing of #undef'ing these #include @@ -138,4 +140,151 @@ UV_EXTERN void uv_mutex_unlock(uv_mutex_t* mutex) 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): minimum stack size a thread + * may be created with. A new thread needs to allocate, among other things, + * a TLS block AND pthread's internal bookkeeping. The exact size is + * arch-dependent. */ +static size_t uv__min_stack_size(void) +{ + static const size_t min = 8192; + +#ifdef PTHREAD_STACK_MIN /* Not defined on NetBSD. */ + if (min < (size_t)PTHREAD_STACK_MIN) + return PTHREAD_STACK_MIN; +#endif + + return min; +} + +/* Copy-pasted from libuv (src/unix/thread.c): on Linux, threads created by + * musl have a much smaller stack than threads created by glibc (80 vs. + * 2048 or 4096 kB). Follow glibc for consistency. */ +static size_t uv__default_stack_size(void) +{ +#if !defined(__linux__) + return 0; +#elif defined(__PPC__) || defined(__ppc__) || defined(__powerpc__) + return 4 << 20; /* glibc default. */ +#else + return 2 << 20; /* glibc default. */ +#endif +} + +/* Copy-pasted from libuv (src/unix/thread.c): on MacOS, threads other than + * the main thread are created with a reduced stack size by default. Adjust + * to RLIMIT_STACK aligned to the page size. */ +static size_t uv__thread_stack_size(void) +{ +#if defined(__APPLE__) || defined(__linux__) + struct rlimit lim; + + /* getrlimit() can fail on some aarch64 systems due to a glibc bug + * where the system call wrapper invokes the wrong system call. Don't + * treat that as fatal, just use the default stack size instead. */ + if (getrlimit(RLIMIT_STACK, &lim)) + return uv__default_stack_size(); + + if (lim.rlim_cur == RLIM_INFINITY) + return uv__default_stack_size(); + + /* pthread_attr_setstacksize() expects page-aligned values. */ + lim.rlim_cur -= lim.rlim_cur % (rlim_t)getpagesize(); + + if (lim.rlim_cur >= (rlim_t)uv__min_stack_size()) + return lim.rlim_cur; +#endif + + return uv__default_stack_size(); +} + +// Copy-pasted from libuv (src/unix/thread.c). The page-rounding and +// min-stack-size clamping is what makes the two abort() calls below safe: +// without them, pthread_attr_setstacksize could legitimately fail with +// EINVAL on a caller-supplied stack_size that is too small or not +// page-aligned, and abort() would be wrong. +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 pagesize; + size_t stack_size; + size_t min_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) { + stack_size = uv__thread_stack_size(); + } else { + pagesize = (size_t)getpagesize(); + /* Round up to the nearest page boundary. */ + stack_size = (stack_size + pagesize - 1) & ~(pagesize - 1); + min_stack_size = uv__min_stack_size(); + if (stack_size < min_stack_size) + stack_size = min_stack_size; + } + + if (stack_size > 0) { + attr = &attr_storage; + + if (pthread_attr_init(attr)) + abort(); + + if (pthread_attr_setstacksize(attr, stack_size)) + abort(); + } + + 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, ¶ms, entry, arg); +} + #endif diff --git a/src/bun.js/bindings/uv-posix-stubs.c b/src/bun.js/bindings/uv-posix-stubs.c index 2ed27895e74..99fcfe4bfa9 100644 --- a/src/bun.js/bindings/uv-posix-stubs.c +++ b/src/bun.js/bindings/uv-posix-stubs.c @@ -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) @@ -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, diff --git a/test/napi/napi-app/binding.gyp b/test/napi/napi-app/binding.gyp index a372cedde4e..f922883822d 100644 --- a/test/napi/napi-app/binding.gyp +++ b/test/napi/napi-app/binding.gyp @@ -232,5 +232,16 @@ "NAPI_VERSION_EXPERIMENTAL=1", ], }, + { + "target_name": "uv_thread_addon", + "sources": ["uv_thread_addon.c"], + "include_dirs": [" +#include +#include + +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; +} diff --git a/test/napi/uv-stub-stuff/good_plugin.c b/test/napi/uv-stub-stuff/good_plugin.c index fd86ba83c71..cfb4294dc1b 100644 --- a/test/napi/uv-stub-stuff/good_plugin.c +++ b/test/napi/uv-stub-stuff/good_plugin.c @@ -1,17 +1,75 @@ #include -#include -#include #include -#include +#include -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; } diff --git a/test/napi/uv-stub-stuff/plugin.c b/test/napi/uv-stub-stuff/plugin.c index 932e776ec35..ba5f4e3e210 100644 --- a/test/napi/uv-stub-stuff/plugin.c +++ b/test/napi/uv-stub-stuff/plugin.c @@ -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}; @@ -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}; diff --git a/test/napi/uv_stub.test.ts b/test/napi/uv_stub.test.ts index 8e635ed8870..61c87efd1f2 100644 --- a/test/napi/uv_stub.test.ts +++ b/test/napi/uv_stub.test.ts @@ -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!"); }); }); diff --git a/test/no-validate-exceptions.txt b/test/no-validate-exceptions.txt index 5936bf7ec47..0b814c7f5a6 100644 --- a/test/no-validate-exceptions.txt +++ b/test/no-validate-exceptions.txt @@ -76,6 +76,7 @@ test/js/third_party/resvg/bbox.test.js test/js/third_party/rollup-v4/rollup-v4.test.ts test/napi/uv.test.ts test/napi/uv_stub.test.ts +test/regression/issue/29260.test.ts test/napi/node-napi-tests/test/js-native-api/2_function_arguments/do.test.ts test/napi/node-napi-tests/test/js-native-api/3_callbacks/do.test.ts test/napi/node-napi-tests/test/js-native-api/4_object_factory/do.test.ts diff --git a/test/regression/issue/29260.test.ts b/test/regression/issue/29260.test.ts new file mode 100644 index 00000000000..15415262cac --- /dev/null +++ b/test/regression/issue/29260.test.ts @@ -0,0 +1,53 @@ +// https://github.com/oven-sh/bun/issues/29260 +import { spawnSync } from "bun"; +import { beforeAll, describe, expect, test } from "bun:test"; +import { bunEnv, bunExe, isWindows } from "harness"; +import { existsSync } from "node:fs"; +import { join } from "node:path"; + +const napiAppDir = join(import.meta.dir, "..", "..", "napi", "napi-app"); +const addonPath = join(napiAppDir, "build", "Debug", "uv_thread_addon.node"); + +// We use real libuv on Windows, so the POSIX stubs don't apply there. +describe.if(!isWindows)("issue/29260", () => { + beforeAll( + () => { + // node-gyp is slow (30s+); skip when the prebuilt output is on disk. + if (existsSync(addonPath)) return; + const install = spawnSync({ + cmd: [bunExe(), "install"], + cwd: napiAppDir, + env: bunEnv, + stdout: "inherit", + stderr: "inherit", + }); + if (!install.success) { + throw new Error("failed to build napi-app (node-gyp)"); + } + }, + // node-gyp cold-build of the whole napi-app is slow. + 5 * 60_000, + ); + + test("uv_thread_self and friends no longer panic when a NAPI module calls them", async () => { + // The addon's NAPI_MODULE_INIT returns `true` iff every thread op + // succeeds, and napi_throw_error otherwise. We print the returned + // value directly (not `typeof`) so the assertion fails if the addon + // ever silently returns `false` or a non-boolean. + await using proc = Bun.spawn({ + cmd: [bunExe(), "-p", `require(${JSON.stringify(addonPath)})`], + env: bunEnv, + cwd: napiAppDir, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + // Diagnose before the assertion so test output is useful on failure. + if (exitCode !== 0 || stdout.trim() !== "true") { + console.error("stdout:", stdout); + console.error("stderr:", stderr); + } + expect(stdout.trim()).toBe("true"); + expect(exitCode).toBe(0); + }); +});