Skip to content

[BUG][Swift6] ThreadSanitizer Swift Access Race in OpenAPI Generator Swift Client #23055

@biovolt

Description

@biovolt

Bug Report Checklist

  • Have you provided a full/minimal spec to reproduce the issue?
  • [NA] Have you validated the input using an OpenAPI validator?
  • Have you tested with the latest master to confirm the issue still exists?
  • Have you searched for related issues/PRs?
  • What's the actual output vs expected output?
  • [Optional] Sponsorship to speed up the bug fix or feature request (example)
Description

SynchronizedDictionary in the generated Swift URLSession client is declared as a struct but used as a mutable stored property (var) on a shared singleton class (URLSessionRequestBuilderConfiguration). This causes ThreadSanitizer to report a Swift access race when multiple concurrent network requests access credentialStore from different threads.

The internal NSRecursiveLock protects the dictionary contents but does not protect against Swift's memory exclusivity violation at the struct-property level. Per SE-0176:

"Calling a method on a value type is an access to the entire value: a write if it's a mutating method, a read otherwise."

"Swift has always considered read/write and write/write races on the same variable to be undefined behaviour."

When two threads concurrently write to credentialStore[key], Swift sees overlapping modifying accesses to the same var credentialStore struct property — this is undefined behaviour regardless of the internal lock.

Why this matters: This is not a TSan false positive. The Swift 5 Exclusivity Enforcement blog post confirms: "Because Point is declared as a struct, it is considered a value type, meaning that all of its properties are part of a whole value, and accessing one property accesses the entire value." The Swift book Memory Safety chapter further states overlapping struct access is only safe when the struct is a local variable not captured by escaping closures — credentialStore fails both conditions.

openapi-generator version

Verified present in:

  • v7.19.0 (generated client we tested against)
  • v7.20.0 (latest public release, Feb 2026) — templates unchanged

Not a regression — the bug has existed since SynchronizedDictionary was introduced as a struct.

OpenAPI declaration file content or url

Any OpenAPI spec will trigger this. The bug is in the generator's infrastructure templates, not in spec-specific generated code. A minimal spec that makes 2+ concurrent API calls is sufficient:

Both swift5 and swift6 generators with urlsession library are affected. The bug is in these template files:

Template Path
swift6 SynchronizedDictionary modules/openapi-generator/src/main/resources/swift6/SynchronizedDictionary.mustache
swift5 SynchronizedDictionary modules/openapi-generator/src/main/resources/swift5/SynchronizedDictionary.mustache
swift6 URLSessionImplementations modules/openapi-generator/src/main/resources/swift6/libraries/urlsession/URLSessionImplementations.mustache
swift5 URLSessionImplementations modules/openapi-generator/src/main/resources/swift5/libraries/urlsession/URLSessionImplementations.mustache
Steps to reproduce
  1. Generate a Swift client using the urlsession library (swift5 or swift6)
  2. Integrate into an iOS/macOS app
  3. Enable ThreadSanitizer in Xcode (Edit Scheme > Diagnostics > Thread Sanitizer)
  4. Trigger 2+ concurrent API requests (e.g., call getA and getB simultaneously)
  5. Observe TSan warning:
WARNING: ThreadSanitizer: Swift access race
  Modifying access of Swift variable at 0x... by thread T7:
    #0 closure #1 (Result<URLRequest, Error>) -> () in URLSessionRequestBuilder.execute(completion:)
    ...

  Previous modifying access of Swift variable at 0x... by thread T14:
    #0 URLSessionRequestBuilder.cleanupRequest()
    ...

  Location is heap block of size 48 at 0x... allocated by thread T10:
    ...
    #5 URLSessionRequestBuilderConfiguration.shared.unsafeMutableAddressor

The two racing call sites are:

Thread Aexecute() interceptor completion (URLSessionImplementations.mustache, line ~204):

URLSessionRequestBuilderConfiguration.shared.credentialStore[dataTask.taskIdentifier] = self.credential

Thread BcleanupRequest() from data task completion (line ~227):

URLSessionRequestBuilderConfiguration.shared.credentialStore[task.taskIdentifier] = nil
Related issues/PRs

No existing issues found for this specific race condition. Searched for SynchronizedDictionary, ThreadSanitizer, credentialStore, and access race in https://github.com/openapitools/openapi-generator/issues.

Suggest a fix

Change SynchronizedDictionary from struct to class in both SynchronizedDictionary.mustache templates:

- internal struct SynchronizedDictionary<K: Hashable, V>: @unchecked Sendable {
+ internal class SynchronizedDictionary<K: Hashable, V>: @unchecked Sendable {
      private var dictionary = [K: V]()
      private let lock = NSRecursiveLock()
      // ... subscript unchanged ...
  }

As a reference type (class), subscript mutations modify the object's internal state via a pointer — they do not trigger a modifying access to the credentialStore property on the owning class. Per SE-0176: "Unlike value types, calling a method on a class doesn't formally access the entire class instance...we only enforce it for individual stored properties."

  • SynchronizedDictionary is only instantiated once (via the singleton's default initializer) and never reassigned
  • No generated code relies on value semantics (copy-on-write) of SynchronizedDictionary
  • The credentialStore property on URLSessionRequestBuilderConfiguration could additionally be changed from var to let since it is never reassigned

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions