Skip to content

[BUG] Data race in DependencyContainer.init on plain configure() — self escapes to WebEntitlementRedeemer before init completes #504

Description

@0xwurdig

New issue checklist

General information

Describe the bug

Thread Sanitizer reports a data race inside DependencyContainer.init on a plain Superwall.configure(apiKey:) call. Default SuperwallOptions, no PurchaseController, no app-side concurrency. 6/6 runs.

DependencyContainer.init constructs WebEntitlementRedeemer and passes itself as the factory. The redeemer's initialiser starts async work that calls back into makeIsContainerReady() on another thread while init is still assigning its own stored properties — so self escapes before initialisation completes.

Steps to reproduce

  1. New SwiftUI app, SuperwallKit 4.16.1 via SPM.
  2. Call Superwall.configure(apiKey: "pk_…") at launch. Nothing else.
  3. Build with -enableThreadSanitizer YES and run on an iOS 26.4 simulator.

TSan reports within a few seconds of launch.

Trace

WARNING: ThreadSanitizer: data race
  Write of size 8 at 0x00032e426590 by main thread (mutexes: write M0):
    #0 DependencyContainer.init(apiKey:purchaseController:options:)   DependencyContainer.swift:122
    #1 DependencyContainer.__allocating_init(apiKey:purchaseController:options:)
    #2 Superwall.__allocating_init(apiKey:purchaseController:options:completion:)  Superwall.swift:462
    #3 static Superwall.configure(apiKey:purchaseController:options:completion:)   Superwall.swift:695

  Previous read of size 8 at 0x00032e426590 by thread T7:
    #0 DependencyContainer.makeIsContainerReady()                     DependencyContainer.swift:648
    #1 protocol witness for WebEntitlementFactory.makeIsContainerReady() in conformance DependencyContainer
    #2 closure #1 in WebEntitlementRedeemer.init(network:storage:entitlementsInfo:delegate:
       purchaseController:receiptManager:factory:notificationScheduler:
       stripePendingPollIntervalNs:stripePendingPollTimeoutNs:superwall:)
                                                                      WebEntitlementRedeemer.swift:120

  Location is heap block of size 336 allocated by main thread:
    #2 Superwall.__allocating_init(apiKey:purchaseController:options:completion:)  Superwall.swift:462

  Thread T7 is a GCD worker thread

SUMMARY: ThreadSanitizer: data race DependencyContainer.swift:122 in DependencyContainer.init(apiKey:purchaseController:options:)

Other information

I have not observed a user-visible failure from this — reporting it as a sanitizer-confirmed race with a clear mechanism, not as a crash. It seemed worth raising because it needs nothing from the integrator: any app that calls configure() and runs with TSan will hit it, which makes it noise in everyone else's sanitizer runs as well.

Two other races turned up in the same investigation and I've commented on the existing issues rather than duplicating here:

Happy to share the harness app or test a patch — all three reproductions are deterministic here.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions