Skip to content

Commit db04f7b

Browse files
jonamesonclaude
andcommitted
coreaudio: unregister interruption observer unconditionally on close
update_audio_session()'s existing close-path observer teardown lived at the bottom of the function, after two [AVAudioSession setCategory:] and one [setActive:] calls that early-return on failure. When setCategory fails during close (phone call, Siri, CarPlay / AirPlay handoff, or a session owned by another app), the early-return skipped the unregister and the SDLInterruptionListener stayed subscribed to NSNotificationCenter with a stale device pointer. The next UIApplicationWillEnterForeground or UIApplicationDidBecomeActive notification then dispatched into interruption_end() on freed memory, crashing with EXC_BAD_ACCESS. Move the close-path unregister to the top of the function so it runs unconditionally, before any early-return. Also add self.device == NULL guards in both listener callbacks as defense in depth so that any future cleanup-flow edit can't silently reintroduce the crash. The sibling setActive:YES leak on last-device close was fixed in 2018 (issue #2900). This closes the setCategory: leak that has remained since. The same root cause is reported in SDL3 as #12660. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent c8ebb14 commit db04f7b

1 file changed

Lines changed: 24 additions & 7 deletions

File tree

src/audio/coreaudio/SDL_coreaudio.m

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,10 @@ @implementation SDLInterruptionListener
351351
- (void)audioSessionInterruption:(NSNotification *)note
352352
{
353353
@synchronized(self) {
354+
/* Defensive: skip if the device was already torn down. */
355+
if (self.device == NULL) {
356+
return;
357+
}
354358
NSNumber *type = note.userInfo[AVAudioSessionInterruptionTypeKey];
355359
if (type.unsignedIntegerValue == AVAudioSessionInterruptionTypeBegan) {
356360
interruption_begin(self.device);
@@ -363,6 +367,9 @@ - (void)audioSessionInterruption:(NSNotification *)note
363367
- (void)applicationBecameActive:(NSNotification *)note
364368
{
365369
@synchronized(self) {
370+
if (self.device == NULL) {
371+
return;
372+
}
366373
interruption_end(self.device);
367374
}
368375
}
@@ -375,6 +382,23 @@ static BOOL update_audio_session(_THIS, SDL_bool open, SDL_bool allow_playandrec
375382
AVAudioSession *session = [AVAudioSession sharedInstance];
376383
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
377384

385+
/* Close path: unregister the interruption observer FIRST, before any
386+
code that can early-return. [AVAudioSession setCategory:] failures
387+
(phone call, Siri, CarPlay / AirPlay handoff, etc.) would otherwise
388+
leave the listener registered with a stale device pointer;
389+
NotificationCenter later delivers foreground events to freed memory.
390+
See #2900 for the sibling setActive:YES leak fix; this closes the
391+
setCategory: leak. */
392+
if (!open && this->hidden->interruption_listener != NULL) {
393+
SDLInterruptionListener *listener =
394+
(SDLInterruptionListener *)CFBridgingRelease(this->hidden->interruption_listener);
395+
this->hidden->interruption_listener = NULL;
396+
[center removeObserver:listener];
397+
@synchronized(listener) {
398+
listener.device = NULL;
399+
}
400+
}
401+
378402
NSString *category = AVAudioSessionCategoryPlayback;
379403
NSString *mode = AVAudioSessionModeDefault;
380404
NSUInteger options = AVAudioSessionCategoryOptionMixWithOthers;
@@ -498,13 +522,6 @@ static BOOL update_audio_session(_THIS, SDL_bool open, SDL_bool allow_playandrec
498522
object:nil];
499523

500524
this->hidden->interruption_listener = CFBridgingRetain(listener);
501-
} else {
502-
SDLInterruptionListener *listener = nil;
503-
listener = (SDLInterruptionListener *)CFBridgingRelease(this->hidden->interruption_listener);
504-
[center removeObserver:listener];
505-
@synchronized(listener) {
506-
listener.device = NULL;
507-
}
508525
}
509526
}
510527

0 commit comments

Comments
 (0)