Swift 5.9 Noncopyable and os_unfair_lock crash

Hello, I recently implemented a lock that uses OSAllocatedUnfairLock on iOS 16+ and os_unfair_lock on below iOS 16.

I know that using os_unfair_lock with Swift is tricky, and it is error-prone. For example, the famous open-source library Alamofire has even been using os_unfair_lock incorrectly in terms of Swift's lifecycle view. (They fixed it as in the link [1] )

So, I implemented a lock like below. To use os_unfair_lock safely, I used the Noncopyable protocol which was added in Swift 5.9 [2]. Also, I allocated memory on the heap to use os_unfair_lock.

public struct UnfairLock: ~Copyable {

    public init() {
        if #available(iOS 16.0, *) {
            _osAllocatedUnfairLock = OSAllocatedUnfairLock()
        } else {
            self.unfairLock = UnsafeMutablePointer.allocate(capacity: 1)
        }
    }

    deinit {
        if #unavailable(iOS 16.0) {
            unfairLock!.deallocate()
        }
    }

    public func lock() {
        if #available(iOS 16.0, *) {
            osAllocatedUnfairLock.lock()
        } else {
            os_unfair_lock_lock(unfairLock!)
        }
    }

    public func unlock() {
        if #available(iOS 16.0, *) {
            osAllocatedUnfairLock.unlock()
        } else {
            os_unfair_lock_unlock(unfairLock!)
        }
    }

    public func with<T>(_ closure: () -> T) -> T {
        lock()
        defer { unlock() }
        return closure()
    }

    private var _osAllocatedUnfairLock: Any?
    private var unfairLock: UnsafeMutablePointer<os_unfair_lock_s>?

    @available(iOS 16.0, *)
    private var osAllocatedUnfairLock: OSAllocatedUnfairLock<Void> {
        // swiftlint:disable force_cast
        _osAllocatedUnfairLock as! OSAllocatedUnfairLock
        // swiftlint:enable force_cast
    }
}

However, I got several crashes on iOS 14-15 users like this (This app targets iOS 14+ and on iOS 16+, it uses OSAllocatedUnfairLock): (Sorry for using a third-party crash reporting tool's log, but I think it is enough to understand the issue)

BUG IN CLIENT OF LIBPLATFORM: os_unfair_lock is corrupt 

Crashed: com.foo.bar.queue
0  libsystem_platform.dylib       0x6144 _os_unfair_lock_corruption_abort + 88
1  libsystem_platform.dylib       0xa20 _os_unfair_lock_lock_slow + 320
2  FoooBarr                       0x159416c closure #1 in static FooBar.baz() + 6321360
3  FoooBarr                       0x2e65b8 thunk for @escaping @callee_guaranteed @Sendable () -> () + 4298794424 (<compiler-generated>:4298794424)
4  libdispatch.dylib              0x1c04 _dispatch_call_block_and_release + 32
5  libdispatch.dylib              0x3950 _dispatch_client_callout + 20
6  libdispatch.dylib              0x6e04 _dispatch_continuation_pop + 504
7  libdispatch.dylib              0x6460 _dispatch_async_redirect_invoke + 596
8  libdispatch.dylib              0x14f48 _dispatch_root_queue_drain + 388
9  libdispatch.dylib              0x15768 _dispatch_worker_thread2 + 164
10 libsystem_pthread.dylib        0x1174 _pthread_wqthread + 228
11 libsystem_pthread.dylib        0xf50 start_wqthread + 8

( libplatform's source code [3] suggests that __ulock_wait returns error, but I don't know the details)

Per @eskimo 's suggestion in [4], I will change my code to use NSLock until OSAllocatedUnfairLock is available on all users' devices (i.e. iOS 16+), but I still want to know why this crash happens.

I believe that making a struct Noncopyable is enough to use os_unfair_lock safely, but it seems that it is not enough. Did I miss something? Or is there any other way to use os_unfair_lock safely?

[1] https://github.com/Alamofire/Alamofire/commit/1b89a57c2f272408b84d20132a2ed6628e95d3e2

[2] https://github.com/apple/swift-evolution/blob/1b0b339bc3072a83b5a6a529ae405a0f076c7d5d/proposals/0390-noncopyable-structs-and-enums.md

[3] https://github.com/apple-open-source/macos/blob/ea4cd5a06831aca49e33df829d2976d6de5316ec/libplatform/src/os/lock.c#L555

[4] https://forums.developer.apple.com/forums/thread/712379

Replies

but I don't know the details

Well, the error code is likely sitting in a register, which would be captured by an Apple crash report. Just sayin’ (-:

Looking at the code you referenced, there’s only one way to get to _os_unfair_lock_corruption_abort, that is, __ulock_wait fails with EOWNERDEAD. That suggests that you hit this line, which indicates that the lock is held by a thread but that thread has terminated (or is corrupt in exactly the right way to suggest that).

Share and Enjoy

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