Mobile Development 15 min read

Why iOS 18 Crashes When exit() Is Called and How to Fix It

This article analyzes a driver‑side iOS 18 crash triggered by BackBoardServices calling exit, explains the underlying XPC and C++ destructor chain, explores failed hook and compiler‑flag workarounds, and presents a reliable atexit‑based solution that eliminates the crash on iOS 18 and later.

Huolala Tech
Huolala Tech
Huolala Tech
Why iOS 18 Crashes When exit() Is Called and How to Fix It

Background

Our driver‑side App started crashing on iOS 18 when a method in the BackBoardServices library triggered an exit call. After exit executes, a C++ global object is destroyed, leading to a crash.

Crash stacks (two patterns)

Pattern 1: -[BKSHIDEventObserver init] + 0 Pattern 2:

-[BKSHIDEventDeliveryManager _initForTestingWithService:] + 0

The issue appears only on iOS 18 and higher, typically after the app has been in the background for a while.

Root Cause Analysis

When the user touches the iPhone, the system daemon backboardd detects the event and uses BackBoardServices to package the data into an IOHIDEvent. This event is sent via BoardServices over IPC to the foreground daemon SpringBoard, which finally forwards it to the app.

The exit path involves: libsystem_c.dylib ___cxa_finalize_ranges + 480 – runs global destructors and atexit callbacks. libsystem_c.dylib _exit + 32 – calls the low‑level exit routine.

During this process, BackBoardServices detects an invalid parameter and calls exit. The C++ global object PosBridgeImpl is then destroyed, which in turn destroys GPSHelper. GPSHelper 's destructor calls TimerEventRunnerObserverImp::detachTimer(), which accesses a freed target object, causing an EXC_BAD_ACCESS (SIGSEGV) at <+36>: ldr x8, [x8, #0x18]. The likely cause is a multithread race in a third‑party library.

The leading underscore in symbols like _exit is an ABI‑mandated name‑mangling that isolates system symbols and aids debugging.

Solution Attempts

Attempt 1 – Hooking

We tried hooking exit in libsystem_c.dylib and two BackBoardServices methods ( -[BKSHIDEventObserver init] and -[BKSHIDEventDeliveryManager _initForTestingWithService:]). The hook would detect a BackBoardServices call and replace exit(0) with _exit(0). This worked in debug and release builds signed with a development certificate (which have get-task-allow), but failed in App Store builds because iOS 14+ introduces Pointer Authentication Codes (PAC). PAC rejects the modified function pointers, causing an instruction‑level abort.

Consequences of PAC failure include:

CPU throws EXC_BAD_ACCESS with code 0x8badf00d.

The offending thread is suspended.

The kernel sends SIGKILL, marking the process as “contaminated”.

The app crashes instantly without a crash log.

Attempt 2 – Suppressing Destructors

We explored two compiler‑level options: [[clang::no_destroy]] – disables destructor generation for specific globals. Not usable because the problematic objects reside in a third‑party framework without source. -fno-c++-static-destructors – disables all static destructors. The flag cannot affect the pre‑compiled third‑party framework, so the crash persisted.

Attempt 3 – Atexit‑Based Fix (Successful)

We registered a custom atexit handler that runs **before** the C++ static destructors (by registering after the framework’s own atexit entries). The handler inspects the current thread’s stack; if it contains BackBoardServices, it calls _exit(0) directly, bypassing the C++ destructor chain.

This approach works for iOS 18 and later, and has been deployed without introducing latency, freezes, or other stability regressions.

Result

After releasing the atexit‑based fix, the crash disappeared completely in the new version, and no new performance or stability issues were observed.

The ideal long‑term solution remains a fix from the third‑party library vendor.

Summary

The iOS 18 crash originates from a system‑level exit triggered by BackBoardServices, which leads to a C++ global destructor accessing a freed object. Hooking fails on App Store builds due to PAC, and compiler flags cannot affect third‑party binaries. Registering a higher‑priority atexit handler that replaces exit with _exit(0) successfully prevents the destructor from running and eliminates the crash.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

mobile developmentiOSC++Crash AnalysisHookingPAC
Huolala Tech
Written by

Huolala Tech

Technology reshapes logistics

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.