From 317e81379995b7e089b5a23b60a260cb93da7109 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Thu, 20 Aug 2026 08:41:58 +0200 Subject: [PATCH 1/2] fix(runtime): extend #8416's C-unwind to the whole closure dispatch family MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #8416 let dynamic Function errors unwind, but converted only js_closure_call1 (and js_closure_call2's outer signature). Every other dispatch edge kept plain extern "C", whose abort-on-unwind shim turns the very unwind #8416 introduced into 'panic in a function that cannot unwind' — the nightly bun_ffi_stage1 abort on Linux (run 32332070993). Convert the family: js_closure_call0..N outer signatures and inner transmuted fn types, dispatch_with_arity's per-arity transmutes (the common arity-adjusted path), js_closure_call_array, js_closure_call_apply_with_spread, the direct-call-site cache, and js_function_bind. Registration entry points (js_register_closure_*) stay extern "C" — no user code runs under them. Validated on macOS: 64 closure unit tests, all 3 bun_ffi_stage1 tests, and the #8416 regression suite green. The Linux abort itself is verified by this PR's scoped CI (the suite file is named in the diff). Fixes #8463 --- .../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 | 6 ++ 6 files changed, 58 insertions(+), 42 deletions(-) diff --git a/crates/perry-runtime/src/closure/dispatch/bound.rs b/crates/perry-runtime/src/closure/dispatch/bound.rs index 69664b7c2c3..9066f86a1d8 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" fn js_function_bind( +pub unsafe extern "C-unwind" fn js_function_bind( target_value: f64, args_ptr: *const f64, args_len: usize, @@ -489,7 +489,7 @@ pub unsafe extern "C" 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" fn(f64, *const f64, usize) -> f64 = +static KEEP_JS_FUNCTION_BIND: unsafe extern "C-unwind" 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 22a50b8f6d9..b8032eb9385 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" fn js_closure_call0(closure: *const ClosureHeader) -> f64 { +pub extern "C-unwind" 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" fn js_closure_call0(closure: *const ClosureHeader) -> f64 { dispatch_with_arity(closure, func_ptr, &[], declared) }, _ => { - let func: extern "C" fn(*const ClosureHeader) -> f64 = + let func: extern "C-unwind" 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" fn(*const ClosureHeader, f64, f64) -> f64 = + let func: extern "C-unwind" 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" fn js_closure_call3( +pub extern "C-unwind" fn js_closure_call3( closure: *const ClosureHeader, arg0: f64, arg1: f64, @@ -117,7 +117,7 @@ pub extern "C" fn js_closure_call3( dispatch_with_arity(closure, func_ptr, &[arg0, arg1, arg2], declared) }, _ => { - let func: extern "C" fn(*const ClosureHeader, f64, f64, f64) -> f64 = + let func: extern "C-unwind" 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" fn js_closure_call3( /// Call a closure with 4 arguments, returning f64 #[no_mangle] -pub extern "C" fn js_closure_call4( +pub extern "C-unwind" fn js_closure_call4( closure: *const ClosureHeader, arg0: f64, arg1: f64, @@ -157,7 +157,7 @@ pub extern "C" fn js_closure_call4( dispatch_with_arity(closure, func_ptr, &[arg0, arg1, arg2, arg3], declared) }, _ => { - let func: extern "C" fn(*const ClosureHeader, f64, f64, f64, f64) -> f64 = + let func: extern "C-unwind" 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" fn js_closure_call4( /// Call a closure with 5 arguments, returning f64 #[no_mangle] -pub extern "C" fn js_closure_call5( +pub extern "C-unwind" fn js_closure_call5( closure: *const ClosureHeader, arg0: f64, arg1: f64, @@ -202,14 +202,14 @@ pub extern "C" fn js_closure_call5( }; } } - let func: extern "C" fn(*const ClosureHeader, f64, f64, f64, f64, f64) -> f64 = + let func: extern "C-unwind" 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" fn js_closure_call6( +pub extern "C-unwind" fn js_closure_call6( closure: *const ClosureHeader, arg0: f64, arg1: f64, @@ -251,7 +251,7 @@ pub extern "C" fn js_closure_call6( }; } } - let func: extern "C" fn(*const ClosureHeader, f64, f64, f64, f64, f64, f64) -> f64 = + let func: extern "C-unwind" 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" fn js_closure_call7( +pub extern "C-unwind" fn js_closure_call7( closure: *const ClosureHeader, arg0: f64, arg1: f64, @@ -316,14 +316,14 @@ pub extern "C" fn js_closure_call7( if let Some(result) = dispatch_rest_or_declared_arity(closure, func_ptr, &args, 7) { return result; } - let func: extern "C" fn(*const ClosureHeader, f64, f64, f64, f64, f64, f64, f64) -> f64 = + let func: extern "C-unwind" 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" fn js_closure_call8( +pub extern "C-unwind" fn js_closure_call8( closure: *const ClosureHeader, arg0: f64, arg1: f64, @@ -349,14 +349,23 @@ pub extern "C" fn js_closure_call8( if let Some(result) = dispatch_rest_or_declared_arity(closure, func_ptr, &args, 8) { return result; } - let func: extern "C" fn(*const ClosureHeader, f64, f64, f64, f64, f64, f64, f64, f64) -> f64 = - unsafe { std::mem::transmute(func_ptr) }; + let func: extern "C-unwind" 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" fn js_closure_call9( +pub extern "C-unwind" fn js_closure_call9( closure: *const ClosureHeader, arg0: f64, arg1: f64, @@ -383,7 +392,7 @@ pub extern "C" fn js_closure_call9( if let Some(result) = dispatch_rest_or_declared_arity(closure, func_ptr, &args, 9) { return result; } - let func: extern "C" fn( + let func: extern "C-unwind" fn( *const ClosureHeader, f64, f64, @@ -402,7 +411,7 @@ pub extern "C" fn js_closure_call9( /// Call a closure with 10 arguments, returning f64 #[no_mangle] -pub extern "C" fn js_closure_call10( +pub extern "C-unwind" fn js_closure_call10( closure: *const ClosureHeader, arg0: f64, arg1: f64, @@ -430,7 +439,7 @@ pub extern "C" fn js_closure_call10( if let Some(result) = dispatch_rest_or_declared_arity(closure, func_ptr, &args, 10) { return result; } - let func: extern "C" fn( + let func: extern "C-unwind" fn( *const ClosureHeader, f64, f64, @@ -450,7 +459,7 @@ pub extern "C" fn js_closure_call10( /// Call a closure with 11 arguments, returning f64 #[no_mangle] -pub extern "C" fn js_closure_call11( +pub extern "C-unwind" fn js_closure_call11( closure: *const ClosureHeader, arg0: f64, arg1: f64, @@ -485,7 +494,7 @@ pub extern "C" fn js_closure_call11( if let Some(result) = dispatch_rest_or_declared_arity(closure, func_ptr, &args, 11) { return result; } - let func: extern "C" fn( + let func: extern "C-unwind" fn( *const ClosureHeader, f64, f64, @@ -506,7 +515,7 @@ pub extern "C" fn js_closure_call11( /// Call a closure with 12 arguments, returning f64 #[no_mangle] -pub extern "C" fn js_closure_call12( +pub extern "C-unwind" fn js_closure_call12( closure: *const ClosureHeader, arg0: f64, arg1: f64, @@ -542,7 +551,7 @@ pub extern "C" fn js_closure_call12( if let Some(result) = dispatch_rest_or_declared_arity(closure, func_ptr, &args, 12) { return result; } - let func: extern "C" fn( + let func: extern "C-unwind" fn( *const ClosureHeader, f64, f64, @@ -564,7 +573,7 @@ pub extern "C" fn js_closure_call12( /// Call a closure with 13 arguments, returning f64 #[no_mangle] -pub extern "C" fn js_closure_call13( +pub extern "C-unwind" fn js_closure_call13( closure: *const ClosureHeader, arg0: f64, arg1: f64, @@ -601,7 +610,7 @@ pub extern "C" fn js_closure_call13( if let Some(result) = dispatch_rest_or_declared_arity(closure, func_ptr, &args, 13) { return result; } - let func: extern "C" fn( + let func: extern "C-unwind" fn( *const ClosureHeader, f64, f64, @@ -624,7 +633,7 @@ pub extern "C" fn js_closure_call13( /// Call a closure with 14 arguments, returning f64 #[no_mangle] -pub extern "C" fn js_closure_call14( +pub extern "C-unwind" fn js_closure_call14( closure: *const ClosureHeader, arg0: f64, arg1: f64, @@ -663,7 +672,7 @@ pub extern "C" fn js_closure_call14( if let Some(result) = dispatch_rest_or_declared_arity(closure, func_ptr, &args, 14) { return result; } - let func: extern "C" fn( + let func: extern "C-unwind" fn( *const ClosureHeader, f64, f64, @@ -688,7 +697,7 @@ pub extern "C" fn js_closure_call14( /// Call a closure with 15 arguments, returning f64 #[no_mangle] -pub extern "C" fn js_closure_call15( +pub extern "C-unwind" fn js_closure_call15( closure: *const ClosureHeader, arg0: f64, arg1: f64, @@ -730,7 +739,7 @@ pub extern "C" fn js_closure_call15( if let Some(result) = dispatch_rest_or_declared_arity(closure, func_ptr, &args, 15) { return result; } - let func: extern "C" fn( + let func: extern "C-unwind" fn( *const ClosureHeader, f64, f64, @@ -756,7 +765,7 @@ pub extern "C" fn js_closure_call15( /// Call a closure with 16 arguments, returning f64 #[no_mangle] -pub extern "C" fn js_closure_call16( +pub extern "C-unwind" fn js_closure_call16( closure: *const ClosureHeader, arg0: f64, arg1: f64, @@ -799,7 +808,7 @@ pub extern "C" fn js_closure_call16( if let Some(result) = dispatch_rest_or_declared_arity(closure, func_ptr, &args, 16) { return result; } - let func: extern "C" fn( + let func: extern "C-unwind" 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 bffbef9a3d9..3370905f6ee 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" fn(*const ClosureHeader, $(define_direct_call_site!(@f64 $arg)),+) -> f64, + extern "C-unwind" 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" fn add3(_c: *const ClosureHeader, a: f64, b: f64, c: f64) -> f64 { + extern "C-unwind" fn add3(_c: *const ClosureHeader, a: f64, b: f64, c: f64) -> f64 { a * 100.0 + b * 10.0 + c } - extern "C" fn sum2(_c: *const ClosureHeader, a: f64, b: f64) -> f64 { + extern "C-unwind" 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 111450546bc..f5ad0bc1124 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" fn js_closure_call_array( +pub unsafe extern "C-unwind" fn js_closure_call_array( closure_env: i64, args_ptr: *const f64, args_len: i64, @@ -661,7 +661,7 @@ pub unsafe extern "C" 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" fn js_closure_call_apply_with_spread( +pub unsafe extern "C-unwind" 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 1f2a44c2297..935a98c5862 100644 --- a/crates/perry-runtime/src/closure/registry.rs +++ b/crates/perry-runtime/src/closure/registry.rs @@ -954,14 +954,15 @@ pub unsafe fn dispatch_with_arity( macro_rules! arm { (@ty $i:tt) => { f64 }; ($($i:tt),* $(,)?) => {{ - let f: extern "C" fn(*const ClosureHeader $(, arm!(@ty $i))*) -> f64 = + let f: extern "C-unwind" fn(*const ClosureHeader $(, arm!(@ty $i))*) -> f64 = std::mem::transmute(func_ptr); f(closure $(, a!($i))*) }}; } match k { 0 => { - let f: extern "C" fn(*const ClosureHeader) -> f64 = std::mem::transmute(func_ptr); + let f: extern "C-unwind" 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 4774ca3615b..6930aabebb8 100644 --- a/crates/perry/tests/bun_ffi_stage1.rs +++ b/crates/perry/tests/bun_ffi_stage1.rs @@ -1,3 +1,9 @@ +// #8463: these fixtures exercise JS exceptions unwinding through the closure +// dispatch family (`js_closure_call*`, `dispatch_with_arity`). After #8416 the +// whole family must be `extern "C-unwind"` — a single plain `extern "C"` edge +// turns a deferred Function error into `panic in a function that cannot +// unwind` and aborts the binary on Linux. Naming this file in a runtime diff +// also opts the suite into the PR tier's e2e-scoped job. //! bun:ffi stages 1-2 (#6562) — e2e: compile TS that dlopens real C-ABI //! dylibs and drive them through the typed call stubs. //! From 6214b30bab3985490f07cd644c90b35ab7a7dc7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Thu, 20 Aug 2026 08:43:26 +0200 Subject: [PATCH 2/2] docs(changelog): fragment for #8464 --- changelog.d/8464-closure-unwind-family.md | 1 + 1 file changed, 1 insertion(+) create 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 new file mode 100644 index 00000000000..3f649fbacea --- /dev/null +++ b/changelog.d/8464-closure-unwind-family.md @@ -0,0 +1 @@ +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"`.