Skip to content

Handle COM disconnect errors when cancelling WinRT async operations (3.0) - #2400

Merged
Sergio0694 merged 2 commits into
staging/3.0from
fix/async-cancel-rpc-disconnect
Apr 21, 2026
Merged

Handle COM disconnect errors when cancelling WinRT async operations (3.0)#2400
Sergio0694 merged 2 commits into
staging/3.0from
fix/async-cancel-rpc-disconnect

Conversation

@Sergio0694

Copy link
Copy Markdown
Member

Summary

Prevent app crashes when an IAsyncInfo.Cancel() call from the cancellation callback in AsyncInfoTaskCompletionSource fails because the backing COM server has been disconnected.

Motivation

When awaiting a WinRT async API such as:

await SomeWinRTAPIAsync().AsTask(token);

if the underlying object lives in a COM server that has died, cancelling token would invoke IAsyncInfo.Cancel() from inside a CancellationToken callback. That call would then fail with RPC_E_DISCONNECTED (or related HRESULTs). The exception would propagate out of the cancellation callback and be re-thrown by CancellationTokenSource.Cancel() on whichever thread invoked it (often a thread pool thread), tearing down the entire process.

The pre-existing try/catch around the cancellation registration only protected the rare path where the token was already cancelled at registration time, since CancellationToken.Register(...) only invokes the callback synchronously in that case. In the typical flow, where cancellation happens later, nothing was guarding the Cancel() call.

Changes

  • src/WinRT.Runtime2/InteropServices/AsyncInfo/TaskCompletionSources/AsyncInfoTaskCompletionSource{TProgress}.cs: collapsed the two cancellation registrations (one to mark the task cancelled, one to call IAsyncInfo.Cancel()) into a single OnCancellation callback. The callback first calls TrySetCanceled(_cancellationToken) to preserve the original ordering (so Canceled wins as the task state), then calls IAsyncInfo.Cancel() inside a try/catch that 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 generic TResult variant.

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 Sergio0694 added bug Something isn't working CsWinRT 3.0 labels Apr 20, 2026
@Sergio0694
Sergio0694 requested a review from manodasanW April 20, 2026 20:36
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.
@Sergio0694
Sergio0694 enabled auto-merge (squash) April 21, 2026 20:45
@Sergio0694
Sergio0694 merged commit 93f6cbe into staging/3.0 Apr 21, 2026
11 checks passed
@Sergio0694
Sergio0694 deleted the fix/async-cancel-rpc-disconnect branch April 21, 2026 21:04
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working CsWinRT 3.0

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants