From ac73e41fc3db8c2dc049dcca3ec3e635bd5fadc8 Mon Sep 17 00:00:00 2001 From: Ricard Rosson Date: Thu, 23 Jul 2026 18:59:54 +0100 Subject: [PATCH 1/2] netutils/telnetd: keep the listen socket out of the standard-stream range When telnetd_daemon() starts without open standard streams - exactly what happens when nsh_telnetstart() spawns "telnetd &" before the console device exists (e.g. CONFIG_NSH_USBCONSOLE boards, where nsh_initialize() runs before the USB console is connected) - socket() returns a descriptor in the 0..2 range. The accept loop's own "go silent" close(0)..close(2) then destroys the listen socket on the first iteration: every subsequent accept4() fails with EBADF and the daemon serves nothing. Observed on hardware (RP2350, CDC-ACM console + CDC-NCM composite): the daemon task was alive but port 23 refused every connection; the thread list showed it looping on the failed accept. Duplicate the descriptor above the standard-stream range before using it. Assisted-by: Claude Fable 5 Signed-off-by: Ricard Rosson --- netutils/telnetd/telnetd_daemon.c | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/netutils/telnetd/telnetd_daemon.c b/netutils/telnetd/telnetd_daemon.c index c1512113c..a4c1e74fe 100644 --- a/netutils/telnetd/telnetd_daemon.c +++ b/netutils/telnetd/telnetd_daemon.c @@ -125,6 +125,28 @@ int telnetd_daemon(FAR const struct telnetd_config_s *config) goto errout; } + /* If the daemon was started without standard streams (e.g. spawned by + * nsh_telnetstart() before a USB console device exists), socket() may + * have returned a descriptor in 0..2. The "go silent" + * close(0)..close(2) at the top of the accept loop below would then + * destroy the listen socket: every subsequent accept4() fails and the + * daemon serves nothing. Move the descriptor above the + * standard-stream range. + */ + + if (listensd <= 2) + { + int highsd = fcntl(listensd, F_DUPFD_CLOEXEC, 3); + if (highsd < 0) + { + nerr("ERROR: F_DUPFD_CLOEXEC failed: %d\n", errno); + goto errout_with_socket; + } + + close(listensd); + listensd = highsd; + } + #ifdef CONFIG_NET_SOCKOPTS /* Set socket to reuse address */ From 01ff92f794bc214e8144de893623e25a9c354bb9 Mon Sep 17 00:00:00 2001 From: Ricard Rosson Date: Thu, 23 Jul 2026 19:00:11 +0100 Subject: [PATCH 2/2] netutils/telnetd: survive transient connection errors instead of exiting Any single failed connection killed the whole daemon: - A peer that resets before the accept completes (a port scanner, or a simple "nc -z" probe) surfaces as an accept4() error such as ECONNABORTED, which took the errout path and closed the listener. - The per-connection error paths after a successful accept (setsockopt, opening /dev/telnet, SIOCTELNET, opening the session device, spawning the session) likewise exited the daemon instead of dropping the one connection. Either way, one bad or aborted connection and the telnet console is dead until reboot - a trivial remote way to take out the NuttX console on any reachable network. Verified on hardware: one "nc -zv" probe permanently killed the listener. Treat these as per-connection failures: drop the connection and keep accepting. Genuinely unrecoverable accept4() errors (EBADF, ENOTSOCK, EINVAL, EOPNOTSUPP - a bad listen socket) still exit loudly, and a short pause on repeated transient failures avoids busy-spinning while an interface is down. Daemon setup errors (socket/bind/listen) exit as before. Assisted-by: Claude Fable 5 Signed-off-by: Ricard Rosson --- netutils/telnetd/telnetd_daemon.c | 69 ++++++++++++++++++++----------- 1 file changed, 46 insertions(+), 23 deletions(-) diff --git a/netutils/telnetd/telnetd_daemon.c b/netutils/telnetd/telnetd_daemon.c index a4c1e74fe..3662fa4aa 100644 --- a/netutils/telnetd/telnetd_daemon.c +++ b/netutils/telnetd/telnetd_daemon.c @@ -126,12 +126,11 @@ int telnetd_daemon(FAR const struct telnetd_config_s *config) } /* If the daemon was started without standard streams (e.g. spawned by - * nsh_telnetstart() before a USB console device exists), socket() may - * have returned a descriptor in 0..2. The "go silent" - * close(0)..close(2) at the top of the accept loop below would then - * destroy the listen socket: every subsequent accept4() fails and the - * daemon serves nothing. Move the descriptor above the - * standard-stream range. + * nsh_telnetstart() before a USB console exists), socket() may have + * returned a descriptor in 0..2. The "go silent" close(0)..close(2) + * at the top of the accept loop below would then destroy the listen + * socket: every subsequent accept4() fails and the daemon serves + * nothing. Move the descriptor above the standard-stream range. */ if (listensd <= 2) @@ -224,17 +223,33 @@ int telnetd_daemon(FAR const struct telnetd_config_s *config) acceptsd = accept4(listensd, &addr.generic, &addrlen, SOCK_CLOEXEC); if (acceptsd < 0) { - /* Just continue if a signal was received */ + int err = errno; - if (errno == EINTR) + /* A bad listen socket can never recover - exiting loudly beats + * spinning forever while serving nothing. + */ + + if (err == EBADF || err == ENOTSOCK || err == EINVAL || + err == EOPNOTSUPP) { - continue; - } - else - { - nerr("ERROR: accept failed: %d\n", errno); + nerr("ERROR: accept failed fatally: %d\n", err); goto errout_with_socket; } + + /* Everything else is transient: a peer that reset before the + * accept completed (a port scan, nc -z, ECONNABORTED), a + * signal, or the interface bouncing. None of those may take + * the daemon down; the pause keeps a persistent transient + * (interface down) from busy-spinning. + */ + + nerr("ERROR: accept failed: %d\n", err); + if (err != EINTR) + { + usleep(100 * 1000); + } + + continue; } #ifdef CONFIG_NET_SOLINGER @@ -247,8 +262,14 @@ int telnetd_daemon(FAR const struct telnetd_config_s *config) if (setsockopt(acceptsd, SOL_SOCKET, SO_LINGER, &ling, sizeof(struct linger)) < 0) { + /* Per-connection failure (e.g. the peer already reset the + * connection - a port scan does this). Drop the connection and + * keep accepting; exiting here lets any probe kill the daemon. + */ + nerr("ERROR: setsockopt failed: %d\n", errno); - goto errout_with_acceptsd; + close(acceptsd); + continue; } #endif @@ -258,7 +279,8 @@ int telnetd_daemon(FAR const struct telnetd_config_s *config) if (drvrfd < 0) { nerr("ERROR: open(/dev/telnet) failed: %d\n", errno); - goto errout_with_acceptsd; + close(acceptsd); + continue; } /* Create a character device to "wrap" the accepted socket descriptor */ @@ -271,9 +293,10 @@ int telnetd_daemon(FAR const struct telnetd_config_s *config) if (ioctl(drvrfd, SIOCTELNET, (unsigned long)((uintptr_t)&session)) < 0) { - nerr("ERROR: open(/dev/telnet) failed: %d\n", errno); + nerr("ERROR: SIOCTELNET failed: %d\n", errno); close(drvrfd); - goto errout_with_acceptsd; + close(acceptsd); + continue; } close(drvrfd); @@ -284,8 +307,12 @@ int telnetd_daemon(FAR const struct telnetd_config_s *config) drvrfd = open(session.ts_devpath, O_RDWR); if (drvrfd < 0) { + /* The socket now belongs to the telnet driver instance; just + * keep accepting. + */ + nerr("ERROR: Failed to open %s: %d\n", session.ts_devpath, errno); - goto errout_with_socket; + continue; } /* Use this driver as stdin, stdout, and stderror */ @@ -341,16 +368,12 @@ int telnetd_daemon(FAR const struct telnetd_config_s *config) if (ret > 0) { nerr("ERROR: Failed start the telnet session: %d\n", ret); - errno = ret; - goto errout_with_socket; + continue; } } #endif } -errout_with_acceptsd: - close(acceptsd); - errout_with_socket: close(listensd); errout: