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: Use custom personality functions on MSVC #48567

Closed
wants to merge 1 commit into from

Conversation

alexcrichton
Copy link
Member

@alexcrichton alexcrichton commented Feb 27, 2018

Exception handling on MSVC for Rust has at this point a long and storied
history to it. We currently use the exception mechanisms on MSVC to implement
panics in Rust. On MSVC both 32 and 64-bit exceptions are based on SEH,
structured exception handling. LLVM long ago added special instructions for
MSVC, and we've been using those for as long as MSVC panics have been
implemented!

Exception handling at the system layer is typically guided by "personality
functions" which are in theory intended to be language specific and allow
programming languages to implement custom logic. Since the beginning, however,
LLVM has had a hardcoded list of "known personalities" as they behave quite
differently. As a result, Rust has historically shoehorned our desired panic
behavior into preexisting personality functions defined on MSVC.

Originally Rust actually used the functions __C_specific_handler (64-bit) and
_except_handler3 (32-bit). Using these personalities was relatively easy in
Rust and only required a "filter function" on our "catch" equivalent to only
catch Rust exceptions. Unfortunately these personalities suffered two
fatal flaws, which caused us to subsequently switch to the
__CxxFrameHandler3 personality.

This personality is intended for C++, but we're mostly like C++ in any case so
it worked quite well for a long time! The default C++ personality didn't run
cleanups on faults and LLVM optimized invokes of nounwind functions well. The
only downside at this point was that the support was sort of scary.

Fast forward to the 1.24.0 release and another fatal flaw is found in our
strategy. Historically we've always declared "unwinding into Rust code from
other languages is undefined behavior" (or unwinding out of Rust code). In
1.24.0 we changed extern functions defined in Rust to enforce this behavior,
forcibly aborting the process if the function panics. Everything was ship shape
until it was discovered that longjmp across Rust frames caused the process to
abort!

It turns out that on MSVC longjmp is implemented with SEH! Furthermore it
turns out that the personality we're using, __CxxFrameHandler3, recognized the
longjmp exception enough to run cleanups. Consequently, when SEH ran past an
extern function in Rust it aborted the process. Sounds like "cleanups run on
segfaults" v2!

Well in any case, that's a long-winded way of describing how shoehorning Rust's
desired behavior into existing personality functions is getting more and more
difficult. As a result, this commit starts taking a new strategy of defining
custom personality functions in Rust (like we do for all other platforms) and
teaching LLVM about these personalities so they're classified correctly and
don't suffer from old bugs.

Alright, so with all that information in your head now this commit can be
described with:

  • New personality functions are added for MSVC: rust_seh{32,64}_personality.
  • These functions are shims around __C_specific_handler and _except_handler3
    like how on Unix we're typically a shim around a gcc personality.
  • Rust's personality functions defer on all exceptions that aren't
    Rust-related. We choose an arbitrary code to represent a Rust exception and
    only exceptions with matching codes execute our cleanups/catches. (the
    prevents previous bugs with the personalities these functions are
    wrapping).
  • LLVM is updated with a small-ish commit to learn about these personality
    functions. The largest change here is, again, ensuring old bugs don't
    resurface
    by teaching LLVM that it can simplify invokes of nounwind
    functions in Rust.
  • Finally, 38e6e5d is partially reverted to restore the old translation
    behavior of the try intrinsic, bringing some scary code back into the
    compiler about llvm.localescape and such.

Overall the intent of this commit is to preserve all existing behavior with
panics on MSVC (aka keep old bugs closed and use the same system in general) but
enable longjmps across Rust code. To this effect a test is checked in which
asserts that we can indeed longjmp across Rust code with destructors and such.

@rust-highfive
Copy link
Collaborator

r? @nikomatsakis

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

@alexcrichton
Copy link
Member Author

Note that this shouldn't land yet as its pointing to my own LLVM fork temporarily while I complete local testing of this PR. I think, however, it's in a good state to start getting feedback of the approach!

cc @majnemer as well, some extra eyes on the LLVM changes would also be appreciated!

Exception handling on MSVC for Rust has at this point a long and storied
history to it. We currently use the exception mechanisms on MSVC to implement
panics in Rust. On MSVC both 32 and 64-bit exceptions are based on SEH,
structured exception handling. LLVM long ago added special instructions for
MSVC, and we've been using those for as long as MSVC panics have been
implemented!

