Skip to content

Restore a native call's copied first argument on unwind - #1189

Merged
schungx merged 1 commit into
rhaiscript:mainfrom
yuvalrakavy:fix-native-call-restore-on-unwind
Oct 17, 2026
Merged

schungx merged 1 commit into
rhaiscript:mainfrom
yuvalrakavy:fix-native-call-restore-on-unwind

Conversation

@yuvalrakavy

@yuvalrakavy yuvalrakavy commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Fixes the abort reported in #1188. The sync panic behind it is split out into #1190, as requested.

exec_native_fn_call swaps a copy of the first argument in through ArgBackup when a non-method native function is called with a variable first argument, and restored it only on the normal-return path. If the native function panics, the unwind drops the ArgBackup unrestored, its Drop assertion panics during the unwind, and the process aborts, so a host's catch_unwind cannot contain it.

let mut engine = Engine::new();
engine.register_fn("buggy", |_x: INT| -> INT { panic!("buggy native function") });
// Aborts the process today:
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| engine.eval::<INT>("let t = 1; buggy(t)")));

The fix restores through the same defer! guard the script-function path in func_call.rs already uses, which also runs on unwind. The assertion is kept.

Test: test_native_panic_with_variable_first_argument_unwinds in tests/native.rs. buggy(t) is the aborting case and runs under every feature combination; the method-call spelling t.buggy() is a control and is gated on no_object, which disables the dot syntax (that was the CI failure on the no_object rows). Without the fix the test aborts with SIGABRT; with it the panic is caught and the engine is usable afterwards.

Checks: cargo test with default features, cargo test --test native under the three no_object rows of the CI matrix, cargo +nightly fmt --check. CHANGELOG line under 1.27.0's bug fixes, linking this PR.

🤖 Generated with Claude Code

@schungx

schungx commented Oct 17, 2026

Copy link
Copy Markdown
Collaborator

Can you split this into two separate PRs just so we can track issues independently?

Also some CI's are failing.

Thanks for catching these!

