Interesting. I extended your example to this file:
import Foundation
import Security
let task = Task { @MainActor in
SecAddSharedWebCredential("example.com" as CFString, "account" as CFString, "password" as CFString) { error in
print(error as Any)
}
}
await task.value
and compiled and ran it:
swiftc -swift-version 6 SecAddSharedWebCredential.swift
./SecAddSharedWebCredential
no crash. But when I run it in lldb,
lldb ./SecAddSharedWebCredential
(lldb) target create "./SecAddSharedWebCredential"
Current executable set to '/Users/<me>/Source/SwiftReductions/SecAddSharedWebCredential' (arm64).
(lldb) r
Process 62251 launched: '/Users/<me>/Source/SwiftReductions/SecAddSharedWebCredential' (arm64)
Process 62251 stopped
* thread #2, queue = 'com.apple.root.default-qos', stop reason = EXC_BREAKPOINT (code=1, subcode=0x18141b9c0)
frame #0: 0x000000018141b9c0 libdispatch.dylib`_dispatch_assert_queue_fail + 120
libdispatch.dylib`_dispatch_assert_queue_fail:
-> 0x18141b9c0 <+120>: brk #0x1
libdispatch.dylib`dispatch_assert_queue_not:
0x18141b9c4 <+0>: pacibsp
0x18141b9c8 <+4>: stp x29, x30, [sp, #-0x10]!
0x18141b9cc <+8>: mov x29, sp
Target 0: (SecAddSharedWebCredential) stopped.
Fortunately, the backtrace has the information we need:
(lldb) bt
* thread #2, queue = 'com.apple.root.default-qos', stop reason = EXC_BREAKPOINT (code=1, subcode=0x18141b9c0)
* frame #0: 0x000000018141b9c0 libdispatch.dylib`_dispatch_assert_queue_fail + 120
frame #1: 0x000000018141b948 libdispatch.dylib`dispatch_assert_queue + 196
frame #2: 0x0000000261bd6e08 libswift_Concurrency.dylib`swift_task_isCurrentExecutorImpl(swift::SerialExecutorRef) + 284
frame #3: 0x0000000100003494 SecAddSharedWebCredential`closure #1 (Swift.Optional<__C.CFErrorRef>) -> () in closure #1 @Swift.MainActor @Sendable () async -> () in SecAddSharedWebCredential + 84
frame #4: 0x0000000100003664 SecAddSharedWebCredential`reabstraction thunk helper from @escaping @callee_guaranteed (@guaranteed Swift.Optional<__C.CFErrorRef>) -> () to @escaping @callee_unowned @convention(block) (@unowned Swift.Optional<__C.CFErrorRef>) -> () + 64
frame #5: 0x000000018471a3d8 Security`__SecAddSharedWebCredential_block_invoke_2 + 52
frame #6: 0x0000000181417854 libdispatch.dylib`_dispatch_call_block_and_release + 32
frame #7: 0x00000001814195b4 libdispatch.dylib`_dispatch_client_callout + 20
frame #8: 0x000000018141c700 libdispatch.dylib`_dispatch_queue_override_invoke + 916
frame #9: 0x000000018142b4cc libdispatch.dylib`_dispatch_root_queue_drain + 392
frame #10: 0x000000018142bcd8 libdispatch.dylib`_dispatch_worker_thread2 + 156
frame #11: 0x00000001815c839c libsystem_pthread.dylib`_pthread_wqthread + 228
Frame 3 is our closure, frame 2 is Swift 6 asserting that the closure is on the actor it expects to be.
What's going on is, SecAddSharedWebCredential
has been imported from C incorrectly — we have:
public func SecAddSharedWebCredential(
_ fqdn: CFString,
_ account: CFString,
_ password: CFString?,
_ completionHandler: @escaping (CFError?) -> Void
)
Which says "this completion handler will be called on the same actor as you called SecAddSharedWebCredential
on. This is wrong (it calls back on some arbitrary global dispatch queue).
It should have been imported as:
public func SecAddSharedWebCredential(
_ fqdn: CFString,
_ account: CFString,
_ password: CFString?,
_ completionHandler: @escaping @Sendable (CFError?) -> Void
)
(sending
would work as well as @Sendable
here since the callback is called only once, but I doubt the clang importer could know that?)
This is a real problem with the clang importer and Swift 6 — importing ObjC closures without @Sendable
is no longer a conservative choice.
Fortunately, we can work around it in our own code:
import Foundation
import Security
let closure = { @Sendable error in
print(error as Any)
}
let task = Task { @MainActor in
SecAddSharedWebCredential("example.com" as CFString, "account" as CFString, "password" as CFString, closure)
}
await task.value
(unfortunately just adding @Sendable
in the inline closure doesn't work, that creates an @MainActor @Sendable
closure which has the same problem; we actually have to declare the closure outside of the global actor isolated scope)
swiftc -swift-version 6 SecAddSharedWebCredential.swift
lldb ./SecAddSharedWebCredential
(lldb) target create "./SecAddSharedWebCredential"
Current executable set to '/Users/<me>/Source/SwiftReductions/SecAddSharedWebCredential' (arm64).
(lldb) r
Process 62701 launched: '/Users/<me>/Source/SwiftReductions/SecAddSharedWebCredential' (arm64)
Optional(Error Domain=NSOSStatusErrorDomain Code=-4 "SecAddSharedWebCredentialSync not supported on this platform" (kCFMessagePortTransportError / kCSIdentityDeletedErr / unimpErr: / / unimplemented core routine) UserInfo={numberOfErrorsDeep=0, NSDescription=SecAddSharedWebCredentialSync not supported on this platform})
Process 62701 exited with status = 0 (0x00000000)