From 36bb825e82c8195f0f1ce3cfbc49a511fc12af1a Mon Sep 17 00:00:00 2001 From: wfurt Date: Wed, 5 Aug 2026 13:58:39 +0200 Subject: [PATCH 1/5] Fix ArgumentException on macOS accept with empty remote address When a peer connects and immediately resets a socket (SO_LINGER=0), macOS accept() can return successfully with an empty remote sockaddr (addrlen=0). The 2024 fix (#108616) guarded the Create call inside FinishOperationAccept, but the shared FinishOperationSyncSuccess path calls EndPoint.Create a second time on the same zero-sized address and throws ArgumentException. For Kestrel this crashes the accept loop and stops accepting new connections while leaving the process alive and the port bound. Guard the second Create call with the same Size > 0 check, and swallow SocketException from the diagnostic NetEventSource.Accepted call whose access of RemoteEndPoint can trigger getpeername returning ENOTCONN. Fixes #121848 --- .../Net/Sockets/SocketAsyncEventArgs.cs | 8 +++- .../tests/FunctionalTests/Accept.cs | 44 +++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/src/libraries/System.Net.Sockets/src/System/Net/Sockets/SocketAsyncEventArgs.cs b/src/libraries/System.Net.Sockets/src/System/Net/Sockets/SocketAsyncEventArgs.cs index c618678c30dc67..1b52df5df2f87b 100644 --- a/src/libraries/System.Net.Sockets/src/System/Net/Sockets/SocketAsyncEventArgs.cs +++ b/src/libraries/System.Net.Sockets/src/System/Net/Sockets/SocketAsyncEventArgs.cs @@ -1013,7 +1013,11 @@ internal void FinishOperationSyncSuccess(int bytesTransferred, SocketFlags flags if (socketError == SocketError.Success) { - _acceptSocket = _currentSocket.UpdateAcceptSocket(_acceptSocket!, _currentSocket._rightEndPoint!.Create(remoteSocketAddress)); + // macOS can return accept() success with an empty remote sockaddr when the peer reset before accept. + EndPoint? remoteEndPoint = remoteSocketAddress.Size > 0 + ? _currentSocket._rightEndPoint!.Create(remoteSocketAddress) + : null; + _acceptSocket = _currentSocket.UpdateAcceptSocket(_acceptSocket!, remoteEndPoint); if (NetEventSource.Log.IsEnabled()) { @@ -1022,6 +1026,8 @@ internal void FinishOperationSyncSuccess(int bytesTransferred, SocketFlags flags NetEventSource.Accepted(_acceptSocket, _acceptSocket.RemoteEndPoint, _acceptSocket.LocalEndPoint); } catch (ObjectDisposedException) { } + // RemoteEndPoint may call getpeername which can fail with ENOTCONN if the peer reset between accept and here. + catch (SocketException) { } } } else diff --git a/src/libraries/System.Net.Sockets/tests/FunctionalTests/Accept.cs b/src/libraries/System.Net.Sockets/tests/FunctionalTests/Accept.cs index 3356a5ac3b7f05..e91ab382d0c976 100644 --- a/src/libraries/System.Net.Sockets/tests/FunctionalTests/Accept.cs +++ b/src/libraries/System.Net.Sockets/tests/FunctionalTests/Accept.cs @@ -493,4 +493,48 @@ public sealed class AcceptEap : Accept { public AcceptEap(ITestOutputHelper output) : base(output) {} } + + public sealed class AcceptDualStackResetTests + { + // Regression: on macOS, when a peer connects to a dual-stack AF_INET6 listener and + // immediately resets (SO_LINGER=0), accept(2) can return successfully with an empty + // remote sockaddr. Without a guard on the resulting zero-sized SocketAddress, the + // shared FinishOperationSyncSuccess path throws ArgumentException from EndPoint.Create + // and permanently breaks the listener (observed in Kestrel's accept loop). + [Fact] + public async Task AcceptAsync_DualStackListener_PeerImmediatelyResets_ListenerStaysHealthy() + { + if (!Socket.OSSupportsIPv6) return; + + using Socket listener = new Socket(AddressFamily.InterNetworkV6, SocketType.Stream, ProtocolType.Tcp); + listener.DualMode = true; + listener.Bind(new IPEndPoint(IPAddress.IPv6Any, 0)); + int port = ((IPEndPoint)listener.LocalEndPoint!).Port; + listener.Listen(128); + + for (int i = 0; i < 200; i++) + { + using Socket ipv6 = new Socket(AddressFamily.InterNetworkV6, SocketType.Stream, ProtocolType.Tcp) { NoDelay = true }; + using Socket ipv4 = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp) { NoDelay = true }; + + Task connect6 = ipv6.ConnectAsync(IPAddress.IPv6Loopback, port); + Task connect4 = ipv4.ConnectAsync(IPAddress.Loopback, port); + await Task.WhenAll(connect6, connect4).WaitAsync(TimeSpan.FromSeconds(5)); + + ipv4.LingerState = new LingerOption(true, 0); + ipv4.Close(); + + using Socket a1 = await listener.AcceptAsync().WaitAsync(TimeSpan.FromSeconds(5)); + using Socket a2 = await listener.AcceptAsync().WaitAsync(TimeSpan.FromSeconds(5)); + } + + Task finalAccept = listener.AcceptAsync(); + using (Socket probe = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)) + { + await probe.ConnectAsync(IPAddress.Loopback, port); + using Socket finalAccepted = await finalAccept.WaitAsync(TimeSpan.FromSeconds(5)); + Assert.True(finalAccepted.Connected); + } + } + } } From a2072e9c34c9e59719278a21627d60389bb76ace Mon Sep 17 00:00:00 2001 From: wfurt Date: Thu, 6 Aug 2026 11:28:02 +0200 Subject: [PATCH 2/5] Log the already-computed remote endpoint instead of RemoteEndPoint Addresses Copilot review feedback: passing the local remoteEndPoint variable to NetEventSource.Accepted avoids the extra getpeername syscall and removes the need for the broad SocketException catch. --- .../src/System/Net/Sockets/SocketAsyncEventArgs.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/libraries/System.Net.Sockets/src/System/Net/Sockets/SocketAsyncEventArgs.cs b/src/libraries/System.Net.Sockets/src/System/Net/Sockets/SocketAsyncEventArgs.cs index 1b52df5df2f87b..f3b28c5ff3e9bc 100644 --- a/src/libraries/System.Net.Sockets/src/System/Net/Sockets/SocketAsyncEventArgs.cs +++ b/src/libraries/System.Net.Sockets/src/System/Net/Sockets/SocketAsyncEventArgs.cs @@ -1023,11 +1023,9 @@ internal void FinishOperationSyncSuccess(int bytesTransferred, SocketFlags flags { try { - NetEventSource.Accepted(_acceptSocket, _acceptSocket.RemoteEndPoint, _acceptSocket.LocalEndPoint); + NetEventSource.Accepted(_acceptSocket, remoteEndPoint, _acceptSocket.LocalEndPoint); } catch (ObjectDisposedException) { } - // RemoteEndPoint may call getpeername which can fail with ENOTCONN if the peer reset between accept and here. - catch (SocketException) { } } } else From 4d4b052f5fcc2ee8edae88e933bdc5823cac8dcc Mon Sep 17 00:00:00 2001 From: wfurt Date: Thu, 6 Aug 2026 11:29:42 +0200 Subject: [PATCH 3/5] Use ConditionalFact for the IPv6 requirement Marks the test as skipped rather than silently passing on hosts without IPv6 support, matching the [ConditionalFact(typeof(Socket), nameof(Socket.OSSupportsUnixDomainSockets))] pattern used elsewhere in this test project. --- .../System.Net.Sockets/tests/FunctionalTests/Accept.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/libraries/System.Net.Sockets/tests/FunctionalTests/Accept.cs b/src/libraries/System.Net.Sockets/tests/FunctionalTests/Accept.cs index e91ab382d0c976..4c8bc4e902f2d9 100644 --- a/src/libraries/System.Net.Sockets/tests/FunctionalTests/Accept.cs +++ b/src/libraries/System.Net.Sockets/tests/FunctionalTests/Accept.cs @@ -501,11 +501,9 @@ public sealed class AcceptDualStackResetTests // remote sockaddr. Without a guard on the resulting zero-sized SocketAddress, the // shared FinishOperationSyncSuccess path throws ArgumentException from EndPoint.Create // and permanently breaks the listener (observed in Kestrel's accept loop). - [Fact] + [ConditionalFact(typeof(Socket), nameof(Socket.OSSupportsIPv6))] public async Task AcceptAsync_DualStackListener_PeerImmediatelyResets_ListenerStaysHealthy() { - if (!Socket.OSSupportsIPv6) return; - using Socket listener = new Socket(AddressFamily.InterNetworkV6, SocketType.Stream, ProtocolType.Tcp); listener.DualMode = true; listener.Bind(new IPEndPoint(IPAddress.IPv6Any, 0)); From f13aa23ddc4d426656d9f2dbda1205cef215894d Mon Sep 17 00:00:00 2001 From: wfurt Date: Tue, 8 Sep 2026 15:07:28 -0700 Subject: [PATCH 4/5] Handle empty addresses in synchronous accept Make the dual-stack reset regression test cover synchronous and asynchronous accept while allowing platform-specific reset behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/System/Net/Sockets/Socket.cs | 6 +- .../tests/FunctionalTests/Accept.cs | 55 +++++++++++-------- 2 files changed, 37 insertions(+), 24 deletions(-) diff --git a/src/libraries/System.Net.Sockets/src/System/Net/Sockets/Socket.cs b/src/libraries/System.Net.Sockets/src/System/Net/Sockets/Socket.cs index 90f2d511473575..7b8859330820f8 100644 --- a/src/libraries/System.Net.Sockets/src/System/Net/Sockets/Socket.cs +++ b/src/libraries/System.Net.Sockets/src/System/Net/Sockets/Socket.cs @@ -1089,8 +1089,10 @@ public Socket Accept() Debug.Assert(!acceptedSocketHandle.IsInvalid); - Socket socket = CreateAcceptSocket(acceptedSocketHandle, _rightEndPoint.Create(socketAddress)); - if (NetEventSource.Log.IsEnabled()) NetEventSource.Accepted(socket, socket.RemoteEndPoint!, socket.LocalEndPoint); + // macOS can return accept() success with an empty remote sockaddr when the peer reset before accept. + EndPoint? remoteEndPoint = socketAddress.Size > 0 ? _rightEndPoint.Create(socketAddress) : null; + Socket socket = CreateAcceptSocket(acceptedSocketHandle, remoteEndPoint); + if (NetEventSource.Log.IsEnabled()) NetEventSource.Accepted(socket, remoteEndPoint, socket.LocalEndPoint); return socket; } diff --git a/src/libraries/System.Net.Sockets/tests/FunctionalTests/Accept.cs b/src/libraries/System.Net.Sockets/tests/FunctionalTests/Accept.cs index 4c8bc4e902f2d9..2384e9c4e53a66 100644 --- a/src/libraries/System.Net.Sockets/tests/FunctionalTests/Accept.cs +++ b/src/libraries/System.Net.Sockets/tests/FunctionalTests/Accept.cs @@ -496,22 +496,20 @@ public AcceptEap(ITestOutputHelper output) : base(output) {} public sealed class AcceptDualStackResetTests { - // Regression: on macOS, when a peer connects to a dual-stack AF_INET6 listener and - // immediately resets (SO_LINGER=0), accept(2) can return successfully with an empty - // remote sockaddr. Without a guard on the resulting zero-sized SocketAddress, the - // shared FinishOperationSyncSuccess path throws ArgumentException from EndPoint.Create - // and permanently breaks the listener (observed in Kestrel's accept loop). - [ConditionalFact(typeof(Socket), nameof(Socket.OSSupportsIPv6))] - public async Task AcceptAsync_DualStackListener_PeerImmediatelyResets_ListenerStaysHealthy() + [ConditionalTheory(typeof(Socket), nameof(Socket.OSSupportsIPv6))] + [SkipOnPlatform(TestPlatforms.Wasi | TestPlatforms.OpenBSD, "These platforms don't support dual-mode sockets")] + [InlineData(false)] + [InlineData(true)] + public async Task Accept_DualStackListener_PeerImmediatelyResets_ListenerStaysHealthy(bool useAsync) { - using Socket listener = new Socket(AddressFamily.InterNetworkV6, SocketType.Stream, ProtocolType.Tcp); - listener.DualMode = true; - listener.Bind(new IPEndPoint(IPAddress.IPv6Any, 0)); - int port = ((IPEndPoint)listener.LocalEndPoint!).Port; - listener.Listen(128); - for (int i = 0; i < 200; i++) { + using Socket listener = new Socket(AddressFamily.InterNetworkV6, SocketType.Stream, ProtocolType.Tcp); + listener.DualMode = true; + listener.Bind(new IPEndPoint(IPAddress.IPv6Any, 0)); + int port = ((IPEndPoint)listener.LocalEndPoint!).Port; + listener.Listen(2); + using Socket ipv6 = new Socket(AddressFamily.InterNetworkV6, SocketType.Stream, ProtocolType.Tcp) { NoDelay = true }; using Socket ipv4 = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp) { NoDelay = true }; @@ -522,16 +520,29 @@ public async Task AcceptAsync_DualStackListener_PeerImmediatelyResets_ListenerSt ipv4.LingerState = new LingerOption(true, 0); ipv4.Close(); - using Socket a1 = await listener.AcceptAsync().WaitAsync(TimeSpan.FromSeconds(5)); - using Socket a2 = await listener.AcceptAsync().WaitAsync(TimeSpan.FromSeconds(5)); - } + byte[] message = [42]; + Assert.Equal(message.Length, ipv6.Send(message)); - Task finalAccept = listener.AcceptAsync(); - using (Socket probe = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)) - { - await probe.ConnectAsync(IPAddress.Loopback, port); - using Socket finalAccepted = await finalAccept.WaitAsync(TimeSpan.FromSeconds(5)); - Assert.True(finalAccepted.Connected); + bool receivedMessage = false; + for (int acceptCount = 0; acceptCount < 2 && !receivedMessage; acceptCount++) + { + using Socket accepted = useAsync + ? await listener.AcceptAsync().WaitAsync(TimeSpan.FromSeconds(5)) + : listener.Accept(); + + try + { + byte[] received = new byte[message.Length]; + int receivedCount = await accepted.ReceiveAsync(received).WaitAsync(TimeSpan.FromSeconds(5)); + receivedMessage = receivedCount == message.Length && received.AsSpan().SequenceEqual(message); + } + catch (SocketException) + { + // Some platforms surface the reset connection from accept(), while others discard it. + } + } + + Assert.True(receivedMessage); } } } From 584e499f7a6ef7ee1d06c48b2ce0bce933c47070 Mon Sep 17 00:00:00 2001 From: wfurt Date: Tue, 8 Sep 2026 15:31:14 -0700 Subject: [PATCH 5/5] Exercise reset connection before healthy accept Connect and reset the IPv4 peer before establishing the healthy IPv6 connection so the regression path is exercised more reliably. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../System.Net.Sockets/tests/FunctionalTests/Accept.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/libraries/System.Net.Sockets/tests/FunctionalTests/Accept.cs b/src/libraries/System.Net.Sockets/tests/FunctionalTests/Accept.cs index 2384e9c4e53a66..ea59634e8d5cb8 100644 --- a/src/libraries/System.Net.Sockets/tests/FunctionalTests/Accept.cs +++ b/src/libraries/System.Net.Sockets/tests/FunctionalTests/Accept.cs @@ -513,13 +513,11 @@ public async Task Accept_DualStackListener_PeerImmediatelyResets_ListenerStaysHe using Socket ipv6 = new Socket(AddressFamily.InterNetworkV6, SocketType.Stream, ProtocolType.Tcp) { NoDelay = true }; using Socket ipv4 = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp) { NoDelay = true }; - Task connect6 = ipv6.ConnectAsync(IPAddress.IPv6Loopback, port); - Task connect4 = ipv4.ConnectAsync(IPAddress.Loopback, port); - await Task.WhenAll(connect6, connect4).WaitAsync(TimeSpan.FromSeconds(5)); - + await ipv4.ConnectAsync(IPAddress.Loopback, port).WaitAsync(TimeSpan.FromSeconds(5)); ipv4.LingerState = new LingerOption(true, 0); ipv4.Close(); + await ipv6.ConnectAsync(IPAddress.IPv6Loopback, port).WaitAsync(TimeSpan.FromSeconds(5)); byte[] message = [42]; Assert.Equal(message.Length, ipv6.Send(message));