From 657a74427228fff373d17aaf90b8b49dcc9455a3 Mon Sep 17 00:00:00 2001 From: ferociousbyte <10356831+ferociousbyte@users.noreply.github.com> Date: Sat, 24 Oct 2026 10:59:20 +0200 Subject: [PATCH] Fixed double return of TLS handshake timeout source in TcpServer After the TLS handshake the timeout scope was returned to the multiplexer's pool but not reset, so a connection that ended before its first message returned it again in HandleConnection's finally. If the pooled source had been disposed meanwhile (e.g. canceled by another connection's handshake timeout, or on shutdown), TryReset threw ObjectDisposedException from the async void method and the process terminated; otherwise the same source could be handed to two connections. Co-Authored-By: Claude Opus 5.5 --- .../Tcp/TcpTransportTests.cs | 44 +++++++++++++++++++ .../NetworkTransport/TransportTestSuite.cs | 2 + .../ConnectionOriented/Tcp/TcpServer.cs | 2 + 3 files changed, 48 insertions(+) diff --git a/src/DotNext.Tests/Net/Cluster/Consensus/Raft/NetworkTransport/ConnectionOriented/Tcp/TcpTransportTests.cs b/src/DotNext.Tests/Net/Cluster/Consensus/Raft/NetworkTransport/ConnectionOriented/Tcp/TcpTransportTests.cs index 615a5daf88..1b2c9663e6 100644 --- a/src/DotNext.Tests/Net/Cluster/Consensus/Raft/NetworkTransport/ConnectionOriented/Tcp/TcpTransportTests.cs +++ b/src/DotNext.Tests/Net/Cluster/Consensus/Raft/NetworkTransport/ConnectionOriented/Tcp/TcpTransportTests.cs @@ -71,6 +71,50 @@ public Task RequestResponse(bool useSsl) return RequestResponseTest(CreateServer, CreateClient); } + [Fact] + public async Task ConnectionClosedAfterTlsHandshake() + { + // backlog 1: the timeout sources come from a pool that keeps one + var serverAddr = new IPEndPoint(IPAddress.Loopback, 3790); + await using var server = new TcpServer(serverAddr, 1, CreateLocalMember(), NullLoggerFactory.Instance) + { + ReceiveTimeout = TimeSpan.FromMilliseconds(300), + MemoryAllocator = MemoryAllocator.Default, + TransmissionBlockSize = 65535, + GracefulShutdownTimeout = 2000, + SslOptions = CreateServerSslOptions() + }; + await server.StartAsync(TestToken); + + // A completes the TLS handshake and sends nothing: the handshake's timeout source goes back to the pool + using var a = new System.Net.Sockets.Socket(serverAddr.AddressFamily, System.Net.Sockets.SocketType.Stream, System.Net.Sockets.ProtocolType.Tcp); + await a.ConnectAsync(serverAddr, TestToken); + await using var tls = new SslStream(new System.Net.Sockets.NetworkStream(a, ownsSocket: false)); + await tls.AuthenticateAsClientAsync(CreateClientSslOptions(), TestToken); + + // B never starts its handshake: it takes the same source from the pool, times out, and the source is disposed + using var b = new System.Net.Sockets.Socket(serverAddr.AddressFamily, System.Net.Sockets.SocketType.Stream, System.Net.Sockets.ProtocolType.Tcp); + await b.ConnectAsync(serverAddr, TestToken); + Equal(0, await b.ReceiveAsync(new byte[1], TestToken)); // closed by the server after the timeout + + // A leaves before its first message: the connection must not return the handshake's source to the pool again + // (it threw ObjectDisposedException from async void HandleConnection, which ended the process) + a.Shutdown(System.Net.Sockets.SocketShutdown.Both); + a.Close(); + await Task.Delay(500, TestToken); + + // the server still answers + using var client = new TcpClient(CreateLocalMember(), serverAddr) + { + MemoryAllocator = MemoryAllocator.Default, + RequestTimeout = DefaultTimeout, + ConnectTimeout = DefaultTimeout, + TransmissionBlockSize = 65535, + SslOptions = CreateClientSslOptions(), + }; + True((await client.As().VoteAsync(42L, 1L, 56L, TestToken)).Value); + } + [Fact] public Task StressTest() { diff --git a/src/DotNext.Tests/Net/Cluster/Consensus/Raft/NetworkTransport/TransportTestSuite.cs b/src/DotNext.Tests/Net/Cluster/Consensus/Raft/NetworkTransport/TransportTestSuite.cs index 85542118b6..275a1ec8ca 100644 --- a/src/DotNext.Tests/Net/Cluster/Consensus/Raft/NetworkTransport/TransportTestSuite.cs +++ b/src/DotNext.Tests/Net/Cluster/Consensus/Raft/NetworkTransport/TransportTestSuite.cs @@ -168,6 +168,8 @@ ValueTask> ILocalMember.PreVoteAsync(ClusterMemberId sende int ILocalMember.Version => 0; } + private protected static ILocalMember CreateLocalMember() => new LocalMember(); + private protected delegate IServer ServerFactory(ILocalMember localMember, EndPoint address, TimeSpan timeout); private protected delegate RaftClusterMember ClientFactory(EndPoint address, ILocalMember localMember, TimeSpan timeout); diff --git a/src/cluster/DotNext.Net.Cluster/Net/Cluster/Consensus/Raft/NetworkTransport/ConnectionOriented/Tcp/TcpServer.cs b/src/cluster/DotNext.Net.Cluster/Net/Cluster/Consensus/Raft/NetworkTransport/ConnectionOriented/Tcp/TcpServer.cs index d34efc2532..e8fb359683 100644 --- a/src/cluster/DotNext.Net.Cluster/Net/Cluster/Consensus/Raft/NetworkTransport/ConnectionOriented/Tcp/TcpServer.cs +++ b/src/cluster/DotNext.Net.Cluster/Net/Cluster/Consensus/Raft/NetworkTransport/ConnectionOriented/Tcp/TcpServer.cs @@ -113,6 +113,8 @@ private async void HandleConnection(Socket remoteClient) await timeoutSource.DisposeAsync().ConfigureAwait(false); } + // returned to the pool above: the finally below must not return it again + timeoutSource = default; protocol = new(ssl, MemoryAllocator, transmissionBlockSize); }