Change:
Hi, It looks like calls to are getting removed when using for a release build. It doesn't seem to occur with or . I've got a test case here: , which can be reproduced like this: (more details in the ) For context, I first noticed this in a project where I am using an to communciate between threads, where a producer thread reads messages from the network, and the main thread saves those to a database. I was relying on the fact that the implementation for a channel will in effect close the channel, so the main thread can find out that the producer has failed, and so we should kill the process. What I saw instead was the producer thread panicing, but the main thread would never get woken up. I've seen this on macOS and Linux .
triage: P-high. Assigning to self. Removing nomination label.
Doing some bisection. This was introduced between Rust 1.32 and 1.33
More specifically, this was introduced between nightly-2018-12-08 and nightly-2018-12-14: (unfortunately no nightlies are available between those two points in the rustup archive.)
These are the PR's that bors merged between those two points:
According to my bisection via local builds, this was injected by (!)
(Have I mentioned before that we probably need to be doing more testing of the various LTO configurations? Would it be feasible to run our test suite in all of the lto modes from {, , } ?)
For what its worth, 's PR fixes this (I think; I need to double-check atop a rebased-on-master version). yes, confirmed now atop rebased version
Thanks for tracking this down
Okay I some instrumentation to the compiler and I think I know where the problem here is coming from. Here's the executive summary: PR changed the logic for where we add the LLVM attribute PR has asserted, from the beginning, that some of that new logic was wrong. In particular, it noted that: I myself was confused by this comment, because it looked to me like the PR as written was taking pains to return for abis that match and . So why is the test crate in this issue illustrating the problem outlined by above? First of all, here is a standalone that reproduces the problem for me. (You still have to turn on to observe the issue, but no other optimizations; e.g. still reproduces the issue for me): Running the compiler with gave me intermediate files that I could disassemble via and then inspect the resulting files. This led me to notice that some functions had on them that I did not expect. Second, I instrumented a local to report (via ) whenever it used either of these two cases: Just imagine the above, with two inserted calls that look like: println!("choosing unwind=false for foreign item {:?} with sig: {:?}", id, sig); and println!("choosing unwind=false for non-foreign {:?} with sig: {:?}", id, sig); And then I reviewed the output this spits out during both the stage1 bootstrapping and the actual compilation of the test case above. Here's the smoking gun from that output, I think: Unfortunately, the impl for does not include the abi (nor the unsafety). I'm guessing we'd see "Rust" there if that were included. namely, we are going through that first branch, already mentioned by , when we compile this declaration: This causes the generated declaration for to be tagged , even though that function is pretty much the function that is going to unwind. (But you need to have a somewhat nasty combination of factors to actually observe the optimization performed by LLVM in this case, which is how I guess this has gone relatively unobserved for so long, apart from what noticed in their own work.)
The short version of my previous comment is that I bet I could make a small change to mark extern fn's with Rust or RustCall ABIs as unwinding, which would have less impact than proposal and thus be a better candidate for backport. Update: Confirmed locally; yes we can fix this via a small targeted change. (Not sure if it will be the right long term solution, but it certainly seems like the right short-term step if only for backport purposes.)

