We are getting a crash _dispatch_assert_queue_fail
when the cancellationHandler on NSProgress is called.
We do not see this with iOS 17.x, only on iOS 18. We are building in Swift 6 language mode and do not have any compiler warnings.
We have a type whose init looks something like this:
init(
request: URLRequest,
destinationURL: URL,
session: URLSession
) {
progress = Progress()
progress.kind = .file
progress.fileOperationKind = .downloading
progress.fileURL = destinationURL
progress.pausingHandler = { [weak self] in
self?.setIsPaused(true)
}
progress.resumingHandler = { [weak self] in
self?.setIsPaused(false)
}
progress.cancellationHandler = { [weak self] in
self?.cancel()
}
When the progress is cancelled, and the cancellation handler is invoked. We get the crash. The crash is not reproducible 100% of the time, but it happens significantly often. Especially after cleaning and rebuilding and running our tests.
* thread #4, queue = 'com.apple.root.default-qos', stop reason = EXC_BREAKPOINT (code=1, subcode=0x18017b0e8)
* frame #0: 0x000000018017b0e8 libdispatch.dylib`_dispatch_assert_queue_fail + 116
frame #1: 0x000000018017b074 libdispatch.dylib`dispatch_assert_queue + 188
frame #2: 0x00000002444c63e0 libswift_Concurrency.dylib`swift_task_isCurrentExecutorImpl(swift::SerialExecutorRef) + 284
frame #3: 0x000000010b80bd84 MyTests`closure #3 in MyController.init() at MyController.swift:0
frame #4: 0x000000010b80bb04 MyTests`thunk for @escaping @callee_guaranteed @Sendable () -> () at <compiler-generated>:0
frame #5: 0x00000001810276b0 Foundation`__20-[NSProgress cancel]_block_invoke_3 + 28
frame #6: 0x00000001801774ec libdispatch.dylib`_dispatch_call_block_and_release + 24
frame #7: 0x0000000180178de0 libdispatch.dylib`_dispatch_client_callout + 16
frame #8: 0x000000018018b7dc libdispatch.dylib`_dispatch_root_queue_drain + 1072
frame #9: 0x000000018018bf60 libdispatch.dylib`_dispatch_worker_thread2 + 232
frame #10: 0x00000001012a77d8 libsystem_pthread.dylib`_pthread_wqthread + 224
Any thoughts on why this is crashing and what we can do to work-around it? I have not been able to extract our code into a simple reproducible case yet. And I mostly see it when running our code in a testing environment (XCTest). Although I have been able to reproduce it running an app a few times, it's just less common.
Well, this is most strange. Consider the MyController
class:
public final class MyController {
public let progress: Progress
init() {
progress = Progress()
progress.cancellationHandler = { }
}
}
On my M3 machine I set a breakpoint on the first line of the initialiser and ran the test. Here’s the code that builds the the Objective-C block that’s stored in cancellationHandler
:
(lldb) disas -f
AaaGggTests`MyController.init():
…
0x1048716e4 <+124>: adrp x8, 0
0x1048716e8 <+128>: add x8, x8, #0x7e0 ; reabstraction thunk helper from @escaping @callee_guaranteed @Sendable () -> () to @escaping @callee_unowned @convention(block) @Sendable () -> () at <compiler-generated>
0x1048716ec <+132>: str x8, [sp, #0x28]
0x1048716f0 <+136>: adrp x8, 7
0x1048716f4 <+140>: add x8, x8, #0x680 ; block_descriptor
0x1048716f8 <+144>: str x8, [sp, #0x30]
0x1048716fc <+148>: bl 0x104875360 ; symbol stub for: _Block_copy
…
So I set a breakpoint on the reabstraction thunk [1]:
(lldb) p/a 0x104871000+0x7e0
(Int) 0x00000001048717e0 AaaGggTests`reabstraction thunk helper from @escaping @callee_guaranteed @Sendable () -> () to @escaping @callee_unowned @convention(block) @Sendable () -> () at <compiler-generated>
(lldb) br set -a 0x00000001048717e0
Breakpoint 2: where = AaaGggTests`thunk for @escaping @callee_guaranteed @Sendable () -> () at <compiler-generated>, address = 0x00000001048717e0
When the progress is cancelled, I land here:
(lldb) disas -a 0x00000001048717e0
AaaGggTests`thunk for @escaping @callee_guaranteed @Sendable () -> ():
0x1048717e0 <+0>: sub sp, sp, #0x30
0x1048717e4 <+4>: stp x20, x19, [sp, #0x10]
0x1048717e8 <+8>: stp x29, x30, [sp, #0x20]
0x1048717ec <+12>: add x29, sp, #0x20
0x1048717f0 <+16>: ldr x8, [x0, #0x20]
0x1048717f4 <+20>: str x8, [sp]
0x1048717f8 <+24>: ldr x20, [x0, #0x28]
0x1048717fc <+28>: str x20, [sp, #0x8]
0x104871800 <+32>: mov x0, x20
0x104871804 <+36>: bl 0x104875624 ; symbol stub for: swift_retain
0x104871808 <+40>: ldr x8, [sp]
0x10487180c <+44>: blr x8
0x104871810 <+48>: ldr x0, [sp, #0x8]
0x104871814 <+52>: bl 0x104875618 ; symbol stub for: swift_release
0x104871818 <+56>: ldp x29, x30, [sp, #0x20]
0x10487181c <+60>: ldp x20, x19, [sp, #0x10]
0x104871820 <+64>: add sp, sp, #0x30
0x104871824 <+68>: ret
Stepping through to +52 and then stepping in, I land here:
(lldb) disas -f
AaaGggTests`closure #1 in MyController.init():
-> 0x1048717cc <+0>: adrp x9, 15
0x1048717d0 <+4>: ldr x8, [x9, #0x3b0]
0x1048717d4 <+8>: add x8, x8, #0x1
0x1048717d8 <+12>: str x8, [x9, #0x3b0]
0x1048717dc <+16>: ret
This the code for your closure. It’s basically an extended no-op, which is exactly what I’d expect.
When I repeat this process on my M1 machine, I see this:
(lldb) disas -f
AaaGggTests`closure #1 in MyController.init():
-> 0x1009bd714 <+0>: sub sp, sp, #0x40
0x1009bd718 <+4>: stp x20, x19, [sp, #0x20]
0x1009bd71c <+8>: stp x29, x30, [sp, #0x30]
0x1009bd720 <+12>: add x29, sp, #0x30
0x1009bd724 <+16>: mov x0, #0x0 ; =0
0x1009bd728 <+20>: bl 0x1009c12ec ; symbol stub for: type metadata accessor for Swift.MainActor
0x1009bd72c <+24>: mov x20, x0
0x1009bd730 <+28>: str x20, [sp, #0x8]
0x1009bd734 <+32>: bl 0x1009c12e0 ; symbol stub for: static Swift.MainActor.shared.getter : Swift.MainActor
0x1009bd738 <+36>: mov x20, x0
0x1009bd73c <+40>: str x20, [sp]
0x1009bd740 <+44>: bl 0x1009bcd68 ; lazy protocol witness table accessor for type Swift.MainActor and conformance Swift.MainActor : Swift.Actor in Swift at <compiler-generated>
0x1009bd744 <+48>: mov x1, x0
0x1009bd748 <+52>: ldr x0, [sp, #0x8]
0x1009bd74c <+56>: bl 0x1009c12d4 ; symbol stub for: dispatch thunk of Swift.Actor.unownedExecutor.getter : Swift.UnownedSerialExecutor
0x1009bd750 <+60>: str x0, [sp, #0x10]
0x1009bd754 <+64>: str x1, [sp, #0x18]
0x1009bd758 <+68>: bl 0x1009c1634 ; symbol stub for: swift_task_isCurrentExecutor
…
Your empty closure has profited a main actor check. That’s the cause of this trap on the M1 Mac.
I’ve no idea why the compiler would generate different code in these two environments, but this about as far as I can take this. I’ve updated your bug (FB15511118
) with my latest analysis and confirmed it’s with the right folks.
Oh, and as part of doing that I retested on the M1 with Xcode 16.1b3 and it continues to fail in this way.
Weird.
Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"
[1] A reabstraction thunk is code generated by the compiler to convert from one calling convention to another. In this case it converts from the Objective-C block calling convention using by NSProgress
to the Swift closure calling convention of the code that you wrote, that is, your empty closure.