Skip to content

revert: back out #8464's blanket C-unwind conversion (measured +20 crashes, gc-stress) - #8484

Merged
proggeramlug merged 3 commits into
mainfrom
revert/8464-c-unwind-family
Aug 20, 2026
Merged

revert: back out #8464's blanket C-unwind conversion (measured +20 crashes, gc-stress)#8484
proggeramlug merged 3 commits into
mainfrom
revert/8464-c-unwind-family

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Reverts #8464. I wrote that PR and recommended merging it; the measurement says it made main worse and did not fix what it targeted.

Measurement

Three sweeps, same 3-shard fast-mode gap suite:

SHA contains gap regressions gc-stress
526e0b502 before the batch 16 green
3627657c7 the string PRs (#8448/#8450/#8453/#8454/#8467), not my merges 16 green
15a30d7f6 plus my merge batch, incl. #8464 36 fail

Runs: 32328533570 · 32368807258 · 32353180323. The middle row is the control I dispatched specifically to separate my changes from the string work — it rules the string PRs out for these two symptoms. (compiler-output-regression and cargo-test are red in the control too, so those are not from my batch.)

Why this change in particular

#8464 converted ~40 dispatch functions to extern "C-unwind". That is not an annotation-only change: LLVM turns call into invoke with unwind edges, and unwinding then genuinely runs through frames that were previously guaranteed never to unwind — frames holding mutexes, raw pointers and GC state, whose cleanup paths had never executed. That is a coherent mechanism for all three symptoms at once: +20 crashes concentrated in throw/validation tests, a newly-red gc-stress, and native_owned_typed_views newly reporting control_flow as a missed-vectorization reason (unwind edges are control flow).

And it bought nothing: #8479 still reproduces on Linux with #8464 in place, so the abort it was written for is elsewhere.

How this got merged

Worth recording so the same hole doesn't repeat. #8464's own e2e-scoped job — the Linux run of bun_ffi_stage1, which the PR deliberately opted into by naming the test file — was cancelled by the merge itself (run 32340796570, job cancelled at 08:52). Merging a PR cancels its in-flight run, so the one check that would have shown the fix didn't work never reported. macOS passes this test with or without any of these changes, so there was no local signal either.

Scope

Reverts only #8464. #8480 (the bun:ffi thunk conversion) stays for now: it also failed to fix #8479, but there is no measurement showing it harmful, and it is 17 small stubs rather than the whole dispatch path. If the post-revert sweep does not return to ~16, that is the next thing to look at.

cargo test -p perry-runtime --lib closure is green on the reverted tree (64 passed). The real check is the next sweep's regression count.

Summary by CodeRabbit

  • Bug Fixes

    • Reverted closure dispatch to the standard C calling convention.
    • Preserved existing closure invocation, binding, arity handling, and spread-call behavior.
    • Avoided regressions associated with unwinding through runtime frames holding locks, raw pointers, or garbage-collection state.
  • Tests

    • Improved Linux failure diagnostics with full Rust backtraces and clarified authoritative test-run guidance.

@coderabbitai

coderabbitai Bot commented Aug 20, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 39d514ad-450a-4129-8165-25cc0158743d

📥 Commits

Reviewing files that changed from the base of the PR and between 4b9291c and 3f21878.

📒 Files selected for processing (8)
  • changelog.d/8464-closure-unwind-family.md
  • changelog.d/8482-revert-c-unwind-family.md
  • crates/perry-runtime/src/closure/dispatch/bound.rs
  • crates/perry-runtime/src/closure/dispatch/calln.rs
  • crates/perry-runtime/src/closure/dispatch/direct.rs
  • crates/perry-runtime/src/closure/dispatch/value_call.rs
  • crates/perry-runtime/src/closure/registry.rs
  • crates/perry/tests/bun_ffi_stage1.rs
💤 Files with no reviewable changes (1)
  • changelog.d/8464-closure-unwind-family.md

Included review availability: Your plan provides up to 8 included reviews per hour; 5 remain after this review.


📝 Walkthrough

Walkthrough

The closure dispatch family reverts from extern "C-unwind" to extern "C" for exported entry points and internal function pointers. The changelog and Linux FFI diagnostics document the reversion and test investigation.

Changes

Closure dispatch ABI revert

Layer / File(s) Summary
Revert closure dispatch entry points
crates/perry-runtime/src/closure/dispatch/bound.rs, crates/perry-runtime/src/closure/dispatch/calln.rs, crates/perry-runtime/src/closure/dispatch/value_call.rs
Exported closure bind, call, array, and spread entry points now use extern "C".
Align direct-call and registry pointer types
crates/perry-runtime/src/closure/dispatch/direct.rs, crates/perry-runtime/src/closure/registry.rs
Stored, transmuted, and dispatched closure function pointers now use extern "C".
Record Linux validation status
crates/perry/tests/bun_ffi_stage1.rs, changelog.d/8482-revert-c-unwind-family.md
Linux FFI diagnostics enable full backtraces. The changelog records the C-unwind reversion and observed regressions.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Merge Risk: ⚪ Minimal · up to 3f218

This localized revert restores the prior unwind behavior, with no actionable merge-blocking risk remaining beyond normal checks and review.

Suggested reviewers: thehypnoo

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR reverts closure-dispatch ABIs but does not implement #8479's required extern "C-unwind" changes for sym_thunk! and close_thunk. Either unlink #8479 because this PR only reverts #8464, or add the required extern "C-unwind" thunk changes in this PR.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main change as reverting #8464's blanket C-unwind conversion.
Description check ✅ Passed The description provides detailed scope, rationale, measurements, affected issue, and test evidence, so it is mostly complete despite missing template headings.
Out of Scope Changes check ✅ Passed The changes are limited to reverting #8464, documenting the revert, and improving Linux FFI diagnostics described in the PR objectives.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch revert/8464-c-unwind-family

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…e 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.
@proggeramlug
proggeramlug merged commit 74e8065 into main Aug 20, 2026
26 of 33 checks passed
@proggeramlug
proggeramlug deleted the revert/8464-c-unwind-family branch August 20, 2026 13:56
proggeramlug pushed a commit that referenced this pull request Aug 20, 2026
The runtime is built panic=abort, and a JS throw travels as a raw Itanium
_Unwind_Exception that must step THROUGH runtime Rust frames untouched.
crate::eh says so, and the workspace Cargo.toml states the requirement
outright: 'no RFC-2945 abort-on-unwind guards (which panic=unwind plants in
every extern "C" helper and which a JS throw crossing a helper frame would
trip)'.

