Crashes are a fiction created by the Swift Runtime and iOS Kernel. You think your program suddenly stopped working, but this is just system code executing as intended—designed to protect against far more catastrophic problems.
Subscribe to Jacob’s Tech Tavern for free to get ludicrously in-depth articles on iOS, Swift, tech, & indie projects in your inbox every week.
“What is a crash?”
This seems like a pretty obvious question at first — a crash is when your app unexpectedly stops working, and exits. But you might be surprised to learn that a crash is actually more like a controlled detonation: the system triggers the crash to keep your device safe.
Today, we’re delving deep into the underlying source code of Swift and iOS to understand what is actually going on when your app crashes.
A Hierarchy of Badness
A crash is far from the worst pain you can inflict on your users. Here’s the 9-Level hierarchy of the cardinal sins:
Perceptible issues: Frame-drops, slight device heating, unnecessary battery drain.
Mild glitches: Small UI bugs, animation hitches, unresponsive UI, and network timeouts.
Moderate bugs: Broken layout, navigation getting stuck, features not working, and long UI hangs.
Mining bitcoin in the background (but to be fair everybody does this*)
Data loss: Failing to submit or persist data, so they waste time and effort.
Crashing: Exit the process and break the user flow, probably also losing some data.
Data corruption: Losing pre-existing data for good. Your customer just became your biggest detractor.
Security breach: Exposing private information that might put your users at risk outside the context of your app.
System compromise: Escaping the app sandbox via system-level vulnerabilities to put the user’s device (and sometimes their life) at risk.
*my lawyer instructed me to say that not everybody does this.
A crash is a proactive measure to protect system integrity.
Put more simply, the system chooses to crash (a Level 6 sin) wherever it mitigates the chances of causing a Level 7, Level 8, or even Level 9 problem.
The Two Types of Crash
While there are countless reasons to crash, crashes in your iOS app originate from 1 of 2 places: the Swift Runtime and the XNU Kernel.
The Runtime
The Swift Runtime, A.K.A. libswiftCore, runs alongside every Swift program. It provides core functionality for executing Swift such as dynamic dispatch, error handling, & memory management.
The runtime has the authority to crash the running process whenever it detects violations of memory safety, such as accessing memory that’s already been deallocated.
The Kernel
The kernel is the core of the operating system. It creates and manages abstractions on top of hardware — converting physical components like CPU, RAM, & SSD into virtual resources such as threads, heap memory, & file systems.
iOS runs on the XNU Kernel (“X is Not Unix”). Kernel crashes are the last line of defence for memory access violations to the system; designed to protect the user from Level 7+ issues like data corruption and system compromise.
XNU’s Mach subsystem is responsible for virtual memory management. It creates the “virtual address space” abstraction for each app process, then maps this virtual memory to physical RAM. It fires out exceptions such as EXC_BAD_ACCESS to protect the integrity of these address spaces.
The kernel has many ways to kill your process (i.e. crash your app). For example, Watchdog kills any running process which is unresponsive for ~20 seconds, if you manage to block the main thread for that long.
Digging into the open source code
Crashes are far from the worst thing that code can do to your device.
Memory safety issues can allow bad actors to crack open your entire system. If the runtime or kernel let you access deallocated objects, they may not be subject to memory safety guardrails.
If the system let you access the 0xFFFFFFFF-th index of an array, code can break out of the process sandbox and compromise the entire OS.
Conveniently, both the Swift Runtime and the XNU Kernel are open-source, so we can dive straight in to their code and check out both flavours of crash in action.
If you want to understand how dangerous deallocated memory can be, look no further than Jailbreak your Enemies with a Link: Remote Execution on iOS.
Swift Runtime Crash: Unowned
Triggering the crash with an Unowned reference
The Swift Runtime has many guardrails to ensure memory safety. For example, one mechanism crashes out whenever code tries to access the memory address of a deallocated object.
To help developers prevent retain cycles, Swift includes unowned references. These behave like weak references, except they include an assumption: we assume that the memory of the referenced object will always outlive the unowned pointer to it.
If we get these object lifetimes wrong, accessing an unowned reference to a deallocated object leads to a crash. This is the famous dangling pointer.
We’re making a trade-off for performance: Unowned references store less metadata than weak references, because they don’t create a side table on the heap object they point to. Accessing an unowned reference is therefore a little bit more performant — to get at the memory, it requires one less jump between pointers.
I’ve written about unowned references before. Honestly, I don’t like them much, but that’s another story. They are, however, a wonderful source of crashes if you get them wrong.

Implementation of a Runtime Crash
We can handily look into the Swift runtime source code to see this in action. Let’s look at the runtime ABI for an unowned reference in include/swift/runtime/HeapObject.h:
/// An unowned reference in memory. This is ABI.
struct UnownedReference {
HeapObject *Value;
};
/// Aborts if the object has been deallocated.
SWIFT_RUNTIME_EXPORT
void swift_unownedCheck(HeapObject *value);This unowned check is found in the implementation for a runtime heap object, stdlib/public/runtime/HeapObject.cpp:
void swift::swift_unownedCheck(HeapObject *object) {
if (!isValidPointerForNativeRetain(object)) return;
assert(object->refCounts.getUnownedCount() &&
"object is not currently unowned-retained");
if (object->refCounts.isDeiniting())
swift::swift_abortRetainUnowned(object);
}Finally, this abort method can be defined in stdlib/public/runtime/Errors.cpp:
// Crash due to retain of a dead unowned reference.
// FIXME: can't pass the object's address from InlineRefCounts without hacks
void swift::swift_abortRetainUnowned(const void *object) {
if (object) {
swift::fatalError(FatalErrorFlags::ReportBacktrace,
"Fatal error: Attempted to read an unowned reference but "
"object %p was already deallocated", object);
} else {
swift::fatalError(FatalErrorFlags::ReportBacktrace,
"Fatal error: Attempted to read an unowned reference but "
"the object was already deallocated");
}
}We’ve tracked down the exact error message we saw in our example:
Fatal error: Attempted to read an unowned reference but object
0x6000000085e0was already deallocated

What is a FatalError?
The swift::fatalError method itself is also defined in stdlib/public/runtime/Errors.cpp:
// Report a fatal error to system console, stderr, and crash logs, then abort.
SWIFT_NORETURN void swift::fatalError(uint32_t flags, const char *format, ...) {
va_list args;
va_start(args, format);
fatalErrorv(flags, format, args);
}
// Report a fatal error to system console, stderr, and crash logs, then abort.
SWIFT_NORETURN void swift::fatalErrorv(uint32_t flags, const char *format,
va_list args) {
char *log;
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wuninitialized"
swift_vasprintf(&log, format, args);
#pragma GCC diagnostic pop
swift_reportError(flags, log);
abort();
}The abort() function actually originates in the C runtime. It actually signals to the Kernel, generating a SIGABRT which terminates the running process — crashing the app.



