-
-
Notifications
You must be signed in to change notification settings - Fork 7.4k
Description
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
- Generate a Swift client using the
urlsessionlibrary (swift5 or swift6) - Integrate into an iOS/macOS app
- Enable ThreadSanitizer in Xcode (Edit Scheme > Diagnostics > Thread Sanitizer)
- Trigger 2+ concurrent API requests (e.g., call
getAandgetBsimultaneously) - 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 A — execute() interceptor completion (URLSessionImplementations.mustache, line ~204):
URLSessionRequestBuilderConfiguration.shared.credentialStore[dataTask.taskIdentifier] = self.credentialThread B — cleanupRequest() from data task completion (line ~227):
URLSessionRequestBuilderConfiguration.shared.credentialStore[task.taskIdentifier] = nilRelated 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."
SynchronizedDictionaryis 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
credentialStoreproperty onURLSessionRequestBuilderConfigurationcould additionally be changed fromvartoletsince it is never reassigned