Skip to content

Commit aaebed9

Browse files
wilhuffryanwilson
authored andcommitted
Fix race condition in FIRComponentContainer instance creation (#3984)
* 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. * format
1 parent 572475d commit aaebed9

File tree

1 file changed

+27
-40
lines changed

1 file changed

+27
-40
lines changed

Firebase/Core/FIRComponentContainer.m

Lines changed: 27 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,10 @@ - (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
104+
withBlock:component.creationBlock];
105+
}
107106
}
108107
}
109108
}
@@ -116,6 +115,8 @@ - (void)populateComponentsFromRegisteredClasses:(NSSet<Class> *)classes forApp:(
116115
/// - Call the block to create an instance if possible,
117116
/// - Validate that the instance returned conforms to the protocol it claims to,
118117
/// - Cache the instance if the block requests it
118+
///
119+
/// Note that this method assumes the caller already has @sychronized on self.
119120
- (nullable id)instantiateInstanceForProtocol:(Protocol *)protocol
120121
withBlock:(FIRComponentCreationBlock)creationBlock {
121122
if (!creationBlock) {
@@ -140,9 +141,7 @@ - (nullable id)instantiateInstanceForProtocol:(Protocol *)protocol
140141

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

148147
return instance;
@@ -153,47 +152,35 @@ - (nullable id)instantiateInstanceForProtocol:(Protocol *)protocol
153152
- (nullable id)instanceForProtocol:(Protocol *)protocol {
154153
// Check if there is a cached instance, and return it if so.
155154
NSString *protocolName = NSStringFromProtocol(protocol);
156-
__block id cachedInstance;
157-
dispatch_sync(_containerQueue, ^{
158-
cachedInstance = self.cachedInstances[protocolName];
159-
});
160155

161-
if (cachedInstance) {
162-
return cachedInstance;
156+
id cachedInstance;
157+
@synchronized(self) {
158+
cachedInstance = self.cachedInstances[protocolName];
159+
if (!cachedInstance) {
160+
// Use the creation block to instantiate an instance and return it.
161+
FIRComponentCreationBlock creationBlock = self.components[protocolName];
162+
cachedInstance = [self instantiateInstanceForProtocol:protocol withBlock:creationBlock];
163+
}
163164
}
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];
165+
return cachedInstance;
168166
}
169167

170168
#pragma mark - Lifecycle
171169

172170
- (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];
171+
@synchronized(self) {
172+
// Loop through the cache and notify each instance that is a maintainer to clean up after
173+
// itself.
174+
for (id instance in self.cachedInstances.allValues) {
175+
if ([instance conformsToProtocol:@protocol(FIRComponentLifecycleMaintainer)] &&
176+
[instance respondsToSelector:@selector(appWillBeDeleted:)]) {
177+
[instance appWillBeDeleted:self.app];
178+
}
188179
}
189-
}
190180

191-
instancesCopy = nil;
192-
193-
// Empty the cache.
194-
dispatch_sync(_containerQueue, ^{
181+
// Empty the cache.
195182
[self.cachedInstances removeAllObjects];
196-
});
183+
}
197184
}
198185

199186
@end

0 commit comments

Comments
 (0)