From: Damien Miller Subject: ssh multiplexing: handle stale sockets better To: tech@openbsd.org Cc: openssh@openssh.com Date: Tue, 15 Sep 2026 17:44:28 +1000 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 .\" Copyright (c) 1995 Tatu Ylonen , 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