From 858895c4a964fb3be0d0084023ba446200b39cff Mon Sep 17 00:00:00 2001 From: Jayesh Shah Date: Sun, 7 Jun 2026 08:21:20 +0530 Subject: [PATCH] Fix CTS disposal in ProcessFetchInBackground that breaks cancellation and causes SemaphoreSlim convoy The lambda passed to ProcessFetchInBackground uses 'using var' to create a linked CancellationTokenSource, but returns the Task without awaiting it. This causes the linked CTS to be disposed before the async operation completes, breaking the link to the parent cancellation token. As a result, WaitAsync on the static SemaphoreSlim(1,1) becomes permanently unkillable. When the token endpoint (IMDS) is temporarily unreachable, every proactive refresh background task becomes a permanent semaphore waiter, forming an unbounded convoy. Foreground threads that later need a token are blocked behind the convoy for hours. Fix: make the lambda async and await the inner call, so 'using var' disposal waits for the async operation to complete and parent cancellation propagates correctly through the linked token. Fixes all 4 affected locations introduced by PR #4471: - ClientCredentialRequest.cs - ManagedIdentityAuthRequest.cs - OnBehalfOfRequest.cs - CacheSilentStrategy.cs --- .../Internal/Requests/ClientCredentialRequest.cs | 7 +++++-- .../Internal/Requests/ManagedIdentityAuthRequest.cs | 7 +++++-- .../Internal/Requests/OnBehalfOfRequest.cs | 7 +++++-- .../Internal/Requests/Silent/CacheSilentStrategy.cs | 7 +++++-- 4 files changed, 20 insertions(+), 8 deletions(-) diff --git a/src/client/Microsoft.Identity.Client/Internal/Requests/ClientCredentialRequest.cs b/src/client/Microsoft.Identity.Client/Internal/Requests/ClientCredentialRequest.cs index 97205a9d67..8be4efe1d8 100644 --- a/src/client/Microsoft.Identity.Client/Internal/Requests/ClientCredentialRequest.cs +++ b/src/client/Microsoft.Identity.Client/Internal/Requests/ClientCredentialRequest.cs @@ -96,11 +96,14 @@ protected override async Task ExecuteAsync(CancellationTok SilentRequestHelper.ProcessFetchInBackground( cachedAccessTokenItem, - () => + async () => { // Use a linked token source, in case the original cancellation token source is disposed before this background task completes. + // IMPORTANT: The lambda must be async and await the inner call. Without async/await, `using var` disposes the linked CTS + // before the async operation completes, breaking cancellation propagation and causing unbounded SemaphoreSlim convoy. + // See https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/issues/6053 using var tokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - return GetAccessTokenAsync(tokenSource.Token, logger); + return await GetAccessTokenAsync(tokenSource.Token, logger).ConfigureAwait(false); }, logger, ServiceBundle, AuthenticationRequestParameters.RequestContext.ApiEvent, AuthenticationRequestParameters.RequestContext.ApiEvent.CallerSdkApiId, AuthenticationRequestParameters.RequestContext.ApiEvent.CallerSdkVersion); diff --git a/src/client/Microsoft.Identity.Client/Internal/Requests/ManagedIdentityAuthRequest.cs b/src/client/Microsoft.Identity.Client/Internal/Requests/ManagedIdentityAuthRequest.cs index eec0728a53..9fce54ffd4 100644 --- a/src/client/Microsoft.Identity.Client/Internal/Requests/ManagedIdentityAuthRequest.cs +++ b/src/client/Microsoft.Identity.Client/Internal/Requests/ManagedIdentityAuthRequest.cs @@ -123,11 +123,14 @@ protected override async Task ExecuteAsync(CancellationTok SilentRequestHelper.ProcessFetchInBackground( cachedAccessTokenItem, - () => + async () => { // Use a linked token source, in case the original cancellation token source is disposed before this background task completes. + // IMPORTANT: The lambda must be async and await the inner call. Without async/await, `using var` disposes the linked CTS + // before the async operation completes, breaking cancellation propagation and causing unbounded SemaphoreSlim convoy. + // See https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/issues/6053 using var tokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - return GetAccessTokenAsync(tokenSource.Token, logger); + return await GetAccessTokenAsync(tokenSource.Token, logger).ConfigureAwait(false); }, logger, ServiceBundle, AuthenticationRequestParameters.RequestContext.ApiEvent, AuthenticationRequestParameters.RequestContext.ApiEvent.CallerSdkApiId, AuthenticationRequestParameters.RequestContext.ApiEvent.CallerSdkVersion); diff --git a/src/client/Microsoft.Identity.Client/Internal/Requests/OnBehalfOfRequest.cs b/src/client/Microsoft.Identity.Client/Internal/Requests/OnBehalfOfRequest.cs index 109a27458b..3d37501e82 100644 --- a/src/client/Microsoft.Identity.Client/Internal/Requests/OnBehalfOfRequest.cs +++ b/src/client/Microsoft.Identity.Client/Internal/Requests/OnBehalfOfRequest.cs @@ -155,11 +155,14 @@ protected override async Task ExecuteAsync(CancellationTok SilentRequestHelper.ProcessFetchInBackground( cachedAccessToken, - () => + async () => { // Use a linked token source, in case the original cancellation token source is disposed before this background task completes. + // IMPORTANT: The lambda must be async and await the inner call. Without async/await, `using var` disposes the linked CTS + // before the async operation completes, breaking cancellation propagation and causing unbounded SemaphoreSlim convoy. + // See https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/issues/6053 using var tokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - return RefreshRtOrFetchNewAccessTokenAsync(tokenSource.Token); + return await RefreshRtOrFetchNewAccessTokenAsync(tokenSource.Token).ConfigureAwait(false); }, logger, ServiceBundle, AuthenticationRequestParameters.RequestContext.ApiEvent, AuthenticationRequestParameters.RequestContext.ApiEvent.CallerSdkApiId, AuthenticationRequestParameters.RequestContext.ApiEvent.CallerSdkVersion); diff --git a/src/client/Microsoft.Identity.Client/Internal/Requests/Silent/CacheSilentStrategy.cs b/src/client/Microsoft.Identity.Client/Internal/Requests/Silent/CacheSilentStrategy.cs index fbc46acf24..9e61b7a21d 100644 --- a/src/client/Microsoft.Identity.Client/Internal/Requests/Silent/CacheSilentStrategy.cs +++ b/src/client/Microsoft.Identity.Client/Internal/Requests/Silent/CacheSilentStrategy.cs @@ -106,11 +106,14 @@ public async Task ExecuteAsync(CancellationToken cancellat SilentRequestHelper.ProcessFetchInBackground( cachedAccessTokenItem, - () => + async () => { // Use a linked token source, in case the original cancellation token source is disposed before this background task completes. + // IMPORTANT: The lambda must be async and await the inner call. Without async/await, `using var` disposes the linked CTS + // before the async operation completes, breaking cancellation propagation and causing unbounded SemaphoreSlim convoy. + // See https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/issues/6053 using var tokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - return RefreshRtOrFailAsync(tokenSource.Token); + return await RefreshRtOrFailAsync(tokenSource.Token).ConfigureAwait(false); }, logger, ServiceBundle, AuthenticationRequestParameters.RequestContext.ApiEvent, AuthenticationRequestParameters.RequestContext.ApiEvent.CallerSdkApiId, AuthenticationRequestParameters.RequestContext.ApiEvent.CallerSdkVersion);