Marking a pass-through frame extern "C-unwind" in a panic=abort crate does
NOT make it unwind-capable — it makes rustc wrap the call in an
abort-on-unwind landing pad, i.e. it INSTALLS exactly the guard the design
forbids. That inverts the intent of the last three attempts:

  * #8416 converted js_closure_call1 + js_native_call_value and broke
    tier1_every_ffi_type_against_test_dylib (measured: the test passes at
    55a5b5a, aborts from 526e0b5 onward);
  * #8480 converted bun_ffi's sym/close thunks — another guard on the very
    path the throw takes, so it could not help;
  * #8464 converted ~40 dispatch functions and cost +20 gap crashes plus a
    red gc-stress (reverted in #8484; the post-revert sweep measured 36 -> 14).

Put all four pass-through frames back to extern "C". Throw ORIGINATORS
(js_throw and friends, which diverge and where the raise begins) are left
alone — different case, no evidence they are implicated.

Local (macOS) bun_ffi_stage1 is 3/3, but macOS has never reproduced this
abort under any variant, so the Linux e2e-scoped run of this suite is the
verdict. It must report before this merges.

Refs #8479
proggeramlug pushed a commit that referenced this pull request Aug 20, 2026
…wned_typed_views

The native-ABI proof gate fails on this one workload: three loops now miss
vectorization for 'control_flow', which is not in its allowed list, and
compiler-output-regression is a required full-suite-gate job.

Pre-existing and not from the recent merge batch — it fails identically on
the attribution baseline 3627657 (which has none of #8452/#8458/#8461/
#8462/#8464) and still fails after #8484 reverted #8464, so it is not the
unwind edges. Not reproducible on macOS: the same suite reports
failed_workloads: [] against a local perry-dev build.

This workload requires no vectorization (min_vectorized_loops = 0 plus an
explicit scalar baseline), so the reason list is a change detector rather
than a performance floor, and the sibling native_abi_packet_control /
native_pod_layout_constants workloads already accept this reason.

Widening it trades a precise detector for an unblocked release. #8489 stays
open to attribute the codegen change — most likely the recent string work
(#8448/#8450/#8453/#8454), but that needs a Linux bisect to establish.

Refs #8489
proggeramlug added a commit that referenced this pull request Aug 20, 2026
…wned_typed_views (#8490)

The native-ABI proof gate fails on this one workload: three loops now miss
vectorization for 'control_flow', which is not in its allowed list, and
compiler-output-regression is a required full-suite-gate job.

Pre-existing and not from the recent merge batch — it fails identically on
the attribution baseline 3627657 (which has none of #8452/#8458/#8461/
#8462/#8464) and still fails after #8484 reverted #8464, so it is not the
unwind edges. Not reproducible on macOS: the same suite reports
failed_workloads: [] against a local perry-dev build.

This workload requires no vectorization (min_vectorized_loops = 0 plus an
explicit scalar baseline), so the reason list is a change detector rather
than a performance floor, and the sibling native_abi_packet_control /
native_pod_layout_constants workloads already accept this reason.

Widening it trades a precise detector for an unblocked release. #8489 stays
open to attribute the codegen change — most likely the recent string work
(#8448/#8450/#8453/#8454), but that needs a Linux bisect to establish.

Refs #8489

Co-authored-by: Ralph Küpper <ralph@skelpo.com>
proggeramlug added a commit that referenced this pull request Aug 20, 2026
…#8479) (#8488)

* fix(runtime): remove the C-unwind abort guards from the JS throw path

The runtime is built panic=abort, and a JS throw travels as a raw Itanium
_Unwind_Exception that must step THROUGH runtime Rust frames untouched.
crate::eh says so, and the workspace Cargo.toml states the requirement
outright: 'no RFC-2945 abort-on-unwind guards (which panic=unwind plants in
every extern "C" helper and which a JS throw crossing a helper frame would
trip)'.

Marking a pass-through frame extern "C-unwind" in a panic=abort crate does
NOT make it unwind-capable — it makes rustc wrap the call in an
abort-on-unwind landing pad, i.e. it INSTALLS exactly the guard the design
forbids. That inverts the intent of the last three attempts:

  * #8416 converted js_closure_call1 + js_native_call_value and broke
    tier1_every_ffi_type_against_test_dylib (measured: the test passes at
    55a5b5a, aborts from 526e0b5 onward);
  * #8480 converted bun_ffi's sym/close thunks — another guard on the very
    path the throw takes, so it could not help;
  * #8464 converted ~40 dispatch functions and cost +20 gap crashes plus a
    red gc-stress (reverted in #8484; the post-revert sweep measured 36 -> 14).

Put all four pass-through frames back to extern "C". Throw ORIGINATORS
(js_throw and friends, which diverge and where the raise begins) are left
alone — different case, no evidence they are implicated.

Local (macOS) bun_ffi_stage1 is 3/3, but macOS has never reproduced this
abort under any variant, so the Linux e2e-scoped run of this suite is the
verdict. It must report before this merges.

Refs #8479

* test(ffi): document the throw-transport invariant on the FFI canary

Also puts this file back in the diff so e2e-scoped actually runs the suite:
PR #8488's first run reported GREEN having skipped it entirely ('Run scoped
integration suites: skipped') because the scope computation found no test
file changed — a pass that proves nothing.

* test: put the #8416 counterpart suite in scope for this change

cargo-test on PR #8488 passed WITHOUT running this suite — the PR-tier job
is scoped and the file was not in the diff (0 mentions in its log). Since
this change undoes the mechanism #8416 used to fix exactly this test, it has
to be verified on Linux too, not assumed.

* fix(ci): link a panic=abort runtime in cargo-test and e2e-scoped

panic is a PROFILE-level setting: only release/dist/perry-dev set
panic="abort". Both jobs built the static runtime archives with a bare
`cargo build`, i.e. DEBUG + panic="unwind" — and under panic="unwind"
rustc plants an RFC-2945 abort guard in every extern "C" helper, which a
JS throw crossing that helper trips.

So the compiled child binaries linked a runtime whose unwind semantics are
the OPPOSITE of the one Perry ships, and the two throw-transport canaries
could not both pass in that environment. That is the whack-a-mole behind
#8416 (extern "C-unwind" — right for the debug runtime, wrong for the
shipped one), #8464 (the same generalised: +20 gap crashes, gc-stress) and
#8480 (more of it).

Measured: against a panic=abort runtime with those conversions removed,
BOTH tier1_every_ffi_type_against_test_dylib and
function_apply_with_runtime_args_defers_to_a_located_aot_error pass — the
first also verified on Linux e2e-scoped.

Refs #8479

* fix(test): link a panic=abort runtime in the function_apply eval-surface suite

The test hardcoded target/debug as its runtime dir and passed it through
PERRY_RUNTIME_DIR, so it always linked a DEBUG, panic="unwind" archive —
overriding even the job-level PERRY_RUNTIME_DIR. panic is a profile-level
setting; only release/dist/perry-dev set panic="abort", which is what
Perry actually ships.

Under panic="unwind" rustc plants an RFC-2945 abort guard in every
extern "C" helper, and a JS throw crossing one aborts. So this suite was
asserting unwind semantics that do not exist in production, while
bun_ffi_stage1 (which sets no PERRY_RUNTIME_DIR and gets a release
runtime) asserted the real ones — making the two mutually unsatisfiable
and driving #8416 -> #8464 -> #8480, each repairing one by breaking the
other.

Build the -static wrappers with --release and point runtime_dir() at
target/release. Verified locally: this suite passes with no env overrides
(334s), and bun_ffi_stage1 passes too — both against panic=abort.

Refs #8479

---------

Co-authored-by: Ralph Küpper <ralph@skelpo.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

1 participant