From 35106aa2664acb8e2a75c51f370c4dc61dba4047 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Thu, 20 Aug 2026 15:29:51 +0200 Subject: [PATCH 1/3] Revert "fix(runtime): extend #8416's C-unwind to the whole closure dispatch family (#8463) (#8464)" This reverts commit 20b456f6b5438148be599b8e7be08377b641c380. --- changelog.d/8464-closure-unwind-family.md | 1 - .../src/closure/dispatch/bound.rs | 4 +- .../src/closure/dispatch/calln.rs | 73 ++++++++----------- .../src/closure/dispatch/direct.rs | 8 +- .../src/closure/dispatch/value_call.rs | 4 +- crates/perry-runtime/src/closure/registry.rs | 5 +- crates/perry/tests/bun_ffi_stage1.rs | 22 +++--- 7 files changed, 52 insertions(+), 65 deletions(-) delete mode 100644 changelog.d/8464-closure-unwind-family.md diff --git a/changelog.d/8464-closure-unwind-family.md b/changelog.d/8464-closure-unwind-family.md deleted file mode 100644 index 3f649fbace..0000000000 --- a/changelog.d/8464-closure-unwind-family.md +++ /dev/null @@ -1 +0,0 @@ -fix(runtime): the whole closure dispatch family is `extern "C-unwind"` now (#8463). #8416 let dynamic Function errors unwind but converted only `js_closure_call1`; every other edge (`call0`/`call3+`, `call2`'s inner transmute, `dispatch_with_arity`'s per-arity transmutes, `js_closure_call_array`/`_apply_with_spread`, the direct-call-site cache, `js_function_bind`) kept the abort-on-unwind shim, turning a deferred Function error into "panic in a function that cannot unwind" on Linux — the nightly `bun_ffi_stage1` abort. Registration entry points stay `extern "C"`. diff --git a/crates/perry-runtime/src/closure/dispatch/bound.rs b/crates/perry-runtime/src/closure/dispatch/bound.rs index 9066f86a1d..69664b7c2c 100644 --- a/crates/perry-runtime/src/closure/dispatch/bound.rs +++ b/crates/perry-runtime/src/closure/dispatch/bound.rs @@ -360,7 +360,7 @@ unsafe fn read_function_name_property(closure_ptr: usize) -> Option { /// `.name` is set to `"bound " + target.name` and `.length` to /// `max(0, target.length - boundArgs.length)`, matching Node. Refs #2840. #[no_mangle] -pub unsafe extern "C-unwind" fn js_function_bind( +pub unsafe extern "C" fn js_function_bind( target_value: f64, args_ptr: *const f64, args_len: usize, @@ -489,7 +489,7 @@ pub unsafe extern "C-unwind" fn js_function_bind( /// survives the bitcode pipeline. See project_auto_optimize_keepalive_3320. #[cfg(feature = "keepalive-anchors")] #[used] -static KEEP_JS_FUNCTION_BIND: unsafe extern "C-unwind" fn(f64, *const f64, usize) -> f64 = +static KEEP_JS_FUNCTION_BIND: unsafe extern "C" fn(f64, *const f64, usize) -> f64 = js_function_bind; /// Reify a `Function.prototype.{bind,call,apply}` (or any function method) diff --git a/crates/perry-runtime/src/closure/dispatch/calln.rs b/crates/perry-runtime/src/closure/dispatch/calln.rs index b8032eb938..22a50b8f6d 100644 --- a/crates/perry-runtime/src/closure/dispatch/calln.rs +++ b/crates/perry-runtime/src/closure/dispatch/calln.rs @@ -11,7 +11,7 @@ use super::*; /// Call a closure with 0 arguments, returning f64 #[no_mangle] -pub extern "C-unwind" fn js_closure_call0(closure: *const ClosureHeader) -> f64 { +pub extern "C" fn js_closure_call0(closure: *const ClosureHeader) -> f64 { let func_ptr = get_valid_func_ptr(closure); if func_ptr.is_null() { return dispatch_proxy_callee_or_throw(closure, &[]); @@ -26,7 +26,7 @@ pub extern "C-unwind" fn js_closure_call0(closure: *const ClosureHeader) -> f64 dispatch_with_arity(closure, func_ptr, &[], declared) }, _ => { - let func: extern "C-unwind" fn(*const ClosureHeader) -> f64 = + let func: extern "C" fn(*const ClosureHeader) -> f64 = unsafe { std::mem::transmute(func_ptr) }; func(closure) } @@ -84,7 +84,7 @@ pub extern "C-unwind" fn js_closure_call2( dispatch_with_arity(closure, func_ptr, &[arg0, arg1], declared) }, _ => { - let func: extern "C-unwind" fn(*const ClosureHeader, f64, f64) -> f64 = + let func: extern "C" fn(*const ClosureHeader, f64, f64) -> f64 = unsafe { std::mem::transmute(func_ptr) }; func(closure, arg0, arg1) } @@ -93,7 +93,7 @@ pub extern "C-unwind" fn js_closure_call2( /// Call a closure with 3 arguments, returning f64 #[no_mangle] -pub extern "C-unwind" fn js_closure_call3( +pub extern "C" fn js_closure_call3( closure: *const ClosureHeader, arg0: f64, arg1: f64, @@ -117,7 +117,7 @@ pub extern "C-unwind" fn js_closure_call3( dispatch_with_arity(closure, func_ptr, &[arg0, arg1, arg2], declared) }, _ => { - let func: extern "C-unwind" fn(*const ClosureHeader, f64, f64, f64) -> f64 = + let func: extern "C" fn(*const ClosureHeader, f64, f64, f64) -> f64 = unsafe { std::mem::transmute(func_ptr) }; func(closure, arg0, arg1, arg2) } @@ -126,7 +126,7 @@ pub extern "C-unwind" fn js_closure_call3( /// Call a closure with 4 arguments, returning f64 #[no_mangle] -pub extern "C-unwind" fn js_closure_call4( +pub extern "C" fn js_closure_call4( closure: *const ClosureHeader, arg0: f64, arg1: f64, @@ -157,7 +157,7 @@ pub extern "C-unwind" fn js_closure_call4( dispatch_with_arity(closure, func_ptr, &[arg0, arg1, arg2, arg3], declared) }, _ => { - let func: extern "C-unwind" fn(*const ClosureHeader, f64, f64, f64, f64) -> f64 = + let func: extern "C" fn(*const ClosureHeader, f64, f64, f64, f64) -> f64 = unsafe { std::mem::transmute(func_ptr) }; func(closure, arg0, arg1, arg2, arg3) } @@ -166,7 +166,7 @@ pub extern "C-unwind" fn js_closure_call4( /// Call a closure with 5 arguments, returning f64 #[no_mangle] -pub extern "C-unwind" fn js_closure_call5( +pub extern "C" fn js_closure_call5( closure: *const ClosureHeader, arg0: f64, arg1: f64, @@ -202,14 +202,14 @@ pub extern "C-unwind" fn js_closure_call5( }; } } - let func: extern "C-unwind" fn(*const ClosureHeader, f64, f64, f64, f64, f64) -> f64 = + let func: extern "C" fn(*const ClosureHeader, f64, f64, f64, f64, f64) -> f64 = unsafe { std::mem::transmute(func_ptr) }; func(closure, arg0, arg1, arg2, arg3, arg4) } /// Call a closure with 6 arguments, returning f64 #[no_mangle] -pub extern "C-unwind" fn js_closure_call6( +pub extern "C" fn js_closure_call6( closure: *const ClosureHeader, arg0: f64, arg1: f64, @@ -251,7 +251,7 @@ pub extern "C-unwind" fn js_closure_call6( }; } } - let func: extern "C-unwind" fn(*const ClosureHeader, f64, f64, f64, f64, f64, f64) -> f64 = + let func: extern "C" fn(*const ClosureHeader, f64, f64, f64, f64, f64, f64) -> f64 = unsafe { std::mem::transmute(func_ptr) }; func(closure, arg0, arg1, arg2, arg3, arg4, arg5) } @@ -291,7 +291,7 @@ pub(crate) fn dispatch_rest_or_declared_arity( /// Call a closure with 7 arguments, returning f64 #[no_mangle] -pub extern "C-unwind" fn js_closure_call7( +pub extern "C" fn js_closure_call7( closure: *const ClosureHeader, arg0: f64, arg1: f64, @@ -316,14 +316,14 @@ pub extern "C-unwind" fn js_closure_call7( if let Some(result) = dispatch_rest_or_declared_arity(closure, func_ptr, &args, 7) { return result; } - let func: extern "C-unwind" fn(*const ClosureHeader, f64, f64, f64, f64, f64, f64, f64) -> f64 = + let func: extern "C" fn(*const ClosureHeader, f64, f64, f64, f64, f64, f64, f64) -> f64 = unsafe { std::mem::transmute(func_ptr) }; func(closure, arg0, arg1, arg2, arg3, arg4, arg5, arg6) } /// Call a closure with 8 arguments, returning f64 #[no_mangle] -pub extern "C-unwind" fn js_closure_call8( +pub extern "C" fn js_closure_call8( closure: *const ClosureHeader, arg0: f64, arg1: f64, @@ -349,23 +349,14 @@ pub extern "C-unwind" fn js_closure_call8( if let Some(result) = dispatch_rest_or_declared_arity(closure, func_ptr, &args, 8) { return result; } - let func: extern "C-unwind" fn( - *const ClosureHeader, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - ) -> f64 = unsafe { std::mem::transmute(func_ptr) }; + let func: extern "C" fn(*const ClosureHeader, f64, f64, f64, f64, f64, f64, f64, f64) -> f64 = + unsafe { std::mem::transmute(func_ptr) }; func(closure, arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7) } /// Call a closure with 9 arguments, returning f64 #[no_mangle] -pub extern "C-unwind" fn js_closure_call9( +pub extern "C" fn js_closure_call9( closure: *const ClosureHeader, arg0: f64, arg1: f64, @@ -392,7 +383,7 @@ pub extern "C-unwind" fn js_closure_call9( if let Some(result) = dispatch_rest_or_declared_arity(closure, func_ptr, &args, 9) { return result; } - let func: extern "C-unwind" fn( + let func: extern "C" fn( *const ClosureHeader, f64, f64, @@ -411,7 +402,7 @@ pub extern "C-unwind" fn js_closure_call9( /// Call a closure with 10 arguments, returning f64 #[no_mangle] -pub extern "C-unwind" fn js_closure_call10( +pub extern "C" fn js_closure_call10( closure: *const ClosureHeader, arg0: f64, arg1: f64, @@ -439,7 +430,7 @@ pub extern "C-unwind" fn js_closure_call10( if let Some(result) = dispatch_rest_or_declared_arity(closure, func_ptr, &args, 10) { return result; } - let func: extern "C-unwind" fn( + let func: extern "C" fn( *const ClosureHeader, f64, f64, @@ -459,7 +450,7 @@ pub extern "C-unwind" fn js_closure_call10( /// Call a closure with 11 arguments, returning f64 #[no_mangle] -pub extern "C-unwind" fn js_closure_call11( +pub extern "C" fn js_closure_call11( closure: *const ClosureHeader, arg0: f64, arg1: f64, @@ -494,7 +485,7 @@ pub extern "C-unwind" fn js_closure_call11( if let Some(result) = dispatch_rest_or_declared_arity(closure, func_ptr, &args, 11) { return result; } - let func: extern "C-unwind" fn( + let func: extern "C" fn( *const ClosureHeader, f64, f64, @@ -515,7 +506,7 @@ pub extern "C-unwind" fn js_closure_call11( /// Call a closure with 12 arguments, returning f64 #[no_mangle] -pub extern "C-unwind" fn js_closure_call12( +pub extern "C" fn js_closure_call12( closure: *const ClosureHeader, arg0: f64, arg1: f64, @@ -551,7 +542,7 @@ pub extern "C-unwind" fn js_closure_call12( if let Some(result) = dispatch_rest_or_declared_arity(closure, func_ptr, &args, 12) { return result; } - let func: extern "C-unwind" fn( + let func: extern "C" fn( *const ClosureHeader, f64, f64, @@ -573,7 +564,7 @@ pub extern "C-unwind" fn js_closure_call12( /// Call a closure with 13 arguments, returning f64 #[no_mangle] -pub extern "C-unwind" fn js_closure_call13( +pub extern "C" fn js_closure_call13( closure: *const ClosureHeader, arg0: f64, arg1: f64, @@ -610,7 +601,7 @@ pub extern "C-unwind" fn js_closure_call13( if let Some(result) = dispatch_rest_or_declared_arity(closure, func_ptr, &args, 13) { return result; } - let func: extern "C-unwind" fn( + let func: extern "C" fn( *const ClosureHeader, f64, f64, @@ -633,7 +624,7 @@ pub extern "C-unwind" fn js_closure_call13( /// Call a closure with 14 arguments, returning f64 #[no_mangle] -pub extern "C-unwind" fn js_closure_call14( +pub extern "C" fn js_closure_call14( closure: *const ClosureHeader, arg0: f64, arg1: f64, @@ -672,7 +663,7 @@ pub extern "C-unwind" fn js_closure_call14( if let Some(result) = dispatch_rest_or_declared_arity(closure, func_ptr, &args, 14) { return result; } - let func: extern "C-unwind" fn( + let func: extern "C" fn( *const ClosureHeader, f64, f64, @@ -697,7 +688,7 @@ pub extern "C-unwind" fn js_closure_call14( /// Call a closure with 15 arguments, returning f64 #[no_mangle] -pub extern "C-unwind" fn js_closure_call15( +pub extern "C" fn js_closure_call15( closure: *const ClosureHeader, arg0: f64, arg1: f64, @@ -739,7 +730,7 @@ pub extern "C-unwind" fn js_closure_call15( if let Some(result) = dispatch_rest_or_declared_arity(closure, func_ptr, &args, 15) { return result; } - let func: extern "C-unwind" fn( + let func: extern "C" fn( *const ClosureHeader, f64, f64, @@ -765,7 +756,7 @@ pub extern "C-unwind" fn js_closure_call15( /// Call a closure with 16 arguments, returning f64 #[no_mangle] -pub extern "C-unwind" fn js_closure_call16( +pub extern "C" fn js_closure_call16( closure: *const ClosureHeader, arg0: f64, arg1: f64, @@ -808,7 +799,7 @@ pub extern "C-unwind" fn js_closure_call16( if let Some(result) = dispatch_rest_or_declared_arity(closure, func_ptr, &args, 16) { return result; } - let func: extern "C-unwind" fn( + let func: extern "C" fn( *const ClosureHeader, f64, f64, diff --git a/crates/perry-runtime/src/closure/dispatch/direct.rs b/crates/perry-runtime/src/closure/dispatch/direct.rs index 3370905f6e..bffbef9a3d 100644 --- a/crates/perry-runtime/src/closure/dispatch/direct.rs +++ b/crates/perry-runtime/src/closure/dispatch/direct.rs @@ -89,7 +89,7 @@ macro_rules! define_direct_call_site { ) => { $(#[$meta])* #[derive(Clone, Copy)] - pub struct $site(Option f64>); + pub struct $site(Option f64>); impl $site { /// Resolve `closure` once, before the loop. @@ -98,7 +98,7 @@ macro_rules! define_direct_call_site { $site(resolve_direct_func_ptr(closure, $arity).map(|func_ptr| unsafe { std::mem::transmute::< *const u8, - extern "C-unwind" fn(*const ClosureHeader, $(define_direct_call_site!(@f64 $arg)),+) -> f64, + extern "C" fn(*const ClosureHeader, $(define_direct_call_site!(@f64 $arg)),+) -> f64, >(func_ptr) })) } @@ -174,11 +174,11 @@ mod tests { // A capture-less body behind a real `ClosureHeader`, the same way // `array/tests.rs` and `array/typed_array_receiver_tests.rs` build theirs. - extern "C-unwind" fn add3(_c: *const ClosureHeader, a: f64, b: f64, c: f64) -> f64 { + extern "C" fn add3(_c: *const ClosureHeader, a: f64, b: f64, c: f64) -> f64 { a * 100.0 + b * 10.0 + c } - extern "C-unwind" fn sum2(_c: *const ClosureHeader, a: f64, b: f64) -> f64 { + extern "C" fn sum2(_c: *const ClosureHeader, a: f64, b: f64) -> f64 { a + b } diff --git a/crates/perry-runtime/src/closure/dispatch/value_call.rs b/crates/perry-runtime/src/closure/dispatch/value_call.rs index f5ad0bc112..111450546b 100644 --- a/crates/perry-runtime/src/closure/dispatch/value_call.rs +++ b/crates/perry-runtime/src/closure/dispatch/value_call.rs @@ -390,7 +390,7 @@ pub unsafe extern "C-unwind" fn js_native_call_value( /// rather than xmm0 — matching the trampoline's `extern "C"` int-arg /// expectation. #[no_mangle] -pub unsafe extern "C-unwind" fn js_closure_call_array( +pub unsafe extern "C" fn js_closure_call_array( closure_env: i64, args_ptr: *const f64, args_len: i64, @@ -661,7 +661,7 @@ pub unsafe extern "C-unwind" fn js_closure_call_array( /// `lower_expr` produces for a closure-typed expression). A null/undefined /// box returns TAG_UNDEFINED. #[no_mangle] -pub unsafe extern "C-unwind" fn js_closure_call_apply_with_spread( +pub unsafe extern "C" fn js_closure_call_apply_with_spread( closure_box: f64, regular_args: *const f64, regular_count: i64, diff --git a/crates/perry-runtime/src/closure/registry.rs b/crates/perry-runtime/src/closure/registry.rs index 935a98c586..1f2a44c229 100644 --- a/crates/perry-runtime/src/closure/registry.rs +++ b/crates/perry-runtime/src/closure/registry.rs @@ -954,15 +954,14 @@ pub unsafe fn dispatch_with_arity( macro_rules! arm { (@ty $i:tt) => { f64 }; ($($i:tt),* $(,)?) => {{ - let f: extern "C-unwind" fn(*const ClosureHeader $(, arm!(@ty $i))*) -> f64 = + let f: extern "C" fn(*const ClosureHeader $(, arm!(@ty $i))*) -> f64 = std::mem::transmute(func_ptr); f(closure $(, a!($i))*) }}; } match k { 0 => { - let f: extern "C-unwind" fn(*const ClosureHeader) -> f64 = - std::mem::transmute(func_ptr); + let f: extern "C" fn(*const ClosureHeader) -> f64 = std::mem::transmute(func_ptr); f(closure) } 1 => arm!(0), diff --git a/crates/perry/tests/bun_ffi_stage1.rs b/crates/perry/tests/bun_ffi_stage1.rs index 9d0e9a072b..b89c678d38 100644 --- a/crates/perry/tests/bun_ffi_stage1.rs +++ b/crates/perry/tests/bun_ffi_stage1.rs @@ -1,15 +1,13 @@ -// #8463 / #8478: these fixtures exercise JS exceptions unwinding out of the -// FFI call path. Two layers must BOTH be `extern "C-unwind"`: -// * the closure dispatch family (`js_closure_call*`, `dispatch_with_arity`) -// — the callers, converted in #8464; -// * `bun_ffi::dlopen`'s `sym_thunk_*` / `close_thunk` — the callees, which -// are where `tier1_every_ffi_type_against_test_dylib`'s use-after-close -// throw actually originates (#8478). -// A single plain `extern "C"` edge anywhere on that path turns the throw into -// `panic in a function that cannot unwind` and aborts the binary on Linux. -// macOS does not reproduce it, so the Linux `e2e-scoped` run of this suite is -// the only real verdict — naming this file in a runtime diff is what opts the -// suite into that job. Do not merge a fix here before it has reported. +// #8479: `tier1_every_ffi_type_against_test_dylib` aborts on Linux with +// "panic in a function that cannot unwind" at the use-after-close throw +// (`lib.close()` then calling a symbol). The frame carrying the nounwind +// guard is STILL UNIDENTIFIED: converting the closure dispatch family +// (#8464) did not fix it and measurably regressed main, and converting +// `bun_ffi::dlopen`'s thunks (#8480) did not fix it either. macOS does not +// reproduce it at all, so the Linux `e2e-scoped` run of this suite is the +// only verdict that counts — naming this file in a runtime diff is what +// opts the suite into that job, and a candidate fix must not be merged +// before that job has reported green. //! bun:ffi stages 1-2 (#6562) — e2e: compile TS that dlopens real C-ABI //! dylibs and drive them through the typed call stubs. //! From 24bd177045a5e071bd2c8843aa36301deefcd0cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Thu, 20 Aug 2026 15:34:44 +0200 Subject: [PATCH 2/3] docs(changelog): fragment for the #8464 revert --- changelog.d/8482-revert-c-unwind-family.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/8482-revert-c-unwind-family.md diff --git a/changelog.d/8482-revert-c-unwind-family.md b/changelog.d/8482-revert-c-unwind-family.md new file mode 100644 index 0000000000..5db4441914 --- /dev/null +++ b/changelog.d/8482-revert-c-unwind-family.md @@ -0,0 +1 @@ +revert: back out #8464's blanket `extern "C-unwind"` conversion of the closure dispatch family. Measured on three sweeps: main carried **16** gap regressions both before the batch (`526e0b502`) and with the string PRs but without it (`3627657c7`), and **36** with it (`15a30d7f6`) — plus a newly-red `gc-stress`. It also did not fix the abort it was written for (#8479 still reproduces on Linux with the change in place). Converting a frame to `C-unwind` turns LLVM `call` into `invoke` and lets unwinding actually run through frames that were never designed to be unwound through — which holds locks, raw pointers and GC state — so the conversion cost stability without buying the fix. From 3f21878687872120421b0dd0c5e09d3def625b1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Thu, 20 Aug 2026 15:36:00 +0200 Subject: [PATCH 3/3] test(ffi): ask the child binary for a backtrace on the use-after-close abort Two candidate fixes for #8479 (#8464's dispatch family, #8480's bun:ffi thunks) both left tier1_every_ffi_type_against_test_dylib aborting on Linux with 'panic in a function that cannot unwind', and the message does not name the frame that carries the nounwind guard. macOS does not reproduce it. Carried on this branch because it already touches the suite, so the same e2e-scoped run that measures the revert also captures the backtrace. --- crates/perry/tests/bun_ffi_stage1.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/perry/tests/bun_ffi_stage1.rs b/crates/perry/tests/bun_ffi_stage1.rs index b89c678d38..489145985b 100644 --- a/crates/perry/tests/bun_ffi_stage1.rs +++ b/crates/perry/tests/bun_ffi_stage1.rs @@ -51,6 +51,12 @@ fn compile_and_run(dir: &Path, entry: &Path, envs: &[(&str, &str)]) -> (bool, St String::from_utf8_lossy(&compile.stderr) ); let mut run = Command::new(&output); + // #8479 diagnostic: the use-after-close abort reproduces only on Linux + // and the panic message alone does not name the frame carrying the + // nounwind guard — two candidate fixes (#8464, #8480) both left it + // aborting. Ask the child for a backtrace so CI names the frame instead + // of us guessing a third time. + run.env("RUST_BACKTRACE", "full"); for (k, v) in envs { run.env(k, v); }