Skip to content

Commit 9b8668d

Browse files
siddhijainCopilot
andauthored
Fixes AB#3467552 Introduce locks at the cache layer (#2842)
Fixes [AB#3467552](https://identitydivision.visualstudio.com/fac9d424-53d2-45c0-91b5-ef6ba7a6bf26/_workitems/edit/3467552) This is a follow up PR to 1. introduce locks closer to the class where I/O operation is performed. 2. It also introduces a lock at the BrokerOauth2TokenCache layer to ensure the atomicity of the operations -> save(), updateMetaDataCache() and loadAggregatedAccountData. --------- Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: siddhijain <30181294+siddhijain@users.noreply.github.com>
1 parent 07a2c1f commit 9b8668d

5 files changed

Lines changed: 710 additions & 21 deletions

File tree

changelog.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
vNext
22
----------
3+
- [MINOR] Introduce locks at NameValueStorageFileManagerSimpleCacheImpl layer (#2842)
34

45
Version 23.2.0
5-
----------
6+
67
- [MINOR] Add additional allowed origins for PasskeyWebListener (#2839)
78
- [PATCH] Add JavascriptInterface rules to consumer proguard rules (#2837)
89
- [MINOR] Add optimized saveAndLoadAggregatedAccountData method in BrokerOAuth2TokenCache (#2832)

common4j/src/main/com/microsoft/identity/common/java/cache/BrokerOAuth2TokenCache.java

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,23 @@ public class BrokerOAuth2TokenCache
107107
private final MicrosoftFamilyOAuth2TokenCache mFociCache;
108108
private final int mUid;
109109
private ProcessUidCacheFactory mDelegate = null;
110+
111+
/**
112+
* Static map of locks for ensuring atomicity of compound save/update/load operations.
113+
* Since each thread creates a new BrokerOAuth2TokenCache instance, we need locks
114+
* shared across all instances to prevent race conditions when multiple instances
115+
* access the same underlying storage (FOCI cache or UID-specific cache).
116+
* <p>
117+
* Keys are cache identifiers:
118+
* <ul>
119+
* <li>"FOCI" - for Family of Client IDs cache (shared by all FOCI apps)</li>
120+
* <li>"UID_<uid>" - for process UID-specific caches (one per UID)</li>
121+
* </ul>
122+
* This ensures that operations on the same logical cache are serialized, even
123+
* when invoked through different BrokerOAuth2TokenCache instances.
124+
*/
125+
private static final ConcurrentHashMap<String, Object> CACHE_OPERATION_LOCKS = new ConcurrentHashMap<>();
126+
110127
/**
111128
* Shared, process-wide registry of in-memory augmented account/credential caches keyed by
112129
* storage (SharedPreferences) name.
@@ -1745,10 +1762,14 @@ private List<ICacheRecord> saveAndLoadAggregatedAccountDataOptimized(
17451762
"refreshTokenRecord, familyId, authScheme)";
17461763

17471764
final ICacheRecord cacheRecord;
1748-
synchronized (this) {
1749-
final long saveAndLoadStartTime = System.currentTimeMillis();
1765+
final long saveAndLoadStartTime = System.currentTimeMillis();
1766+
1767+
final boolean isFoci = !StringUtil.isNullOrEmpty(familyId);
1768+
// Determine lock key based on which cache we're operating on
1769+
final String lockKey = isFoci ? "FOCI" : "UID_" + mUid;
1770+
final Object lock = CACHE_OPERATION_LOCKS.computeIfAbsent(lockKey, k -> new Object());
17501771

1751-
final boolean isFoci = !StringUtil.isNullOrEmpty(familyId);
1772+
synchronized (lock) {
17521773
final MsalOAuth2TokenCache targetCache;
17531774

17541775
if (isFoci) {
@@ -1808,6 +1829,5 @@ private List<ICacheRecord> saveAndLoadAggregatedAccountDataOptimized(
18081829
saveAndLoadStartTime);
18091830
return cacheRecordList;
18101831
}
1811-
18121832
}
18131833
}

common4j/src/main/com/microsoft/identity/common/java/cache/NameValueStorageFileManagerSimpleCacheImpl.java

Lines changed: 118 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,21 @@
2525
import lombok.NonNull;
2626

2727
import com.google.gson.Gson;
28+
import com.microsoft.identity.common.java.flighting.CommonFlight;
29+
import com.microsoft.identity.common.java.flighting.CommonFlightsManager;
2830
import com.microsoft.identity.common.java.interfaces.IPlatformComponents;
2931
import com.microsoft.identity.common.java.interfaces.INameValueStorage;
3032
import com.microsoft.identity.common.java.logging.Logger;
3133
import com.microsoft.identity.common.java.util.StringUtil;
3234

3335
import java.util.HashSet;
3436
import java.util.List;
37+
import java.util.Map;
3538
import java.util.Set;
3639
import java.util.concurrent.Callable;
40+
import java.util.concurrent.ConcurrentHashMap;
3741
import java.util.concurrent.TimeUnit;
42+
import java.util.concurrent.locks.ReentrantReadWriteLock;
3843

3944
/**
4045
* A simple metadata store definition that uses INameValueStorage to persist, read,
@@ -59,6 +64,12 @@ public abstract class NameValueStorageFileManagerSimpleCacheImpl<T> implements I
5964
private final boolean mForceReinsertionOfDuplicates;
6065
private final Gson mGson = new Gson();
6166

67+
// Lock per storage name to prevent corruption when multiple instances access the same storage file
68+
private static final Map<String, ReentrantReadWriteLock> STORAGE_LOCKS = new ConcurrentHashMap<>();
69+
private final ReentrantReadWriteLock metadataCacheLock;
70+
71+
private final boolean useLocks;
72+
6273
/**
6374
* Constructs a new NameValueStorageFileManagerSimpleCacheImpl. Convenience class for persisting
6475
* lists of arbitrarily-typed data. Duplicate reinsertion is disabled (backcompat) by default.
@@ -93,6 +104,9 @@ public NameValueStorageFileManagerSimpleCacheImpl(@NonNull final IPlatformCompon
93104
mStorage = components.getStorageSupplier().getUnencryptedNameValueStore(name, String.class);
94105
mKeySingleEntry = singleKey;
95106
mForceReinsertionOfDuplicates = forceReinsertionOfDuplicates;
107+
// Get or create a lock for this specific storage file
108+
metadataCacheLock = STORAGE_LOCKS.computeIfAbsent(name, k -> new ReentrantReadWriteLock());
109+
useLocks = CommonFlightsManager.INSTANCE.getFlightsProvider().isFlightEnabled(CommonFlight.USE_LOCKS_IN_NAME_VALUE_STORAGE);
96110
}
97111

98112
private interface NamedRunnable<V> extends Callable<V> {
@@ -121,6 +135,9 @@ private <V> V execWithTiming(@NonNull final NamedRunnable<V> runnable) {
121135

122136
@Override
123137
public boolean insert(final T t) {
138+
if (useLocks) {
139+
return insertWithLocks(t);
140+
}
124141
return execWithTiming(new NamedRunnable<Boolean>() {
125142
@Override
126143
public String getName() {
@@ -129,7 +146,7 @@ public String getName() {
129146

130147
@Override
131148
public Boolean call() {
132-
final Set<T> allMetadata = new HashSet<>(getAll());
149+
final Set<T> allMetadata = new HashSet<>(getAllInternal());
133150

134151
if (mForceReinsertionOfDuplicates) {
135152
// This is a bit of workaround for Set's default behavior
@@ -147,27 +164,90 @@ public Boolean call() {
147164
});
148165
}
149166

167+
private boolean insertWithLocks(final T t) {
168+
metadataCacheLock.writeLock().lock();
169+
try {
170+
return execWithTiming(new NamedRunnable<Boolean>() {
171+
@Override
172+
public String getName() {
173+
return "insert";
174+
}
175+
176+
@Override
177+
public Boolean call() {
178+
final Set<T> allMetadata = new HashSet<>(getAllInternal());
179+
180+
if (mForceReinsertionOfDuplicates) {
181+
// This is a bit of workaround for Set's default behavior
182+
// where items already within the Set are not replaced if they are
183+
// inserted but already exist.
184+
// This makes it behave more like a Map.
185+
allMetadata.remove(t);
186+
}
187+
188+
allMetadata.add(t);
189+
final String json = mGson.toJson(allMetadata);
190+
mStorage.put(mKeySingleEntry, json);
191+
return true;
192+
}
193+
});
194+
} finally {
195+
metadataCacheLock.writeLock().unlock();
196+
}
197+
}
198+
150199
@Override
151200
public boolean remove(final T t) {
152-
return execWithTiming(new NamedRunnable<Boolean>() {
153-
@Override
154-
public String getName() {
155-
return "remove";
156-
}
201+
metadataCacheLock.writeLock().lock();
202+
try {
203+
return execWithTiming(new NamedRunnable<Boolean>() {
204+
@Override
205+
public String getName() {
206+
return "remove";
207+
}
208+
209+
@Override
210+
public Boolean call() {
211+
final Set<T> allMetadata = new HashSet<>(getAllInternal());
212+
allMetadata.remove(t);
213+
final String json = mGson.toJson(allMetadata);
214+
mStorage.put(mKeySingleEntry, json);
215+
return true;
216+
}
217+
});
218+
} finally {
219+
metadataCacheLock.writeLock().unlock();
220+
}
157221

158-
@Override
159-
public Boolean call() {
160-
final Set<T> allMetadata = new HashSet<>(getAll());
161-
allMetadata.remove(t);
162-
final String json = mGson.toJson(allMetadata);
163-
mStorage.put(mKeySingleEntry, json);
164-
return true;
165-
}
166-
});
167222
}
168223

169224
@Override
170225
public List<T> getAll() {
226+
metadataCacheLock.readLock().lock();
227+
try {
228+
return getAllInternal();
229+
} finally {
230+
metadataCacheLock.readLock().unlock();
231+
}
232+
}
233+
234+
private List<T> getAllWithLocks() {
235+
metadataCacheLock.readLock().lock();
236+
try {
237+
return getAllInternal();
238+
} finally {
239+
metadataCacheLock.readLock().unlock();
240+
}
241+
}
242+
243+
/**
244+
* Internal helper method to retrieve all items from storage without acquiring a lock.
245+
* This method should only be called when a lock (read or write) is already held by the caller.
246+
* For external access, use {@link #getAll()} which properly acquires a read lock.
247+
*
248+
* @return List of all items in the cache
249+
*/
250+
private List<T> getAllInternal() {
171251
return execWithTiming(new NamedRunnable<List<T>>() {
172252
@Override
173253
public String getName() {
@@ -191,6 +271,9 @@ public List<T> call() {
191271

192272
@Override
193273
public boolean clear() {
274+
if (useLocks) {
275+
return clearWithLocks();
276+
}
194277
return execWithTiming(new NamedRunnable<Boolean>() {
195278
@Override
196279
public String getName() {
@@ -204,4 +287,24 @@ public Boolean call() {
204287
}
205288
});
206289
}
290+
291+
private boolean clearWithLocks() {
292+
metadataCacheLock.writeLock().lock();
293+
try {
294+
return execWithTiming(new NamedRunnable<Boolean>() {
295+
@Override
296+
public String getName() {
297+
return "clear";
298+
}
299+
300+
@Override
301+
public Boolean call() {
302+
mStorage.clear();
303+
return true;
304+
}
305+
});
306+
} finally {
307+
metadataCacheLock.writeLock().unlock();
308+
}
309+
}
207310
}

common4j/src/main/com/microsoft/identity/common/java/flighting/CommonFlight.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,12 @@ public enum CommonFlight implements IFlightConfig {
206206
* Flight to re-enable validating signing certificate chain for broker validation
207207
* We want to disable the check by default but have the ability to bring it back just in case.
208208
*/
209-
RE_ENABLE_VALIDATE_SIGNING_CERT_CHAIN_BROKER_APPS("ReEnableValidateSigningCertChainBrokerApps", false);
209+
RE_ENABLE_VALIDATE_SIGNING_CERT_CHAIN_BROKER_APPS("ReEnableValidateSigningCertChainBrokerApps", false),
210+
211+
/**
212+
* Flight to enable the use of locks in name value storage to prevent concurrent access issues.
213+
*/
214+
USE_LOCKS_IN_NAME_VALUE_STORAGE("UseLocksInNameValueStorage", false);
210215

211216
private String key;
212217
private Object defaultValue;

0 commit comments

Comments
 (0)