draft: prevent LocalCommand 'started' leak on stale SSH master socket#569
draft: prevent LocalCommand 'started' leak on stale SSH master socket#569DarthPJB wants to merge 1 commit into
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughUpdates SSH command fallback handling to prevent the master startup handshake from entering the Nix protocol stream, and adds cached SSH master PID liveness checks before reusing control sockets. ChangesSSH master handling
Estimated code review effort: 2 (Simple) | ~10 minutes Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/libstore/ssh.cc`:
- Around line 194-201: Update the SSH argument handling around the useMaster
condition to locate the existing LocalCommand=echo started option and replace it
in place with the no-op LocalCommand setting. Do not append a second
LocalCommand argument, ensuring fallback direct connections cannot emit started
into the Nix protocol stream.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
7bba349 to
42e1e1a
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/libstore/ssh.cc`:
- Around line 200-201: Update the master SSH option construction in the
useMaster branch of ssh.cc so the ControlPersist setting is 15m instead of
disabled. Ensure the corresponding master startup option near the existing
ControlPersist=no configuration uses -oControlPersist=15m, preserving the other
SSH arguments unchanged.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
| if (useMaster) | ||
| args.push_back(OS_STR("-oLocalCommand=true")); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Apply the intended ControlPersist=15m change.
The master is still started with -oControlPersist=no at Line [273], so it exits after the last multiplexed session and later commands can repeatedly encounter the stale socket/fallback path. Change the master option to -oControlPersist=15m as described by the PR objective.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/libstore/ssh.cc` around lines 200 - 201, Update the master SSH option
construction in the useMaster branch of ssh.cc so the ControlPersist setting is
15m instead of disabled. Ensure the corresponding master startup option near the
existing ControlPersist=no configuration uses -oControlPersist=15m, preserving
the other SSH arguments unchanged.
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#441, NixOS#14132
42e1e1a to
16e8d81
Compare
Motivation
When
maxConnections > 1(the Determinate default is 64),SSHMastercreates SSH masters with-M -N. Command SSHs connect through the master socket. When the master dies (ControlPersist=nois the default with-M), the socket becomes stale. The next command SSH falls back to a direct connection, which runsLocalCommand=echo started. SincestartCommand()skips reading"started"whenuseMaster=true(it assumes the master already consumed it), the string leaks into the nix protocol stream:Upstream Nix defaults
maxConnectionsto 1, souseMaster=falseand the bug is never reachable. Determinate's default of 64 enables SSH master mode, exposing this latent code path.Context
ControlMaster autobreaksssh-ng://remote store NixOS/nix#14132 — "SSHControlMaster autobreaksssh-ng://remote store"ControlMasterandControlPersistNixOS/nix#8329 — same bug variant withControlPersist=yesLocalCommand: SSH: don't erase password prompt if it is displayed NixOS/nix#8018 / PR SSH: don't erase password prompt if it is displayed NixOS/nix#8018 — introducedLocalCommand=echo startedto prevent progress bar output from garbling SSH password promptsThe
LocalCommand=echo startedmechanism was designed for theuseMaster=falsecase (direct connections). WhenuseMaster=true, the code correctly skips reading"started"from command SSHs because OpenSSH does not runLocalCommandon connections through a live master socket. The bug only manifests when the master is dead and the command SSH falls back to a direct connection.Implementation
Two changes in
src/libstore/ssh.cc:1. Override
LocalCommandto no-op on command SSHs whenuseMaster=trueIn
startCommand(), afterextraSshArgsare spliced into the args list:SSH processes
-ooptions in order; the last value for a keyword wins.addCommonSSHOpts()adds-oLocalCommand=echo startedearlier. Our override-oLocalCommand=true(the POSIX no-op command) comes later and wins.Behavioral matrix after fix:
LocalCommandfires?echo started"started"consumed bystartCommand()(correct)true(no-op)2. (Optional) Change
ControlPersistfromnoto15mIn
startMaster():- OsStrings args = {"ssh", hostnameAndUser.c_str(), "-M", "-N", "-oControlPersist=no"}; + OsStrings args = {"ssh", hostnameAndUser.c_str(), "-M", "-N", "-oControlPersist=15m"};This keeps masters alive longer, reducing the frequency of master death and fallback scenarios. Not required for the fix (Vector 4 handles the fallback correctly), but a pragmatic performance optimization.
Alternative Approaches Considered
"started"instartCommand()— Broken. WhenuseMaster=trueand master is alive, command SSH stdout is the nix protocol stream.readLine()would readWORKER_MAGIC_2as"started", causing immediate failure."started"— Inherently racy.isMasterRunning()is a point-in-time check; the master can die between the check and the read (TOCTOU).-F /dev/nullto ignore SSH config — Breaks SSH config-basedProxyJump,IdentityFile, andStrictHostKeyChecking.max-connections=1forssh-nginmachines.cc— Limits functionality. Defeats the purpose of themaxConnections=64default.The chosen approach (no-op
LocalCommandoverride) is deterministic, eliminates the bug at the producer side, has no race conditions, and is backward compatible.Summary by CodeRabbit