ricardgb opened a new pull request, #19564:
URL: https://github.com/apache/nuttx/pull/19564

   ## Summary
   
   `accept()` can hand back a socket that reports itself connected while the
   underlying TCP connection is already dead, which makes a subsequent blocking
   `send()` on that socket hang forever.
   
   `tcp_start_monitor()` (`net/tcp/tcp_monitor.c`) is called from `accept()`
   (`net/inet/inet_sockif.c`) for every 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 — e.g. any close with `SO_LINGER {1, 0}` — the connection
   is moved to `TCP_CLOSED` with no buffered data, yet `accept()` still returns 
a
   socket for which `_SS_ISCONNECTED()` is true. 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 ever
   generated for a closed connection, so the waiter is never woken.
   
   I traced this on hardware with an in-RAM event ring buffer read over SWD. For
   each such connection the trace shows, in order:
   
   ```
   RST  conn=X tcpstateflags=0x04 s_flags=0x0000   # peer RST while conn 
ESTABLISHED, still in accept backlog
   SEND conn=X tcpstateflags=0x00 s_flags=0x0040   # session send(): conn now 
TCP_CLOSED, but _SF_CONNECTED is set
   ```
   
   The fix: only return `OK` from the already-closed path when there is actually
   read-ahead data to drain (`conn->readahead != NULL`). Otherwise the 
connection
   is dead, so fall through to the existing `-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.
   
   The change is a single hunk in `tcp_start_monitor()`:
   
   ```c
         if ((conn->tcpstateflags == TCP_CLOSED ||
              conn->tcpstateflags == TCP_LAST_ACK) &&
             conn->readahead != NULL)
           {
             return OK;
           }
   ```
   
   ## Impact
   
   - Affects any TCP server that accepts a connection and writes before reading.
     Previously a peer that resets right after the handshake would leave the
     accepting task blocked forever in `send()`. The standard telnet daemon
     (`netutils/telnetd`) is one such server: the accepted session task blocks 
in
     `send()` and never completes.
   - User-visible behavior change: for a connection that was aborted before
     `accept()` and has no buffered data, `accept()` now fails with `-ENOTCONN`
     instead of returning a broken socket. Well-behaved accept loops already 
retry
     on transient accept errors. Connections closed gracefully with pending data
     are unaffected (still accepted so the data can be read, followed by EOF).
   - No build, hardware, or API changes. IPv4/IPv6, buffered and unbuffered TCP
     send paths are all covered because the fix is upstream of the send path, in
     the accept/monitor logic.
   
   ## Testing
   
   **Host:** Linux (x86_64).
   
   **Target:** RP2350 (Raspberry Pi Pico 2 W class board), arm-none-eabi-gcc 
13.2,
   `CONFIG_NET_TCP` unbuffered send path (`CONFIG_NET_TCP_WRITE_BUFFERS=n`). The
   board runs the standard `netutils/telnetd` over a USB CDC-NCM network link
   (host `192.168.7.2`, board `192.168.7.1`). To reproduce: connect to telnet
   port 23 and immediately close with `SO_LINGER {1, 0}` (a TCP RST), then 
attempt
   a normal telnet login.
   
   **Before (unpatched):** a connection reset right after the handshake leaves 
the
   spawned session task stuck in `send()`; halting the target over SWD shows the
   stuck `Telnet_session` task:
   
   ```
   baseline normal session: SERVED
   reset connection sent: 1
     PID   STATE     EVENT     COMMAND
       5   Waiting  Semaphore  telnetd
       8   Waiting  Semaphore  Telnet_session
   ```
   
   **After (this patch):** the session task is no longer left stuck; halting the
   target over SWD shows zero stuck `Telnet_session` tasks and normal sessions
   keep serving:
   
   ```
   baseline normal session: SERVED
   reset connections sent: 10
   post normal session: SERVED
   reset connections sent: 15
   post normal session: SERVED
   (gdb) stuck Telnet_session count = 0
   ```
   
   **Regression check:** 8/8 consecutive normal telnet sessions log in, run a
   command and disconnect cleanly with the patch applied.
   
   `tools/checkpatch.sh -c -u -m -g` passes on the commit.
   
   ---
   *Disclosure: this change was developed with the help of an AI agent and
   reviewed by a human before submission.*
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to