Discussion:
} else if codegen_fn_attrs.flags.contains(CodegenFnAttrFlags::RUSTC_ALLOCATOR_NOUNWIND) { // Special attribute for allocator functions, which can't unwind false <del> } else if let Some(id) = id { </del> <ins> } else if let Some(_) = id { // rust-lang/rust#64655, rust-lang/rust#63909: to minimize // risk associated with changing cases where nounwind // attribute is attached, this code is deliberately mimicking // old control flow based on whether `id` is `Some` or `None`. // // However, in the long term we should either: // - fold this into final else (i.e. stop inspecting `id`) // - or, adopt Rust PR #63909. // // see also Rust RFC 2753. </ins> let sig = cx.tcx.normalize_erasing_late_bound_regions(ty::ParamEnv::reveal_all(), &sig); <del> if cx.tcx.is_foreign_item(id) { // Foreign items like `extern "C" { fn foo(); }` are assumed not to // unwind false } else if sig.abi != Abi::Rust && sig.abi != Abi::RustCall { // Any items defined in Rust that *don't* have the `extern` ABI are // defined to not unwind. We insert shims to abort if an unwind // happens to enforce this. false } else { // Anything else defined in Rust is assumed that it can possibly // unwind </del> <ins> if sig.abi == Abi::Rust || sig.abi == Abi::RustCall { // Any Rust method (or `extern "Rust" fn` or `extern // "rust-call" fn`) is explicitly allowed to unwind // (unless it has no-unwind attribute, handled above). </ins> true <ins> } else { // Anything else is either: // //  1. A foreign item using a non-Rust ABI (like `extern "C" { fn foo(); }`), or // //  2. A Rust item using a non-Rust ABI (like `extern "C" fn foo() { ... }`). // // Foreign items (case 1) are assumed to not unwind; it is // UB otherwise. (At least for now; see also // rust-lang/rust#63909 and Rust RFC 2753.) // // Items defined in Rust with non-Rust ABIs (case 2) are also // not supposed to unwind. Whether this should be enforced // (versus stating it is UB) and *how* it would be enforced // is currently under discussion; see rust-lang/rust#58794. // // In either case, we mark item as explicitly nounwind. false </ins> } } else { // assume this can possibly unwind, avoiding the application of a // `nounwind` attribute below. <ins> // // (But: See comments in previous branch. Specifically, it is // unclear whether there is real value in the assumption this // can unwind. The conservatism here may just be papering over // a real problem by making some UB a bit harder to hit.) </ins> true });

<ins> // run-pass // ignore-wasm32-bare compiled with panic=abort by default // ignore-emscripten no threads support // rust-lang/rust#64655: with panic=unwind, a panic from a subroutine // should still run destructors as it unwinds the stack. However, // bugs with how the nounwind LLVM attribute was applied led to this // simple case being mishandled *if* you had fat LTO turned on. // Unlike issue-64655-extern-rust-must-allow-unwind.rs, the issue // embodied in this test cropped up regardless of optimization level. // Therefore it seemed worthy of being enshrined as a dedicated unit // test. // LTO settings cannot be combined with -C prefer-dynamic // no-prefer-dynamic // The revisions just enumerate lto settings (the opt-level appeared irrelevant in practice) // revisions: no thin fat //[no]compile-flags: -C lto=no //[thin]compile-flags: -C lto=thin //[fat]compile-flags: -C lto=fat #![feature(core_panic)] // (For some reason, reproducing the LTO issue requires pulling in std // explicitly this way.) #![no_std] extern crate std; fn main() { use std::sync::atomic::{AtomicUsize, Ordering}; use std::boxed::Box; static SHARED: AtomicUsize = AtomicUsize::new(0); assert_eq!(SHARED.fetch_add(0, Ordering::SeqCst), 0); let old_hook = std::panic::take_hook(); std::panic::set_hook(Box::new(|_| { } )); // no-op on panic. let handle = std::thread::spawn(|| { struct Droppable; impl Drop for Droppable { fn drop(&mut self) { SHARED.fetch_add(1, Ordering::SeqCst); } } let _guard = Droppable; let s = "issue-64655-allow-unwind-when-calling-panic-directly.rs"; core::panicking::panic(&("???", s, 17, 4)); }); let wait = handle.join(); // Reinstate handler to ease observation of assertion failures. std::panic::set_hook(old_hook); assert!(wait.is_err()); assert_eq!(SHARED.fetch_add(0, Ordering::SeqCst), 1); } </ins>

<ins> // run-pass // ignore-wasm32-bare compiled with panic=abort by default // ignore-emscripten no threads support // rust-lang/rust#64655: with panic=unwind, a panic from a subroutine // should still run destructors as it unwinds the stack. However, // bugs with how the nounwind LLVM attribute was applied led to this // simple case being mishandled *if* you had optimization *and* fat // LTO turned on. // This test is the closest thing to a "regression test" we can do // without actually spawning subprocesses and comparing stderr // results. // // This test takes the code from the above issue and adapts it to // better fit our test infrastructure: // // * Instead of relying on `println!` to observe whether the destructor //   is run, we instead run the code in a spawned thread and //   communicate the destructor's operation via a synchronous atomic //   in static memory. // // * To keep the output from confusing a casual user, we override the //   panic hook to be a no-op (rather than printing a message to //   stderr). // // (pnkfelix has confirmed by hand that these additions do not mask // the underlying bug.) // LTO settings cannot be combined with -C prefer-dynamic // no-prefer-dynamic // The revisions combine each lto setting with each optimization // setting; pnkfelix observed three differing behaviors at opt-levels // 0/1/2+3 for this test, so it seems prudent to be thorough. // revisions: no0 no1 no2 no3 thin0 thin1 thin2 thin3 fat0 fat1 fat2  fat3 //[no0]compile-flags: -C opt-level=0 -C lto=no //[no1]compile-flags: -C opt-level=1 -C lto=no //[no2]compile-flags: -C opt-level=2 -C lto=no //[no3]compile-flags: -C opt-level=3 -C lto=no //[thin0]compile-flags: -C opt-level=0 -C lto=thin //[thin1]compile-flags: -C opt-level=1 -C lto=thin //[thin2]compile-flags: -C opt-level=2 -C lto=thin //[thin3]compile-flags: -C opt-level=3 -C lto=thin //[fat0]compile-flags: -C opt-level=0 -C lto=fat //[fat1]compile-flags: -C opt-level=1 -C lto=fat //[fat2]compile-flags: -C opt-level=2 -C lto=fat //[fat3]compile-flags: -C opt-level=3 -C lto=fat fn main() { use std::sync::atomic::{AtomicUsize, Ordering}; static SHARED: AtomicUsize = AtomicUsize::new(0); assert_eq!(SHARED.fetch_add(0, Ordering::SeqCst), 0); let old_hook = std::panic::take_hook(); std::panic::set_hook(Box::new(|_| { } )); // no-op on panic. let handle = std::thread::spawn(|| { struct Droppable; impl Drop for Droppable { fn drop(&mut self) { SHARED.fetch_add(1, Ordering::SeqCst); } } let _guard = Droppable; None::<()>.expect("???"); }); let wait = handle.join(); // reinstate handler to ease observation of assertion failures. std::panic::set_hook(old_hook); assert!(wait.is_err()); assert_eq!(SHARED.fetch_add(0, Ordering::SeqCst), 1); } </ins>