Skip to content

[iOS] Crash in iOS: -[CDVSound audioFileForResource:withId:doValidation:forRecording:suppressValidationErrors:] #405

Description

@TheNotorius0

Bug Report

I have a few crashes on iOS. This is the crash from Crashalytics: [CDVSound audioFileForResource:withId:doValidation:forRecording:suppressValidationErrors:]

Problem

I have a few different stack traces that leads to the same crash. For example:

Fatal exception: com.apple.root.default-qos
EXC_BAD_ACCESS KERN_INVALID_ADDRESS 0x316e72403b08646b

          Crashed: com.apple.root.default-qos
0  libobjc.A.dylib                0x3008 objc_msgSend + 8
1  CoreFoundation                 0x3918 -[__NSDictionaryM objectForKey:] + 168
2  Azmar                          0x19d6c -[CDVSound audioFileForResource:withId:doValidation:forRecording:suppressValidationErrors:] + 224 (CDVSound.m:224)
3  Azmar                          0x1a908 __30-[CDVSound startPlayingAudio:]_block_invoke + 416 (CDVSound.m:416)
4  libdispatch.dylib              0x1aac _dispatch_call_block_and_release + 32
5  libdispatch.dylib              0x1b584 _dispatch_client_callout + 16
6  libdispatch.dylib              0x3725c _dispatch_queue_override_invoke.cold.3 + 32
7  libdispatch.dylib              0x61f8 _dispatch_queue_override_invoke + 848
8  libdispatch.dylib              0x13db0 _dispatch_root_queue_drain + 364
9  libdispatch.dylib              0x1454c _dispatch_worker_thread2 + 156
10 libsystem_pthread.dylib        0x4624 _pthread_wqthread + 232
11 libsystem_pthread.dylib        0x19f8 start_wqthread + 8

and also:

Fatal exception: com.apple.root.default-qos
EXC_BREAKPOINT 0x0000000197cb6fa8

          Crashed: com.apple.root.default-qos
0  CoreFoundation                 0x3fa8 -[__NSCFString isEqual:] + 248
1  CoreFoundation                 0x3918 -[__NSDictionaryM objectForKey:] + 168
2  Azmar                          0x19d6c -[CDVSound audioFileForResource:withId:doValidation:forRecording:suppressValidationErrors:] + 224 (CDVSound.m:224)
3  Azmar                          0x1a908 __30-[CDVSound startPlayingAudio:]_block_invoke + 416 (CDVSound.m:416)
4  libdispatch.dylib              0x1aac _dispatch_call_block_and_release + 32
5  libdispatch.dylib              0x1b584 _dispatch_client_callout + 16
6  libdispatch.dylib              0x3725c _dispatch_queue_override_invoke.cold.3 + 32
7  libdispatch.dylib              0x61f8 _dispatch_queue_override_invoke + 848
8  libdispatch.dylib              0x13db0 _dispatch_root_queue_drain + 364
9  libdispatch.dylib              0x1454c _dispatch_worker_thread2 + 156
10 libsystem_pthread.dylib        0x4624 _pthread_wqthread + 232
11 libsystem_pthread.dylib        0x19f8 start_wqthread + 8

What is expected to happen?

The app shouldn't crash.

What does actually happen?

The app crashes. Maybe it's a race condition because I may load multiple sounds or stop multiple musics? Who knows.

Information

I may call many times the stop function for different musics, and also load quite a few musics and sounds, since my app is a game.

