I guess I was being too vague when I said “C exceptions”, because you’re right,
that’s not actually a specific thing. More concretely, I was thinking of either
C++ exceptions, or Obj-C exceptions.
One situation I was thinking of would be a rust library that exposes `extern
“C”` functions. Let’s say one of these functions, frob(), takes an `extern “C”`
callback and calls it. Now let’s say I’m writing an Obj-C app, and I do
something like the following:
static void callback() {
[NSException raise:@“Foo Exception” format:@“I just felt like unwinding”];
}
@try {
frob(callback)
}
@catch (NSException *e) {
NSLog(@“I caught my exception!”);
}
This will unwind through the Rust code, before being caught again. It’s hard to
come up with a scenario where this actually results in unsafe state within the
rust library, but let’s say `frob()` uses some sort of shared state (e.g. a
RWArc<>), and is in the middle of modifying the shared state in a way that
causes invariants to be broken temporarily. If unwinding happens when the
invariants are in the broken state, then they’ll be left in the broken state
and the next bit of Rust code that tries to access this shared state will
likely blow up.
This is obviously pretty contrived, but the question is, is this legal to do,
and if so, do we need to do anything?
Daniel’s response says that it’s undefined for a C++ function to throw an
exception past an `extern “C”` boundary. This is something I did not know. But
what about Obj-C exceptions? I haven’t heard about any undefined behavior
regarding Obj-C exceptions. It is generally recognized that throwing an Obj-C
exception past a framework boundary is unsafe (i.e. throwing an exception in
user code that unwinds through Foundation code), but this is because Obj-C code
rarely bothers with @try/@finally and doesn’t have stack objects, so unwinding
through code that doesn’t expect it will often leave data structures in invalid
states.
If all forms of unwinding are considered to be undefined when passing through
an `extern “C”` boundary, then I guess we can consider this issue to be covered
by undefined behavior. Although this doesn’t make me particularly happy,
because it means that it may be impossible to truly contain the unsafety in FFI
functions. One possible way to mitigate this would be to provide a way to wrap
an FFI function with a stub that catches C++/Obj-C exceptions (I think Obj-C
exceptions are unified with the C++ exception machinery on all modern platforms
(i.e. not 32-bit PPC, and I’m not sure about 32-bit x86)) and triggers task
failure. This would mean any attempt to unwind through a Rust function (using
C++ or Obj-C exceptions) would be contained, after a fashion.
Though this does raise another question. What if a C++ function calls a Rust
function, and the Rust function triggers task failure? Is it possible for the
C++ function to catch the task failure using a catch(…) block? Furthermore,
since Daniel said it’s undefined for a C++ exception to be thrown past an
`extern “C”` boundary, does this mean that it’s technically undefined for Rust
to trigger task failure in any function that’s rooted in an `extern “C”`
function?
-Kevin
On Nov 12, 2013, at 11:35 AM, Alex Crichton <[email protected]> wrote:
> You're correct about the safeness of catching failure at a task
> boundary. Rust's invariants about spawning a task involve knowing a
> fair bit about what's allowable to share between a task boundary, and
> that allows us to reason about the failure unwinding to the task
> boundary being a safe operation (as opposed to stopping unwinding at
> an arbitrary location).
>
> I'm not entirely sure what you mean by throwing an exception from C (I
> think there are many flavors of doing this). Right now we implement
> unwinding via C++ exceptions. When a task unwinds, it actually throws
> a magical uint token which then triggers all the C++ machinery for
> unwinding the stack. When compiling with LLVM, we using LLVM's invoke
> instruction + landing pads to generate our "cleanup locations", and
> LLVM will codegen the right code such that all the landing pads are
> invoked during unwinding. What this means is that the C++ exception
> throwing infrastructure will probably fly right past all C code
> because none of it is hooked into the exception handling stuff of C++.
> This may mean, however, that intermediate C++ code may have landing
> pads run (not entirely sure).
>
> All that being said, that's just how it's currently implemented today.
> I don't think that we're guaranteeing this sort of behavior to always
> happen. It will probably always be the case that C stack frames are
> always sailed past during unwinding, but we may implement unwinding
> via precise stack tables and manual stack unwinding at some point
> which wouldn't trigger C++ landing pads (or use LLVM's landing pad
> infrastructure the same way that we're using it today).
>
> Right now it's basically the case that intermingling C with Rust stack
> frames and then triggering failure will only trigger unwinding in rust
> functions (what does it mean to unwind in C?), and I'm not sure I'd
> recommend that as a safe strategy for implementing bindings to a C
> function (all intermediate C allocations are leaked).
>
> Does that make sense? It may not quite answer your question, but
> hopefully that clears up at least a little bit about how it's
> implemented today.
>
> On Tue, Nov 12, 2013 at 11:07 AM, Kevin Ballard <[email protected]> wrote:
>> Right now, Rust does not support catching task failure from within a task,
>> it only supports preventing task failure from cascading into other tasks. My
>> understanding is that this limitation is done because of safety; if a task
>> unwinds through a few frames of code, and then stops unwinding, data
>> structure invariants may have been broken by the unwinding, leaving the task
>> in an unsafe state. Is this correct?
>>
>> Given this assumption, my worry now is about task unwinding outside of the
>> control of Rust. Namely, if I’m using Rust to write a library with extern
>> “C” functions, or I’m providing callbacks to C code from within Rust, (and
>> my Rust code calls back into C at some point), then it’s very possible for
>> the called C code to throw an exception that is then caught in the calling C
>> code a few frames up. The net effect is that the thread will unwind through
>> my Rust code, but it will then be caught before unwinding any further,
>> potentially leaving any data structures in an invalid state (assuming that
>> there’s still Rust code higher up on this same stack that cares).
>>
>> Has this been considered before? Is this actually a danger or am I just
>> being paranoid?
>>
>> -Kevin
>> _______________________________________________
>> Rust-dev mailing list
>> [email protected]
>> https://mail.mozilla.org/listinfo/rust-dev
_______________________________________________
Rust-dev mailing list
[email protected]
https://mail.mozilla.org/listinfo/rust-dev