Skip to content

Main-thread crash (objc_retain) in BTAPIClient.setupHTTPCredentials via init-fired fetchOrReturnRemoteConfiguration (v7) #1818

Description

@BhosaleNikhil

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions