From 45905c0c090a54b1c3f8e113c33f14a94445f916 Mon Sep 17 00:00:00 2001 From: zerotomas Date: Wed, 9 Sep 2026 19:25:58 -0700 Subject: [PATCH] fix: serialize lastUsedIdentity to avoid EXC_BAD_ACCESS under concurrent async callers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `getFeatureFlags(forIdentity:...)` writes `lastUsedIdentity = identity` on its first line, before dispatching the actual request. That write runs on whatever executor the caller was on — for the async overloads in `Flagsmith+Concurrency.swift`, that's whichever cooperative-pool thread the awaited continuation happens to resume on. When two `Task { await Flagsmith.shared.hasFeatureFlag(...) }` fire on different cooperative threads, both hit the unsynchronized write. Under concurrent writes, the refcounted heap storage of the `String?` can be stomped, and the next reader dereferences a corrupted strong reference — `EXC_BAD_ACCESS` with a small integer-looking address, inside the SDK frame. Back `lastUsedIdentity` with a serialized accessor through the existing `apiManager.propertiesSerialAccessQueue`, matching the pattern used by `_defaultFlags` and `_cacheConfig` immediately below in the same file (and by every ivar on `APIManager`). No API change; no behavior change for single-threaded callers. --- FlagsmithClient/Classes/Flagsmith.swift | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/FlagsmithClient/Classes/Flagsmith.swift b/FlagsmithClient/Classes/Flagsmith.swift index b6bd426..3cdbbe1 100644 --- a/FlagsmithClient/Classes/Flagsmith.swift +++ b/FlagsmithClient/Classes/Flagsmith.swift @@ -24,8 +24,16 @@ public final class Flagsmith: @unchecked Sendable { // The last time we got flags via the API private var lastUpdatedAt: Double = 0.0 - // The last identity used for fetching flags - private var lastUsedIdentity: String? + // The last identity used for fetching flags. Backed by a serialized + // accessor because `getFeatureFlags(forIdentity:...)` writes it + // unconditionally on entry, on whatever executor the caller is on — + // concurrent async callers race here and can corrupt the String? storage. + private var _lastUsedIdentity: String? + private var lastUsedIdentity: String? { + get { apiManager.propertiesSerialAccessQueue.sync { _lastUsedIdentity } } + set { apiManager.propertiesSerialAccessQueue.sync { _lastUsedIdentity = newValue } } + } + // The last result from fetching flags internal var lastFlags: [Flag]?