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

rustc: Tweak funclet cleanups of ffi functions #48572

Merged
merged 1 commit into from
Mar 2, 2018

Conversation

alexcrichton
Copy link
Member

This commit is targeted at addressing #48251 by specifically fixing a case where
a longjmp over Rust frames on MSVC runs cleanups, accidentally running the
"abort the program" cleanup as well. Added in #46833 extern ABI functions in
Rust will abort the process if Rust panics, and currently this is modeled as a
normal cleanup like all other destructors.

Unfortunately it turns out that longjmp on MSVC is implemented with SEH, the
same mechanism used to implement panics in Rust. This means that longjmp over
Rust frames will run Rust cleanups (even though we don't necessarily want it
to). Notably this means that if you longjmp over a Rust stack frame then that
probably means you'll abort the program because one of the cleanups will abort
the process.

After some discussion on IRC it turns out that longjmp doesn't run cleanups
for caught exceptions, it only runs cleanups for cleanup pads. Using this
information this commit tweaks the codegen for an extern function to
a catch-all clause for exceptions instead of a cleanup block. This catch-all is
equivalent to the C++ code:

try {
    foo();
} catch (...) {
    bar();
}

and in fact our codegen here is designed to match exactly what clang emits for
that C++ code!

With this tweak a longjmp over Rust code will no longer abort the process. A
longjmp will continue to "accidentally" run Rust cleanups (destructors) on MSVC.
Other non-MSVC platforms will not rust destructors with a longjmp, so we'll
probably still recommend "don't have destructors on the stack", but in any case
this is a more surgical fix than #48567 and should help us stick to standard
personality functions a bit longer.

@rust-highfive
Copy link
Collaborator

r? @petrochenkov

(rust_highfive has picked a reviewer for you, use r? to override)

@rust-highfive rust-highfive added the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. label Feb 27, 2018
let cs = cs_bx.catch_switch(None, None, 1);
cs_bx.add_handler(cs, cp_bx.llbb());

// No idea what this null/64 are for, but it's what clang does!

Choose a reason for hiding this comment

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

The pointer usually points to an RTTI type descriptor, catch(...) doesn't have a type so this is null.
As for the 64, its a bit in a bitfield describing if the catch is by reference, const, etc. In this case, 64 indicates catch-all.

Copy link
Member Author

Choose a reason for hiding this comment

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

Aha, thanks!

@petrochenkov
Copy link
Contributor

I'm not qualified to review this.
r? @nagisa @vadimcn @eddyb

@nagisa
Copy link
Member

nagisa commented Feb 27, 2018 via email

@eddyb
Copy link
Member

eddyb commented Feb 27, 2018

@bors r+

LGTM, although I'm wondering if putting the aborts in MIR (instead of the backend) was a mistake.

@bors
Copy link
Contributor

bors commented Feb 27, 2018

📌 Commit e2af240 has been approved by eddyb

@bors bors added S-waiting-on-bors Status: Waiting on bors to run and complete tests. Bors will change the label on completion. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Feb 27, 2018
foo();
}

