Index | Thread | Search

From:
Damien Miller <djm@mindrot.org>
Subject:
ssh multiplexing: handle stale sockets better
To:
tech@openbsd.org
Cc:
openssh@openssh.com
Date:
Tue, 15 Sep 2026 17:44:28 +1000

Download raw body.

Thread
Hi,

This makes ssh create it multiplexing socket earlier when in master
mode and more gracefully handle the case where it already exists.

This is https://bugzilla.mindrot.org/show_bug.cgi?id=3971

ok?

Index: usr.bin/ssh/clientloop.h
===================================================================
RCS file: /cvs/src/usr.bin/ssh/clientloop.h,v
diff -u -p -r1.41 clientloop.h
--- usr.bin/ssh/clientloop.h	3 Mar 2026 09:57:25 -0000	1.41
+++ usr.bin/ssh/clientloop.h	15 Sep 2026 07:42:48 -0000
@@ -78,6 +78,7 @@ void client_expect_confirm(struct ssh *,
 #define SSHMUX_COMMAND_CONNINFO		9	/* Show connection information */
 #define SSHMUX_COMMAND_CHANINFO		10	/* Show channels information */
 
+int	muxserver(const char *);
 void	muxserver_listen(struct ssh *);
 int	muxclient(const char *);
 void	mux_exit_message(struct ssh *, Channel *, int);
Index: usr.bin/ssh/mux.c
===================================================================
RCS file: /cvs/src/usr.bin/ssh/mux.c,v
diff -u -p -r1.113 mux.c
--- usr.bin/ssh/mux.c	2 Apr 2026 07:39:57 -0000	1.113
+++ usr.bin/ssh/mux.c	15 Sep 2026 07:42:49 -0000
@@ -1313,21 +1313,20 @@ mux_tty_alloc_failed(struct ssh *ssh, Ch
 	sshbuf_free(m);
 }
 
-/* Prepare a mux master to listen on a Unix domain socket. */
-void
-muxserver_listen(struct ssh *ssh)
+/*
+ * Create a listening Unix domain socket for mux master or find out that
+ * one exists already.
+ */
+int
+muxserver(const char *path)
 {
 	mode_t old_umask;
-	char *orig_control_path = options.control_path;
+	char *tmp_path;
 	char rbuf[16+1];
 	u_int i, r;
 	int oerrno;
 
-	if (options.control_path == NULL ||
-	    options.control_master == SSHCTL_MASTER_NO)
-		return;
-
-	debug("setting up multiplex master socket");
+	debug("trying to set-up a multiplex master socket");
 
 	/*
 	 * Use a temporary path before listen so we can pseudo-atomically
@@ -1342,51 +1341,53 @@ muxserver_listen(struct ssh *ssh)
 		    '0' + r - 26 - 26;
 	}
 	rbuf[sizeof(rbuf) - 1] = '\0';
-	options.control_path = NULL;
-	xasprintf(&options.control_path, "%s.%s", orig_control_path, rbuf);
-	debug3_f("temporary control path %s", options.control_path);
+	xasprintf(&tmp_path, "%s.%s", path, rbuf);
+	debug3_f("temporary control path %s", tmp_path);
 
 	old_umask = umask(0177);
-	muxserver_sock = unix_listener(options.control_path, 64, 0);
+	muxserver_sock = unix_listener(tmp_path, 64, 0);
 	oerrno = errno;
 	umask(old_umask);
 	if (muxserver_sock < 0) {
 		if (oerrno == EINVAL || oerrno == EADDRINUSE) {
-			error("ControlSocket %s already exists, "
-			    "disabling multiplexing", options.control_path);
- disable_mux_master:
-			if (muxserver_sock != -1) {
-				close(muxserver_sock);
-				muxserver_sock = -1;
-			}
-			free(orig_control_path);
-			free(options.control_path);
-			options.control_path = NULL;
-			options.control_master = SSHCTL_MASTER_NO;
-			return;
+			error("Temporary ControlSocket %s already exists", tmp_path);
+			free(tmp_path);
+			return -1;
 		} else {
 			/* unix_listener() logs the error */
 			cleanup_exit(255);
 		}
 	}
+	set_nonblock(muxserver_sock);
 
 	/* Now atomically "move" the mux socket into position */
-	if (link(options.control_path, orig_control_path) != 0) {
+	if (link(tmp_path, path) != 0) {
 		if (errno != EEXIST) {
 			fatal_f("link mux listener %s => %s: %s",
-			    options.control_path, orig_control_path,
+			    tmp_path, path,
 			    strerror(errno));
 		}
-		error("ControlSocket %s already exists, disabling multiplexing",
-		    orig_control_path);
-		unlink(options.control_path);
-		goto disable_mux_master;
+		debug("ControlSocket %s already exists", path);
+		close(muxserver_sock);
+		muxserver_sock = -1;
 	}
-	unlink(options.control_path);
-	free(options.control_path);
-	options.control_path = orig_control_path;
+	unlink(tmp_path);
+	free(tmp_path);
+	return muxserver_sock;
+}
 