I asked Gemini for a solution, and he mostly suggested me to add a @synchronized(self) { block in the audioFileForResource, which is the function that crashes, not sure how it's useful though:

- (CDVAudioFile*)audioFileForResource:(NSString*)resourcePath withId:(NSString*)mediaId doValidation:(BOOL)bValidate forRecording:(BOOL)bRecord suppressValidationErrors:(BOOL)bSuppress
{
    BOOL bError = NO;
    CDVMediaError errcode = MEDIA_ERR_NONE_SUPPORTED;
    NSString* errMsg = @"";
    CDVAudioFile* audioFile = nil;
    NSURL* resourceURL = nil;

    // --- BEGIN SIMPLE WORKAROUND ---

    // 1. Basic check: Ensure mediaId is actually a string before using it.
    //    This might not be the cause, but it's simple defensive coding.
    if (![mediaId isKindOfClass:[NSString class]]) {
        NSLog(@"CDVSound Error: Received non-string mediaId: %@", mediaId);
        bError = YES;
        errcode = MEDIA_ERR_ABORTED;
        errMsg = @"Internal error: Invalid media ID type";
        // Go straight to error reporting
        goto HandleError;
    }

    // 2. Synchronize only the block of code that accesses the shared soundCache dictionary.
    @synchronized(self) {
        if ([self soundCache] == nil) {
            // Initialize the cache if it doesn't exist (thread-safe initialization)
            [self setSoundCache:[NSMutableDictionary dictionaryWithCapacity:1]];
        } else {
            // Safely attempt to retrieve the object from the cache
            audioFile = [[self soundCache] objectForKey:mediaId]; // Line ~224 - Now wrapped
        }

        // If not found in cache, create and add it (still within the synchronized block)
        if (audioFile == nil) {
            // Basic validation of resourcePath (can stay inside for simplicity)
            if ((resourcePath == nil) || ![resourcePath isKindOfClass:[NSString class]] || [resourcePath isEqualToString:@""]) {
                bError = YES; // Mark error, handle after sync block
                errcode = MEDIA_ERR_ABORTED;
                errMsg = @"invalid media src argument";
            } else {
                // Create and add the new audioFile object while synchronized
                audioFile = [[CDVAudioFile alloc] init];
                audioFile.resourcePath = resourcePath;
                audioFile.resourceURL = nil;  // Will be validated later
                [[self soundCache] setObject:audioFile forKey:mediaId];
            }
        }
    } // --- END Synchronization Block ---

    // --- END SIMPLE WORKAROUND ---


    // If an error occurred during creation inside the sync block, handle it now
    if (bError) {
        goto HandleError;
    }

    // Proceed with URL validation if needed (outside the synchronized block)
    // Note: audioFile might be nil here if resourcePath validation failed inside the sync block.
    // However, the bError flag should catch that. We rely on audioFile being non-nil if bError is NO.
    if (bValidate && (audioFile.resourceURL == nil)) {
        if (bRecord) {
            resourceURL = [self urlForRecording:resourcePath];
        } else {
            resourceURL = [self urlForPlaying:resourcePath];
        }
        if ((resourceURL == nil) && !bSuppress) {
            bError = YES;
            errcode = MEDIA_ERR_ABORTED;
            errMsg = [NSString stringWithFormat:@"Cannot use audio file from resource '%@'", resourcePath];
        } else {
            // It's generally safe to set properties on the audioFile object itself here,
            // as the object reference `audioFile` is local to this function execution.
            // The risk would be if another thread `release`d this specific mediaId
            // *between* the synchronized block above and this line.
            // The simplest approach assumes this is less likely than the dictionary corruption.
            audioFile.resourceURL = resourceURL;
        }
    }

HandleError: // Common error handling point
    if (bError) {
        // Use the validated mediaId (or a placeholder if it was invalid) for the error callback
        NSString* errorMediaId = [mediaId isKindOfClass:[NSString class]] ? mediaId : @"<invalid_id>";
        [self onStatus:MEDIA_ERROR mediaId:errorMediaId param:
            [self createMediaErrorWithCode:errcode message:errMsg]];
        // Return nil upon error
        return nil;
    }

    // Return the found or created audioFile object
    return audioFile;
}

Command or Code

Environment, Platform, Device

Most iPhones ranging from iPhone 11 to iPhone 16, 20% iOS 17 and 80% iOS 18.

Version information

"cordova": "^12.0.0",
"cordova-ios": "^7.1.1",

Checklist

  • I searched for existing GitHub issues
  • I updated all Cordova tooling to most recent version
  • I included all the necessary information above

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions