GameSaveSyncedDirectory.openDirectory() crashes in CloudDocs initUIApplicationWillResignActiveNotification

We use the GameSave framework to sync player save data to iCloud. Since iOS 27, our game crashes a few seconds after a cold launch, the first time it calls GameSaveSyncedDirectory.openDirectory(). We call the API only from the main actor, but the crash is on a cooperative thread inside GameSave:

EXC_BAD_ACCESS (SIGSEGV) KERN_INVALID_ADDRESS at 0x400000000000bad0
libobjc.A.dylib  objc_release
CloudDocs        initUIApplicationWillResignActiveNotification
CloudDocs        -[BRQuery _setQueryState:]
CloudDocs        -[BRQuery _runQuery]
CloudDocs        -[BRQuery executeWithOptions:]
Foundation       -[NSMetadataQuery startQuery]
GameSave         BundleUpdatesWaiter.initMetadataQuery()
GameSave         BundleUpdatesWaiter.init(bundleURL:)
GameSave         closure #1 in GameSyncedDirectoryHolder.allowReaders(for:)

Other threads are inside the same BRQuery path at the same moment. The crash reproduces without GameSave: starting twoNSMetadataQuery objects at once on background threads in a fresh process crashes 94 times in 100 in the iOS 27.0 Simulator, and never on iOS 26.5, so this seems like a regression. Starting one query on a single thread never crashes.

I've submitted a support request (FB25352978) so hopefully this will be looked at fairly soon - if anyone else runs into this it would be good to know!

Answered by DTS Engineer in 913532022
Written by ToadLobe in 913030022

This is the first iOS bug I've reported … ?

Hey hey, you’ve done well so far!

Written by ToadLobe in 913030022

so I'm not sure what the fix proccess/timeline looks like for something like this?

Unfortunately it’s hard to offer predictions here. Sometimes bugs like this get fixed in the next software update, sometimes they lurk for (literally, and sadly) decades.

Written by ToadLobe in 913030022

In the meantime, this is the workaround I'm using:

Yeah, I think that’ll work, for reasons that should be obvious once I fill in some details about what’s crashing.


Thanks for the crash report.

In that report we see that the crashing thread’s backtrace looks like this:

Thread 5 Crashed:
0   libobjc.A.dylib … objc_release + 8
1   CloudDocs       … initUIApplicationWillResignActiveNotification + 52
2   CloudDocs       … -[BRQuery _setQueryState:] + 356

This is doing what’s called a soft link [1]. It’s a technique used by Apple frameworks to resolve layering loops. In this case, CloudDocs [2] depends on Foundation but NSMetadataQuery, which exists within Foundation, depends on CloudDocs. This soft link resolves the issue by having Foundation load CloudDocs at runtime, using dlopen and dlsym.

With that in mind, check out the backtrace of thread 12:

Thread 12:
0   dyld_sim  … mach_o::GenericTrie::find(CString, bool) const + 388
1   dyld_sim  … mach_o::ExportsTrie::hasExportedSymbol(CString, mach_o::Symbol&, bool) const + 44
2   dyld_sim  … dyld4::Loader::hasExportedSymbol(…) const + 624
3   dyld_sim  … dyld4::Loader::hasExportedSymbol(…) const + 1024
4   dyld_sim  … dyld4::Loader::hasExportedSymbol(…) const + 1024
5   dyld_sim  … dyld4::Loader::hasExportedSymbol(…) const + 1024
6   dyld_sim  … dyld4::Loader::hasExportedSymbol(…) const + 1024
7   dyld_sim  … dyld4::Loader::hasExportedSymbol(…) const + 1024
8   dyld_sim  … dyld4::Loader::hasExportedSymbol(…) const + 1024
9   dyld_sim  … dyld4::Loader::hasExportedSymbol(…) const + 1024
10  dyld_sim  … dyld4::APIs::dlsym(void*, char const*) + 1116
11  CloudDocs … initUIApplicationDidBecomeActiveNotification + 28
12  CloudDocs … -[BRQuery _setQueryState:] + 408

It’s actively working within the dynamic linker to resolve this soft link. So it seems that you have two threads, 5 and 12, doing the same work without any serialisation, and hence this crash.

As I said, this is our bug to fix.

And it also speaks to your workaround. By running a single NSMetadataQuery request in a serialised fashion, there’s only one thread attempting to resolve the soft link at a time. That resolution is then cached, and so subsequent requests use the cached value and don’t collide with each other.

The only mystery here is how this worked prior to iOS 27. I looked at the code and I didn’t see any obvious changes, so I just don’t have an answer to that. Fortunately, I don’t think I need one, because we have a clear bug report and a workaround, and that means my work here is done (-:

Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] This isn’t a term I’ve seen used a lot of outside of Apple. Do not confuse it with a weak link. That’s a different concept entirely. Well, actually, it’s two different concepts. See An Apple Library Primer for more on that topic.