-	set_nonblock(muxserver_sock);
+/* Prepare a mux master to listen on the previously created Unix domain socket. */
+void
+muxserver_listen(struct ssh *ssh)
+{
+	if (options.control_path == NULL ||
+	    options.control_master == SSHCTL_MASTER_NO)
+		return;
+
+	debug("setting up multiplex master socket");
+	if (muxserver_sock < 0) {
+		muxserver_sock = muxserver(options.control_path);
+	}
 
 	mux_listener_channel = channel_new(ssh, "mux listener",
 	    SSH_CHANNEL_MUX_LISTENER, muxserver_sock, muxserver_sock, -1,
Index: usr.bin/ssh/ssh-keygen.1
===================================================================
RCS file: /cvs/src/usr.bin/ssh/ssh-keygen.1,v
diff -u -p -r1.241 ssh-keygen.1
--- usr.bin/ssh/ssh-keygen.1	15 Sep 2026 07:40:58 -0000	1.241
+++ usr.bin/ssh/ssh-keygen.1	15 Sep 2026 07:42:49 -0000
@@ -1,4 +1,4 @@
-.\"	$OpenBSD: ssh-keygen.1,v 1.241 2026/09/15 07:40:58 djm Exp $
+.\"	$OpenBSD: ssh-keygen.1,v 1.240 2026/08/07 05:49:53 djm Exp $
 .\"
 .\" Author: Tatu Ylonen <ylo@cs.hut.fi>
 .\" Copyright (c) 1995 Tatu Ylonen <ylo@cs.hut.fi>, Espoo, Finland
@@ -35,7 +35,7 @@
 .\" (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 .\" THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 .\"
-.Dd $Mdocdate: September 15 2026 $
+.Dd $Mdocdate: August 7 2026 $
 .Dt SSH-KEYGEN 1
 .Os
 .Sh NAME
@@ -289,7 +289,7 @@ When saving a private key, this option s
 rounds used.
 Higher numbers result in slower passphrase verification and increased
 resistance to brute-force password cracking (should the keys be stolen).
-The default is 24 rounds.
+The default is 16 rounds.
 .It Fl B
 Show the bubblebabble digest of specified private or public key file.
 .It Fl b Ar bits
Index: usr.bin/ssh/ssh.c
===================================================================
RCS file: /cvs/src/usr.bin/ssh/ssh.c,v
diff -u -p -r1.637 ssh.c
--- usr.bin/ssh/ssh.c	7 Aug 2026 05:03:56 -0000	1.637
+++ usr.bin/ssh/ssh.c	15 Sep 2026 07:42:49 -0000
@@ -1612,7 +1612,11 @@ main(int ac, char **av)
 		fatal("No ControlPath specified for \"-O\" command");
 	if (options.control_path != NULL) {
 		int sock;
-		if ((sock = muxclient(options.control_path)) >= 0) {
+
+		if (muxclient_command == 0 &&
+		    (sock = muxserver(options.control_path)) >= 0) {
+			debug("We will be multiplex master, not client.");
+		} else if ((sock = muxclient(options.control_path)) >= 0) {
 			if (ssh_packet_set_connection(ssh, sock, sock) == NULL)
 				fatal("ssh_packet_set_connection failed");
 			ssh_packet_set_mux(ssh);
Index: regress/usr.bin/ssh/multiplex.sh
===================================================================
RCS file: /cvs/src/regress/usr.bin/ssh/multiplex.sh,v
diff -u -p -r1.41 multiplex.sh
--- regress/usr.bin/ssh/multiplex.sh	7 Dec 2025 02:59:53 -0000	1.41
+++ regress/usr.bin/ssh/multiplex.sh	15 Sep 2026 07:42:49 -0000
@@ -42,6 +42,24 @@ if [ $? -ne 0 ]; then
 	fail "environment not found"
 fi
 
+start_auto_mux_master()
+{
+	trace "start master (with ControlMaster=auto), fork to background"
+	${SSH} -Nn2 -o ControlMaster=auto -S$CTL -F $OBJ/ssh_config -oSendEnv="_XXX_TEST" somehost \
+	    -E $TEST_REGRESS_LOGFILE 2>&1 &
+	# NB. $SSH_PID will be killed by test-exec.sh:cleanup on fatal errors.
+	SSH_PID=$!
+	sleep 2
+	wait_for_mux_master_ready
+}
+
+verbose "test $tid: stale control socket"
+trace "correctly handle stale control socket"
+kill -9 ${SSH_PID} 2>/dev/null
+wait ${SSH_PID}
+test -e "$CTL" || fail "control socket did not remain (after killing ssh command)"
+start_auto_mux_master
+
 verbose "test $tid: envpass"
 trace "env passing over multiplexed connection"
 ${SSH} -F $OBJ/ssh_config -oSetEnv="_XXX_TEST=foo" -S$CTL otherhost sh << 'EOF'
@@ -189,6 +207,7 @@ ${SSH} -F $OBJ/ssh_config -S $CTL -Oexit
 # Wait for master to exit
 wait $SSH_PID
 kill -0 $SSH_PID >/dev/null 2>&1 && fail "exit command failed"
+test ! -e "$CTL" || fail "control socket still exists after exit command"
 
 # Enable compression and alternative kex for next conninfo test.
 if $SSH -Q compression | grep zlib@openssh.com >/dev/null; then