Skip to content

Commit fb1dd11

Browse files
committed
Fix race condition in FIRComponentContainer instance creation
Reported in #3967. Users sometimes seeing a crash when multiple threads concurrently try to create Firestore instances. By inspection I found that there's a race here where two threads can check for presence in the cache, see nothing, proceed to creation, both store a value in the cache, and both return their copies. This would lead to two Firestores being created for a single App. This replaces `dispatch_sync` on a serial queue with good old `@synchronized` blocks. `dispatch_sync` will self-deadlock if called from a thread already on the queue while `@synchronized` allows recursion. No new tests are added because several tests like `testDependencyDoesntBlock` already cover this case.
1 parent ca8570a commit fb1dd11

File tree

1 file changed

+25
-40
lines changed

1 file changed

+25
-40
lines changed

Firebase/Core/FIRComponentContainer.m

Lines changed: 25 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,7 @@
2323

2424
NS_ASSUME_NONNULL_BEGIN
2525

26-
@interface FIRComponentContainer () {
27-
dispatch_queue_t _containerQueue;
28-
}
26+
@interface FIRComponentContainer ()
2927

3028
/// The dictionary of components that are registered for a particular app. The key is an NSString
3129
/// of the protocol.
@@ -69,8 +67,6 @@ - (instancetype)initWithApp:(FIRApp *)app registrants:(NSMutableSet<Class> *)all
6967
_app = app;
7068
_cachedInstances = [NSMutableDictionary<NSString *, id> dictionary];
7169
_components = [NSMutableDictionary<NSString *, FIRComponentCreationBlock> dictionary];
72-
_containerQueue =
73-
dispatch_queue_create("com.google.FirebaseComponentContainer", DISPATCH_QUEUE_SERIAL);
7470

7571
[self populateComponentsFromRegisteredClasses:allRegistrants forApp:app];
7672
}
@@ -103,7 +99,9 @@ - (void)populateComponentsFromRegisteredClasses:(NSSet<Class> *)classes forApp:(
10399
(component.instantiationTiming == FIRInstantiationTimingEagerInDefaultApp &&
104100
[app isDefaultApp]);
105101
if (shouldInstantiateEager || shouldInstantiateDefaultEager) {
106-
[self instantiateInstanceForProtocol:component.protocol withBlock:component.creationBlock];
102+
@synchronized (self) {
103+
[self instantiateInstanceForProtocol:component.protocol withBlock:component.creationBlock];
104+
}
107105
}
108106
}
109107
}
@@ -116,6 +114,8 @@ - (void)populateComponentsFromRegisteredClasses:(NSSet<Class> *)classes forApp:(
116114
/// - Call the block to create an instance if possible,
117115
/// - Validate that the instance returned conforms to the protocol it claims to,
118116
/// - Cache the instance if the block requests it
117+
///
118+
/// Note that this method assumes the caller already has @sychronized on self.
119119
- (nullable id)instantiateInstanceForProtocol:(Protocol *)protocol
120120
withBlock:(FIRComponentCreationBlock)creationBlock {
121121
if (!creationBlock) {
@@ -140,9 +140,7 @@ - (nullable id)instantiateInstanceForProtocol:(Protocol *)protocol
140140

141141
// The instance is ready to be returned, but check if it should be cached first before returning.
142142
if (shouldCache) {
143-
dispatch_sync(_containerQueue, ^{
144-
self.cachedInstances[protocolName] = instance;
145-
});
143+
self.cachedInstances[protocolName] = instance;
146144
}
147145

148146
return instance;
@@ -153,47 +151,34 @@ - (nullable id)instantiateInstanceForProtocol:(Protocol *)protocol
153151
- (nullable id)instanceForProtocol:(Protocol *)protocol {
154152
// Check if there is a cached instance, and return it if so.
155153
NSString *protocolName = NSStringFromProtocol(protocol);
156-
__block id cachedInstance;
157-
dispatch_sync(_containerQueue, ^{
158-
cachedInstance = self.cachedInstances[protocolName];
159-
});
160154

161-
if (cachedInstance) {
162-
return cachedInstance;
155+
id cachedInstance;
156+
@synchronized (self) {
157+
cachedInstance = self.cachedInstances[protocolName];
158+
if (!cachedInstance) {
159+
// Use the creation block to instantiate an instance and return it.
160+
FIRComponentCreationBlock creationBlock = self.components[protocolName];
161+
cachedInstance = [self instantiateInstanceForProtocol:protocol withBlock:creationBlock];
162+
}
163163
}
164-
165-
// Use the creation block to instantiate an instance and return it.
166-
FIRComponentCreationBlock creationBlock = self.components[protocolName];
167-
return [self instantiateInstanceForProtocol:protocol withBlock:creationBlock];
164+
return cachedInstance;
168165
}
169166

170167
#pragma mark - Lifecycle
171168

172169
- (void)removeAllCachedInstances {
173-
// Loop through the cache and notify each instance that is a maintainer to clean up after itself.
174-
// Design note: we're getting a copy here, unlocking the cached instances, iterating over the
175-
// copy, then locking and removing all cached instances. A race condition *could* exist where a
176-
// new cached instance is created between the copy and the removal, but the chances are slim and
177-
// side-effects are significantly smaller than including the entire loop in the `dispatch_sync`
178-
// block (access to the cache from inside the block would deadlock and crash).
179-
__block NSDictionary<NSString *, id> *instancesCopy;
180-
dispatch_sync(_containerQueue, ^{
181-
instancesCopy = [self.cachedInstances copy];
182-
});
183-
184-
for (id instance in instancesCopy.allValues) {
185-
if ([instance conformsToProtocol:@protocol(FIRComponentLifecycleMaintainer)] &&
186-
[instance respondsToSelector:@selector(appWillBeDeleted:)]) {
187-
[instance appWillBeDeleted:self.app];
170+
@synchronized (self) {
171+
// Loop through the cache and notify each instance that is a maintainer to clean up after itself.
172+
for (id instance in self.cachedInstances.allValues) {
173+
if ([instance conformsToProtocol:@protocol(FIRComponentLifecycleMaintainer)] &&
174+
[instance respondsToSelector:@selector(appWillBeDeleted:)]) {
175+
[instance appWillBeDeleted:self.app];
176+
}
188177
}
189-
}
190178

191-
instancesCopy = nil;
192-
193-
// Empty the cache.
194-
dispatch_sync(_containerQueue, ^{
179+
// Empty the cache.
195180
[self.cachedInstances removeAllObjects];
196-
});
181+
}
197182
}
198183

199184
@end

0 commit comments

Comments
 (0)