Skip to content

Handle SocketException while configuring an accepted socket in TcpListener - #1165

Open
follesoe wants to merge 1 commit into
zeromq:masterfrom
BluEye-Robotics:fix-tcplistener-accept-crash
Open

Handle SocketException while configuring an accepted socket in TcpListener#1165
follesoe wants to merge 1 commit into
zeromq:masterfrom
BluEye-Robotics:fix-tcplistener-accept-crash

Conversation

@follesoe

Copy link
Copy Markdown

Problem

A TCP peer that connects and immediately resets the connection can kill the whole process. When the RST lands between the proactor completing an accept and TcpListener.InCompleted applying socket options to the accepted socket, the option calls throw — on macOS as SocketException (22): Invalid argument from acceptedSocket.NoDelay = true:

Unhandled exception. System.Net.Sockets.SocketException (22): Invalid argument
   at AsyncIO.AsyncSocket.set_NoDelay(Boolean value)
   at NetMQ.Core.Transports.Tcp.TcpListener.InCompleted(SocketError socketError, Int32 bytesTransferred)
   at NetMQ.Core.IOObject.InCompleted(SocketError socketError, Int32 bytesTransferred)
   at NetMQ.Core.Utils.Proactor.Loop()

InCompleted runs on the proactor thread, where nothing catches the exception, so it terminates the process. Any bound socket is exposed to this through a flapping or rapidly reconnecting client — a test suite that hammers connect/disconnect against a bound PUB socket crashed its test host roughly 1 run in 10 on macOS arm64.

This is the accept-side sibling of #1161, which handled the same macOS EINVAL behaviour for outbound sockets in TcpConnector.

Solution

Treat a failure to set up the accepted socket as a failed accept, the way libzmq's tcp_listener treats tune_socket failure: dispose the accepted socket, raise EventAcceptFailed, and keep accepting. The guarded region also covers the StreamEngine construction, whose constructor touches the socket too (send/receive buffer sizes) and shares the same failure window.

Two deliberate choices, called out for review:

  • The catch is SocketException broadly, not Handle macOS EINVAL when enabling TCP_NODELAY on outbound TCP sockets #1161's when (SocketErrorCode == InvalidArgument) filter. On the connector path the socket is kept afterwards, so that filter must be narrow; here the socket is being dropped on any setup failure, and a peer-reset socket does not necessarily fail with EINVAL on every OS. Dropping is also safe for a hypothetically healthy socket — the peer's reconnect logic recovers — whereas an escaped exception is fatal.
  • EventAcceptFailed reports ErrorCode.ConnectionReset instead of mapping the caught SocketErrorCode through ToErrorCode(), because ToErrorCode hits Debug.Assert(false) on unmapped values — an assertion failure inside crash recovery would defeat the purpose. Connection reset is the effective outcome regardless of the exact errno.

Proof

The new regression test floods a bound PublisherSocket with 400 raw TCP connects that reset immediately (SO_LINGER=0 + close), then asserts the listener still accepts and serves a real subscriber. On macOS arm64 (.NET 10):

  • Without the fix: 12/12 runs crash the test host with the exact exception above.
  • With the fix: 12/12 runs pass in ~170 ms.

The liveness assertion keeps the test meaningful on platforms that never throw here, and would also catch the alternative failure mode where the exception is swallowed without re-arming Accept(), which leaves the listener permanently deaf.

The full NetMQ.Tests suite passes locally on net10.0 (267 passed, 4 pre-existing skips).

🤖 Generated with Claude Code

…tener

Problem: a peer that connects and immediately resets the connection can
kill the whole process. When the RST lands between the proactor
completing an accept and InCompleted applying socket options to the
accepted socket, the option calls throw (EINVAL on macOS) on the
proactor thread, where nothing catches the exception.

Solution: treat setup failure as a failed accept, the way libzmq treats
tune_socket failure: dispose the accepted socket, raise
EventAcceptFailed and keep accepting. The guarded region includes the
StreamEngine construction, whose constructor also touches the socket
(send/receive buffer sizes). The monitor event reports ConnectionReset
rather than mapping the raw error through ToErrorCode, which
Debug.Asserts on unmapped values.

Accept-side sibling of the outbound-socket fix in zeromq#1161. Adds a
regression test that floods a listener with immediately-reset connects
and asserts it stays alive and accepting; without the fix it crashes
the test host 12/12 times on macOS arm64.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant