Skip to content

error: fix macOS terminate hang + dump ObjC NSException details#1813

Open
lugt wants to merge 1 commit into
SFTtech:masterfrom
lugt:feature/objc-exception-dump-in-terminate
Open

error: fix macOS terminate hang + dump ObjC NSException details#1813
lugt wants to merge 1 commit into
SFTtech:masterfrom
lugt:feature/objc-exception-dump-in-terminate

Conversation

@lugt

@lugt lugt commented Jul 17, 2026

Copy link
Copy Markdown

Summary

Two related fixes for the terminate handler on Apple platforms, so uncaught exceptions (especially ObjC NSExceptions crossing the C++ boundary) crash with a non-zero exit and are diagnosable. Currently on macOS they hang forever in an infinite loop.

1. std::terminate()std::abort() (the real bug)

On macOS the default terminate handler is libobjc's _objc_terminate(). terminate_handler restores it (std::set_terminate(old_terminate_handler)) then calls std::terminate(). But _objc_terminate does:

type = __cxa_current_exception_type();
if (type) __cxa_rethrow();   // rethrow the in-flight exception

The rethrow_exception(e_ptr) + catch(...) dispatch earlier in terminate_handler does not clear the exception's __cxa "uncaught" state, so _objc_terminate sees an in-flight exception, __cxa_rethrows it, the rethrow is again uncaught → std::terminate_objc_terminate__cxa_rethrow → ... an infinite std::terminate__terminate loop instead of aborting.

This affects all uncaught exceptions on macOS (verified with a plain std::runtime_error), not just ObjC. On Linux the default handler aborts directly without inspecting the exception, so it was never visible there.

Fix: std::abort() terminates unconditionally (SIGABRT, exit 134). On other platforms the default handler already aborts, so behaviour is equivalent.

2. Dump ObjC NSException details from catch(...)

An NSException thrown across a C++ boundary does not derive from std::exception and lands in catch(...) with its name/reason/callStack lost.

  • install_objc_exception_tracker() installs an objc_setExceptionPreprocessor that records the most recent NSException and its throw-time call stack ([NSThread callStackSymbols]) per thread.
  • dump_objc_exception() prints them from the catch(...) arm.
  • Recording at throw time (rather than rethrowing inside catch(...)) avoids disturbing the C++ exception runtime — an earlier rethrow-based attempt (objc_exception_rethrow / std::rethrow_exception + @catch) turned out to be the same class of bug as Age of Empires II (and Definitive Edition) is being preferred over openage #1.

The .mm is built only on Apple (enable_language(OBJCXX) guarded by APPLE); other platforms get no-op stubs.

Testing (macOS 26.3, Apple Silicon; Qt 6.11, LLVM clang 22)

scenario before after
uncaught std::runtime_error infinite loop (hang) FATAL + exception info + stack, exit 134
uncaught ObjC NSException infinite loop (hang) FATAL + name/reason/callStack, exit 134

lldb confirmed the loop root cause: old_terminate_handler resolves to libobjc.A.dylib_objc_terminate(), which __cxa_rethrow`s the in-flight exception.

Default-handler baseline (no libopenage linked): an uncaught ObjC NSException aborts with exit 134 via ObjC's own uncaught-exception path — confirming libopenage's terminate_handler is what introduced the hang.

Changes

  • libopenage/error/handlers.cppstd::terminate()std::abort(); call dump_objc_exception() in catch(...); install/uninstall the tracker alongside the existing SIGSEGV/terminate handlers.
  • libopenage/error/objc_exception.h / .mminstall_objc_exception_tracker() / uninstall_objc_exception_tracker() / dump_objc_exception().
  • CMakeLists.txtenable_language(OBJCXX) on Apple.
  • libopenage/error/CMakeLists.txt — build objc_exception.mm on Apple.

Relation to PR #1812

Independent — based on master (9a5a7ccb), zero file overlap with #1812. Can be merged separately, in either order.

🤖 Generated with Claude Code

Two related fixes for the terminate handler on Apple platforms, so that
uncaught exceptions (especially ObjC NSExceptions crossing the C++ boundary)
actually crash with a non-zero exit and are diagnosable.

1. std::terminate() -> std::abort() in terminate_handler.

   On macOS the default terminate handler is libobjc's _objc_terminate(),
   which calls __cxa_rethrow on the still-in-flight exception. The
   rethrow_exception + catch(...) dispatch above does NOT clear the
   exception's __cxa "uncaught" state, so _objc_terminate would rethrow ->
   std::terminate -> _objc_terminate ..., looping forever instead of
   aborting. This affects ALL uncaught exceptions on macOS, not just ObjC.
   std::abort() terminates unconditionally (SIGABRT, exit 134). On other
   platforms the default handler already aborts, so the behaviour is
   equivalent.

2. Dump ObjC NSException details (name/reason/callStack) from catch(...).

   An NSException thrown across a C++ boundary does not derive from
   std::exception and lands in catch(...) with its details lost. An ObjC
   exception preprocessor records the most recent NSException and its
   throw-time call stack per thread; dump_objc_exception() prints them.
   Recording at throw time (rather than rethrowing inside catch(...))
   avoids disturbing the C++ exception runtime - an earlier rethrow-based
   attempt turned out to be the same class of bug as SFTtech#1.

   The .mm is built only on Apple (enable_language(OBJCXX) guarded by
   APPLE); other platforms get no-op stubs.

Tested on macOS 26.3 / Apple Silicon (LLVM clang 22, Qt 6.11):
- uncaught std::runtime_error  -> FATAL + exception info + stack, exit 134
- uncaught ObjC NSException    -> FATAL + name/reason/callStack, exit 134
Both previously hung in an infinite std::terminate <-> __terminate loop.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@lugt
lugt force-pushed the feature/objc-exception-dump-in-terminate branch from 95e5dd2 to 1f05ca7 Compare July 17, 2026 06:35
@lugt lugt changed the title error: dump ObjC NSException details from the terminate handler error: fix macOS terminate hang + dump ObjC NSException details Jul 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant