Stream: wg-ffi-unwind

Topic: longjmp interaction with destructors


Amanieu (Oct 28 2019 at 13:44, on Zulip):

I would just make it UB unless there's actual demand for people relying on longjmp running destructors.

gnzlbg (Oct 28 2019 at 13:45, on Zulip):

What does longjmp do for automatic variables in a frame ?

gnzlbg (Oct 28 2019 at 13:45, on Zulip):

Depending on how its implemented, it semantically either calls mem::drop, or it calls mem::forget

gnzlbg (Oct 28 2019 at 13:45, on Zulip):

both are ok

gnzlbg (Oct 28 2019 at 13:45, on Zulip):

and actually safe

Amanieu (Oct 28 2019 at 13:46, on Zulip):

Only if the code that is being jumped over expects this.

gnzlbg (Oct 28 2019 at 13:46, on Zulip):

the only real issue is that some types have a "Safety" requirement that says "you can't deallocate this type without calling destructors", making forget illegal on them

gnzlbg (Oct 28 2019 at 13:46, on Zulip):

but that's a safety invariant that unsafe code working with these types must upheld

gnzlbg (Oct 28 2019 at 13:47, on Zulip):

Only if the code that is being jumped over expects thi

Well yes, but APIs can write arbitrary requirements in the safety clause.

gnzlbg (Oct 28 2019 at 13:47, on Zulip):

I don't think this one is very different from any other pre-condition

gnzlbg (Oct 28 2019 at 13:48, on Zulip):

So... IMO the rules for longjmp follow from the rules of mem::forget

Amanieu (Oct 28 2019 at 13:48, on Zulip):

IMO code should be able to assume that any callbacks either return properly or unwind and run all destructors.

gnzlbg (Oct 28 2019 at 13:48, on Zulip):

For this types, you can put them in some memory address on the heap, and if you deallocate them with GlobalAlloc::dealloc without running their destructor the behavior is undefined as well

gnzlbg (Oct 28 2019 at 13:48, on Zulip):

that's "library UB" because the code violates a safety invariant expressed in a library API comment

Amanieu (Oct 28 2019 at 13:49, on Zulip):

Because DropGuards can be used to maintain safety invariants, and currently a lot of code in the wild does this (even code in libstd).

gnzlbg (Oct 28 2019 at 13:49, on Zulip):

one doesn't need longjmp for that

gnzlbg (Oct 28 2019 at 13:50, on Zulip):

I don't see a reason to forbid longjmp over frames with destructors that lack safety invariants

gnzlbg (Oct 28 2019 at 13:50, on Zulip):

it's a perfectly correct thing to do, we'll just be introducing UB to forbid something that has no issues

gnzlbg (Oct 28 2019 at 13:52, on Zulip):

Also, DropGuard has no issues in safe Rust

Amanieu (Oct 28 2019 at 13:52, on Zulip):

Here's an example: slice::sort is unsound if Ord::cmp longjmps without running destructors.

gnzlbg (Oct 28 2019 at 13:52, on Zulip):

if that frame calls some other unsafe Rust code that violates its safety invariants..

gnzlbg (Oct 28 2019 at 13:52, on Zulip):

Sure, but then that Ord::cmp implementation is unsound

gnzlbg (Oct 28 2019 at 13:52, on Zulip):

because it is safe code that calls unsafe code that does not properly encapsulate the unsafety

gnzlbg (Oct 28 2019 at 13:54, on Zulip):

Maybe i'm miscommunicating: i'm not proposing to make longjmp safe, it would still be unsafe to call, and the caller needs to make sure that doing that does not result in UB.

gnzlbg (Oct 28 2019 at 13:55, on Zulip):

There are many types with destructors that can be longjmp'ed without issues, I'd rather not introduce a completely new different kind of UB just for longjmp

Amanieu (Oct 28 2019 at 13:56, on Zulip):

My point is, slice::sort should be able to assume that Ord::cmp won't longjmp out without running destructors, without having to list it in the safety invariants in the slice::sort docs.

gnzlbg (Oct 28 2019 at 13:57, on Zulip):

Sure, it can do that

gnzlbg (Oct 28 2019 at 13:57, on Zulip):

Ord::cmp is a safe Rust function

gnzlbg (Oct 28 2019 at 13:57, on Zulip):

those functions cannot longjmp

gnzlbg (Oct 28 2019 at 13:58, on Zulip):

If we allow safe Rust code to longjmp, we would make safe Rust unsound

gnzlbg (Oct 28 2019 at 13:58, on Zulip):

So we can't do that

Amanieu (Oct 28 2019 at 13:58, on Zulip):

Anyways, I feel that unless you have a particular use case for allowing longjmp to skip destructors, we should just make it UB.

gnzlbg (Oct 28 2019 at 13:59, on Zulip):

The problem is that skipping destructors is ok

gnzlbg (Oct 28 2019 at 13:59, on Zulip):

