EXC_CRASH from NSManagedObjectContext executeFetchRequest

Hello,

I have an iOS app for which I've received a number of similar crash reports over the last few months. Despite a lot of effort, I haven't been able to replicate the crash myself and I'm finding it difficult to diagnose.

The main view of the app loads a list of items from Core Data using @FetchRequest and looking at the logs it appears to me that this is the most likely source of the crash as the call stack includes SwiftUI 0x19c78c368 FetchRequest.update() + 472 (FetchRequest.swift:406). It also appears as if this happens on launch as the crash times and launch times are always very similar.

I've attempted lots of things to try and replicate the crash, such as launching the app a lot of times, creating lots of items so that the fetch request has a lot of data to retrieve, performing any other database related actions in the app immediately after launch to try and drive out any concurrency issues and simulating degraded thermal and network conditions for the device.

I've included a sample crash report, I'd be very grateful if anyone has any suggestions for diagnosing the issue.

are you doing anything with core data in background threads?

The main view of the app loads a list of items from Core Data using @FetchRequest and looking at the logs it appears to me that this is the most likely source of the crash as the call stack includes SwiftUI 0x19c78c368 FetchRequest.update() + 472 (FetchRequest.swift:406). It also appears as if this happens on launch as the crash times and launch times are always very similar.

This is just guesswork, but I do have two ideas.

A) Did you see and follow this warning:

"Always declare properties that have a fetch request wrapper as private. This lets the compiler help you avoid accidentally setting the property from the memberwise initializer of the enclosing view."

The documentation doesn't say this explicitly, but I think what they're warning about there is that it's posible to end up in a situation where the process of updating your interface based on a fetch request can end up accidentally modifying that data it's trying to display, which basically ends up rentering CoreData. I think this still trigger a crash in the same way calling "performBlockAndWait" inside a "performBlockAndWait" call would.

B) On the reproduction side, this is worth thinking about more:

"It also appears as if this happens on launch as the crash times and launch times are always very similar."

The work you're doing isn't what I'd associate with a "basic" app launch, so I suspect that this might be tied to some form of state restoration. Testing wise, I'd focus on putting your app into interesting/unusual state, not just "brute force" testing. Also, under "Settings.app-> Developer", there is a switch labeled "State Restoration Testing-> Fast App Termination" which could be very helpful here. Turning that on means that system will terminate your app whenever it suspends, forcing your app to restore state every time it comes to the foreground. That will let you test this code path while avoiding the disruption force quitting could cause.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Thanks for the help.

A) Did you see and follow this warning:

"Always declare properties that have a fetch request wrapper as private. This lets the compiler help you avoid accidentally setting the property from the memberwise initializer of the enclosing view."

The documentation doesn't say this explicitly, but I think what they're warning about there is that it's posible to end up in a situation where the process of updating your interface based on a fetch request can end up accidentally modifying that data it's trying to display, which basically ends up rentering CoreData. I think this still trigger a crash in the same way calling "performBlockAndWait" inside a "performBlockAndWait" call would.

On A), this is actually something I did relatively recently but unfortunately the crash is still being reported in the current version with this change in place. It did change the call stack in the crash reports slightly but nothing which added any additional context to help diagnose the issue.

B) On the reproduction side, this is worth thinking about more:

"It also appears as if this happens on launch as the crash times and launch times are always very similar." The work you're doing isn't what I'd associate with a "basic" app launch, so I suspect that this might be tied to some form of state restoration. Testing wise, I'd focus on putting your app into interesting/unusual state, not just "brute force" testing. Also, under "Settings.app-> Developer", there is a switch labeled "State Restoration Testing-> Fast App Termination" which could be very helpful here. Turning that on means that system will terminate your app whenever it suspends, forcing your app to restore state every time it comes to the foreground. That will let you test this code path while avoiding the disruption force quitting could cause.

For B), thanks for pointing me towards the 'Fast App Termination' setting, it's not something I was aware of and I've been using it today to try and trigger the crash. Unfortunately without success.

Looking further into the reports, it looks like although the majority of them occur on launch there are instances where it happens when the app has been suspended and opened again and even when the app is opened in the background (in response to a Watch Connectivity message). So it seems like it's something which isn't just limited to the app launching.

Unfortunately I'm still a bit stuck as I can't think of any other way to try and replicate the issue. At the moment the next thing I can think to try is to change @FetchRequest to use an NSFetchRequest and see if that shows anything different in future reports.

Looking further into the reports, it looks like although the majority of them occur on launch there are instances where it happens when the app has been suspended and opened again and even when the app is opened in the background (in response to a Watch Connectivity message). So it seems like it's something which isn't just limited to the app launching.

Are you sure this isn't about launching into the background? The log you sent showed WatchConnectivity activity, but it had also been running for a very short period of time AND the app was not in the foreground.

Triggering this bug may actually require three things:

  1. Your app to be in a particular configuration, section of the UI, etc.

  2. Needs to have been terminated so that it's state is restored on launch.

  3. Your app needs to be launched into the background, possibly even by WatchConnectivity.

The log you sent had a very short run time:

Date/Time:           2024-07-22 20:03:08.3984 +0100
Launch Time:         2024-07-22 20:03:07.5817 +0100

And was launched into the background:

Role:                Non UI

Is that true of all logs? If not, what's different about the "exceptions"? What about logs that don't show WatchConnectivity?

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Are you sure this isn't about launching into the background? The log you sent showed WatchConnectivity activity, but it had also been running for a very short period of time AND the app was not in the foreground.

...

Is that true of all logs? If not, what's different about the "exceptions"? What about logs that don't show WatchConnectivity?

The logs seem to cover four scenarios, which are pretty much all the ways that the app supports opening.

I've attached sample crash logs for each scenario. The call stack of the crash is pretty much the same for all of them.

Foreground/Suspended

App is suspended and opened by the user. Launch time and crash time are different and other threads can show CoreData or WatchConnectivity usage as the app initialises both when it opens.

Foreground/Launch

App is launched by the user. Launch time and crash time are similar and other threads can show CoreData or WatchConnectivity usage as the app initialises both when it opens. Other threads almost always show CoreData usage from NSCloudKitMirroringDelegate _performSetupRequest.

Background/Suspended

App is suspended and opened in the background as a result of WatchConnectivity. Launch time and crash time are different and other threads only seem to show WatchConnectivity usage.

Background/Launch

App launched in the background as a result of WatchConnectivity. Launch time and crash time are similar and other threads only seem to show WatchConnectivity usage.

The logs seem to cover four scenarios, which are pretty much all the ways that the app supports opening.

I've attached sample crash logs for each scenario. The call stack of the crash is pretty much the same for all of them.

I've looked over them all and there just isn't a lot to go on here. I have a few more ideas that might be helpful, but this is also one of those cases where the log just doesn't have a lot to "say".

  • Is there any direct connection between your interactions with WatchConnectivity and your CoreData stack? That's a pretty vagues question, but WatchConnectivity seems to have some involvement here which is worth looking at more closely.

  • I would set a symbolic breakpoint on "FetchRequest.update", do some experimenting with your app to try and the overall "stack" you're seeing. Here are a few different things I'd be looking at here:

  1. Can you replicate this "exact" call stack and, based on that, determine exactly what data it was trying to fetch? Note that this is also about timing, not just the exact call stack. For example, you have a time window the crash occurs in and that could let you guess our whether this was the first fetch request or some later request.

  2. How common is this state? Does your app ALWAYS call "FetchRequest.update" when starting up? Or only on particular screens?

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

I would set a symbolic breakpoint on "FetchRequest.update", do some experimenting with your app to try and the overall "stack" you're seeing.

