From 16e8d81a64234bba2a027313a28ee8bb1aeb3442 Mon Sep 17 00:00:00 2001 From: John Bargman Date: Thu, 16 Jul 2026 10:47:38 +0000 Subject: [PATCH] fix: replace LocalCommand in-place and fix stale master detection When useMaster=true, command SSHs through a live master socket do not run LocalCommand. But when the master dies (ControlPersist=no) and the command SSH falls back to a direct connection, LocalCommand fires and 'echo started' leaks into the nix protocol stream, causing: error: protocol mismatch, got 'started' This fix: 1. Replaces all -oLocalCommand=* args with -oLocalCommand=true on command SSHs when useMaster=true (OpenSSH uses first-match-wins, so appending would be silently ignored; prefix match handles NIX_SSHOPTS and extraSshArgs injection). 2. Skips the readLine('started') readiness check when useMaster=true, since LocalCommand is now a no-op. The command SSH either connects through the live master or fails loudly via stderr. 3. Fixes startMaster() stale state: checks master process liveness with waitpid(WNOHANG) before returning cached socket path. Uses Pid::release() (not operator=) to avoid kill()/wait() on the already-reaped child. Handles EINTR/ECHILD properly. Refs: DeterminateSystems/nix-src#441, NixOS/nix#14132 --- src/libstore/ssh.cc | 38 +++++++++++++++++++++++++++++++++----- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/src/libstore/ssh.cc b/src/libstore/ssh.cc index 5e82ecbbbd69..2dc76a1b3ded 100644 --- a/src/libstore/ssh.cc +++ b/src/libstore/ssh.cc @@ -7,6 +7,11 @@ #include "nix/util/exec.hh" #include "nix/util/base-n.hh" +#ifndef _WIN32 +# include +# include +#endif + namespace nix { static std::string parsePublicHostKey(std::string_view host, std::string_view sshPublicHostKey) @@ -191,6 +196,16 @@ std::unique_ptr SSHMaster::startCommand(OsStrings && comm if (verbosity >= lvlChatty) args.push_back("-v"); args.splice(args.end(), std::move(extraSshArgs)); + // Override LocalCommand to no-op on command SSHs; master + // already consumed "started". On fallback, "echo started" + // would leak into the nix protocol stream. #441 + if (useMaster) { + for (auto & arg : args) { + if (arg.starts_with(OS_STR("-oLocalCommand="))) { + arg = OS_STR("-oLocalCommand=true"); + } + } + } args.push_back("--"); } @@ -206,9 +221,9 @@ std::unique_ptr SSHMaster::startCommand(OsStrings && comm in.readSide = INVALID_DESCRIPTOR; out.writeSide = INVALID_DESCRIPTOR; - // Wait for the SSH connection to be established, - // So that we don't overwrite the password prompt with our progress bar. - if (!fakeSSH && !(socketPath && isMasterRunning(*socketPath))) { + // Skip readLine when useMaster: SSH connects via master socket, or + // falls back to direct connection where LocalCommand is now a no-op. + if (!fakeSSH && !useMaster && !(socketPath && isMasterRunning(*socketPath))) { std::string reply; try { reply = readLine(out.readSide.get()); @@ -237,8 +252,21 @@ std::optional SSHMaster::startMaster() auto state(state_.lock()); - if (state->sshMaster != INVALID_DESCRIPTOR) - return state->socketPath; + if (state->sshMaster != INVALID_DESCRIPTOR) { + // Check if master is still alive before returning cached socket. + pid_t result = waitpid(state->sshMaster, nullptr, WNOHANG); + if (result == 0) + return state->socketPath; + if (result == -1) { + if (errno == EINTR) + return state->socketPath; // assume alive; worst case SSH falls back + if (errno != ECHILD) + warn("waitpid failed for SSH master %d: %s", + (pid_t) state->sshMaster, strerror(errno)); + } + // Master gone — release Pid to avoid kill()/wait() on already-reaped child. + state->sshMaster.release(); + } state->socketPath = tmpDir->path() / "ssh.sock";