Crash with Progress type, Swift 6, iOS 18

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.

Answered by DTS Engineer in 809846022

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.

Found a reproducible case that works on about half of our machines. Updated the FB15511118 with the project.

Updated the FB15511118 with the project.

Thanks.

I tried that myself and I still can’t reproduce it. However, I am on an M3 Mac so it’s possible that’s the deciding factor. Later today I’ll try it on an M1 machine.

Did you get a chance to try this in a new user account?

Share and Enjoy

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

No, I didn't try that because with the latest project I sent I have 5 other machines on our team that can reproduce the issue. The common factor seems to be some version of the M1 chip. We have 4-5 people with M3 that cannot reproduce and one person with an M2 that cannot reproduce.

Also, for the record, we do see this bug on M3 machines with our production code-base. Since the bug has to do with the file structure of the package, and with the code in at least 2 source files, I don't know what the file structure needs to be to reproduce on an M3. I wasn't aware this was processor-specific when I was tearing our project apart looking for the trigger.

Hopefully fixing this bug with the reproducible case on an M1 will also fix it for all cases.

I tried with a brand new user as you asked and it reproduces in that environment as well.

Later today I’ll try it on an M1 machine.

Well, would you look at that. Testing on my M1 Mac reproduces the problem. Fascinating.

Sadly, I’ve no time left to look at the wreckage today, but it’s progress [r r r] of a sort.

Share and Enjoy

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

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.

Thank you for your diligence on this issue Quinn!

Crash with Progress type, Swift 6, iOS 18
 
 
Q