Comment thread CHANGELOG.md Outdated
* Failed function resolutions are now cached properly for calls with other than two arguments - a regression from version 1.10 (thanks [`@ImTheSquid`](https://github.com/ImTheSquid) [`#1183`](https://github.com/rhaiscript/rhai/pull/1183)).
* A function resolution cache is now created (if none exists) when entering a block. Previously, a loop body evaluated with no cache yet would resolve every function call again on each iteration (thanks [`@ImTheSquid`](https://github.com/ImTheSquid) [`#1183`](https://github.com/rhaiscript/rhai/pull/1183)).
* Assigning a constant into a container variable (array, map etc.) now works properly without making the value read-only (thanks [`@cuervolu`](https://github.com/cuervolu) [`#1187`](https://github.com/rhaiscript/rhai/pull/1187)).
* A panic inside a native Rust function that takes its first argument by value, called with a variable first argument, now unwinds normally and can be caught by `catch_unwind`. Previously the copied first argument was restored only on the normal-return path, so the unwind tripped an internal assertion and the process aborted ([`#1188`](https://github.com/rhaiscript/rhai/issues/1188)).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better to link to this PR (#1189) than to the issue. Also good to add your username (similar style as others) as credit!

Comment thread CHANGELOG.md Outdated
* A function resolution cache is now created (if none exists) when entering a block. Previously, a loop body evaluated with no cache yet would resolve every function call again on each iteration (thanks [`@ImTheSquid`](https://github.com/ImTheSquid) [`#1183`](https://github.com/rhaiscript/rhai/pull/1183)).
* Assigning a constant into a container variable (array, map etc.) now works properly without making the value read-only (thanks [`@cuervolu`](https://github.com/cuervolu) [`#1187`](https://github.com/rhaiscript/rhai/pull/1187)).
* A panic inside a native Rust function that takes its first argument by value, called with a variable first argument, now unwinds normally and can be caught by `catch_unwind`. Previously the copied first argument was restored only on the normal-return path, so the unwind tripped an internal assertion and the process aborted ([`#1188`](https://github.com/rhaiscript/rhai/issues/1188)).
* Under `sync`, a method call on a shared variable with a closure argument that captures the same variable now raises `ErrorDataRace`, as it does without `sync`, instead of panicking while resolving the function. `Dynamic::is_locked` now returns `true` under `sync` when the value cannot be read, which includes being locked for write by the current thread ([`#1188`](https://github.com/rhaiscript/rhai/issues/1188)).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same

@yuvalrakavy
yuvalrakavy force-pushed the fix-native-call-restore-on-unwind branch from de75c2b to 377df35 Compare October 17, 2026 12:14
@yuvalrakavy yuvalrakavy changed the title Restore a native call's copied first argument on unwind; report self-locked shared values under sync Restore a native call's copied first argument on unwind Oct 17, 2026
@yuvalrakavy

Copy link
Copy Markdown
Contributor Author

Done, thanks for the quick look.

  • Split: this PR now carries only the restore-on-unwind fix (one commit, force-pushed). The is_locked-under-sync change is Report a shared value locked by its own thread as locked under sync #1190.
  • CI: both failures were the new tests' scripts under feature combinations that cannot run them, not the fixes. t.buggy() needs the dot syntax, which no_object disables (MissingToken(";") at parse, so nothing panicked for catch_unwind to see); the aborting case buggy(t) now runs under every row and the method-call spelling is gated. The data-race test needs closures, so it is gated on no_closure too. Both verified locally under the exact rows that failed.
  • NoStdBuild: undefined symbol: __rustc::rust_eh_personality while linking no_std_test. It builds on both main and this branch with nightly 2026-10-09 here, the branch touches nothing no_std-specific, and main's own run of 2026-10-13 was green, so it looks like the newer nightly now expects the __rustc-namespaced personality symbol that no_std_test/src/main.rs defines under the old name. If it fails on main for you as well I am happy to send a one-line PR for the harness.
  • CHANGELOG: both lines now link their PR and carry the credit, in the same style as the others.

@yuvalrakavy

Copy link
Copy Markdown
Contributor Author

NoStdBuild: it is the nightly, not this branch. rust-lang/rust#148413 (nightly 2026-10-16) mangles the personality symbol, so no_std_test's #[no_mangle] rust_eh_personality no longer links. Fix for the harness in #1191; this PR touches nothing no_std-specific.

@schungx

schungx commented Oct 17, 2026

Copy link
Copy Markdown
Collaborator

There seem to be merge conflicts...

Can you resolve them? I'll merge if it can.

exec_native_fn_call swaps a copy of the first argument in through ArgBackup
when a non-method native function is called with a variable first argument,
and restored it only on the normal-return path. If the native function
panicked, the unwind dropped the ArgBackup unrestored, its Drop assertion
panicked during the unwind, and the process aborted, so no catch_unwind in the
host could contain it.

Restore through the same defer! guard the script-function path in this file
already uses, which runs on unwind as well. The assertion is kept.

The regression test runs the aborting case, `buggy(t)`, under every feature
combination; the method-call spelling is a control and needs the dot syntax,
which `no_object` disables, so it is gated.

Fixes rhaiscript#1188 (the abort half).
@yuvalrakavy

Copy link
Copy Markdown
Contributor Author

Rebased onto main (after #1190 and #1191); the CHANGELOG conflict is resolved with both lines kept. Thanks!

@yuvalrakavy
yuvalrakavy force-pushed the fix-native-call-restore-on-unwind branch from 377df35 to 5c8a405 Compare October 17, 2026 22:46
@schungx
schungx merged commit aaf9090 into rhaiscript:main Oct 17, 2026
56 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A panic inside a native function called with a variable first argument aborts the process (ArgBackup::drop asserts during unwinding)

2 participants