Skip to content

ffi: bun:ffi symbol thunks are extern "C" — a use-after-close throw aborts on Linux (cargo-test still red after #8464) #8479

Description

@proggeramlug

cargo-test still fails on main after #8464: tier1_every_ffi_type_against_test_dylib aborts on Linux with

thread '<unnamed>' panicked at library/core/src/panicking.rs:225:5:
panic in a function that cannot unwind
thread caused non-unwinding panic. aborting.

Measured on the scheduled sweep of 15a30d7f6 (run 32353180323), which contains #8464 — so the closure-dispatch conversion did not fix this.

Where it actually aborts. The captured stdout stops right after external-terminated: 2 and never reaches closed-throws: — the step that deliberately throws:

lib.close();
try { s.ffi_i32_add1(1); } catch (e: any) { closedError = String(e && e.message); }
console.log("closed-throws:", closedError.includes("close()"));

That throw originates in bun_ffi::dlopen::invoke_from_closure, which calls throw_error_with_code("bun:ffi: symbol … was called after close() …"). It is reached through the per-arity stubs generated by sym_thunk!:

extern "C" fn $name(closure: *const ClosureHeader $(, $a: f64)*) -> f64 {
    unsafe { invoke_from_closure(closure, &args) }
}

Those stubs are extern "C", i.e. nounwind, so the unwind hits Rust's abort-on-unwind shim inside the thunk itself. #8464 made the closure-dispatch callers unwind-capable; these thunks are the callees it could not see — a call through an extern "C-unwind" function pointer does not remove the callee's own nounwind guard.

close_thunk has the same shape: it takes a Mutex and unwrap()s, and a poison panic there would abort identically.

Fix: generate both as extern "C-unwind" (PR #8479).

Process note worth recording. #8464 was merged while its own e2e-scoped job — the Linux run of exactly this suite, which the PR deliberately opted into by naming the test file — was still in flight; merging cancelled it (run 32340796570, job cancelled at 08:52). The check that would have caught this was destroyed by the merge. macOS passes this test with and without either fix, so the Linux e2e-scoped result is the only verdict, and it must be allowed to report before merging.

Activity

  1. added 2 commits that reference this issue on Aug 20, 2026
  2. proggeramlug commented on Aug 20, 2026

    @proggeramlug
    ContributorAuthor

    Status update — two failed theories, and the scope is now clear.

    This abort predates the recent merge batch. It reproduces identically on the attribution baseline 3627657c7 (run 32368807258), which contains none of #8452/#8458/#8461/#8462/#8464. Comparing cargo-test across full-tier runs, it appeared between 55a5b5a06 (where cargo-test failed on function_apply_… instead) and 526e0b502 — a window whose relevant change is #8416, the original 'let dynamic Function errors unwind' fix.

    Two candidate fixes have now been tried and both failed on Linux:

    A RUST_BACKTRACE=full run is also not sufficient on its own: the perry-compiled child is stripped, so every frame symbolizes to <unknown>. The one useful signal from it is that frame 18 is __libc_start_main — this is the main execution path, roughly 19 frames deep, not a background thread.

    What would actually identify the frame (for whoever picks this up):

    1. compile the fixture with symbols retained so the backtrace resolves — that is the cheapest decisive step;
    2. or bisect the ABI: with fix: preserve deferred Function errors on Linux #8416's two conversions reverted, does tier1 pass and function_apply_… fail again? That would confirm fix: preserve deferred Function errors on Linux #8416 as the origin and frame the real question as 'which frame between the throw and the catch is nounwind'.

    Note for anyone attempting a fix: macOS passes this test with and without every variant tried so far, so a green local run proves nothing. The Linux e2e-scoped job is the only verdict, and it must be allowed to report before merging — #8464 was merged while exactly that job was in flight and the merge cancelled it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions