dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
14.91k stars 4.63k forks source link

Raise the AppDomain.UnhandledException event from Objective-C bridge #102730

Open rolfbjarne opened 3 months ago

rolfbjarne commented 3 months ago

Background and motivation

The ObjectiveCMarshal.Initialize method takes a callback that's called if an exception is supposed to be thrown when returning from managed code to native code.

This works fine, but if we determine that the exception is truly unhandled, there doesn't seem to be a way for us to invoke the AppDomain.UnhandledException event before terminating the process.

This event is often used by customers when logging app crashes, so the fact that we don't always raise it becomes a problem (https://github.com/xamarin/xamarin-macios/issues/15252). We have other events (our own) we raise, but if we could raise the event everybody else uses that would be preferrable.

~Note that we'd need the same for Mono.~ Looks like we can use mono_unhandled_exception for Mono.

API Proposal

namespace System.Runtime.InteropServices.ObjectiveC;

public static class ObjectiveCMarshal 
{
    // This raises the AppDomain.UnhandledException event
    public void OnUnhandledException (Exception exception);
}

API Usage

The method would be called when we detect that there are no Objective-C exception handlers nor any managed exception handlers on the stack.

Currently this happens:

It would also be nice if we could tell the debugger about these unhandled exception as well, but I don't have any idea how that would look.

Alternative Designs

No response

Risks

No response

rolfbjarne commented 3 months ago

CC @AaronRobinsonMSFT

dotnet-policy-service[bot] commented 3 months ago

Tagging subscribers to this area: @dotnet/interop-contrib See info in area-owners.md if you want to be subscribed.

AaronRobinsonMSFT commented 3 months ago

@rolfbjarne Would https://github.com/dotnet/runtime/issues/101560 help with this issue? If not, is this a .NET 9 ask?

rolfbjarne commented 3 months ago

@rolfbjarne Would #101560 help with this issue?

No, I don't think so, that's a proposal to support ignoring unhandled exceptions, while our exceptions can't be ignored, the process will terminate soon after no matter what the handler does.

The fatal error handler would eventually be called, but getting the unhandled managed exception (to log it for instance) after the error handled has been called due to a SIGABRT wouldn't be easy, so it's not really a good alternative.

If not, is this a .NET 9 ask?

Yes, please!

janvorli commented 3 months ago

Maybe that in the Objective-C unhandled exception callback, we can unwind to the first managed frame and rethrow the managed exception from there. That way, the exception that was not handled by the Objective-C stuff would continue flowing into the managed caller of that code and possibly get even handled in the managed code or hit the standard unhandled exception path that would also raise the AppDomain.UnhandledException event. There are some unknowns to me though that can complicate things. For example, I am not sure if objective-C frames can be unwound using the libunwind.

rolfbjarne commented 3 months ago

Maybe that in the Objective-C unhandled exception callback, we can unwind to the first managed frame and rethrow the managed exception from there.

There isn't necessarily any managed frames up the stack (in fact probably won't be any).

Also note that there may be both managed and Objective-C exception handled up the stack... because Apple does something like this sometimes:

void someFunction ()
{
    @try {
        callUserCode ();
    } @catch (NSException *ex) {
        terminateDueToUnhandledObjectiveCException (ex);
    }
}

Stack trace: https://gist.github.com/rolfbjarne/5ab15ce73bc6476d4cee83cff387dd30

frame 0 is our unhandled Objective-C exception callback. frame 6 is (probably) something like the someFunction from above. frame 20 has an Objective-C catch clause. frames 26-23 are managed frames.

In any case, it doesn't sound like a good idea to try to recover and continue executing from this stack trace:

    frame #2: 0x000000018005fbe4 libobjc.A.dylib`_objc_terminate() + 112
    frame #3: 0x000000018028e150 libc++abi.dylib`std::__terminate(void (*)()) + 12
    frame #4: 0x000000018028e100 libc++abi.dylib`std::terminate() + 52
    frame #5: 0x00000001800842d4 libobjc.A.dylib`objc_terminate + 12

That way, the exception that was not handled by the Objective-C stuff would continue flowing into the managed caller of that code and possibly get even handled in the managed code or hit the standard unhandled exception path that would also raise the AppDomain.UnhandledException event.

We end up with a kind of chicken-and-egg problem here: exceptions are only truly unhandled if there are neither Objective-C exception handlers nor managed exception handlers up the stack.

  1. We convert Objective-C exceptions into managed exceptions after calling Objective-C selectors that throw Objective-C exceptions.
  2. We convert managed exceptions into Objective-C exceptions when returning to native code and a managed exception occurred.

This means that (almost) all unhandled managed exceptions will actually be converted into Objective-C exceptions at some point.

We reach the end when the there are no Objective-C exception handlers on the stack, and the unhandled Objective-C exception callback is called. At this point we can inspect the Objective-C exception, and determine whether it originated from a managed exception (and this is where we want to call AppDomain.UnhandledException).

janvorli commented 3 months ago

Ok, I can see I have not considered the cases when objective-C thread is reverse-pinvoking managed code or when there is an objective-C host. What I was talking about was a case when managed code app would call objective-C method. I have also not expected that objective C would call its unhandled exception handler down the call chain from the std::terminate in what I assume is a termination handler. I am not sure if it would be safe to execute any managed code (the AppDomain.UnhandledException) on that thread from that state at all.

AaronRobinsonMSFT commented 3 months ago

@VSadov Do you have any thoughts on adding an additional API to https://github.com/dotnet/runtime/issues/101560? The issue here is attempting to trigger the runtime's UnhandledException infrastructure on demand. It seems like it fits nicely with the aforementioned API. The API itself is likely uninteresting, but the semantics definitely are.

namespace System.Runtime.ExceptionServices
{
    public static class ExceptionHandling
    {
        /// <summary>
        /// Triggers the runtime's UnhandledException infrastructure.
        /// </summary>
        /// <param name="exception">Exception to trigger</exception>
        /// <remarks>
        /// QUESTION: What happens on return?
        /// </remarks>
        public static void TriggerUnhandledExceptionHandlers(Exception exception);
    }
}

/cc @jkotas