-
Notifications
You must be signed in to change notification settings - Fork 13
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
Double-panic behavior on C++->Rust unwinding #6
Comments
Initially, I think it's reasonable to consider this undefined behavior. We will need to look at how the double |
I think we probably want to extend the UB initially to the opposite case as well, that is, when a Rust "C unwind" function panics and in C++ a This means that a safe |
This is what I was referring to on Zulip about "defining the behavior of native (non-Rust) code when attempting to throw while a Rust panic is already in-flight." I don't understand how Rust could specify this behavior for C++. |
@BatmanAoD ah, I completely misunderstood you then, since I left very similar comments in the PR. So yes, I agree this is wrong:
We can't specify this. What we can do is document that, if this happens, the C++ standard doesn't make any guarantees about this behavior, and document what guarantees each of the target implementations makes. For example, Itanium-C++ says that if a foreign exception, e.g., thrown by Rust, causes C++ to throw while unwinding, the behavior is undefined. |
For future reference (https://itanium-cxx-abi.github.io/cxx-abi/abi-eh.html#base-personality):
EDIT: this is the behavior that C++ on targets with the Itanium ABI has, when a foreign exception, such as the one caused from unwinding from Rust into C++, triggers a double-drop in C++. |
Okay, thanks for the clarifications. Would you mind submitting a PR adding some verbiage to the roadmap to the effect that |
So I made the behavior of this implementation defined in #9 , which on x86_64-apple-darwin and x86_64-unknown-linux-gnu (and probably all other Itanium targets, like FreeBSD) can be defined to |
Guard against unwinding in cleanup code Currently the only safe guard we have against double unwind is the panic count (which is local to Rust). When double unwinds indeed happen (e.g. C++ exception + Rust panic, or two C++ exceptions), then the second unwind actually goes through and the first unwind is leaked. This can cause UB. cc rust-lang/project-ffi-unwind#6 E.g. given the following C++ code: ```c++ extern "C" void foo() { throw "A"; } extern "C" void execute(void (*fn)()) { try { fn(); } catch(...) { } } ``` This program is well-defined to terminate: ```c++ struct dtor { ~dtor() noexcept(false) { foo(); } }; void a() { dtor a; dtor b; } int main() { execute(a); return 0; } ``` But this Rust code doesn't catch the double unwind: ```rust extern "C-unwind" { fn foo(); fn execute(f: unsafe extern "C-unwind" fn()); } struct Dtor; impl Drop for Dtor { fn drop(&mut self) { unsafe { foo(); } } } extern "C-unwind" fn a() { let _a = Dtor; let _b = Dtor; } fn main() { unsafe { execute(a) }; } ``` To address this issue, this PR adds an unwind edge to an abort block, so that the Rust example aborts. This is similar to how clang guards against double unwind (except clang calls terminate per C++ spec and we abort). The cost should be very small; it's an additional trap instruction (well, two for now, since we use TrapUnreachable, but that's a different issue) for each function with landing pads; if LLVM gains support to encode "abort/terminate" info directly in LSDA like GCC does, then it'll be free. It's an additional basic block though so compile time may be worse, so I'd like a perf run. r? `@ghost` `@rustbot` label: F-c_unwind
Now that rust-lang/rust#92911 has been merged, I believe the only remaining issue here (discussed in Zulip) is a Rust panic escaping into the C++ runtime while a C++ exception is in-flight. In practice, it should usually be safe, but the Itanium ABI specifies that it's UB; I think it's therefore reasonable to formally specify that this is UB for Rust as well. |
There are usability problems if this is UB. To avoid UB, you have to either:
Neither sounds ideal. |
In practice, both libc++ (Clang) and libstdc++ (GCC) will call For the reverse case, Rust panics don't have any thread-local state while they are running and are effectively invisible to the C++ exception machinery. So nesting a C++ exception while a Rust panic is unwinding "just works". |
@nbdd0121 I'm not sure I see why this is particularly problematic "sharp edge", though. Cross-language exception handling is a fairly niche use case; panicking from within a I think ideally we'd want to be able to guarantee an |
The specific case to trigger the UB is: #[no_mangle]
extern "C-unwind" fn rust_panic() {
panic!();
} int main() {
try {
rust_panic();
} catch (...) {
// The Rust panic is caught here.
try {
// Throw a C++ exception
throw exception();
} catch (exception&) {
// UB happens here: you can't have a live foreign exception (in the catch block) at the same time as a C++ exception.
// This is because internally C++ keeps a linked list of live exceptions, but this doesn't work with foreign exception objects.
}
}
} |
Oh, I thought we were talking about UB that only occurs when the That still seems...not that bad, to be honest. Or, more precisely: it seems like it's not our problem. AFAIK C++ doesn't (and probably never will) attempt to define behavior involving "foreign exceptions". We've done our due diligence on the Rust side, I think, of trying to make Rust a well-behaved citizen; I'm not sure it makes much sense to try to ensure that C++ is also a well-behaved neighbor. |
} catch (...) {
// The Rust panic is caught here.
try {
// Throw a C++ exception
throw exception();
} catch (exception&) {
// UB?
}
} The case @Amanieu mentioned above. In my test case, Rust will still give Basically, the result is same as: // foreign exception from Rust
} catch(...) {
// code
} The code will also be executed but fatal runtime error at the end. So not so bad... just like @BatmanAoD said? |
I don't think this is worth solving. In practice it's going to abort, even though the spec technically says it's UB. |
Ideally I think we should specify that this is not UB for Rust code. The fact that it's technically defined as UB per C++ Itanium ABI for C++ code is beyond our control. |
@nbdd0121 Doesn't that mean that the behavior might actually be different on different Itanium boards, regardless of what code we generate? I'd be okay with a note in the reference saying something like "in practice, this should cause the process to abort, but is formally undefined," but I don't want to say that Rust guarantees this isn't UB if we don't actually control it. |
This code:
is guaranteed to panic twice, once printing
"bar"
and once again printing"Foo"
, and then, the program aborts due to the double panic printing"thread panicked while panicking. aborting"
.I wonder what would happen if we replace
bar
above with anextern "C unwind" { fn bar(); }
function implemented in C++ asextern "C" void bar() { throw "bar"; }
or similar (e.g. throwing astd::string("bar")
,std::runtime_exception("bar")
, etc.).The text was updated successfully, but these errors were encountered: