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