Mobile Development 16 min read

iOS 17 Text‑to‑Speech Crash: Root Cause and Effective Fixes

This article investigates a recurring text‑to‑speech crash on iOS 17 devices, detailing the EXC_BAD_ACCESS error, analyzing stack traces, exploring internal AVAudioEngine and AUAudioUnit_XPC structures, and presenting two remediation strategies—including a hook‑based approach that safely bypasses problematic dealloc and stop calls.

Huolala Tech
Huolala Tech
Huolala Tech
iOS 17 Text‑to‑Speech Crash: Root Cause and Effective Fixes

Background

Since iOS 17, the driver app experiences a text‑to‑speech crash that mainly occurs during navigation and driver fulfillment voice prompts. The crash is confined to iOS 17.0‑iOS 17.2 versions.

Crash stack patterns

Two typical stacks are observed:

-[AVAudioEngine dealloc] + 56
-[AVAudioEngine stop] + 48

Both stacks point to an EXC_BAD_ACCESS (SIGSEGV) caused by a dangling pointer.

Root Cause Investigation

The crash type is EXC_BAD_ACCESS (SIGSEGV), indicating a wild pointer. By reproducing the issue on a release build and stepping through the code, the following observations were made:

In the lower‑level stack, the instruction offset 92‑4 = 88 points to auoop::RenderPipeUser::~RenderPipeUser().

The RenderPipeUser object holds an NSXPCConnection pointer ( _xpcConnection).

The same NSXPCConnection pointer is also stored in the private variable _xpcConnection of the AUAudioUnit_XPC instance.

Because the _xpcConnection is accessed from multiple threads (dealloc and stop are called on background threads), the pointer becomes a dangling reference, leading to the crash.

Solutions

Attempt 1 (Failed) : Replace the private _xpcConnection with a proxy object ( XLAudioUnitXpcWrapper ) that forwards all messages to a serial queue. The wrapper is a subclass of NSProxy and implements methodSignatureForSelector: and forwardInvocation: . Although this removed the race condition, it introduced new crashes because some internal calls bypass Objective‑C message forwarding (they use direct address offsets).

@interface XLAudioUnitXpcWrapper : NSProxy {
    NSXPCConnection *_xpcConnection;
}
- (instancetype)initWithXpcConnection:(NSXPCConnection *)xpcConnection;
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel;
- (void)forwardInvocation:(NSInvocation *)invocation;
@end

@implementation XLAudioUnitXpcWrapper {
    NSXPCConnection *_xpcConnection;
}
- (instancetype)initWithXpcConnection:(NSXPCConnection *)xpcConnection {
    self = [self class];
    _xpcConnection = xpcConnection;
    return self;
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    return [_xpcConnection methodSignatureForSelector:sel];
}
- (void)forwardInvocation:(NSInvocation *)invocation {
    static dispatch_queue_t queue = nil;
    if (!queue) {
        queue = dispatch_queue_create("com.platform.taskqueue", 0);
    }
    dispatch_sync(queue, ^{ [invocation invokeWithTarget:_xpcConnection]; });
}
@end

After deploying this fix on iOS 17.1 devices, the original crash disappeared but a comparable number of new crashes appeared, confirming that the proxy does not handle the low‑level C++/MRC calls. Attempt 2 (Successful) : Instead of modifying the internal AUAudioUnit_XPC , hook the high‑level AVAudioEngine methods that are invoked only when the crash occurs. By intercepting -[AVAudioEngine stop] and -[AVAudioEngine dealloc] , we check the current thread stack for the keyword TextToSpeech . If it is a text‑to‑speech operation, we skip the original implementation, log the event, and optionally post a notification.

@implementation AVAudioEngine (XLSystemCrashFix)
+ (void)startAVAudioEngineCrashFix {
    BOOL isHookDealloc = [self hookInstanceMethodOf:NSSelectorFromString(@"dealloc") with:@selector(xl_dealloc)];
    BOOL isHookStop    = [self hookInstanceMethodOf:NSSelectorFromString(@"stop")    with:@selector(xl_stop)];
    if (!isHookDealloc || !isHookStop) {
        #if DEBUG
        NSLog(@"AVAudioEngine hook unSuccess");
        #endif
    }
}

- (void)xl_dealloc {
    NSString *stack = [[NSThread callStackSymbols] componentsJoinedByString:@"
"];
    BOOL isTextSpeech = [stack containsString:@"TextToSpeech"];
    if ([[HLLSafeBox standardBox] boolForKey:XLAVAudioEngineDellocKey]) {
        if (!isTextSpeech) {
            [self xl_dealloc];
        }
    } else {
        [self xl_dealloc];
    }
    if (isTextSpeech) {
        [[NSNotificationCenter defaultCenter] postNotificationName:XLAVAudioEngineCrashNoti object:stack];
    }
}

- (void)xl_stop {
    NSString *stack = [[NSThread callStackSymbols] componentsJoinedByString:@"
"];
    BOOL isTextSpeech = [stack containsString:@"TextToSpeech"];
    if ([[HLLSafeBox standardBox] boolForKey:XLAVAudioEngineStopKey]) {
        if (!isTextSpeech) {
            [self xl_stop];
        }
    } else {
        [self xl_stop];
    }
    if (isTextSpeech) {
        [[NSNotificationCenter defaultCenter] postNotificationName:XLAVAudioEngineCrashNoti object:stack];
    }
}
@end

Only the two methods are hooked; the rest of the audio engine remains untouched. The hook checks a runtime flag (set per version) and the presence of the TextToSpeech keyword before deciding whether to execute the original logic. After releasing this fix, the crash vanished in versions 1.7.10 and later, while no new stability issues were observed in logs, ANR reports, or user feedback.

Conclusion

The investigation revealed that the iOS 17 text‑to‑speech crash originates from a multithreaded race on the private _xpcConnection inside AUAudioUnit_XPC . Directly fixing the low‑level object proved risky, so the final solution safely bypasses the problematic dealloc and stop calls at the AVAudioEngine level for text‑to‑speech scenarios. This approach eliminated the crash without impacting normal audio‑engine usage.

iOSHookCrashObjective‑Ctext-to-speechAVAudioEngine
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.