Exception handling at the system layer is typically guided by "personality
functions" which are in theory intended to be language specific and allow
programming languages to implement custom logic. Since the beginning, however,
LLVM has had a hardcoded list of "known personalities" as they behave quite
differently. As a result, Rust has historically shoehorned our desired panic
behavior into preexisting personality functions defined on MSVC.

Originally Rust actually used the functions `__C_specific_handler` (64-bit) and
`_except_handler3` (32-bit). Using these personalities was relatively easy in
Rust and only required a "filter function" on our "catch" equivalent to only
catch Rust exceptions. Unfortunately these personalities suffered two
[fatal][f1] [flaws][f2], which caused us to subsequently [switch] to the
`__CxxFrameHandler3` personality.

This personality is intended for C++, but we're mostly like C++ in any case so
it worked quite well for a long time! The default C++ personality didn't run
cleanups on faults and LLVM optimized invokes of nounwind functions well. The
only downside at this point was that the support was [sort of scary][scary].

Fast forward to the 1.24.0 release and another [fatal flaw][f3] is found in our
strategy. Historically we've always declared "unwinding into Rust code from
other languages is undefined behavior" (or unwinding out of Rust code). In
1.24.0 we changed `extern` functions defined in Rust to enforce this behavior,
forcibly aborting the process if the function panics. Everything was ship shape
until it was discovered that `longjmp` across Rust frames caused the process to
abort!

It turns out that on MSVC `longjmp` is implemented with SEH! Furthermore it
turns out that the personality we're using, `__CxxFrameHandler3`, recognized the
`longjmp` exception enough to run cleanups. Consequently, when SEH ran past an
`extern` function in Rust it aborted the process. Sounds like "cleanups run on
segfaults" v2!

Well in any case, that's a long-winded way of describing how shoehorning Rust's
desired behavior into existing personality functions is getting more and more
difficult. As a result, this commit starts taking a new strategy of defining
custom personality functions in Rust (like we do for all other platforms) and
teaching LLVM about these personalities so they're classified correctly and
don't suffer from [old bugs][f2].

Alright, so with all that information in your head now this commit can be
described with:

* New personality functions are added for MSVC: `rust_seh{32,64}_personality`.
* These functions are shims around `__C_specific_handler` and `_except_handler3`
  like how on Unix we're typically a shim around a gcc personality.
* Rust's personality functions defer on all exceptions that *aren't*
  Rust-related. We choose an arbitrary code to represent a Rust exception and
  only exceptions with matching codes execute our cleanups/catches. (the
  prevents [previous bugs][f1] with the personalities these functions are
  wrapping).
* LLVM is updated with a small-ish commit to learn about these personality
  functions. The largest change here is, again, [ensuring old bugs don't
  resurface][f2] by teaching LLVM that it can simplify invokes of nounwind
  functions in Rust.
* Finally, bedbad6 is partially reverted to restore the old translation
  behavior of the `try` intrinsic, bringing some scary code back into the
  compiler about `llvm.localescape` and such.

Overall the intent of this commit is to preserve all existing behavior with
panics on MSVC (aka keep old bugs closed and use the same system in general) but
enable longjmps across Rust code. To this effect a test is checked in which
asserts that we can indeed longjmp across Rust code with destructors and such.

[f1]: rust-lang#33112
[f2]: rust-lang#33116
[switch]: rust-lang@38e6e5d0
[scary]: https://github.com/rust-lang/rust/blob/bedbad61195d2eae69b43eca49c6d3e2aee8f208/src/libpanic_unwind/seh.rs#L107-L294
[f3]: rust-lang#48251
@alexcrichton
Copy link
Member Author

After some more discussion this may also not be the strategy we should take. I'm going to test out a different strategy locally.

@Mark-Simulacrum
Copy link
Member

Finally, bedbad6 is partially reverted to restore the old translation
behavior of the try intrinsic, bringing some scary code back into the
compiler about llvm.localescape and such.

The bedbad6 commit appears to be not what you actually wanted to say -- at least it seems to point to an irrelevant commit.

@alexcrichton
Copy link
Member Author

Ok, given the previous discussion I'll close this in favor of #48572

@alexcrichton alexcrichton deleted the noexcept-msvc branch February 27, 2018 03:54
alexcrichton added a commit to alexcrichton/rust that referenced this pull request Feb 28, 2018
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.
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants