Skip to content

draft: prevent LocalCommand 'started' leak on stale SSH master socket#569

Open
DarthPJB wants to merge 1 commit into
DeterminateSystems:mainfrom
DarthPJB:fix/ssh-master-localcommand-protocol-leak
Open

draft: prevent LocalCommand 'started' leak on stale SSH master socket#569
DarthPJB wants to merge 1 commit into
DeterminateSystems:mainfrom
DarthPJB:fix/ssh-master-localcommand-protocol-leak

Conversation

@DarthPJB

@DarthPJB DarthPJB commented Jul 16, 2026

Copy link
Copy Markdown

Motivation

When maxConnections > 1 (the Determinate default is 64), SSHMaster creates SSH masters with -M -N. Command SSHs connect through the master socket. When the master dies (ControlPersist=no is the default with -M), the socket becomes stale. The next command SSH falls back to a direct connection, which runs LocalCommand=echo started. Since startCommand() skips reading "started" when useMaster=true (it assumes the master already consumed it), the string leaks into the nix protocol stream:

error: cannot open connection to remote store 'ssh-ng://build@10.10.127.43': protocol mismatch, got 'started'

Upstream Nix defaults maxConnections to 1, so useMaster=false and the bug is never reachable. Determinate's default of 64 enables SSH master mode, exposing this latent code path.

Context

Implementation

Two changes in src/libstore/ssh.cc:

1. Override LocalCommand to no-op on command SSHs when useMaster=true

In startCommand(), after extraSshArgs are spliced into the args list:

if (useMaster)
    args.push_back(OS_STR("-oLocalCommand=true"));

SSH processes -o options in order; the last value for a keyword wins. addCommonSSHOpts() adds -oLocalCommand=echo started earlier. Our override -oLocalCommand=true (the POSIX no-op command) comes later and wins.
Behavioral matrix after fix:

Scenario LocalCommand fires? What runs? stdout output
Through live multiplex No Nothing (correct)
Direct connection (no master) Yes echo started "started" consumed by startCommand() (correct)
Fallback (stale socket) Yes true (no-op) Nothing (correct)

2. (Optional) Change ControlPersist from no to 15m

In 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

  1. Always consume "started" in startCommand() — Broken. When useMaster=true and master is alive, command SSH stdout is the nix protocol stream. readLine() would read WORKER_MAGIC_2 as "started", causing immediate failure.
  2. Detect dead master before consuming "started" — Inherently racy. isMasterRunning() is a point-in-time check; the master can die between the check and the read (TOCTOU).
  3. -F /dev/null to ignore SSH config — Breaks SSH config-based ProxyJump, IdentityFile, and StrictHostKeyChecking.
  4. Set max-connections=1 for ssh-ng in machines.cc — Limits functionality. Defeats the purpose of the maxConnections=64 default.
    The chosen approach (no-op LocalCommand override) is deterministic, eliminates the bug at the producer side, has no race conditions, and is backward compatible.

Summary by CodeRabbit

  • Bug Fixes
    • Improved reliability of SSH command sessions when master connections end unexpectedly and fall back to direct connections.
    • Prevented master “started” handshake output from being sent into the protocol stream during fallback.
    • Enhanced cached SSH control-socket reuse by verifying the master is still alive before reusing the connection.

@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0490e22c-0fc0-4f41-b1da-4fc5902c491f

📥 Commits

Reviewing files that changed from the base of the PR and between 42e1e1a and 16e8d81.

📒 Files selected for processing (1)
  • src/libstore/ssh.cc

📝 Walkthrough

Walkthrough

Updates 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.

Changes

SSH master handling

Layer / File(s) Summary
Fallback command protocol isolation
src/libstore/ssh.cc
Master-backed command sessions override LocalCommand with a no-op and skip waiting for the started stdout line when using fallback connections.
Cached master liveness validation
src/libstore/ssh.cc
Cached control sockets are reused only after waitpid confirms the master is alive; interrupted waits assume liveness, while reaped children release the cached PID handle.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

Suggested reviewers: edolstra

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: preventing LocalCommand 'started' leakage with a stale SSH master socket.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 178b88d5-3355-407d-8546-c2fcffb19cf9

📥 Commits

Reviewing files that changed from the base of the PR and between 469a08e and 7bba349.

📒 Files selected for processing (1)
  • src/libstore/ssh.cc

Comment thread src/libstore/ssh.cc Outdated
Comment thread src/libstore/ssh.cc Outdated
@DarthPJB
DarthPJB force-pushed the fix/ssh-master-localcommand-protocol-leak branch from 7bba349 to 42e1e1a Compare July 18, 2026 12:07

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 67dfe1cf-c51d-403c-8c6d-999e8ede838f

📥 Commits

Reviewing files that changed from the base of the PR and between 7bba349 and 42e1e1a.

📒 Files selected for processing (1)
  • src/libstore/ssh.cc

Comment thread src/libstore/ssh.cc Outdated
Comment on lines +200 to +201
if (useMaster)
args.push_back(OS_STR("-oLocalCommand=true"));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 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.

@DarthPJB DarthPJB changed the title fix: prevent LocalCommand 'started' leak on stale SSH master socket draft: prevent LocalCommand 'started' leak on stale SSH master socket Jul 18, 2026
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
@DarthPJB
DarthPJB force-pushed the fix/ssh-master-localcommand-protocol-leak branch from 42e1e1a to 16e8d81 Compare July 18, 2026 18:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants