How to fix a crash in SecAddSharedWebCredential under Swift 6?

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) 
6 Likes