sshnet / sshnet/SSH.NET

Exception during ExecuteAsync throws Exception that cannot be handled

Open
#1,832 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
C#
Stars
4.4k
Forks
993
Avg merge
9d 21h
Merged PRs (30d)
1

Description

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.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start at SshCommand.ExecuteAsync and trace the startup sequence from _tcs creation through _channel.Open() and SendExecRequest(...), then inspect how Dispose() handles an incomplete task. Verify behavior during a startup exception and ensure the failure is returned or no hidden faulted task remains.

Written by the indexing model from the issue text.

Assessment

Tech stack
csharp
Domain
networking
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.