Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

catch_unwind incorrectly tries to stop ForceUnwinds #65441

Closed
gnzlbg opened this issue Oct 15, 2019 · 5 comments · Fixed by #65646
Closed

catch_unwind incorrectly tries to stop ForceUnwinds #65441

gnzlbg opened this issue Oct 15, 2019 · 5 comments · Fixed by #65646
Labels
A-runtime Area: std's runtime and "pre-main" init for handling backtraces, unwinds, stack overflows C-bug Category: This is a bug. T-libs-api Relevant to the library API team, which will review and decide on the PR/issue.

Comments

@gnzlbg
Copy link
Contributor

gnzlbg commented Oct 15, 2019

MWE:

use std::os::unix::thread::JoinHandleExt;
use libc::pthread_cancel;

struct DropGuard;
impl Drop for DropGuard {
    fn drop(&mut self) {
        println!("unwinding foo");
    }
}

fn foo() {
    let _x = DropGuard;
    println!("thread started");
    std::thread::sleep(std::time::Duration::new(1, 0));
    println!("thread finished");
}

fn main() {
    let handle0 = std::thread::spawn(foo);
    std::thread::sleep(std::time::Duration::new(0, 10_000));
    unsafe { 
        let x = pthread_cancel(handle0.as_pthread_t());
        assert_eq!(x, 0);
    }
    handle0.join();
}

(Playground)

Output:

thread started
unwinding foo

Errors:

   Compiling playground v0.0.1 (/playground)
warning: unused `std::result::Result` that must be used
  --> src/main.rs:25:5
   |
25 |     handle0.join();
   |     ^^^^^^^^^^^^^^^
   |
   = note: `#[warn(unused_must_use)]` on by default
   = note: this `Result` may be an `Err` variant, which should be handled

    Finished release [optimized] target(s) in 0.86s
     Running `target/release/playground`
FATAL: exception not rethrown
timeout: the monitored command dumped core
/root/entrypoint.sh: line 8:     7 Aborted                 timeout --signal=KILL ${timeout} "$@"

According to the Itanium ABI:

  • "forced" unwinding (such as caused by longjmp or thread termination).
    [...]
    During "forced unwinding", on the other hand, an external agent is driving the unwinding. For instance, this can be the longjmp routine. This external agent, not each personality routine, knows when to stop unwinding. The fact that a personality routine is not given a choice about whether unwinding will proceed is indicated by the _UA_FORCE_UNWIND flag.

So catch_unwind definitely needs to always catch Rust exceptions, and at the thread boundary it probably also want to catch foreign exceptions and abort since whatever that should do probably won't work. When not on a thread boundary, aborting on foreign exceptions might be a conservative step until a better solution is discussed (an alternative solution would be not to catch them and just let them escape, or to catch them and have the Any be some ForeignPanic type defined somewhere in std::panic that users can use to detect that a foreign exception was raised). What it probably shouldn't do is catch "ForceUnwind" otherwise longjmp might not work across it and threads will not be canceled, but... maybe the conservative thing to do would be for this to abort for the time being?

@gnzlbg
Copy link
Contributor Author

gnzlbg commented Oct 15, 2019

cc @Amanieu @alexcrichton

@jonas-schievink jonas-schievink added A-runtime Area: std's runtime and "pre-main" init for handling backtraces, unwinds, stack overflows C-bug Category: This is a bug. T-libs-api Relevant to the library API team, which will review and decide on the PR/issue. labels Oct 15, 2019
@Amanieu
Copy link
Member

Amanieu commented Oct 15, 2019

I feel that we should simply let all foreign exceptions unwind through catch_unwind without catching them. This roughly matches the behavior of a C++ catch clause which only catches the rust_unwind exception type. This should of course be documented.

@gnzlbg
Copy link
Contributor Author

gnzlbg commented Oct 15, 2019

This roughly matches the behavior of a C++ catch clause which only catches the rust_unwind exception type.