fn foo() {
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm trying to understand how this test works. According to earlier comment, longjmp will still run destructors on MSVC. As such, A's destructor will run, causing a panic, but since we're in unwind mode, this will be a double unwind, causing an abort for that reason, or...?

Copy link
Member Author

Choose a reason for hiding this comment

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

Ah yes that was a mistake on my part, this test was originally included in a PR where it was fixed, but I changed strategies at the last minute.

@Manishearth
Copy link
Member

Manishearth commented Feb 28, 2018

Fails on appveyor

#48615 (comment)

This commit is targeted at addressing rust-lang#48251 by specifically fixing a case where
a longjmp over Rust frames on MSVC runs cleanups, accidentally running the
"abort the program" cleanup as well. Added in rust-lang#46833 `extern` ABI functions in
Rust will abort the process if Rust panics, and currently this is modeled as a
normal cleanup like all other destructors.

Unfortunately it turns out that `longjmp` on MSVC is implemented with SEH, the
same mechanism used to implement panics in Rust. This means that `longjmp` over
Rust frames will run Rust cleanups (even though we don't necessarily want it
to). Notably this means that if you `longjmp` over a Rust stack frame then that
probably means you'll abort the program because one of the cleanups will abort
the process.

After some discussion on IRC it turns out that `longjmp` doesn't run cleanups
for *caught* exceptions, it only runs cleanups for cleanup pads. Using this
information this commit tweaks the codegen for an `extern` function to
a catch-all clause for exceptions instead of a cleanup block. This catch-all is
equivalent to the C++ code:

    try {
        foo();
    } catch (...) {
        bar();
    }

and in fact our codegen here is designed to match exactly what clang emits for
that C++ code!

With this tweak a longjmp over Rust code will no longer abort the process. A
longjmp will continue to "accidentally" run Rust cleanups (destructors) on MSVC.
Other non-MSVC platforms will not rust destructors with a longjmp, so we'll
probably still recommend "don't have destructors on the stack", but in any case
this is a more surgical fix than rust-lang#48567 and should help us stick to standard
personality functions a bit longer.
@alexcrichton
Copy link
Member Author

@bors: r=eddyb

@bors
Copy link
Contributor

bors commented Feb 28, 2018

📌 Commit 804666f has been approved by eddyb

@bors
Copy link
Contributor

bors commented Feb 28, 2018

🌲 The tree is currently closed for pull requests below priority 200, this pull request will be tested once the tree is reopened

bors added a commit that referenced this pull request Feb 28, 2018
Rollup of 10 pull requests

- Successful merges: #48355, #48359, #48380, #48419, #48420, #48461, #48522, #48570, #48572, #48603
- Failed merges:
Manishearth added a commit to Manishearth/rust that referenced this pull request Mar 1, 2018
rustc: Tweak funclet cleanups of ffi functions

This commit is targeted at addressing rust-lang#48251 by specifically fixing a case where
a longjmp over Rust frames on MSVC runs cleanups, accidentally running the
"abort the program" cleanup as well. Added in rust-lang#46833 `extern` ABI functions in
Rust will abort the process if Rust panics, and currently this is modeled as a
normal cleanup like all other destructors.

Unfortunately it turns out that `longjmp` on MSVC is implemented with SEH, the
same mechanism used to implement panics in Rust. This means that `longjmp` over
Rust frames will run Rust cleanups (even though we don't necessarily want it
to). Notably this means that if you `longjmp` over a Rust stack frame then that
probably means you'll abort the program because one of the cleanups will abort
the process.

After some discussion on IRC it turns out that `longjmp` doesn't run cleanups
for *caught* exceptions, it only runs cleanups for cleanup pads. Using this
information this commit tweaks the codegen for an `extern` function to
a catch-all clause for exceptions instead of a cleanup block. This catch-all is
equivalent to the C++ code:

    try {
        foo();
    } catch (...) {
        bar();
    }

and in fact our codegen here is designed to match exactly what clang emits for
that C++ code!

With this tweak a longjmp over Rust code will no longer abort the process. A
longjmp will continue to "accidentally" run Rust cleanups (destructors) on MSVC.
Other non-MSVC platforms will not rust destructors with a longjmp, so we'll
probably still recommend "don't have destructors on the stack", but in any case
this is a more surgical fix than rust-lang#48567 and should help us stick to standard
personality functions a bit longer.
@bors bors merged commit 804666f into rust-lang:master Mar 2, 2018
@alexcrichton alexcrichton added beta-nominated Nominated for backporting to the compiler in the beta channel. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. labels Mar 2, 2018
@alexcrichton alexcrichton deleted the noexcept-msvc2 branch March 2, 2018 17:36
@alexcrichton
Copy link
Member Author

Nominating for a backport to beta as I think this turned out to be a small enough change and this'll help re-close #48251

@fweimer
Copy link

fweimer commented Mar 3, 2018

Note that this:

try {
    foo();
} catch (...) {
    bar();
}

still breaks thread cancellation on GNU/Linux if bar() aborts unconditionally. The assumptions that FFI calls cannot unwind is not true on this platform.

@alexcrichton
Copy link
Member Author

@fweimer can you expand a bit on that? I wasn't aware of this problem!

@fweimer
Copy link

fweimer commented Mar 4, 2018

@alexcrichton Not sure where to start. I previously tried to ask about this over there:

@vadimcn
Copy link
Contributor

vadimcn commented Mar 4, 2018

Indiscriminate unwinding through an FFI boundary is a very likely to end badly. There may be code on the other side written in a language that does not support unwinding (e.g. C). Unwinding through such code will leak all its resources (memory, open files, acquired mutexes, etc), even if the compiler has generated unwind tables.
Btw, Rust itself does not promise to execute cleanups on foreign exceptions. I know for fact that this is the case for for x86_64-pc-windows-gnu target, not sure about others. (Should we consider putting unwind guards on the caller side of FFI as well?)

There are cases when you do know that it's ok to unwind through FFI, but doing so should be opt-in. We already have the #[unwind] attribute for this purpose, though it's currently unstable.

@alexcrichton
Copy link
Member Author

@fweimer it sounds like pthread_cancel has problems with C++ noexcept anyway? In that sense I'd at least by default not expect pthread_cancel to work whereas longjmp/setjmp seems more standard to be expected to work at least. That's not to say we couldn't get it working, but as @vadimcn mentioned it's pretty scary to unwind without Rust exceptions so it's in general best to avoid that.

@fweimer
Copy link

fweimer commented Mar 7, 2018

@alexcrichton pthread_cancel has problems with the current implementation of std::thread, I think. (EDIT I was wrong, this is something that was fixed back in 2012 in libstdc++.) You can still call pthread_create directly and use pthread_cancel with the thread. If I understand the current Rust changes correctly, you have just removed that possibility from the released Rust compiler, without providing a replacement.

@alexcrichton
Copy link
Member Author

Er sorry I'm not really sure I understand, can you provide an example of the breakage?

@nagisa
Copy link
Member

nagisa commented Mar 7, 2018

This specific PR only affects Windows, not POSIX platforms. Admittedly, there might be yet another case where on Windows unwinding is used and, unlike longjmp, is caught by catch(...), but we are not currently aware of any. pthread_cancel is not something supported by Windows, and such is of no concern.


On POSIX, meanwhile, code like one below, is and always was undefined Rust (ignore all the bad bindings and stuff, it is something I prototyped quickly to demonstrate the point):

use std::os::unix::thread::*;

extern "C" {
    pub fn pthread_cancel(h: RawPthread) -> i32;
    pub fn pthread_create(t: *mut RawPthread, a: *mut (), t: extern "C" fn(*const ()) -> *const (), b: *mut ()) -> i32;
}

extern "C" fn banana(x: *const ()) -> *const () {
    ::std::thread::sleep(::std::time::Duration::from_millis(500)); // might unwind due to pthread_cancel and is UB
    x
}

fn main() {
    unsafe {
        let mut t: RawPthread = ::std::mem::zeroed();
        pthread_create(&mut t, ::std::ptr::null_mut(), banana, ::std::ptr::null_mut());
        pthread_cancel(t);
    }
    ::std::thread::sleep(::std::time::Duration::from_millis(1000));
}

Currently, the safety implemented in #46833 is disabled, so this code will silently work (it does not work when the safety net is re-enabled), however, to really make sure this code is not undefined, you need to specify the #[unwind] attribute on top of banana, as such:

#[unwind]
extern "C" fn banana(x: *const ()) -> *const () {
    ::std::thread::sleep(::std::time::Duration::from_millis(500)); // might unwind due to pthread_cancel and is not UB anymore, because `banana` is specified to unwind.
    x
}

This is exactly the same rule, that applies to any extern "C" function that might unwind for any reason.

@nikomatsakis
Copy link
Contributor

We discussed in the @rust-lang/compiler meeting. We would prefer not to backport this -- hence I'm removing the label. The main reason is that there doesn't seem to be an urgency; the aborts are currently disabled, so we can just let this ride the trains as usual.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
S-waiting-on-bors Status: Waiting on bors to run and complete tests. Bors will change the label on completion. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue.
Projects
None yet
Development

Successfully merging this pull request may close these issues.