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
  • Damien Miller:

    ssh multiplexing: handle stale sockets better

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