Why Does UIDictationConnection Crash on iOS 16+? A Deep Dive into EXC_BAD_ACCESS and Fixes
This article analyzes the UIDictationConnection crash on iOS 16 and later, explains the EXC_BAD_ACCESS (SIGSEGV) cause, walks through assembly‑level stack inspection, and provides a thread‑safe locking solution with runtime hooks to prevent the wild‑pointer failure.
Background
Our product experiences a crash when the keyboard voice input ends and the speech input is cancelled via UIDictationConnection cancelSpeech. The crash reports are widely posted on Apple forums but lack concrete fixes.
The crash only occurs on iOS 16 and above, indicating it is introduced by the system version upgrade.
Root Cause Analysis
Crash Type The crash is an EXC_BAD_ACCESS (SIGSEGV) , which points to a dangling pointer.
EXC_BAD_ACCESS means a bad address exception. It can be caused by: Writing to a read‑only region. Accessing memory that has been freed or is out of bounds (SIGSEGV). Unaligned memory, non‑existent physical address, or hardware error (SIGBUS). In our case, the most common scenario is a wild pointer.
Stack Restoration We reproduce the crash on a matching or close system version in release mode. Release mode enables compiler optimisations that keep the generated assembly consistent with the production binary.
Assembly Instruction Debugging The last line of the stack shows _objc_msgSend + 32 . Tracing this offset leads to the instruction that reads the Class object's cache bucket, which eventually accesses an invalid address because the UIDictationController instance has already been deallocated.
Key registers observed during debugging: x0 : pointer to the destroyed object (nil). x1 : address of the method name ( lastHypothesis ). x13 : isa pointer of the object. x16 : Class pointer.
Solution
Add a lock to UIDictationController 's setLastHypothesis: and lastHypothesis methods to guarantee thread safety.
Hook these methods with a lock, then copy the returned NSString via mutableCopy before releasing the lock, ensuring the object is not reclaimed while another thread reads it.
Restrict the fix to iOS 16‑iOS 17 with a system‑version check and provide a downgrade switch for older versions.
#import <objc/runtime.h>
#import "FJFClassInfoPrint.h"
@implementation FJFClassInfoPrint
+ (void)printClassVarWithClassName:(NSString *)className {
unsigned int numIvars;
Ivar *vars = class_copyIvarList(NSClassFromString(className), &numIvars);
for (int i = 0; i < numIvars; i++) {
Ivar thisIvar = vars[i];
NSString *key = [NSString stringWithUTF8String:ivar_getName(thisIvar)];
NSLog(@"%@ variable name :%@", className, key);
NSString *type = [NSString stringWithUTF8String:ivar_getTypeEncoding(thisIvar)];
NSLog(@"%@ variable type :%@", className, type);
}
free(vars);
}
+ (void)printClassMethodWithClassName:(NSString *)className {
unsigned int numMethods = 0;
Method *methods = class_copyMethodList(NSClassFromString(className), &numMethods);
for (int i = 0; i < numMethods; i++) {
Method m = methods[i];
SEL sel = method_getName(m);
const char *name = sel_getName(sel);
NSLog(@"%@ method :%s", className, name);
}
free(methods);
}
@endHook implementation (simplified):
[NSClassFromString(@"UIDictationController") hd_hookMethod:NSSelectorFromString(@"setLastHypothesis:") option:HDHookOptionInstead handle:^(HDInvocation *invocation){
[invocation.target hdcore_lock] lock];
[invocation invoke];
[invocation.target hdcore_lock] unlock];
} error:nil];
[NSClassFromString(@"UIDictationController") hd_hookMethod:NSSelectorFromString(@"lastHypothesis") option:HDHookOptionInstead handle:^(HDInvocation *invocation){
[invocation.target hdcore_lock] lock];
__autoreleasing NSString *orgHypothesis;
[invocation invokeWithReturnValue:&orgHypothesis];
orgHypothesis = [orgHypothesis mutableCopy];
[invocation.target hdcore_lock] unlock];
return orgHypothesis;
} error:nil];Conclusion
The crash stems from multithreaded access to UIDictationController.lastHypothesis after the object has been released, leading to a wild‑pointer SIGSEGV. By adding proper locking around the setter and getter (and copying the returned string), the issue is eliminated on iOS 16‑iOS 17 devices.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
