-
Notifications
You must be signed in to change notification settings - Fork 34
Open
Description
Thread Safety Bug: Random Crashes in FeatureEvaluator
Summary
Getting EXC_BAD_ACCESS crashes in production when using GrowthBook SDK v1.0.84. The crash happens in FeatureEvaluator.swift line 37 when features are evaluated from multiple threads.
Crash: EXC_BAD_ACCESS (code=1, address=0x8000000000000008)
The Problem
The SDK's FeatureEvaluator mutates a shared Set<String> without thread synchronization:
// FeatureEvaluator.swift:33-37
context.stackContext.evaluatedFeatures.insert(featureKey)
defer {
context.stackContext.evaluatedFeatures.remove(featureKey) // ⚠️ CRASH: accessing deallocated Set
}Swift's Set is not thread-safe. When multiple threads access features simultaneously (especially with backgroundSync: true), the Set can be deallocated while another thread is still using it.
When It Happens
This crashes when:
- SwiftUI views read features on main thread
backgroundSync: truerefreshes features on background threadsetAttributes()orrefreshCache()called while evaluating features- Multiple concurrent
getFeatureValue()calls
Reproduction
let sdk = GrowthBookBuilder(
apiHost: "...",
clientKey: "...",
backgroundSync: true // Background thread updates
).initializer()
// Main thread: SwiftUI view reads feature
let enabled = sdk.getFeatureValue(feature: "my-feature", default: .null)
// Background thread: Combine publisher updates attributes
userPublisher.sink { user in
sdk.setAttributes(attributes: [...]) // Race condition!
sdk.refreshCache()
}Suggested Fix
Add thread-safety to the evaluatedFeatures Set. Two options:
Option 1: Lock in FeatureEvaluator
class FeatureEvaluator {
private let lock = NSLock()
func evaluateFeature() -> FeatureResult {
lock.lock()
defer { lock.unlock() }
context.stackContext.evaluatedFeatures.insert(featureKey)
defer {
lock.lock()
context.stackContext.evaluatedFeatures.remove(featureKey)
lock.unlock()
}
// ... rest
}
}Option 2: Thread-safe StackContext
@objc public class StackContext : NSObject {
private let queue = DispatchQueue(label: "com.growthbook.stack")
private var _evaluatedFeatures: Set<String> = []
public var evaluatedFeatures: Set<String> {
get { queue.sync { _evaluatedFeatures } }
set { queue.sync { _evaluatedFeatures = newValue } }
}
}Workaround (for now)
Users can wrap SDK calls with synchronization:
class GrowthBookService {
private let sdk: GrowthBookSDK
private let queue = DispatchQueue(label: "com.app.growthbook", attributes: .concurrent)
func getFeature(key: String) -> JSON {
queue.sync {
sdk.getFeatureValue(feature: key, default: .null)
}
}
func setAttributes(_ attrs: [String: Any]) {
queue.async(flags: .barrier) {
sdk.setAttributes(attributes: attrs)
}
}
}Thank you!
Metadata
Metadata
Assignees
Labels
No labels