Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

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.

Assessment

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

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.

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.

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

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.

More from sshnet/SSH.NET

All issues in sshnet/SSH.NET

Similar issues

More C# issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.