[git commit] telnetd: if vfork in make_new_session() fails, do close socket.

Denys Vlasenko vda.linux at googlemail.com
Mon Feb 23 23:03:20 UTC 2026


commit: https://git.busybox.net/busybox/commit/?id=4bdbb7cc0a1451809f136cac36e04b0178887357
branch: https://git.busybox.net/busybox/log/?h=master

function                                             old     new   delta
telnetd_main                                         372     385     +13
make_new_session                                     550     552      +2
xgetpty                                               81      78      -3
------------------------------------------------------------------------------
(add/remove: 0/0 grow/shrink: 2/1 up/down: 15/-3)              Total: 12 bytes

Signed-off-by: Denys Vlasenko <vda.linux at googlemail.com>
---
 libbb/getpty.c       | 82 +++++++++++++++++++++++++++++++++++++---------------
 networking/telnetd.c | 48 ++++++++++++++++--------------
 2 files changed, 84 insertions(+), 46 deletions(-)

diff --git a/libbb/getpty.c b/libbb/getpty.c
index 9ec6265ad..2471812c6 100644
--- a/libbb/getpty.c
+++ b/libbb/getpty.c
@@ -9,35 +9,69 @@
 
 #define DEBUG 0
 
+// On modern systems (old ways were more kludgy with setuid "pt_chown" binary):
+// ptyfd = open("/dev/ptmx"):
+// Kernel creates slave /dev/pts/N with owner = real UID of opener,
+// group and mode as configured by /dev/pts mount options.
+// The mode is safe with standard mount options (gid=5,mode=620).
+//
+// grantpt(ptyfd) is now only a verification step. In glibc:
+// Calls ioctl(fd, TIOCGPTN, &minor) to confirm ptyfd is valid master pty.
+// If ioctl fails with ENOTTY, remaps to EINVAL (POSIX).
+// Otherwise returns 0. (No chown/chmod occurs.)
+//
+// unlockpt(ptyfd) is ioctl(TIOCSPTLCK, 0): clears optional protection flag
+// on slave. If flag was set, open(slave) fails with EIO for everyone (even root)
+// until unlocked. [Do modern kernels still set the lock in open("/dev/ptmx")?]
+// The lock historically prevented opening slave before grantpt() fixes perms
+// in misconfigured/old setups where kernel didn't set 0620 atomically.
+// Today it's mostly belt-and-suspenders + POSIX compatibility.
+//
+// The attack thwarted by the lock (assuming unprivileged adversary):
+// you open /dev/ptmx, kernel allocates /dev/pts/N with overly permissive mode.
+// Adversary manages to open /dev/pts/N before your grantpt() call
+// locks down permissions.
+// Typically, /dev/pts mount options are gid=5,mode=620, 5 = "tty" group,
+// system is set up with only trusted binaries and no users in "tty" group.
+// In this case, the locking mechanism is overkill (would be safe without it).
+// To fortify more, you can mount with gid=0,mode=600. Tools like "wall",
+// "mesg" would stop working.
+// Processes with my own UID can still race against me, but I probably
+// trust myself...
+//
+// ptsname(ptyfd), internally ioctl(TIOCGPTN): get index, build
+// "/dev/pts/NN" string which refers to the slave pty.
 int FAST_FUNC xgetpty(char *line)
 {
-	int p;
-
 #if ENABLE_FEATURE_DEVPTS
-	p = open("/dev/ptmx", O_RDWR);
-	if (p >= 0) {
-		grantpt(p); /* chmod+chown corresponding slave pty */
-		unlockpt(p); /* (what does this do?) */
+	int ptyfd;
+
+	ptyfd = open("/dev/ptmx", O_RDWR);
+	if (ptyfd < 0)
+		bb_simple_perror_msg_and_die("can't find free pty");
+	grantpt(ptyfd); /* chmod+chown corresponding slave pty */
+	unlockpt(ptyfd); /* allow open() on slave /dev node */
 # ifndef HAVE_PTSNAME_R
-		{
-			const char *name;
-			name = ptsname(p); /* find out the name of slave pty */
-			if (!name) {
-				bb_simple_perror_msg_and_die("ptsname error (is /dev/pts mounted?)");
-			}
-			safe_strncpy(line, name, GETPTY_BUFSIZE);
-		}
-# else
-		/* find out the name of slave pty */
-		if (ptsname_r(p, line, GETPTY_BUFSIZE-1) != 0) {
+	{
+		const char *name;
+		name = ptsname(ptyfd); /* find out the name of slave pty */
+		if (!name) {
 			bb_simple_perror_msg_and_die("ptsname error (is /dev/pts mounted?)");
 		}
-		line[GETPTY_BUFSIZE-1] = '\0';
-# endif
-		return p;
+		safe_strncpy(line, name, GETPTY_BUFSIZE);
 	}
+# else
+	/* find out the name of slave pty */
+	if (ptsname_r(ptyfd, line, GETPTY_BUFSIZE-1) != 0) {
+		bb_simple_perror_msg_and_die("ptsname error (is /dev/pts mounted?)");
+	}
+	line[GETPTY_BUFSIZE-1] = '\0';
+# endif
+	return ptyfd;
+
 #else
 	struct stat stb;
+	int ptyfd;
 	int i;
 	int j;
 
@@ -53,13 +87,13 @@ int FAST_FUNC xgetpty(char *line)
 			line[9] = j < 10 ? j + '0' : j - 10 + 'a';
 			if (DEBUG)
 				fprintf(stderr, "Trying to open device: %s\n", line);
-			p = open(line, O_RDWR | O_NOCTTY);
-			if (p >= 0) {
+			ptyfd = open(line, O_RDWR | O_NOCTTY);
+			if (ptyfd >= 0) {
 				line[5] = 't';
-				return p;
+				return ptyfd;
 			}
 		}
 	}
-#endif /* FEATURE_DEVPTS */
 	bb_simple_error_msg_and_die("can't find free pty");
+#endif /* FEATURE_DEVPTS */
 }
diff --git a/networking/telnetd.c b/networking/telnetd.c
index 831c4f658..3eb9446a2 100644
--- a/networking/telnetd.c
+++ b/networking/telnetd.c
@@ -375,9 +375,9 @@ static int net_to_pty__have_data_to_write(void *this)
 
 	if (buf[1] >= 240 && buf[1] <= 249) {
 		/* 2-byte commands (240..250 and 255):
-		 * IAC IAC (255) Literal 255. Supported.
+		 * IAC IAC (255) Literal 255.
 		 * IAC SE  (240) End of subnegotiation. Treated as NOP.
-		 * IAC NOP (241) NOP. Supported.
+		 * IAC NOP (241) NOP.
 		 * IAC BRK (243) Break. Like serial line break. TODO via tcsendbreak()?
 		 * IAC AYT (246) Are you there.
 		 *  These don't look useful:
@@ -385,7 +385,7 @@ static int net_to_pty__have_data_to_write(void *this)
 		 * IAC IP  (244) Suspend, interrupt or abort the process. (Ancient cousin of ^C).
 		 * IAC AO  (245) Abort output. "You can continue running, but do not send me the output".
 		 * IAC EC  (247) Erase character. The receiver should delete the last received char.
-		 * IAC EL  (248) Erase line. The receiver should delete everything up tp last newline.
+		 * IAC EL  (248) Erase line. The receiver should delete everything up to last newline.
 		 * IAC GA  (249) Go ahead. For half-duplex lines: "now you talk".
 		 */
 		if (buf[1] == AYT && ts->sibling) /* notify other pipe that AYT was seen */
@@ -852,13 +852,13 @@ static void make_new_session(ioloop_state_t *io, int sockrd)
 	const int sockwr = sockrd != 0 ? sockrd : 1;
 	const char *login_argv[2];
 	struct termios termbuf;
-	int fd, pid;
+	int ptyfd, pid;
 	char tty_name[GETPTY_BUFSIZE];
 
-	/* Got a new connection, set up a tty */
-	fd = xgetpty(tty_name);
-	ndelay_on(fd);
-	close_on_exec_on(fd);
+	/* Got a new connection, set up a pty */
+	ptyfd = xgetpty(tty_name);
+	ndelay_on(ptyfd);
+	close_on_exec_on(ptyfd);
 
 	/* SO_KEEPALIVE by popular demand */
 	setsockopt_keepalive(sockrd);
@@ -885,7 +885,7 @@ static void make_new_session(ioloop_state_t *io, int sockrd)
 //Theoretically, our "WILL X" are requests and should only activate when client responds with "DO X".
 //However, we do not wait/check for "DO"s. Why?
 //There is nothing to "activate" on our side for TELOPT_ECHO.
-//For TELOPT_SGA, we don't even have code to support sending/understandig GAs,
+//For TELOPT_SGA, we don't even have code to support sending/understanding GAs,
 //so SGA is "always activated".
 		/* Just stuff it into TCP stream! (no error check...) */
 		safe_write(sockwr, iacs_to_send, sizeof(iacs_to_send));
@@ -894,8 +894,8 @@ static void make_new_session(ioloop_state_t *io, int sockrd)
 	fflush_all();
 	pid = vfork(); /* NOMMU-friendly */
 	if (pid < 0) {
-		close(fd);
-		/* socket will be closed by caller */
+		close(ptyfd);
+		close(sockrd);
 		bb_simple_perror_msg("vfork");
 		return;
 	}
@@ -904,8 +904,8 @@ static void make_new_session(ioloop_state_t *io, int sockrd)
 		pty_to_net_t *to_net;
 		net_to_pty_t *to_pty;
 
-		to_net = new_pty_to_net(fd, sockwr);
-		to_pty = new_net_to_pty(sockrd, fd);
+		to_net = new_pty_to_net(ptyfd, sockwr);
+		to_pty = new_net_to_pty(sockrd, ptyfd);
 		dbg("to_net:%p to_pty:%p", to_net, to_pty);
 		to_pty->sibling = to_net;
 		to_net->sibling = to_pty;
@@ -926,7 +926,7 @@ static void make_new_session(ioloop_state_t *io, int sockrd)
 	//pid = getpid(); // redundant, setsid gives us our pid
 	pid = setsid();
 
-	/* Open the child's side of the tty */
+	/* Open the child's side of the pty */
 	/* NB: setsid() disconnects from any previous ctty's. Therefore
 	 * we must open child's side of the tty AFTER setsid! */
 	close(0);
@@ -943,7 +943,7 @@ static void make_new_session(ioloop_state_t *io, int sockrd)
 			free(lsa);
 		}
 		write_new_utmp(pid, LOGIN_PROCESS, tty_name, /*username:*/ "LOGIN", hostname);
-		free(hostname);
+		IF_FEATURE_CLEAN_UP(free(hostname);)
 	}
 
 	/* The pseudo-terminal allocated to the client is configured to operate
@@ -969,8 +969,8 @@ static void make_new_session(ioloop_state_t *io, int sockrd)
 	login_argv[1] = NULL;
 	/* exec busybox applet (if PREFER_APPLETS=y), if that fails,
 	 * exec external program.
-	 * NB: sock is either 0 or has CLOEXEC set on it.
-	 * fd has CLOEXEC set on it too. These two fds will be closed here.
+	 * NB: sockrd is either 0 or has CLOEXEC set on it.
+	 * ptyfd has CLOEXEC set on it too. These two fds will be closed here.
 	 */
 	BB_EXECVP(G.loginpath, (char **)login_argv);
 	/* _exit is safer with vfork, and we shouldn't send message
@@ -1138,9 +1138,9 @@ static int test_vhangup(void)
 			last_change_ns = now_ns;
 		}
 
-		/* Exit if no change for 2 seconds */
+		/* Exit if no change for a few seconds */
 		if ((now_ns - last_change_ns) > 1000*1000*1000) {
-			bb_simple_info_msg("no change for 1 second, stopping read() loop");
+			bb_simple_info_msg("no change for 1 second, loop stopped");
 			break;
 		}
 
