Skip to content

Conversation

@purplesyringa
Copy link
Contributor

@purplesyringa purplesyringa commented Oct 29, 2025

core::intrinsics::wasm32::throw is a function that unwinds by calling the throw intrinsic. When such core is linked into a -C panic=abort binary crate, rustc recognizes that throw is a non-FFI function that nevertheless unwinds. Before this PR, this caused rustc to emit an error.

With this PR, functions can opt into being recognized as unwinding even if the ambient panic strategy is abort. This allows wrappers around unwinding FFI functions, like the throw function wrapping the throw Wasm intrinsic, to behave just like the wrapped function would behave with regards to the ffi_unwind_calls lint and the required panic strategy logic.

I don't like adding a new attribute, but this functionality seems to be very rarely needed: it only matters for -C panic=unwind crates statically linked into binary crates with -C panic=abort, and only core/std are typically compiled separately from the whole crate tree.

Fixes #148246.

r? @alexcrichton

@rustbot
Copy link
Collaborator

rustbot commented Oct 29, 2025

Some changes occurred in compiler/rustc_codegen_ssa

cc @WaffleLapkin

stdarch is developed in its own repository. If possible, consider making this change to rust-lang/stdarch instead.

cc @Amanieu, @folkertdev, @sayantn

Some changes occurred to MIR optimizations

cc @rust-lang/wg-mir-opt

@rustbot rustbot added A-attributes Area: Attributes (`#[…]`, `#![…]`) S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. T-libs Relevant to the library team, which will review and decide on the PR/issue. labels Oct 29, 2025
@rust-log-analyzer
Copy link
Collaborator

The job aarch64-gnu-llvm-20-1 failed! Check out the build log: (web) (plain enhanced) (plain)

Click to see the possible cause of the failure (guessed by this bot)
   Compiling rustc_builtin_macros v0.0.0 (/checkout/compiler/rustc_builtin_macros)
[RUSTC-TIMING] rustc_metadata test:false 19.073
   Compiling rustc_resolve v0.0.0 (/checkout/compiler/rustc_resolve)

Session terminated, killing shell...::group::Clock drift check
  local time: Wed Oct 29 20:40:06 UTC 2025
##[error]The runner has received a shutdown signal. This can happen when the runner service is stopped, or a manually started runner is canceled.
  network time: Wed, 29 Oct 2025 20:40:06 GMT
##[endgroup]
 ...killed.
##[error]The operation was canceled.
Cleaning up orphan processes