Note that C++ with Itanium ABI also catches foreign exceptions if a catch(...) clause is used. That is, in C++, if your implementations supports it (which AFAIK none does right now), you can use catch(__rust_exception const& e) to catch a Rust panic (you can't modify it though). According to the Itanium ABI, the __rust_exception type should be defined in the <exception> header.

@Amanieu
Copy link
Member

Amanieu commented Oct 16, 2019

Actually, looking at the implementation of catch_unwind, there is no check that the exception object that was caught is actually a panic generated by Rust. The code just blindly casts the exception pointer to a pointer to struct Exception. This will fail horribly if the exception was not generated by Rust.

@gnzlbg
Copy link
Contributor Author

gnzlbg commented Oct 17, 2019

Yes, I think that's correct at the language spec level, because right now, letting a foreign exception (or any kind of exception actually) unwind into Rust is UB, so we do not provide any kind of guarantees about what catch_unwind does.

As you mention, what we currently do for foreign exceptions is horrible. Since there are some platforms where we can easily do better (e.g. just aborting), I think we should do so as a "quality-of-implementation" issue at least, even though it is still UB.

bors added a commit that referenced this issue Oct 24, 2019
Allow foreign exceptions to unwind through Rust code

This PR allows C++ exceptions (and any other exceptions) to safely unwind through Rust code. In particular:
- Drop code will be executed as the exception unwinds through the stack, as with a Rust panic.
- `catch_unwind` will *not* catch the exception, instead the exception will silently continue unwinding past it.

Incidentally, Rust panics can unwind just fine through C++ code as long as you mark the function with `#[unwind(allowed)]`. I've added a test for it to be sure.

I haven't updated any of the documentation, so officially unwinding through FFI is still UB. However this is a step towards making it well-defined.

Fixes #65441

cc @gnzlbg
r? @alexcrichton
bors added a commit that referenced this issue Oct 26, 2019
Allow foreign exceptions to unwind through Rust code

This PR allows C++ exceptions (and any other exceptions) to safely unwind through Rust code. In particular:
- Drop code will be executed as the exception unwinds through the stack, as with a Rust panic.
- `catch_unwind` will *not* catch the exception, instead the exception will silently continue unwinding past it.

Incidentally, Rust panics can unwind just fine through C++ code as long as you mark the function with `#[unwind(allowed)]`. I've added a test for it to be sure.

I haven't updated any of the documentation, so officially unwinding through FFI is still UB. However this is a step towards making it well-defined.

Fixes #65441

cc @gnzlbg
r? @alexcrichton
bors added a commit that referenced this issue Oct 26, 2019
Allow foreign exceptions to unwind through Rust code

This PR allows C++ exceptions (and any other exceptions) to safely unwind through Rust code. In particular:
- Drop code will be executed as the exception unwinds through the stack, as with a Rust panic.
- `catch_unwind` will *not* catch the exception, instead the exception will silently continue unwinding past it.

Incidentally, Rust panics can unwind just fine through C++ code as long as you mark the function with `#[unwind(allowed)]`. I've added a test for it to be sure.

I haven't updated any of the documentation, so officially unwinding through FFI is still UB. However this is a step towards making it well-defined.

Fixes #65441

cc @gnzlbg
r? @alexcrichton
bors added a commit that referenced this issue Oct 28, 2019
Allow foreign exceptions to unwind through Rust code

This PR allows C++ exceptions (and any other exceptions) to safely unwind through Rust code. In particular:
- Drop code will be executed as the exception unwinds through the stack, as with a Rust panic.
- `catch_unwind` will *not* catch the exception, instead the exception will silently continue unwinding past it.

Incidentally, Rust panics can unwind just fine through C++ code as long as you mark the function with `#[unwind(allowed)]`. I've added a test for it to be sure.

I haven't updated any of the documentation, so officially unwinding through FFI is still UB. However this is a step towards making it well-defined.

Fixes #65441

cc @gnzlbg
r? @alexcrichton
bors added a commit that referenced this issue Nov 3, 2019
Allow foreign exceptions to unwind through Rust code and Rust panics to unwind through FFI

This PR fixes interactions between Rust panics and foreign (mainly C++) exceptions.

C++ exceptions (and other FFI exceptions) can now safely unwind through Rust code:
- The FFI function causing the unwind must be marked with `#[unwind(allowed)]`. If this is not the case then LLVM may optimize landing pads away with the assumption that they are unreachable.
- Drop code will be executed as the exception unwinds through the stack, as with a Rust panic.
- `catch_unwind` will *not* catch the exception, instead the exception will silently continue unwinding past it.

Rust panics can now safely unwind through C++ code:
- C++ destructors will be called as the stack unwinds.
- The Rust panic can only be caught with `catch (...)`, after which it can be either rethrown or discarded.
- C++ cannot name the type of the Rust exception object used for unwinding, which means that it can't be caught explicitly or have its contents inspected.

Tests have been added to ensure all of the above works correctly.

Some notes about non-C++ exceptions:
- `pthread_cancel` and `pthread_exit` use unwinding on glibc. This has the same behavior as a C++ exception: destructors are run but it cannot be caught by `catch_unwind`.
- `longjmp` on Windows is implemented using unwinding. Destructors are run on MSVC, but not on MinGW. In both cases the unwind cannot be caught by `catch_unwind`.
- As with C++ exceptions, you need to mark the relevant FFI functions with `#[unwind(allowed)]`, otherwise LLVM will optimize out the destructors since they seem unreachable.

I haven't updated any of the documentation, so officially unwinding through FFI is still UB. However this is a step towards making it well-defined.

Fixes #65441

cc @gnzlbg
r? @alexcrichton
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-runtime Area: std's runtime and "pre-main" init for handling backtraces, unwinds, stack overflows C-bug Category: This is a bug. T-libs-api Relevant to the library API team, which will review and decide on the PR/issue.
Projects
None yet
3 participants