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.

It’s hard to say exactly what’s going on here, but the immediate cause of the crash is a trap inserted by the compiler to ensure correct concurrent data isolation. I posted an in-depth analysis of a similar issue here. Read that before reading the rest of my reply.

In the code snippet you posted from the MyController type in frame 3 of your backtrace? Is that main actor isolated? Or an actor?

Share and Enjoy

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

MyController is a simple class not bound to any actor. The init code is above, it is also not bound to any actor.

I tried binding the init to the MainActor, that didn't help.

I also tried making the closures passed into progress @Sendable and that did not fix it, which surprised me.

I’m not sure what’s going on here. I tried to create a small test project to see if I could reproduce it, but that didn’t work (my failed attempt is below, just FYI).

Can you flesh this out into a test project that reproduces the problem?

Share and Enjoy

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


class MyController {
    init() {
        progress = Progress()
        progress.kind = .file
        progress.fileOperationKind = .downloading
        
        progress.pausingHandler = { [weak self] in
            self?.setIsPaused(true)
        }
        
        progress.resumingHandler = { [weak self] in
            self?.setIsPaused(false)
        }
        
        progress.cancellationHandler = { [weak self] in
            self?.cancel()
        }
    }

    let progress: Progress

    private func setIsPaused(_ newValue: Bool) {
    }

    private func cancel() {
    }
}

struct ContentView: View {
    @State var controllerQ: MyController? = nil
    var body: some View {
        VStack {
            Button("Start") {
                guard self.controllerQ == nil else { return }
                print("will start")
                self.controllerQ = .init()
            }
            Button("Cancel") {
                guard let controller = self.controllerQ else { return }
                print("will cancel")
                controller.progress.cancel()
            }
        }
        .padding()
    }
}

I've got the same basic project that I extracted to try to get a reproducer. I also setup unit tests with both Swift Testing and XCTest (we mostly see this issue in our test environment). Unfortunately, I have not been able to reproduce it outside of our codebase.

Our code is in a Swift Package, so one thing that I haven't tried is breaking out this reproducible code into a package and making that package a dependency of an app. There is no practical difference I can identify between the code we have tried to reproduce the crash and our production code.

Within our production codebase, I can reproduce with this simple class:

public final class MyController {
    public let progress: Progress
    
    init() {
        progress = Progress()
        progress.cancellationHandler = {}
    }
}

And this simple test:

import Testing

@Test
func reproDispatchAssertQueueFail() async throws {
    let dc = MyController()
    dc.progress.cancel()
}

In this case, MyController is defined in a Package, which the test imports. I cannot reproduce this when MyController is defined in the same file as the test.

Pulling that same code into a brand new sample project, I cannot reproduce this.

Also, I do not see this issue when testing against iOS 17.5, I'm only seeing it on iOS 18 simulators.

Also, it readily reproduces after running the test immediately after a clean and rebuild of the package defining MyController.

Interestingly enough, I just ran our test against Mac Catalyst and it doesn't crash, but I get a purple runtime warning:

warning: data race detected: @MainActor function at MyController.swift:8 was not called on the main thread

Well, that’s weird. I’m sure there’s an obvious explanation for all of this, but it’s eluding me at the moment.

Within our production codebase, I can reproduce with this simple class:

OK. Well that certainly helps simplify the problem. If only it reproduced outside of your main project )-:

I’d like to confirm two things:

  • You’re testing this from Xcode 16.0.

  • Targeting the iOS 18.0 simulator.

Right?

I just ran our test against Mac Catalyst and it doesn't crash, but I get a purple runtime warning:

That is interesting. It suggests that the compiler thinks that MyController is bound to the main actor. I can’t see anything in your code that would generate that belief.

I have a couple of suggestions. First, add this your code:

enum Test {
    @MainActor static var counter: Int = 0
}

and then in MyController.init() add this:

init() {
    Test.counter += 1
    …

On my machine that generates an error, which is what I’d expect because MyController.init() is not bound to the main actor and so it can’t access directly Test.counter. What happens in your main project test setup?

Second, tweak your setup like this:

progress.cancellationHandler = { @Sendable in }

Does that prevent the trap?

Share and Enjoy

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

I’d like to confirm two things: You’re testing this from Xcode 16.0. Targeting the iOS 18.0 simulator.

Yes on both.

First, add this your code:

The Test.counter line generates this error:

Main actor-isolated static property 'counter' can not be mutated from a nonisolated context

Second, tweak your setup like this: progress.cancellationHandler = { @Sendable in }

Unfortunately, that does not prevent the trap, no.

Thanks for your help.

The Test.counter line generates this error:

Right. That’s the error I’d expect, indicating that the compiler doesn’t think that MyController is main actor isolated. So why is it inserting these asserts? Hmmm, it’s a mystery.

At this point I’m running low on ideas )-: But there’s one last thing I’d like to check. Can you post a full crash report of this issue? See Posting a Crash Report for advice on how to do that.

Share and Enjoy

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

Crash report attached.

Thanks for that.

I had another go at reproducing this myself, with no luck. To investigate further I added a breakpoint in the cancellation handler, that is, on the print(…) call in this slight tweak of your code:

progress.cancellationHandler = {
    print("here")
}

My goal was twofold. First, I wanted to check that the cancellation handler was running in a secondary thread, as shown in your crash report. It is. So my version of your test is replicating that part of the equation.

Here’s that backtrace:

(lldb) bt
* thread #4, queue = 'com.apple.root.user-initiated-qos', stop reason = breakpoint 1.1
  * frame #0: … Test765388.debug.dylib`closure #1 in MyController.init() at MyController.swift:9:19
    frame #1: … Test765388.debug.dylib`thunk for @escaping @callee_guaranteed @Sendable () -> () at <compiler-generated>:0
    frame #2: … Foundation`__20-[NSProgress cancel]_block_invoke_3 + 28
    frame #3: … libdispatch.dylib`_dispatch_call_block_and_release + 24
    frame #4: … libdispatch.dylib`_dispatch_client_callout + 16
    frame #5: … libdispatch.dylib`_dispatch_root_queue_drain + 1072
    frame #6: … libdispatch.dylib`_dispatch_worker_thread2 + 232
    frame #7: … libsystem_pthread.dylib`_pthread_wqthread + 224
…

I then disassembled my part of the equation, that is, frames 0 and 1, looking for where Swift has inserted the swift_task_isCurrentExecutorImpl(…) call that we see in your crash report. It’s just not there.


So, this is clearly something specific to your build setup. You’ve already gone through all the obvious steps to try to isolate this, to no avail. My next recommendation is that you clone your main project and start ripping stuff out of that clone, that it, deleting all the code that’s not necessary to reproduce this issue. Eventually you’ll get to one of two states:

  • Something you remove will cause this problem to go away, and that’ll be a useful clue for further investigation.

  • You’ll delete all the code that you care about, at which point you can file a bug with that project.

Share and Enjoy

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

I have a reproducible project. I zipped it up but I can't seem to attach a zip file. It's only 91KB

I have a reproducible project.

Yay!

I zipped it up but I can't seem to attach a zip file.

Yeah, DevForums doesn’t support arbitrary attachments )-:

At this point I think it makes sense for you to file a bug about this issue. If you post your bug number here, I can pick up the attachment from there.

Share and Enjoy

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

FB15511118

This continues to be a mystery.

I grabbed the test project you attached to FB15511118 and tried it here in my office. Things continue to work for me. This is testing with Xcode 16.0 on macOS 14.7 targeting an iPhone 16.0 / iOS 18.0 simulator. That’s close enough to your setup that it should reproduce. But it doesn’t )-:

One interesting thing I noticed is that the only other code in your test project is using WebKit, and I’ve seen another weird bug a bit like this, where the compiler misidentifies the concurrency properties of some code. See this thread for the details.

Still, that’s just me clutching at straws. Without being able to reproduce the issue, I’m still in the dark. [Whoah, mixed metaphor!]

I’d like to ask you for help again, to rule out one more potential cause of this problem. If you create a new user account on your Mac (in System Settings > Users & Groups) and use Xcode to run your test over there, does that have the same problem?

Share and Enjoy

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

Crash with Progress type, Swift 6, iOS 18
 
 
Q