#[allow(ffi_unwind_calls)]
// Allow `core` built with `-C panic=unwind` to be linked into `-C panic=abort` programs.
#[rustc_propagate_ffi_unwind]
pub unsafe fn throw<const TAG: i32>(ptr: *mut u8) -> ! {
Copy link
Member

Choose a reason for hiding this comment

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

This function must not be callable if any crate is compiled panic=abort. Otherwise you can unwind through panic=abort functions and thus skip destructors, which is unsound.

Copy link
Member

Choose a reason for hiding this comment

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

Perhaps the declaration of this intrinsic should be moved back to the panic_unwind crate?

Copy link
Contributor Author

@purplesyringa purplesyringa Oct 29, 2025

Choose a reason for hiding this comment

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

I don't think that's the case -- since #[rustc_propagate_ffi_unwind] functions are always considered unwinding, the abort_unwinding_calls pass will insert the correct logic whenever it's called from a panic=abort crate; and if it's called indirectly via some other rustic function, then that nested function will be recognized as unwinding and ffi_unwind_calls will force the panic strategy to unwind. I think that #[rustc_propagate_ffi_unwind] functions behave identically to FFI functions (i.e. without available source) in this regard.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think the propagate-ffi-unwind.rs test demonstrates this behavior; please correct me if I'm wrong.

Copy link
Member

Choose a reason for hiding this comment

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

I think that #[rustc_propagate_ffi_unwind] functions behave identically to FFI functions (i.e. without available source) in this regard.

When calling extern "C" we assume that no panic can happen, when defining an extern "C" function unwinding out of it will abort unconditionally. When calling extern "C-unwind" from a panic=abort crate we insert an aborting landingpad. And if the extern "C-unwind" function is declared in a panic=unwind crate that forces the final compilation to be panic=unwind as we don't know if any panic=unwind function called it that could allow exceptions to escape into panic=abort crates. For calling extern "Rust" functions with a hidden origin no landingpads get inserted because if any crate is panic=abort, we ensure there is no way to unwind through any rust crate thanks to the extern "C-unwind" landingpad + guarantee that if any crate is panic=abort, all extern "C-unwind" functions are declared in panic=abort crates too and thus the landingpads are forced.

AFAICT with this PR you could call throw from a panic=unwind crate (which thus doesn't have an aborting landingpad) and then call this function from a panic=abort crate which also doesn't have an aborting landingpad due to the assumption that you can't unwind into any Rust code if any crate is panic=abort. This unwinding into a panic=abort crate then results in UB.

Copy link
Contributor Author

@purplesyringa purplesyringa Oct 29, 2025

Choose a reason for hiding this comment

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

Oh, you're right -- I forgot to update has_ffi_unwind_calls to consider calls to #[rustc_propagate_ffi_unwind] functions as tainting even if the function has rustic ABI. I think that's sufficient to fix the issue?

EDIT: scratch that, that doesn't work due to function pointers. It kind of seems like this would require a new ABI, then...

EDIT 2: Maybe we can just make throw C-unwind (and also use this attribute)? That sounds like it works.

@purplesyringa
Copy link
Contributor Author

purplesyringa commented Oct 29, 2025

Perhaps the declaration of this intrinsic should be moved back to the panic_unwind crate?

I want to reply to this question outside the review thread because I think it's fundamental to whether the approach in this PR is the one we want to pursue.

My argument is that supporting user-controlled unwinding is a goal to keep in mind. project-ffi-unwind has tinkered around with interacting with other runtimes' exceptions, and while we are not there yet, this still sounds like a valuable direction to pursue to me.

Even today, userland Rust crates can throw arbitrary exceptions on most platforms by calling foreign functions like _Unwind_RaiseException (though not catch them). Wasm is a notable exception in that it doesn't provide such a foreign function and instead has to use an intrinsic. If such an intrinsic does not become generally available, it becomes an inconsistency in expression power between Wasm and all other platforms. We may decide to drop this topic for now if the approach in this PR is deemed too complex, but at some point, we'll likely have to face this exact problem. I thought that tackling it like this was better than rolling back progress.

I guess it could be argued that this intrinsic can still be reasonably moved into panic_unwind, but then we'd eventually have to figure out how to make it public if we wanted to follow through with the goal I'm describing here, and exposing panic_unwind seems like more of mess than this.

@bjorn3
Copy link
Member

bjorn3 commented Oct 29, 2025

Even today, userland Rust crates can throw arbitrary exceptions on most platforms by calling foreign functions like _Unwind_RaiseException (though not catch them).

Which would be an extern "C-unwind" function and thus declaring such a function in a panic=unwind crate would prevent using panic=abort.

Wasm is a notable exception in that it doesn't provide such a foreign function and instead has to use an intrinsic.

Outside of wasm32-unknown-unknown _Unwind_RaiseException does actually exist: https://github.com/WebAssembly/tool-conventions/blob/main/EHScheme.md#_unwind_raiseexception

I guess it could be argued that this intrinsic can still be reasonably moved into panic_unwind, but then we'd eventually have to figure out how to make it public if we wanted to follow through with the goal I'm describing here, and exposing panic_unwind seems like more of mess than this.

I don't think we can reasonably expose the raw intrinsic to end users given that there is no specification for what arbitrary tag indices should do. LLVM doesn't expose a way for the user to generate arbitrary tags and then point to those. Currently it only supports a tag for C++ exceptions, so I think we should only expose a function that specifically throws C++ exceptions.

By the way it seems like std::panic::catch_unwind on wasm doesn't catch any exceptions that don't follow the C++ exception ABI, which is unsound. Edit: Opened #148273

@purplesyringa
Copy link
Contributor Author

Which would be an extern "C-unwind" function and thus declaring such a function in a panic=unwind crate would prevent using panic=abort.

That's kind of my point -- typically, this rule is not restrictive, since users can opt into linking to _Unwind_RaiseException if they want to. But if we wanted to expose the intrinsic, we'd have to put a similar extern "C-unwind" declaration in core, because we clearly don't expect users to access LLVM intrinsic directly; and this action of putting it in core causes problems.

Other than that, agreed. I didn't know that _Unwind_RaiseException exists under Wasm as well, so that put me on the wrong path. Is there a reason we aren't using it under Emscripten/WASI? Though I guess we'd still have to worry about wasm32-unknown-unknown, so it wouldn't simplify things. I can submit a PR for moving the intrinsic back to panic_unwind if you think that's a good idea.

By the way it seems like std::panic::catch_unwind on wasm doesn't catch any exceptions that don't follow the C++ exception ABI, which is unsound.

Well, that looks wrong...

@rustbot rustbot removed the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. label Oct 29, 2025
@purplesyringa
Copy link
Contributor Author

Well, this is awkward: throw has never been defined in panic_unwind, but rather it was defined in unwind, which is built as part of std and would thus be subject to the exact same problem as core. We can still move it into panic_unwind, though.

@purplesyringa
Copy link
Contributor Author

purplesyringa commented Oct 30, 2025

Oh, that explains it... we do in fact use external _Unwind_RaiseException under both WASI and Emscripten. That makes everything much easier.

bors added a commit that referenced this pull request Oct 30, 2025
Move wasm `throw` intrinsic back to `unwind`

Fixes #148246, less invasive than the previously proposed #148269. Removes the publicly visible unstable intrinsic tracked in #122465 since it's not clear how to export it in a sound manner.

r? `@bjorn3`

---

rustc assumes that regular `extern "Rust"` functions unwind only if the `unwind` panic runtime is linked. `throw` was annotated as such, but unwound unconditionally. This could cause UB when a crate built with `-C panic=abort` called `throw` from `core` built with `-C panic=unwind`, since no terminator was added to handle the panic arising from calling an allegedly non-unwinding `extern "Rust"` function.

rustc was taught to recognize this condition since #144225 and prevented such linkage, but this caused regressions in
#148246, since this meant that Emscripten projects could not be built with `-C panic=abort` without recompiling std.

The most straightforward solution would be to move `throw` into the `panic_unwind` crate, so that it's only compiled if the panic runtime is guaranteed to be `unwind`, but this is messy due to our architecture. Instead, move it into `unwind::wasm`, which is only compiled for bare-metal targets that default to `panic = "abort"`, rendering the issue moot.
Kobzol pushed a commit to Kobzol/stdarch that referenced this pull request Nov 2, 2025
Move wasm `throw` intrinsic back to `unwind`

Fixes rust-lang/rust#148246, less invasive than the previously proposed rust-lang/rust#148269. Removes the publicly visible unstable intrinsic tracked in rust-lang/rust#122465 since it's not clear how to export it in a sound manner.

r? `@bjorn3`

---

rustc assumes that regular `extern "Rust"` functions unwind only if the `unwind` panic runtime is linked. `throw` was annotated as such, but unwound unconditionally. This could cause UB when a crate built with `-C panic=abort` called `throw` from `core` built with `-C panic=unwind`, since no terminator was added to handle the panic arising from calling an allegedly non-unwinding `extern "Rust"` function.

rustc was taught to recognize this condition since rust-lang/rust#144225 and prevented such linkage, but this caused regressions in
rust-lang/rust#148246, since this meant that Emscripten projects could not be built with `-C panic=abort` without recompiling std.

The most straightforward solution would be to move `throw` into the `panic_unwind` crate, so that it's only compiled if the panic runtime is guaranteed to be `unwind`, but this is messy due to our architecture. Instead, move it into `unwind::wasm`, which is only compiled for bare-metal targets that default to `panic = "abort"`, rendering the issue moot.
makai410 pushed a commit to makai410/rustc_public that referenced this pull request Nov 4, 2025
Move wasm `throw` intrinsic back to `unwind`

Fixes rust-lang/rust#148246, less invasive than the previously proposed rust-lang/rust#148269. Removes the publicly visible unstable intrinsic tracked in rust-lang/rust#122465 since it's not clear how to export it in a sound manner.

r? `@bjorn3`

---

rustc assumes that regular `extern "Rust"` functions unwind only if the `unwind` panic runtime is linked. `throw` was annotated as such, but unwound unconditionally. This could cause UB when a crate built with `-C panic=abort` called `throw` from `core` built with `-C panic=unwind`, since no terminator was added to handle the panic arising from calling an allegedly non-unwinding `extern "Rust"` function.

rustc was taught to recognize this condition since rust-lang/rust#144225 and prevented such linkage, but this caused regressions in
rust-lang/rust#148246, since this meant that Emscripten projects could not be built with `-C panic=abort` without recompiling std.

The most straightforward solution would be to move `throw` into the `panic_unwind` crate, so that it's only compiled if the panic runtime is guaranteed to be `unwind`, but this is messy due to our architecture. Instead, move it into `unwind::wasm`, which is only compiled for bare-metal targets that default to `panic = "abort"`, rendering the issue moot.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-attributes Area: Attributes (`#[…]`, `#![…]`) T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. T-libs Relevant to the library team, which will review and decide on the PR/issue.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Core crate doesn't support panic="abort" in 1.90 wasm32-unknown-emscripten

5 participants