A deadlock occurs when initializing SDKVersion or calling SDKVersion.register from a background thread. This often happens implicitly when initializing components like OAuth2Client. The method holds a private lock while synchronously intercepting the Main Thread to retrieve UIDevice information. If the Main Thread simultaneously attempts to access SDKVersion (e.g., reading userAgent), the application deadlocks.
- Background Initialization: Initialize an instance of
OAuth2Client(or other SDK components) from a background thread. In Swift < 6, unstructuredTask { }blocks often run on a background executor. - Transitive Registration: The
OAuth2Clientinitializer implicitly accessesSDKVersion.authFoundation, which triggers static initialization and callsSDKVersion.register. - Lock Acquisition:
registeracquires the privateSDKVersion.lock. - Main Thread Block: While holding the lock, the code attempts to read
UIDevice.current.systemVersion. This forces a synchronous thread hop (DispatchQueue.main.sync) to the Main Thread. - Deadlock: The background thread is now blocked waiting for the Main Thread. If the Main Thread is blocked or waiting on any resource (like the same
SDKVersion.lock), the app deadlocks.
// Background Thread initialization
// In Swift < 6, `Task { ... }` runs on the background generic executor.
// Initializing `OAuth2Client` (or other components) transitively calls `SDKVersion.register`.
// The deadlock occurs when initializing the `OAuth2Client` class itself.
```swift
// Background Thread initialization
// In Swift < 6, `Task { ... }` runs on the background generic executor.
// Initializing `OAuth2Client` (or other components) transitively calls `SDKVersion.register`.
Task {
// Acquires lock during initialization, then performs dispatch_sync to Main Thread
let client = OAuth2Client(issuerURL: URL(string: "https://example.com")!,
clientId: "clientId",
scope: "openid")
}When the deadlock occurs, the console will show the following logs and then stop indefinitely:
🏁 Starting Deadlock Reproduction
Background: Initializing OAuth2Client... (Acquiring Lock)
Main: Accessing SDKVersion.userAgent... (Requiring Lock)
Note: The success messages (✅) will never appear.
The initialization of OAuth2Client triggers the deadlock through the following chain:
OAuth2Client.initcallassert(SDKVersion.authFoundation != nil)- Accessing
SDKVersion.authFoundationtriggers its static initialization. - Static closure calls
SDKVersion.register(...) registerholdsSDKVersion.lockwhile synchronously dispatching to the Main Thread for device info.
The register method holds a lock before accessing systemVersion, which triggers the main thread sync.
// Sources/AuthFoundation/Migration/SDKVersion.swift
public static func register(sdk: SDKVersion) -> SDKVersion {
lock.withLock { // [!] Lock acquired here
// ...
// [!] Accessing systemVersion triggers MainActor.nonisolatedUnsafe
_userAgent = "\(sdkVersionString) \(systemName)/\(systemVersion) Device/\(deviceModel)"
return sdk
}
}The helper utility forces a synchronous wait on the Main Thread.
// Sources/CommonSupport/ExpressionUtilities.swift
public static func nonisolatedUnsafe<T: Sendable>(_ block: @MainActor () -> T) -> T {
if Thread.isMainThread {
return MainActor.assumeIsolated { block() }
} else {
return DispatchQueue.main.sync { // [!] Synchronous wait for Main Thread
// ...
block()
// ...
}
}
}Issue: Inversion of Control / Unsafe Threading Assumption.
The SDKVersion.register method is designed with a critical flaw: it holds a lock while synchronously waiting for the Main Thread (DispatchQueue.main.sync). This implementation implicitly assumes that registration will either occur on the Main Thread or when the Main Thread is free.
However, initializing core components like OAuth2Client on a background thread (a common pattern for performance) triggers this registration. This creates a deadlock trap: the background thread holds the lock and waits for Main, while any Main Thread operation needing that lock (such as fetching User-Agent for headers) will hang indefinitely.
Fix:
Decouple the UIDevice access from the lock scope. Fetch the system version independently or asynchronously before acquiring the lock, or cache it safely without blocking the critical section.