Hello, I found this race condition while investigating an unrelated matter.
Method SshSession.OpenChannelWithInitialRequestAsync computes a local variable isExtensionSupported based on whether Config.ProtocolExtensions and this.ProtocolExtensions both contain the extension identifier "open-channel-request@microsoft.com".
The computed property this.ProtocolExtensions returns this.Protocol.Extensions, which is:
- Initialized from
Config in method SshSession.ConnectAsync.
- When the SSH_MSG_EXT_INFO is received from the server, method
SshSession.HandleMessageAsync(ExtensionInfoMessage, CancellationToken) assigns a new, empty dictionary to this.Protocol.Extensions, then uses this.Protocol.Extensions.Add() to fill it with the extensions received from the server.
The sequence of events that leads to the race is the following:
- Calling
SshClient.OpenSessionAsync invokes SshSession.ConnectAsync, which starts the background message-processing loop in SshSession.ProcessMessages and returns immediately.
- Calling
SshClientSession.AuthenticateClientAsync sends the authentication messages, and also returns immediately without waiting for a response.
- Caller does not wait for the
TaskCompletionSource passed to SshClientSession.AuthenticateClientAsync to complete (a scenario recommended by the documentation of that function, for performance purposes).
- Calling
SshClientSession.OpenChannelAsync reads from this.ProtocolExtensions. At this point,:
- If the background message-processing loop has already processed the SSH_MSG_EXT_INFO message, the program will behave properly.
- If the background message-processing loop has not yet processed the SSH_MSG_EXT_INFO message, the program will instead see the extensions from the configuration of the session, which will lead it to assume that
"open-channel-request@microsoft.com" is supported by the server, even if it isn't.
- If the background message-processing loop is currently processing SSH_MSG_EXT_INFO and has already assigned an empty dictionary to
this.Protocol.Extensions but has not yet filled it, the program will assume that "open-channel-request@microsoft.com" is not supported by the server even if it actually is.
- If the background message-processing loop is currently processing SSH_MSG_EXT_INFO, and is in the process of executing
this.Protocol.Extensions.Add, then a System.Collections.Dictionary<,> is being both read and written to at the same time by two different threads, which may lead to misbehavior (although given the current implementation, this would require the server to send enough extensions to cause problems).
Waiting for the for the TaskCompletionSource passed to SshClientSession.AuthenticateClientAsync to complete eliminates the race condition.
Hello, I found this race condition while investigating an unrelated matter.
Method
SshSession.OpenChannelWithInitialRequestAsynccomputes a local variableisExtensionSupportedbased on whetherConfig.ProtocolExtensionsandthis.ProtocolExtensionsboth contain the extension identifier"open-channel-request@microsoft.com".The computed property
this.ProtocolExtensionsreturnsthis.Protocol.Extensions, which is:Configin methodSshSession.ConnectAsync.SshSession.HandleMessageAsync(ExtensionInfoMessage, CancellationToken)assigns a new, empty dictionary tothis.Protocol.Extensions, then usesthis.Protocol.Extensions.Add()to fill it with the extensions received from the server.The sequence of events that leads to the race is the following:
SshClient.OpenSessionAsyncinvokesSshSession.ConnectAsync, which starts the background message-processing loop inSshSession.ProcessMessagesand returns immediately.SshClientSession.AuthenticateClientAsyncsends the authentication messages, and also returns immediately without waiting for a response.TaskCompletionSourcepassed toSshClientSession.AuthenticateClientAsyncto complete (a scenario recommended by the documentation of that function, for performance purposes).SshClientSession.OpenChannelAsyncreads fromthis.ProtocolExtensions. At this point,:"open-channel-request@microsoft.com"is supported by the server, even if it isn't.this.Protocol.Extensionsbut has not yet filled it, the program will assume that"open-channel-request@microsoft.com"is not supported by the server even if it actually is.this.Protocol.Extensions.Add, then aSystem.Collections.Dictionary<,>is being both read and written to at the same time by two different threads, which may lead to misbehavior (although given the current implementation, this would require the server to send enough extensions to cause problems).Waiting for the for the
TaskCompletionSourcepassed toSshClientSession.AuthenticateClientAsyncto complete eliminates the race condition.