I've made some progress and think I've identified the crash.

I wasn't able to set a symbolic breakpoint on FetchRequest.update() (I tried a few variations but it never seemed to catch anything) but instead I set one on -[NSFetchedResultsController performFetch:] as that is just a little further down in the call stack.

The app has some custom Core Data migration functionality that enables adhoc data transformation before continuing with a lightweight migration. When the breakpoint was triggered I noticed that one of the other threads was calling the migration functionality so it got me to take a look, that's where I noticed the following line of code.

// Initalizing a coordinator with the same model more than once results in errors when fetching entities.
let persistentStoreCoordinator = migrationStep.destinationVersionIsCurrent ? appPersistentStoreCoordinator : NSPersistentStoreCoordinator(managedObjectModel: migrationStep.destinationModel)

This happens before triggering the migration and determines the coordinator used during the migration, with appPersistentStoreCoordinator passed in as an already created NSPersistentStoreCoordinator.

Originally it was creating a new NSPersistentStoreCoordinator each time but I changed it to the above, sometimes using the already created NSPersistentStoreCoordinator, as a result of the error described in the comment. I tested the code path where the new NSPersistentStoreCoordinator was created and the app crashed with the following errors.

executeFetchRequest:error: A fetch request must have an entity.
warning: Multiple NSEntityDescriptions claim the NSManagedObject subclass 'Activity' so +entity is unable to disambiguate.
warning:  	 'Activity' (0x30252c370) from NSManagedObjectModel (0x303131900) claims 'Activity'.
error: +[Activity entity] Failed to find a unique match for an NSEntityDescription to a managed object subclass
CoreData: error: +[Activity entity] Failed to find a unique match for an NSEntityDescription to a managed object subclass
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'executeFetchRequest:error: A fetch request must have an entity.'
*** First throw call stack:
(0x1a0120f20 0x197fce018 0x1a821ad88 0x1a82c0b18 0x1a82110dc 0x1a82870ec 0x1a8286600 0x1a828620c 0x1a493b6e8 0x1a5663f08 0x1a5664368 0x1a40c3b70 0x1a40bf504 0x1a40bf320 0x1a40bf294 0x1a40bf224 0x19ea10068 0x1a4139688 0x1a41373bc 0x1a40e9b30 0x1c8d41010 0x1c8d40bfc 0x1c8d3acc0 0x1c8d3a854 0x1a40f8810 0x1a40f8568 0x1a40e9b30 0x1c8d41010 0x1c8d40bfc 0x1c8d407d8 0x1a41536f8 0x1a415288c 0x1a415164c 0x1a414fd7c 0x1a414fb0c 0x1a4064c6c 0x1a232ea4c 0x1a178d3b4 0x1a178cf38 0x1a17e80e0 0x1a175d028 0x1a24e3678 0x1a0102c9c 0x1a00f0dec 0x1a00f0498 0x1a00efcd8 0x1e4fa01a8 0x1a272890c 0x1a27dc9d0 0x1a42e0148 0x1a428c714 0x1a42984d0 0x100441530 0x1004415e0 0x1c37a1e4c)
libc++abi: terminating due to uncaught exception of type NSException

I'm not sure why I left in the code path to create a new NSPersistentStoreCoordinator but it meant that if a user was updating their app and there was more than one migration to apply, it would create a new NSPersistentStoreCoordinator and the app would crash. Changing this to always use the already created NSPersistentStoreCoordinator (appPersistentStoreCoordinator) resolves the issue.

I would have expected to see some reference to the migration functionality in one of the other threads in the crash log but perhaps it's because the migration was already complete and the crash occurred on the next fetch. Hopefully this fixes the crash I've been seeing in the crash reports.

Thanks for your help. I'm not sure I would have stumbled across the code causing the crash if it hadn't been for your suggestions.

EXC_CRASH from NSManagedObjectContext executeFetchRequest
 
 
Q