in many cases

gnzlbg (Oct 28 2019 at 13:59, on Zulip):

and there is no longjmp in the Rust spec

gnzlbg (Oct 28 2019 at 13:59, on Zulip):

so we can't talk about it

gnzlbg (Oct 28 2019 at 13:59, on Zulip):

So we would be introducing a clause somewhere saying that skipping destructors in safe Rust in certain cases is UB

gnzlbg (Oct 28 2019 at 13:59, on Zulip):

which already is

gnzlbg (Oct 28 2019 at 14:00, on Zulip):

but that does not ban longjmp

gnzlbg (Oct 28 2019 at 14:00, on Zulip):

it just ban longjmp from those uses

gnzlbg (Oct 28 2019 at 14:03, on Zulip):

Best case we maybe we can say that "Unwinding a frame without invoking destructors is UB", but that means that longjmp on MSVC is ok, because it does run destructors there

Amanieu (Oct 28 2019 at 14:04, on Zulip):

(assuming you've used #[unwind(allowed)] in all the right places)

gnzlbg (Oct 28 2019 at 14:04, on Zulip):

sure

gnzlbg (Oct 28 2019 at 14:04, on Zulip):

otherwise destructors might be optimized away

Amanieu (Oct 28 2019 at 14:04, on Zulip):

OK that sounds reasonable to me.

Amanieu (Oct 28 2019 at 14:05, on Zulip):

Though I would still prefer outright making skipping destructors UB.

gnzlbg (Oct 28 2019 at 14:05, on Zulip):

When one thinks of the stack in terms of miri, you have a Vec<Frame> where Frame { vars..: Box<T> }

gnzlbg (Oct 28 2019 at 14:05, on Zulip):

A panic drops a frame, and a longjmp mem::forgets a frame

gnzlbg (Oct 28 2019 at 14:05, on Zulip):

We already have the rules for both in the language

gnzlbg (Oct 28 2019 at 14:06, on Zulip):

and, e.g., if you have a Box<Pin<....>> and you deallocate the Pin without running destructors, you get the exact same of UB that you get if you longjmp over a Frame containing a Pin

gnzlbg (Oct 28 2019 at 14:07, on Zulip):

So the rules that there is UB for certain longjmps are already there, and they apply to more cases than just the longjmp themselves

gnzlbg (Oct 28 2019 at 14:08, on Zulip):

So to me not adding an extra rule to forbid longjmps over frames with destructors even when that is not really UB of another kind complicates the language for little reason

gnzlbg (Oct 28 2019 at 14:08, on Zulip):

I mean, a longjmp over a Pin isn't "real UB"

gnzlbg (Oct 28 2019 at 14:09, on Zulip):

the UB is when the storage of the Pin is deallocated without running the destructor, and a different thread tries later to do a use-after-free

gnzlbg (Oct 28 2019 at 14:10, on Zulip):

but if that does not happen, everything is still "fine" (your program might be non portable or whatever, but it does not have UB)

gnzlbg (Oct 28 2019 at 14:11, on Zulip):

If that does happen, you have a use-after-free, whether that was caused by a longjmp, or a call to Alloc::dealloc, isn't really that different

Kyle Strand (Oct 28 2019 at 17:15, on Zulip):

Re: this bit in the PR thread:

Actually longjmp does run destructors (both C++ and Rust) because it uses a SEH exception to unwind. This was already the case before this PR. However _CxxFrameHandler3 specifically ignores longjmp exceptions for catch blocks.

This behavior is documented in MSVC, and is allowed by the C++ standard since it is UB to unwind across frames with destructors.

Maybe you already knew this, @Amanieu , but what I have been told is that MSVC does not invoke C++ destructors on longjmp unwind, but Clang-targeting-MSVC does.

Amanieu (Oct 28 2019 at 17:19, on Zulip):

https://docs.microsoft.com/en-us/cpp/cpp/using-setjmp-longjmp?view=vs-2019

If you use an /EH option to compile C++ code, destructors for local objects are called during the stack unwind. However, if you use /EHs or /EHsc to compile, and one of your functions that uses noexcept calls longjmp, then the destructor unwind for that function might not occur, depending on the optimizer state.

Kyle Strand (Oct 28 2019 at 17:22, on Zulip):

Oops. Yes, I've read that, but apparently forgot it.

nikomatsakis (Nov 01 2019 at 15:35, on Zulip):

I've (intentionally) withheld from reading this thread

nikomatsakis (Nov 01 2019 at 15:35, on Zulip):

I would like to make a request

nikomatsakis (Nov 01 2019 at 15:35, on Zulip):

could somebody -- @gnzlbg, @Amanieu, @Kyle Strand -- open a PR against the ffi-unwind repo that summarizes how longjmp interacts with destructurs on various tier 1 platforms? (i.e., summarizes what was learned from this thread?)

Last update: Nov 15 2019 at 11:15UTC