[2] This is a private framework but it’s not hard to guess what it does (-:

This looks like Just A Bug™. Although it’s an interesting bug (-:

Written by ToadLobe in 853970021

I've submitted a support request (FB25352978)

Thanks. That’s the best path for this.

I got one of your crash reports from the bug and went digging. I have some info to share as to what’s going wrong, but it’d be easier for me to talk about that if I had a crash report to reference. Can you post one here? Ideally, a JSON (.ips) crash report. See Posting a Crash Report for advice on how to post stuff like this.

If not, that’s fine. Reply here to let me know and I’ll tailor my reply based on the info you’ve already shared.

I want to stress, however, that AFAICT this really is a bug on our end, so my analysis is unlikely to actually help you. Although I hope you’ll find it interesting!

Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Hi Quinn, thanks for the quick reply.

race-2026-10-28-180244.0002.txt(63 KB)

This is one from the simulator.

This is the first iOS bug I've reported so I'm not sure what the fix proccess/timeline looks like for something like this?

In the meantime, this is the workaround I'm using:

private func warmUpMetadataQueries() {
	let query = NSMetadataQuery()
	query.searchScopes = [NSMetadataQueryUbiquitousDataScope]
	query.predicate = NSPredicate(value: true)
	query.start()
	query.stop()
}

Calling it once on app startup seems to prevent the crash, but I'm not sure if there are any caveats to this approach - maybe you could adivse.

Best,
Pablo

Written by ToadLobe in 913030022

This is the first iOS bug I've reported … ?

Hey hey, you’ve done well so far!

Written by ToadLobe in 913030022

so I'm not sure what the fix proccess/timeline looks like for something like this?

Unfortunately it’s hard to offer predictions here. Sometimes bugs like this get fixed in the next software update, sometimes they lurk for (literally, and sadly) decades.

Written by ToadLobe in 913030022

In the meantime, this is the workaround I'm using:

Yeah, I think that’ll work, for reasons that should be obvious once I fill in some details about what’s crashing.


Thanks for the crash report.

In that report we see that the crashing thread’s backtrace looks like this:

Thread 5 Crashed:
0   libobjc.A.dylib … objc_release + 8
1   CloudDocs       … initUIApplicationWillResignActiveNotification + 52
2   CloudDocs       … -[BRQuery _setQueryState:] + 356

This is doing what’s called a soft link [1]. It’s a technique used by Apple frameworks to resolve layering loops. In this case, CloudDocs [2] depends on Foundation but NSMetadataQuery, which exists within Foundation, depends on CloudDocs. This soft link resolves the issue by having Foundation load CloudDocs at runtime, using dlopen and dlsym.

With that in mind, check out the backtrace of thread 12:

Thread 12:
0   dyld_sim  … mach_o::GenericTrie::find(CString, bool) const + 388
1   dyld_sim  … mach_o::ExportsTrie::hasExportedSymbol(CString, mach_o::Symbol&, bool) const + 44
2   dyld_sim  … dyld4::Loader::hasExportedSymbol(…) const + 624
3   dyld_sim  … dyld4::Loader::hasExportedSymbol(…) const + 1024
4   dyld_sim  … dyld4::Loader::hasExportedSymbol(…) const + 1024
5   dyld_sim  … dyld4::Loader::hasExportedSymbol(…) const + 1024
6   dyld_sim  … dyld4::Loader::hasExportedSymbol(…) const + 1024
7   dyld_sim  … dyld4::Loader::hasExportedSymbol(…) const + 1024
8   dyld_sim  … dyld4::Loader::hasExportedSymbol(…) const + 1024
9   dyld_sim  … dyld4::Loader::hasExportedSymbol(…) const + 1024
10  dyld_sim  … dyld4::APIs::dlsym(void*, char const*) + 1116
11  CloudDocs … initUIApplicationDidBecomeActiveNotification + 28
12  CloudDocs … -[BRQuery _setQueryState:] + 408

It’s actively working within the dynamic linker to resolve this soft link. So it seems that you have two threads, 5 and 12, doing the same work without any serialisation, and hence this crash.

As I said, this is our bug to fix.

And it also speaks to your workaround. By running a single NSMetadataQuery request in a serialised fashion, there’s only one thread attempting to resolve the soft link at a time. That resolution is then cached, and so subsequent requests use the cached value and don’t collide with each other.

The only mystery here is how this worked prior to iOS 27. I looked at the code and I didn’t see any obvious changes, so I just don’t have an answer to that. Fortunately, I don’t think I need one, because we have a clear bug report and a workaround, and that means my work here is done (-:

Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] This isn’t a term I’ve seen used a lot of outside of Apple. Do not confuse it with a weak link. That’s a different concept entirely. Well, actually, it’s two different concepts. See An Apple Library Primer for more on that topic.

[2] This is a private framework but it’s not hard to guess what it does (-: