apache / apache/cordova-plugin-media

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

Open
#405 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
JavaScript
Stars
391
Forks
762
PR merge metrics
No merged PRs in 30d

Description

# 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 : @"";
[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

- [x] I searched for existing GitHub issues
- [x] I updated all Cordova tooling to most recent version
- [x] I included all the necessary information above

Contributor guide

Open the contributing guide

Research direction

Start in CDVSound.m at audioFileForResource:withId:doValidation:forRecording:suppressValidationErrors: around line 224, then trace the startPlayingAudio: block around line 416. Exercise concurrent audio loads and stop calls using the reported iOS versions and compare the cache access with the supplied crash traces. Done means the reported EXC_BAD_ACCESS and EXC_BREAKPOINT crashes no longer occur under that workload.

Written by the indexing model from the issue text.

Assessment

Tech stack
objective-c
Domain
mobile-dev
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.