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..372f4c213ad 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 @@ -123,7 +123,7 @@ export const symbols = [ "uv_handle_size", "uv_handle_type_name", "uv_has_ref", - // Defined in uv-posix-polyfills.cpp + // Defined in uv-posix-polyfills.c // "uv_hrtime", "uv_idle_init", "uv_idle_start", @@ -162,7 +162,7 @@ export const symbols = [ "uv_loop_size", "uv_metrics_idle_time", "uv_metrics_info", - // Defined in uv-posix-polyfills.cpp + // Defined in uv-posix-polyfills.c // "uv_mutex_destroy", // "uv_mutex_init", // "uv_mutex_init_recursive", @@ -170,7 +170,7 @@ export const symbols = [ // "uv_mutex_trylock", // "uv_mutex_unlock", "uv_now", - // Defined in uv-posix-polyfills.cpp + // Defined in uv-posix-polyfills.c // "uv_once", "uv_open_osfhandle", "uv_os_environ", @@ -182,9 +182,9 @@ export const symbols = [ "uv_os_get_passwd2", "uv_os_getenv", "uv_os_gethostname", - // Defined in uv-posix-polyfills.cpp + // Defined in uv-posix-polyfills.c // "uv_os_getpid", - // Defined in uv-posix-polyfills.cpp + // Defined in uv-posix-polyfills.c // "uv_os_getppid", "uv_os_getpriority", "uv_os_homedir", @@ -280,7 +280,8 @@ export const symbols = [ "uv_thread_getname", "uv_thread_getpriority", "uv_thread_join", - "uv_thread_self", + // Defined in uv-posix-polyfills.c + // "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..54392efa11f 100644 --- a/src/bun.js/bindings/uv-posix-polyfills.c +++ b/src/bun.js/bindings/uv-posix-polyfills.c @@ -57,6 +57,13 @@ UV_EXTERN void uv_once(uv_once_t* guard, void (*callback)(void)) abort(); } +// Copy-pasted from libuv (src/unix/thread.c). +// uv_thread_t is pthread_t on POSIX (see uv/unix.h). +UV_EXTERN uv_thread_t uv_thread_self(void) +{ + return pthread_self(); +} + UV_EXTERN uint64_t uv_hrtime(void) { return uv__hrtime(UV_CLOCK_PRECISE); diff --git a/src/bun.js/bindings/uv-posix-stubs.c b/src/bun.js/bindings/uv-posix-stubs.c index 2ed27895e74..adea2d8708e 100644 --- a/src/bun.js/bindings/uv-posix-stubs.c +++ b/src/bun.js/bindings/uv-posix-stubs.c @@ -1745,12 +1745,6 @@ UV_EXTERN int uv_thread_join(uv_thread_t* tid) __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/uv-stub-stuff/plugin.c b/test/napi/uv-stub-stuff/plugin.c index 932e776ec35..263b3907a4e 100644 --- a/test/napi/uv-stub-stuff/plugin.c +++ b/test/napi/uv-stub-stuff/plugin.c @@ -2089,12 +2089,6 @@ napi_value call_uv_func(napi_env env, napi_callback_info info) { 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-stuff/uv_impl.c b/test/napi/uv-stub-stuff/uv_impl.c index ca3b4358c57..01b025a97eb 100644 --- a/test/napi/uv-stub-stuff/uv_impl.c +++ b/test/napi/uv-stub-stuff/uv_impl.c @@ -1,5 +1,6 @@ #include +#include #include #include #include @@ -102,6 +103,23 @@ static napi_value test_uv_once(napi_env env, napi_callback_info info) { return ret; } +// Test uv_thread_self — regression test for #29223. +// ffi-napi calls uv_thread_self during NAPI module init; Bun used to panic +// because uv_thread_self was a stubbed libuv symbol on POSIX. +static napi_value test_thread_self(napi_env env, napi_callback_info info) { + // Call uv_thread_self twice — both from the current (main) thread. Also + // compare against pthread_self() to catch any bogus implementation that + // returns a consistent-but-wrong constant. + uv_thread_t a = uv_thread_self(); + uv_thread_t b = uv_thread_self(); + + napi_value ret; + // pthread_equal returns non-zero for equal threads. Return a boolean so + // the JS side can check it without caring about uv_thread_t layout. + napi_get_boolean(env, pthread_equal(a, b) != 0 && pthread_equal(a, pthread_self()) != 0, &ret); + return ret; +} + // Test uv_hrtime static napi_value test_hrtime(napi_env env, napi_callback_info info) { uint64_t time1 = uv_hrtime(); @@ -150,6 +168,9 @@ napi_value Init(napi_env env, napi_value exports) { napi_create_function(env, NULL, 0, test_uv_once, NULL, &fn); napi_set_named_property(env, exports, "testUvOnce", fn); + napi_create_function(env, NULL, 0, test_thread_self, NULL, &fn); + napi_set_named_property(env, exports, "testThreadSelf", fn); + napi_create_function(env, NULL, 0, test_hrtime, NULL, &fn); napi_set_named_property(env, exports, "testHrtime", fn); diff --git a/test/napi/uv.test.ts b/test/napi/uv.test.ts index 319d481a0bd..678b8b786b4 100644 --- a/test/napi/uv.test.ts +++ b/test/napi/uv.test.ts @@ -91,6 +91,12 @@ describe.if(!isWindows)("uv stubs", () => { expect(nativeModule.testUvOnce()).toBe(1); }); + // Regression test for #29223: ffi-napi calls uv_thread_self during NAPI + // module init. Used to panic because it was a stubbed libuv symbol. + test("uv_thread_self (#29223)", () => { + expect(nativeModule.testThreadSelf()).toBe(true); + }); + test("hrtime", () => { const result = nativeModule.testHrtime(); diff --git a/test/regression/issue/29223.test.ts b/test/regression/issue/29223.test.ts new file mode 100644 index 00000000000..f4ae4e9176f --- /dev/null +++ b/test/regression/issue/29223.test.ts @@ -0,0 +1,90 @@ +// https://github.com/oven-sh/bun/issues/29223 +import { beforeAll, describe, expect, test } from "bun:test"; +import { bunEnv, bunExe, isWindows, tempDirWithFiles } from "harness"; +import path from "node:path"; + +// Windows uses real libuv; the POSIX-stub code path does not apply there. +describe.if(!isWindows)("issue #29223", () => { + let tempdir: string = ""; + + // Build the addon once in beforeAll (same pattern as test/napi/uv.test.ts). + beforeAll(async () => { + const addonSource = ` +#include +#include +#include + +napi_value Init(napi_env env, napi_value exports) { + // This is what ffi-napi does: call uv_thread_self() while the NAPI + // module is being constructed. Before the fix this panicked Bun. + uv_thread_t self = uv_thread_self(); + + // Also check that calling it twice from the same thread agrees with + // pthread_self() — proves we actually implemented it rather than + // returning a garbage value. + uv_thread_t again = uv_thread_self(); + int equal = pthread_equal(self, again) != 0 && pthread_equal(self, pthread_self()) != 0; + + napi_value equal_js; + napi_get_boolean(env, equal, &equal_js); + napi_set_named_property(env, exports, "equal", equal_js); + return exports; +} + +NAPI_MODULE(NODE_GYP_MODULE_NAME, Init) +`; + + const files = { + "addon.c": addonSource, + "package.json": JSON.stringify({ + name: "issue-29223-addon", + version: "0.0.0", + private: true, + scripts: { "build:napi": "node-gyp configure && node-gyp build" }, + dependencies: { "node-gyp": "10.2.0" }, + }), + "binding.gyp": `{ + "targets": [ + { + "target_name": "addon", + "sources": [ "addon.c" ], + "include_dirs": [ ".", "./libuv" ], + "cflags": ["-fPIC"] + } + ] +}`, + "index.js": `const addon = require("./build/Release/addon.node"); +if (addon.equal !== true) { + console.error("FAIL: uv_thread_self returned inconsistent results"); + process.exit(2); +} +console.log("OK"); +`, + }; + + tempdir = tempDirWithFiles("issue-29223", files); + + // node-gyp uses the libuv headers we vendor for the stubs. + const libuvDir = path.join(import.meta.dir, "../../../src/bun.js/bindings/libuv"); + await Bun.$`cp -R ${libuvDir} ${path.join(tempdir, "libuv")}`.env(bunEnv); + await Bun.$`${bunExe()} install && ${bunExe()} run build:napi`.env(bunEnv).cwd(tempdir); + }); + + test("NAPI addon calling uv_thread_self during Init does not crash", () => { + // spawnSync because the baseline (pre-fix) crashes via panic + abort; + // spawn + proc.exited can hang on such aborts under the test runner. + const { stdout, exitCode } = Bun.spawnSync({ + cmd: [bunExe(), "index.js"], + env: bunEnv, + cwd: tempdir, + stderr: "pipe", + stdout: "pipe", + }); + + // The addon prints "OK" from index.js only if require() succeeded and + // uv_thread_self() returned a thread id consistent with pthread_self(). + // Pre-fix, require() panics the process and stdout stays empty. + expect(stdout.toString().trim()).toBe("OK"); + expect(exitCode).toBe(0); + }); +});