netutils/telnetd: fix silent listen-socket loss and per-connection daemon death#3658
netutils/telnetd: fix silent listen-socket loss and per-connection daemon death#3658ricardgb wants to merge 2 commits into
Conversation
…ange 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 <noreply@anthropic.com> Signed-off-by: Ricard Rosson <ricard@groundbits.com>
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 <noreply@anthropic.com> Signed-off-by: Ricard Rosson <ricard@groundbits.com>
|
|
||
| /* If the daemon was started without standard streams (e.g. spawned by | ||
| * nsh_telnetstart() before a USB console exists), socket() may have | ||
| * returned a descriptor in 0..2. The "go silent" close(0)..close(2) |
There was a problem hiding this comment.
could we fix the original problem directly @ricardgb
There was a problem hiding this comment.
Something like this?
diff --git a/netutils/telnetd/telnetd_daemon.c b/netutils/telnetd/telnetd_daemon.c
index 3662fa4aa..e0a7df5be 100644
--- a/netutils/telnetd/telnetd_daemon.c
+++ b/netutils/telnetd/telnetd_daemon.c
@@ -81,6 +81,7 @@ int telnetd_daemon(FAR const struct telnetd_config_s *config)
socklen_t addrlen;
int listensd;
int acceptsd;
+ int nullfd;
#ifdef CONFIG_NET_SOCKOPTS
int optval;
#endif
@@ -115,6 +116,31 @@ int telnetd_daemon(FAR const struct telnetd_config_s *config)
}
#endif /* CONFIG_SCHED_HAVE_PARENT && CONFIG_ENABLE_ALL_SIGNALS */
+ /* If the daemon was started without standard streams (e.g. spawned by
+ * nsh_telnetstart() before a USB console exists), descriptors 0..2 are
+ * free and any descriptor created below -- the listen socket, an
+ * accepted socket or a session driver -- could be allocated in the
+ * standard-stream range, where the per-session dup2(0..2) would clobber
+ * it. Anchor any free slot in 0..2 to /dev/null first so that can
+ * never happen.
+ */
+
+ for (; ; )
+ {
+ nullfd = open("/dev/null", O_RDWR);
+ if (nullfd < 0)
+ {
+ nerr("ERROR: open(/dev/null) failed: %d\n", errno);
+ goto errout;
+ }
+
+ if (nullfd > 2)
+ {
+ close(nullfd);
+ break;
+ }
+ }
+
/* Create a new TCP socket to use to listen for connections */
listensd = socket(config->d_family, SOCK_STREAM | SOCK_CLOEXEC, 0);
@@ -125,27 +151,6 @@ 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 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 */
@@ -211,11 +216,24 @@ int telnetd_daemon(FAR const struct telnetd_config_s *config)
#endif
int drvrfd;
- /* Now go silent. */
+ /* Now go silent: detach from the previous session's driver (or the
+ * startup streams) by re-anchoring 0..2 to /dev/null instead of
+ * closing them. Closing would leave 0..2 free, and accept4() or
+ * open() below could then allocate into the standard-stream range
+ * where the dup2(0..2) for the next session would clobber it.
+ */
- close(0);
- close(1);
- close(2);
+ nullfd = open("/dev/null", O_RDWR);
+ if (nullfd >= 0)
+ {
+ dup2(nullfd, 0);
+ dup2(nullfd, 1);
+ dup2(nullfd, 2);
+ if (nullfd > 2)
+ {
+ close(nullfd);
+ }
+ }
ninfo("Accepting connections on port %d\n", ntohs(config->d_port));
This removes the listen-socket relocation entirely and instead (1) anchors any free slot in 0..2 to /dev/null before the socket is created, and (2) re-anchors 0..2 to /dev/null at the top of the accept loop instead of closing them — so no descriptor (listen, accept or driver) can ever be allocated into the standard-stream range in the first place. Compile-tested (rp2350 telnet config, builds clean, nxstyle clean); I'd re-run the on-hardware test before pushing it. If you prefer this shape I'll update the PR.
| { | ||
| nerr("ERROR: accept failed: %d\n", errno); | ||
| goto errout_with_socket; | ||
| usleep(100 * 1000); |
| */ | ||
|
|
||
| if (err == EBADF || err == ENOTSOCK || err == EINVAL || | ||
| err == EOPNOTSUPP) |
There was a problem hiding this comment.
why not ignore the error and continue directly
| * 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 |
There was a problem hiding this comment.
move to previous patch if you really want to modify
Summary
Two robustness fixes for the telnet daemon (
netutils/telnetd/telnetd_daemon.c). As it stands, the standard NuttX telnet console can be disabled by a single aborted connection, and on some configurations it silently serves nothing from boot.1. Keep the listen socket out of the standard-stream range.
When the daemon starts without open standard streams — exactly what happens when
nsh_telnetstart()spawnstelnetd &before the console device exists (e.g.CONFIG_NSH_USBCONSOLEboards, wherensh_initialize()runs before the USB console is connected) —socket()returns a descriptor in 0..2. The accept loop's own "go silent"close(0)..close(2)then destroys the listen socket on the first iteration; every subsequentaccept4()fails and the daemon serves nothing, while appearing alive in the task list. Fix:F_DUPFD_CLOEXECthe descriptor above 2 before use.2. Survive transient connection errors instead of exiting.
Previously any single failed connection killed the whole daemon:
nc -zprobe) surfaces as anaccept4()error such asECONNABORTEDand took the errout path;/dev/telnetopen,SIOCTELNET, session-device open, session spawn) likewise exited the daemon.One bad or aborted connection and the telnet console is dead until reboot — a trivial remote way to take out the console of any reachable NuttX device. Fix: treat these as per-connection failures (drop the connection, keep accepting). Genuinely unrecoverable
accept4()errors (EBADF/ENOTSOCK/EINVAL/EOPNOTSUPP) 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.Impact
Any board using
netutils/telnetd(directly or viaCONFIG_NSH_TELNET). Bug 1 affects configurations where the daemon can start before its standard streams exist (USB-console boards being the common case). Bug 2 affects everyone: a subnetnmapsweep kills every reachable NuttX telnet console.Testing
Reproduced and verified on hardware: Raspberry Pi Pico 2 W (RP2350), composite USB CDC-ACM console + CDC-NCM network,
CONFIG_NSH_USBCONSOLE, telnetd auto-started viansh_telnetstart().close(0).nc -zv <addr> 23probe permanently killed a manually started, correctly listening daemon.nc -zprobe barrages, and interactive nsh telnet sessions work.tools/nxstyle.cpasses on the modified file.Note
These changes were developed with the assistance of an AI agent (Claude) and have been human-reviewed and tested on real hardware as described above.