From aeaa13227e256af57afdd2000e9a04353d5d9a15 Mon Sep 17 00:00:00 2001 From: Ricard Rosson Date: Fri, 24 Jul 2026 07:20:49 +0100 Subject: [PATCH] net/tcp: don't accept a reset connection as connected (fixes send hang) tcp_start_monitor() is called from accept() (net/inet/inet_sockif.c) for each newly accepted connection. When the peer had already closed the connection before accept() ran, the monitor takes an early-return path so that any read-ahead data buffered on the connection can still be drained; it returns OK in that case. accept() (net/socket/accept.c) then marks the new socket _SF_CONNECTED unconditionally. If the peer aborts the connection with an RST immediately after the three-way handshake completes (for example any close with SO_LINGER {1, 0}), the connection is moved to TCP_CLOSED with no buffered data, yet accept() still hands back a socket that reports _SS_ISCONNECTED. A subsequent blocking send() on that socket passes the connected check, registers a send callback and waits on its semaphore forever: the only TCP_ABORT event was delivered before the callback existed, and no further ACK, POLL or disconnect event is generated for a closed connection, so the waiter is never woken. Any server that writes before reading can hit this; the telnet daemon (netutils/telnetd) is one example, where the accepted session task blocks in send() and never completes. Only return OK from the already-closed path when there is actually read-ahead data to drain. Otherwise the connection is dead, so fall through to the -ENOTCONN return: accept() then fails cleanly instead of handing back a socket wedged on a connection that will never make progress. The graceful-close-with-pending-data case (the reason the OK path exists) is preserved by the conn->readahead check. Signed-off-by: Ricard Rosson Assisted-by: Claude (Anthropic Claude Code) --- net/tcp/tcp_monitor.c | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/net/tcp/tcp_monitor.c b/net/tcp/tcp_monitor.c index 1ba7b4ce9df..11b43e95675 100644 --- a/net/tcp/tcp_monitor.c +++ b/net/tcp/tcp_monitor.c @@ -272,13 +272,25 @@ int tcp_start_monitor(FAR struct socket *psock) tcp_shutdown_monitor(conn, TCP_ABORT); - /* If the peer close the connection before we call accept, - * in order to allow user to read the readahead data, - * return OK. + /* If the peer closed the connection before we called accept, and + * there is buffered read-ahead data, return OK so that the caller + * still gets a socket from which the pending data can be drained + * (followed by EOF). + * + * If there is no buffered data, however, the connection is dead: + * presenting it to the caller as a successfully-accepted socket + * makes accept() mark it _SF_CONNECTED (see net/socket/accept.c), + * and a subsequent blocking send() would then wait forever on a + * connection that will never post another event. This happens when + * a peer resets the connection immediately after the handshake (for + * example a close with SO_LINGER {1, 0}). Report it as not-connected + * instead so accept() fails cleanly rather than handing back a wedged + * socket. */ - if (conn->tcpstateflags == TCP_CLOSED || - conn->tcpstateflags == TCP_LAST_ACK) + if ((conn->tcpstateflags == TCP_CLOSED || + conn->tcpstateflags == TCP_LAST_ACK) && + conn->readahead != NULL) { return OK; }