Skip to content

Commit 66b1390

Browse files
author
Mohit
committed
Fix ABBA deadlock between AzureActiveDirectory and AzureActiveDirectoryAuthority
- Extract authority.getAuthorityURL() outside synchronized(sLock) in Authority.isKnownAuthority() to avoid lock-ordering inversion - Extract authority.getAuthorityURL() outside synchronized scope in AzureActiveDirectory.ensureCloudDiscoveryForAuthority(Authority) - Remove synchronized from read-only ConcurrentHashMap methods in AzureActiveDirectory (hasCloudHost, getAzureActiveDirectoryCloud, getAzureActiveDirectoryCloudFromHostName, isValidCloudHost) - Remove synchronized from AzureActiveDirectoryAuthority.getAzureActiveDirectoryCloud() - Add null check for authorityUrl in isKnownAuthority() - Improve error handling in getKnownAuthorityResult(): propagate original discovery exception instead of replacing with UNKNOWN_AUTHORITY - Make KnownAuthorityResult fields final - Add 5 concurrency regression tests in AzureActiveDirectoryTest - Expand AuthorityKnownAuthorityTest with sovereign cloud, error propagation, and mock-based getKnownAuthorityResult tests AB#3578299
1 parent 1df7e03 commit 66b1390

5 files changed

Lines changed: 511 additions & 68 deletions

File tree

common4j/src/main/com/microsoft/identity/common/java/authorities/Authority.java

Lines changed: 39 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -318,40 +318,42 @@ public static void addKnownAuthorities(List<Authority> authorities) {
318318
}
319319

320320
/**
321-
* Authorities are either known by the developer and communicated to the library via configuration or they
322-
* are known to Microsoft based on the list of clouds returned from:
321+
* Authorities are either known by the developer and communicated to the library via
322+
* configuration, or they are known to Microsoft based on the cloud discovery metadata cache.
323323
*
324324
* @param authority Authority to check against.
325325
* @return True if the authority is known to Microsoft or defined in the configuration.
326326
*/
327327
public static boolean isKnownAuthority(Authority authority) {
328-
final String methodName = ":isKnownAuthority";
328+
final String methodTag = TAG + ":isKnownAuthority";
329329
boolean knownToDeveloper = false;
330330
boolean knownToMicrosoft;
331331

332332
if (authority == null) {
333-
Logger.warn(
334-
TAG + methodName,
335-
"Authority is null"
336-
);
333+
Logger.warn(methodTag, "Authority is null");
334+
return false;
335+
}
336+
337+
// Resolve the authority URL outside any lock to avoid calling a potentially-
338+
// synchronized polymorphic method (e.g. AzureActiveDirectoryAuthority.getAuthorityUri())
339+
// while holding sLock, which could create a lock-ordering inversion.
340+
final URL authorityUrl = authority.getAuthorityURL();
341+
342+
if (authorityUrl == null) {
343+
Logger.warn(methodTag, "Authority URL is null");
337344
return false;
338345
}
339346

340-
if (BuildConfig.ALLOW_ONEBOX_AUTHORITIES && AzureActiveDirectoryEnvironment.ONEBOX_AUTHORITY.equals(authority.getAuthorityURL().getAuthority())) {
347+
if (BuildConfig.ALLOW_ONEBOX_AUTHORITIES && AzureActiveDirectoryEnvironment.ONEBOX_AUTHORITY.equals(authorityUrl.getAuthority())) {
341348
return true; // onebox authorities are always considered to be known.
342349
}
343350

344-
//Check if authority was added to configuration
345351
synchronized (sLock) {
346352
for (final Authority currentAuthority : knownAuthorities) {
347353
if (currentAuthority.mAuthorityUrlString != null &&
348-
authority.getAuthorityURL() != null &&
349-
authority.getAuthorityURL().getAuthority() != null &&
354+
authorityUrl.getAuthority() != null &&
350355
currentAuthority.mAuthorityUrlString.toLowerCase(Locale.ROOT).contains(
351-
authority
352-
.getAuthorityURL()
353-
.getAuthority()
354-
.toLowerCase(Locale.ROOT))) {
356+
authorityUrl.getAuthority().toLowerCase(Locale.ROOT))) {
355357
knownToDeveloper = true;
356358
break;
357359
}
@@ -360,19 +362,14 @@ public static boolean isKnownAuthority(Authority authority) {
360362

361363
// Check whether the authority is known to Microsoft or not. Microsoft can recognize authorities that exist within public clouds.
362364
// Microsoft does not maintain a list of B2C authorities or a list of ADFS or 3rd party authorities (issuers).
363-
knownToMicrosoft = AzureActiveDirectory.hasCloudHost(authority.getAuthorityURL());
365+
knownToMicrosoft = AzureActiveDirectory.hasCloudHost(authorityUrl);
364366

365367
final boolean isKnown = (knownToDeveloper || knownToMicrosoft);
366368

367-
Logger.verbose(
368-
TAG + methodName,
369-
"Authority is known to developer? [" + knownToDeveloper + "]"
370-
);
371-
372-
Logger.verbose(
373-
TAG + methodName,
374-
"Authority is known to Microsoft? [" + knownToMicrosoft + "]"
375-
);
369+
Logger.verbose(methodTag,
370+
"Authority is known to developer? [" + knownToDeveloper + "]");
371+
Logger.verbose(methodTag,
372+
"Authority is known to Microsoft? [" + knownToMicrosoft + "]");
376373

377374
return isKnown;
378375
}
@@ -383,37 +380,43 @@ public static KnownAuthorityResult getKnownAuthorityResult(Authority authority)
383380
methodTag,
384381
"Getting known authority result..."
385382
);
386-
ClientException clientException = null;
387-
boolean known = false;
388383

384+
ClientException discoveryException = null;
389385
try {
390386
AzureActiveDirectory.ensureCloudDiscoveryForAuthority(authority);
391387
Logger.info(methodTag, "Cloud discovery complete.");
392388
} catch (final ClientException ex) {
393389
// Cloud discovery failed (e.g. network error).
394390
// Log but continue — the authority may still be known via hardcoded
395391
// metadata or developer configuration.
392+
discoveryException = ex;
396393
Logger.warn(methodTag,
397394
"Cloud discovery failed, will check hardcoded/configured authorities. Error: "
398395
+ ex.getErrorCode());
399396
}
400397

401-
if (!isKnownAuthority(authority)) {
402-
clientException = new ClientException(
398+
if (isKnownAuthority(authority)) {
399+
Logger.info(methodTag, "Authority is known.");
400+
return new KnownAuthorityResult(true, null);
401+
}
402+
403+
// Authority is not known via any source.
404+
if (discoveryException != null) {
405+
// Discovery failed — propagate the original error (keeping the original behavior that was before PR#3027)
406+
return new KnownAuthorityResult(false, discoveryException);
407+
} else {
408+
// Discovery succeeded but authority is not in any known list.
409+
final ClientException clientException = new ClientException(
403410
ClientException.UNKNOWN_AUTHORITY,
404411
"Provided authority is not known. MSAL will only make requests to known authorities"
405412
);
406-
} else {
407-
Logger.info(methodTag, "Cloud is known.");
408-
known = true;
413+
return new KnownAuthorityResult(false, clientException);
409414
}
410-
411-
return new KnownAuthorityResult(known, clientException);
412415
}
413416

414417
public static class KnownAuthorityResult {
415-
private boolean mKnown;
416-
private ClientException mClientException;
418+
private final boolean mKnown;
419+
private final ClientException mClientException;
417420

418421
KnownAuthorityResult(boolean known, ClientException exception) {
419422
mKnown = known;

common4j/src/main/com/microsoft/identity/common/java/authorities/AzureActiveDirectoryAuthority.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ public class AzureActiveDirectoryAuthority extends Authority {
7171

7272
/** Gets {@link AzureActiveDirectoryCloud}, if the cloud metadata is already initialized. */
7373
@Nullable
74-
private static synchronized AzureActiveDirectoryCloud getAzureActiveDirectoryCloud(
74+
private static AzureActiveDirectoryCloud getAzureActiveDirectoryCloud(
7575
@NonNull final AzureActiveDirectoryAudience audience) {
7676
final String methodName = ":getAzureActiveDirectoryCloud";
7777

common4j/src/main/com/microsoft/identity/common/java/providers/microsoft/azureactivedirectory/AzureActiveDirectory.java

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -164,11 +164,11 @@ public AzureActiveDirectoryOAuth2Strategy createOAuth2Strategy(@NonNull final Az
164164
return new AzureActiveDirectoryOAuth2Strategy(config, parameters);
165165
}
166166

167-
public static synchronized boolean hasCloudHost(@NonNull final URL authorityUrl) {
167+
public static boolean hasCloudHost(@NonNull final URL authorityUrl) {
168168
return sAadClouds.containsKey(authorityUrl.getHost().toLowerCase(Locale.US));
169169
}
170170

171-
public static synchronized boolean isValidCloudHost(@NonNull final URL authorityUrl) {
171+
public static boolean isValidCloudHost(@NonNull final URL authorityUrl) {
172172
return hasCloudHost(authorityUrl) && getAzureActiveDirectoryCloud(authorityUrl).isValidated();
173173
}
174174

@@ -191,15 +191,15 @@ public static synchronized Environment getEnvironment() {
191191
* @param authorityUrl URL
192192
* @return AzureActiveDirectoryCloud
193193
*/
194-
public static synchronized AzureActiveDirectoryCloud getAzureActiveDirectoryCloud(@NonNull final URL authorityUrl) {
194+
public static AzureActiveDirectoryCloud getAzureActiveDirectoryCloud(@NonNull final URL authorityUrl) {
195195
return sAadClouds.get(authorityUrl.getHost().toLowerCase(Locale.US));
196196
}
197197

198198
/**
199199
* @param preferredCacheHostName String
200200
* @return AzureActiveDirectoryCloud
201201
*/
202-
public static synchronized AzureActiveDirectoryCloud getAzureActiveDirectoryCloudFromHostName(@NonNull final String preferredCacheHostName) {
202+
public static AzureActiveDirectoryCloud getAzureActiveDirectoryCloudFromHostName(@NonNull final String preferredCacheHostName) {
203203
return sAadClouds.get(preferredCacheHostName.toLowerCase(Locale.US));
204204
}
205205

@@ -368,11 +368,16 @@ public static synchronized void ensureCloudDiscovery() throws ClientException {
368368
*
369369
* @param authority The authority whose URL determines the discovery endpoint, or null to use the default.
370370
*/
371-
public static synchronized void ensureCloudDiscoveryForAuthority(@Nullable final Authority authority)
371+
public static void ensureCloudDiscoveryForAuthority(@Nullable final Authority authority)
372372
throws ClientException {
373-
ensureCloudDiscoveryForAuthority(
374-
authority != null ? authority.getAuthorityURL() : null
375-
);
373+
// Resolve the URL outside the synchronized scope to avoid deadlock:
374+
// getAuthorityURL() on AzureActiveDirectoryAuthority acquires
375+
// AzureActiveDirectoryAuthority.class then AzureActiveDirectory.class,
376+
// while ensureCloudDiscoveryForAuthority(URL) acquires AzureActiveDirectory.class.
377+
// Calling getAuthorityURL() while holding AzureActiveDirectory.class would
378+
// create a lock-ordering inversion with any concurrent getAuthorityURL() caller.
379+
final URL authorityUrl = authority != null ? authority.getAuthorityURL() : null;
380+
ensureCloudDiscoveryForAuthority(authorityUrl);
376381
}
377382

378383
/**

0 commit comments

Comments
 (0)