C++ exceptions on iOS

I use Crashlytics to generate crash reports for an iOS app. When C++ exceptions are thrown on the main thread, they are caught by a CFRunLoop exception handler and rethrown, which destroys all the information about where in the code the original exception is thrown.


There is an open radar for this, opened two years ago, that was marked as "duplicate", but the duplicate doesn't exist as far as I can tell: http://www.openradar.me/radar?id=4943586562932736


This is a Stackoverflow thread about the issue: https://stackoverflow.com/questions/13777446/ios-how-to-get-stack-trace-of-an-unhandled-stdexception


I've tried all sorts of workarounds, including printing the stack trace with [NSThread callStackSymbols] in the constructor for the exception I use. This sort of works in debug builds (at least I can see the trace, but Crashlytics still lumps them all into "std::terminate"). But this does not work in release builds. The symbols are random symbols that don't correspond at all to the actual stack trace. The end result is that I have no usable stack information for any crashes on the main thread from C++ exceptions, which is unfortunate because my whole app is written in C++.


It's been several years since this was reported to Apple. The proper solution is to allow us to turn off the CFRunLoop exception trapper. Should I file another radar (hopefully it won't also be closed as duplicate) or what else should I do to draw Apple's attention to this? Being unable to report C++ exceptions is a pretty big deal.

Accepted Reply

Bug report is here

Thanks! I looked at your bug (r. 50481364) and it’s definitely landed in the right place and been interpreted correctly. Alas, I can’t offer any predictions as to when it might be resolved.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Replies

Any update on this?

I have been depending on dynamically rebind __cxa_throw in the loaded libraries to catch the original stack trace, implementation explained here https://github.com/facebook/fishhook

The problem is after iOS 14.5 it's not working anymore because I cannot find the Mach-O section where "lazy symbol pointer table" is found, and thus not able to rebind __cxa_throw_. I have discussed this in a lap in WWDC21, it looks related to a change called "Chained fixups" that I'm still searching for.

If the bug reported here is fixed I won't need that workaround at all.

More info:

Sections in > iOS 14.5

Image .... /usr/lib/libc++.1.dylib
sect name: __got_weak in __DATA 
found non lazy symbol pointer table ✔️
sect name: __data in __DATA 
sect name: __bss in __DATA 
sect name: __common in __DATA 
sect name: __got in __DATA_CONST 
found non lazy symbol pointer table ✔️
sect name: __const in __DATA_CONST 

Sections < iOS 14.5

Image .... /usr/lib/libc++.1.dylib
sect name: __got_weak in __DATA 
found non lazy symbol pointer table ✔️
sect name: __la_weak_ptr in __DATA 
found lazy symbol pointer table ✔️
sect name: __got in __DATA_CONST 
found non lazy symbol pointer table ✔️
sect name: __la_symbol_ptr in __DATA_CONST 
found lazy symbol pointer table ✔️
✅ found __cxa_throw in __la_symbol_ptr in __DATA_CONST

Any update on this?

Nope. The bug in question (r. 50481364) remains unfixed.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

@eskimo If you are unable to resolve this issue for historical reasons, may be Apple can allows to set custom exception handling / filter to CFRunLoop/libDispatch. Then the user can manually record the stack trace using [NSThread callStackSymbols] and manually attach to the crash-log-system. And yes, it's still weak, but it's better than nothing.

may be Apple can allows to set custom exception handling / filter

You should feel free to ask for that in an enhancement request.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

First up, I have nothing new to report regarding this bug (r. 50481364). However, I recently stumbled across something that might be of interest for folks in this situation, namely, the C++ noexcept modifier.

Consider this Objective-C++ code:

 1 #import "AppDelegate.h"
 2 
 …
25 
26 @implementation AppDelegate {
27     Monkey * _monkey;
28 }
29 
30 @synthesize window = _window;
31 
 …
45 - (IBAction)seeAction:(id)sender {
46     #pragma unused(sender)
47     self->_monkey->see();
48 }
49 
50 - (IBAction)hearAction:(id)sender {
51     #pragma unused(sender)
52     self->_monkey->hear();
53 }
54 
55 - (IBAction)speakAction:(id)sender {
56     #pragma unused(sender)
57     auto ok = noexcept( self->_monkey->speak() );
58     assert(ok);
59 }
60 
61 @end

which calls through to this C++ back end:

class Monkey {
public:
    void see() {
        this->evil();
    }
    void hear() noexcept {
        this->evil();
    }
    void speak() {
        this->evil();
    }
private:
    void evil() {
        throw 42;
    }
};

In the -seeAction: case you get this in the crash report:

Thread 0 Crashed::  Dispatch queue: com.apple.main-thread
0   libsystem_kernel.dylib  … __pthread_kill + 8
1   libsystem_pthread.dylib … pthread_kill + 288
2   libsystem_c.dylib       … abort + 180
3   libc++abi.dylib         … abort_message + 132
4   libc++abi.dylib         … demangling_terminate_handler() + 348
5   libobjc.A.dylib         … _objc_terminate() + 160
6   libc++abi.dylib         … std::__terminate(void (*)()) + 16
7   libc++abi.dylib         … __cxa_rethrow + 148
8   libobjc.A.dylib         … objc_exception_rethrow + 44
9   AppKit                  … -[NSApplication _handleEvent:] + 120

which is not helpful.

Note This on macOS 14.1.2. I’m building with Xcode 15.0. The noexcept modifier is supported in C++11 and later.

OTOH, in the -hearAction: case, where the underlying C++ method is marked as noexcept, you get this:

Thread 0 Crashed::  Dispatch queue: com.apple.main-thread
0   libsystem_kernel.dylib  … __pthread_kill + 8
1   libsystem_pthread.dylib … pthread_kill + 288
2   libsystem_c.dylib       … abort + 180
3   libc++abi.dylib         … abort_message + 132
4   libc++abi.dylib         … demangling_terminate_handler() + 348
5   libobjc.A.dylib         … _objc_terminate() + 160
6   libc++abi.dylib         … std::__terminate(void (*)()) + 16
7   libc++abi.dylib         … std::terminate() + 56
8   TestNoExcept            … -[AppDelegate hearAction:] + 64 (AppDelegate.mm:52)
9   AppKit                  … -[NSApplication(NSResponder) sendAction:to:from:] + 460

Because the method is noexcept, the runtime traps when you throw an exception out of it. That’s a noticeable improvement, in that at least you can tell where the C++ code broke its contract.

The -speakAction: case is for when you’re unable to decorate your C++ method with noexcept. It results in this:

Thread 0 Crashed::  Dispatch queue: com.apple.main-thread
0   libsystem_kernel.dylib  … __pthread_kill + 8
1   libsystem_pthread.dylib … pthread_kill + 288
2   libsystem_c.dylib       … abort + 180
3   libsystem_c.dylib       … __assert_rtn + 284
4   TestNoExcept            … -[AppDelegate speakAction:] + 88 (AppDelegate.mm:58)
5   AppKit                  … -[NSApplication(NSResponder) sendAction:to:from:] + 460

You could, of course, wrap this in a macro or function that makes it a bit less clunky.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

noexcept approach is great. Although not feasible for us since we have lot of calls to cpp libs spread all over the codebase...

Hi Quinn,

I just clicked through to this from another thread.

     auto ok = noexcept( self->_monkey->speak() );

I'm not sure if that's doing what you think it' doing. It does a compile-time check of whether the expression is declared noexcept. It's not doing a run-time check of whether the expression has thrown, which is what I think you wanted. Have I understood correctly?

https://en.cppreference.com/w/cpp/language/noexcept