General information
- SDK version: 7.5.0 (verified the crashing code path is unchanged on
main / 7.7.0)
- Integration: Swift Package Manager
- Affected clients:
BTThreeDSecureClient, BTDataCollector, BTApplePayClient (each created with authorization: String)
- Environment: production, real devices
Describe the bug
Since upgrading from 6.32.0 to 7.x we see a recurring main-thread crash. It is an objc_retain on a freed/torn pointer, occurring in the @MainActor continuation of fetchOrReturnRemoteConfiguration(_:) that runs setupHTTPCredentials after the config load resumes.
We receive two crash groups with byte-identical stack traces:
Crashed: com.apple.main-thread
0 libobjc.A.dylib objc_retain_x0 + 16
1 libobjc.A.dylib objc_retain + 16
2 app BTAPIClient.setupHTTPCredentials(_:) (BTHTTP.swift)
3 app closure #1 in BTAPIClient.fetchOrReturnRemoteConfiguration(_:) (BTAPIClient.swift:86)
4 libswift_Concurrency swift::runJobInEstablishedExecutorContext(swift::Job*) + 472
5 libswift_Concurrency swift_job_runImpl(...) + 156
6 libdispatch _dispatch_main_queue_drain.cold.6 + 612
...
15 app main (AppDelegate.swift)
Analysis
BTAPIClient.init fires fetchOrReturnRemoteConfiguration { _, _ in }, which spawns Task { @MainActor in let config = try await configurationLoader.getConfig(); setupHTTPCredentials(config); ... }. getConfig() is isolated to the app-wide @ConfigurationActor, while setupHTTPCredentials runs on @MainActor, and deinit (both BTAPIClient and ConfigurationLoader, calling session.finishTasksAndInvalidate()) runs on an arbitrary thread.
BTAPIClient's mutable reference-typed stored properties (http, graphQLHTTP, payPalHTTP) and the BTConfiguration returned from getConfig are non-Sendable and cross these three concurrency domains without synchronization. The shared singletons (ConfigurationActor.shared, ConfigurationCache.shared) are used by every BTAPIClient instance. Under load — e.g. creating BTDataCollector + BTThreeDSecureClient + BTApplePayClient close together during a single checkout — the continuation appears to retain an object through a torn/raced pointer, crashing.
Suggested fix
Synchronize the HTTP property access across the @MainActor / @ConfigurationActor / deinit boundaries (e.g. isolate setupHTTPCredentials state, or capture [weak self] in the config Task with a guard, and avoid mutating shared HTTP properties from deinit concurrently).
Steps to reproduce
Create the v7 clients with an authorization: string and exercise multiple Braintree flows concurrently (device data collection + 3DS + Apple Pay) on production traffic; crash surfaces intermittently on the main thread.
General information
main/ 7.7.0)BTThreeDSecureClient,BTDataCollector,BTApplePayClient(each created withauthorization: String)Describe the bug
Since upgrading from 6.32.0 to 7.x we see a recurring main-thread crash. It is an
objc_retainon a freed/torn pointer, occurring in the@MainActorcontinuation offetchOrReturnRemoteConfiguration(_:)that runssetupHTTPCredentialsafter the config load resumes.We receive two crash groups with byte-identical stack traces:
Analysis
BTAPIClient.initfiresfetchOrReturnRemoteConfiguration { _, _ in }, which spawnsTask { @MainActor in let config = try await configurationLoader.getConfig(); setupHTTPCredentials(config); ... }.getConfig()is isolated to the app-wide@ConfigurationActor, whilesetupHTTPCredentialsruns on@MainActor, anddeinit(bothBTAPIClientandConfigurationLoader, callingsession.finishTasksAndInvalidate()) runs on an arbitrary thread.BTAPIClient's mutable reference-typed stored properties (http,graphQLHTTP,payPalHTTP) and theBTConfigurationreturned fromgetConfigare non-Sendableand cross these three concurrency domains without synchronization. The shared singletons (ConfigurationActor.shared,ConfigurationCache.shared) are used by everyBTAPIClientinstance. Under load — e.g. creatingBTDataCollector+BTThreeDSecureClient+BTApplePayClientclose together during a single checkout — the continuation appears to retain an object through a torn/raced pointer, crashing.Suggested fix
Synchronize the HTTP property access across the
@MainActor/@ConfigurationActor/deinitboundaries (e.g. isolatesetupHTTPCredentialsstate, or capture[weak self]in the config Task with a guard, and avoid mutating shared HTTP properties fromdeinitconcurrently).Steps to reproduce
Create the v7 clients with an
authorization:string and exercise multiple Braintree flows concurrently (device data collection + 3DS + Apple Pay) on production traffic; crash surfaces intermittently on the main thread.