@@ -1248,6 +1248,7 @@ static int test_net_to_pty_data_integrity(void)
 			if (input_data[i + 1] == SB) {
 				/* IAC SB ... skip subnegotiation */
 				if (input_data[i + 2] == TELOPT_NAWS) {
+//TODO: simulate unusual NAWS
 					/* IAC SB TELOPT_NAWS [4 bytes] */
 					i += 3; /* skip IAC SB TELOPT_NAWS */
 					/* Skip 4 bytes of window size */
@@ -1338,7 +1339,7 @@ static int test_net_to_pty_data_integrity(void)
 		close(ptyfd);
 
 		slave_fd = xopen(tty_name, O_RDWR);
-		setsid(); /* this loses ctty (no tty signals ^C, ^Z, ^\; no SIGHUP on master close (probably?) */
+		setsid(); /* this loses ctty (no tty signals ^C, ^Z, ^\; no SIGHUP on master close (probably?)) */
 
 		tcgetattr(slave_fd, &termbuf);
 		cfmakeraw(&termbuf);
@@ -1372,7 +1373,7 @@ static int test_net_to_pty_data_integrity(void)
 				if (retry < 3) continue;
 				break; /* error (parent closed master) - expected */
 			}
-			/* If happens, probably kenel bug! */
+			/* If happens, probably kernel bug! */
 			if (retry != 0) bb_error_msg_and_die("READ DATA AFTER EOF/ERROR RETRY!!!");
 #if SAVE_NET2PTY_ACTUAL
 			xwrite(out_fd, read_buf, n);
@@ -1701,10 +1702,14 @@ int telnetd_main(int argc UNUSED_PARAM, char **argv)
 #if ENABLE_FEATURE_TELNETD_STANDALONE
 	else {
 		int master_fd = 0;
+		/* For -w SEC, listening socket is 0. Otherwise: */
 		if (!(opt & OPT_WAIT)) {
 			unsigned portnbr = CONFIG_FEATURE_TELNETD_PORT_DEFAULT;
 			if (opt & OPT_PORT)
 				portnbr = xatou16(opt_portnbr);
+			/* If someone would run "telnetd 0>&-", we can get terribly confused. */
+			/* Nake sure master_fd, or accept() result fd, etc, cant become 0 or 1: */
+			bb_sanitize_stdio();
 			master_fd = create_and_bind_stream_or_die(opt_bindaddr, portnbr);
 			xlisten(master_fd, 1);
 		}
@@ -1712,7 +1717,6 @@ int telnetd_main(int argc UNUSED_PARAM, char **argv)
 		ioloop_insert_conn(&G.io, (void *)new_accept_conn(master_fd));
 	}
 #endif
-
 	/* We don't want to die if just one session is broken */
 	signal(SIGPIPE, SIG_IGN);
 


More information about the busybox-cvs mailing list