Handle COM disconnect errors when cancelling WinRT async operations (3.0) - #2400
Merged
Conversation
When awaiting a WinRT async API via 'AsTask(token)', cancellation of the token would invoke 'IAsyncInfo.Cancel()' from inside a CancellationToken callback. If the backing COM server had been disconnected (e.g. RPC_E_DISCONNECTED), the exception would propagate out of the callback and be re-thrown by 'CancellationTokenSource.Cancel()' on whichever thread invoked it, often a thread pool thread, crashing the process. The previous try/catch around the registration only caught the rare case where the token was already canceled at registration time. Combine the two cancellation registrations into one callback that calls 'IAsyncInfo.Cancel()' inside try/catch. On failure the task is faulted with the underlying exception so the awaiter observes (and can handle) the RPC error; on success the task is marked canceled as before. Co-Authored-By: Copilot <223556219+Copilot@users.noreply.github.com>
Sergio0694
commented
Apr 20, 2026
Narrow the catch in AsyncInfoTaskCompletionSource(C)ancel to only swallow known COMExceptions with specific HRESULTs (RPC_E_DISCONNECTED, RPC_S_SERVER_UNAVAILABLE, JSCRIPT_E_CANTEXECUTE) so infrastructure disconnects don't crash while not hiding other bugs. Add System.Runtime.InteropServices using and suppress IDE0055 warning. Include an explanatory comment to keep the handled HRESULT set in sync with ExceptionDispatchInfoExtensions.ThrowAsync.
manodasanW
approved these changes
Apr 21, 2026
Sergio0694
enabled auto-merge (squash)
April 21, 2026 20:45
Sergio0694
added a commit
that referenced
this pull request
Jul 31, 2026
The previous test asserted that the process survives a 'Completed' handler failing with 'E_FAIL', which is the opposite of the intended behavior: any 'HRESULT' outside the well known disconnect allow-list is rethrown on the thread pool by 'ExceptionDispatchInfoExtensions.ThrowAsync', tearing down the process. That made the test fail by killing the whole unit test run. Cover the behavior that was actually implemented instead (see #1969 and #2399/#2400), ie. that failures from a completion handler indicating the peer process is gone are swallowed. The test component now takes the 'HRESULT' to fail with, and exposes a flag set right before the handler throws, so the test can wait for the handler deterministically. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0ad9ec63-3e5f-4e9a-be7f-e1769e060136
Sergio0694
added a commit
that referenced
this pull request
Jul 31, 2026
The previous test asserted that the process survives a 'Completed' handler failing with 'E_FAIL', which is the opposite of the intended behavior: any 'HRESULT' outside the well known disconnect allow-list is rethrown on the thread pool by 'ExceptionDispatchInfoExtensions.ThrowAsync', tearing down the process. That made the test fail by killing the whole unit test run. Cover the behavior that was actually implemented instead (see #1969 and #2399/#2400), ie. that failures from a completion handler indicating the peer process is gone are swallowed. The test component now takes the 'HRESULT' to fail with, and exposes a flag set right before the handler throws, so the test can wait for the handler deterministically. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0ad9ec63-3e5f-4e9a-be7f-e1769e060136
Sergio0694
added a commit
that referenced
this pull request
Jul 31, 2026
The previous test asserted that the process survives a 'Completed' handler failing with 'E_FAIL', which is the opposite of the intended behavior: any 'HRESULT' outside the well known disconnect allow-list is rethrown on the thread pool by 'ExceptionDispatchInfoExtensions.ThrowAsync', tearing down the process. That made the test fail by killing the whole unit test run. Cover the behavior that was actually implemented instead (see #1969 and #2399/#2400), ie. that failures from a completion handler indicating the peer process is gone are swallowed. The test component now takes the 'HRESULT' to fail with, and exposes a flag set right before the handler throws, so the test can wait for the handler deterministically. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0ad9ec63-3e5f-4e9a-be7f-e1769e060136
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Prevent app crashes when an
IAsyncInfo.Cancel()call from the cancellation callback inAsyncInfoTaskCompletionSourcefails because the backing COM server has been disconnected.Motivation
When awaiting a WinRT async API such as:
if the underlying object lives in a COM server that has died, cancelling
tokenwould invokeIAsyncInfo.Cancel()from inside aCancellationTokencallback. That call would then fail withRPC_E_DISCONNECTED(or related HRESULTs). The exception would propagate out of the cancellation callback and be re-thrown byCancellationTokenSource.Cancel()on whichever thread invoked it (often a thread pool thread), tearing down the entire process.The pre-existing
try/catcharound the cancellation registration only protected the rare path where the token was already cancelled at registration time, sinceCancellationToken.Register(...)only invokes the callback synchronously in that case. In the typical flow, where cancellation happens later, nothing was guarding theCancel()call.Changes
src/WinRT.Runtime2/InteropServices/AsyncInfo/TaskCompletionSources/AsyncInfoTaskCompletionSource{TProgress}.cs: collapsed the two cancellation registrations (one to mark the task cancelled, one to callIAsyncInfo.Cancel()) into a singleOnCancellationcallback. The callback first callsTrySetCanceled(_cancellationToken)to preserve the original ordering (soCanceledwins as the task state), then callsIAsyncInfo.Cancel()inside atry/catchthat swallows any exception. This ensures no exception can escape the callback and crash the process.src/WinRT.Runtime2/InteropServices/AsyncInfo/TaskCompletionSources/AsyncInfoTaskCompletionSource{TResult, TProgress}.cs: same treatment for the genericTResultvariant.