Skip to content

Exception during ExecuteAsync throws Exception that cannot be handled #1832

Description

@thomasneubergersdx

SshCommand.ExecuteAsync appears to have a race during command startup.

In the current implementation, _tcs is created before _channel.Open() and _channel.SendExecRequest(...) are called:

_tcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
_userToken = cancellationToken;

_channel.DataReceived += Channel_DataReceived;
_channel.ExtendedDataReceived += Channel_ExtendedDataReceived;
_channel.RequestReceived += Channel_RequestReceived;
_channel.Closed += Channel_Closed;
_channel.Open();

_ = _channel.SendExecRequest(CommandText);

return _tcs.Task;

If _channel.Open() or _channel.SendExecRequest(...) throws synchronously due to a connection/session teardown, the method exits before returning _tcs.Task.
If the caller uses the command in a using block, disposal may then execute while _tcs.Task is still incomplete:

if (_tcs is { Task.IsCompleted: false } tcs)
{
    _ = tcs.TrySetException(new ObjectDisposedException(GetType().FullName));
}

This can produce a faulted task that was never returned to the caller and therefore can never be awaited or observed. Later, that can surface via TaskScheduler.UnobservedTaskException.

Expected behavior

If command startup fails after _tcs has been created, the failure should not leave behind an orphaned faulted task.
Possible acceptable outcomes:

  • ExecuteAsync returns a faulted task, or
  • ExecuteAsync catches startup exceptions after _tcs creation, completes _tcs accordingly, and returns it, or
  • startup is arranged so no internal task exists before all synchronous failure points have passed.

Actual behavior

Under transient network/session failure, a caller can observe:

  • a synchronous exception from ExecuteAsync, and later
  • an unrelated TaskScheduler.UnobservedTaskException caused by the internal _tcs.Task being faulted during Dispose() without ever having been returned.

Observed trigger

In our case this happened around network loss / socket failure. We saw a preceding socket error similar to:

System.Net.Sockets.SocketException: A socket operation was attempted to an unreachable network

That seems consistent with the session being torn down while a command is being started.

Suggested fix direction

Wrap the startup section after _tcs creation in a try/catch, and ensure that any exception either:

  • completes _tcs and returns _tcs.Task, or
  • resets internal state so no hidden faulted task remains.

A minimal fix would likely be around the section between _tcs = new TaskCompletionSource(...) and return _tcs.Task.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions