SSH integration tests share one sshd user and collide under parallel jobs #27
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Now that several jobs run
scripts/verify integrationon the dev VM at the same time, the SSH tests (crates/ssh/tests/real_sshd.rsand the SSH contract gate) fail intermittently: host-trust setup errors, and theowner_loss_terminates_shellflake seen twice before.Likely cause: every run shares the single test
sshdon 127.0.0.1:2222 and the single usersshtarget, and each test run rewrites/home/sshtarget/.ssh/authorized_keys(last rewritten at 09:54 CEST today, during parallel runs). Concurrent runs overwrite each other's keys and host-key expectations.Fix: each test run gets its own isolated SSH target: an ephemeral
sshdon a random localhost port (or in a labelled throwaway container) with its own host key, its own user orAuthorizedKeysFile, and teardown. Keep real sshd (no mocks). Updatescripts/provision-dev-vmif the shared target is no longer needed.Starting work in
/home/kayg/wt/ssh-test-isolationon branchssh-test-isolation, based ondevat7bda001e5bd8ff42c3768fb1e6cbba317d22789e.I will isolate every SSH test and benchmark behind a real per-run local
sshd, identify the acknowledgement timeout's cause, remove the sharedsshtargetfixture if no other consumer remains, and run the requested concurrent SSH suite plus verification gates.Research and fixture decision:
sshd -D -f <private config>on a random loopback port. On this Debian 13 VM, I ran it as uid 1001 (kayg) without sudo. A generated per-run host key and per-runAuthorizedKeysFileauthenticated a real SSH command askayg, withUsePAM noandStrictModes yes.sshd_config(5)explicitly saysUsePAM yesprevents runningsshdas non-root; it documentsStrictModesandAuthorizedKeysFile: https://manpages.debian.org/trixie/openssh-server/sshd_config.5.en.html.sshd(8)documents foreground-Dand per-server key/config options: https://manpages.debian.org/trixie/openssh-server/sshd.8.en.html.sshtargetfailed withFailed to set uids to 30033, while the daemon ran as uid 1001. An unprivileged daemon can serve the test process's own login UID, so tests and benches will use that account.StrictModes yesrejects an authorized-keys path under world-writable/tmp; the per-run directory must be placed under a user-owned, non-writable parent such as the test user's home.The new unprivileged sshd exposed a runner-specific PTY issue: the SSH test logs in as the current user, whose interactive
.bashrcstarts Zellij. The shell command was echoed, then Zellij took over the terminal;owner_loss_terminates_shelltimed out waiting for24 80without any timeout change. AddingForceCommand /bin/bash --noprofile --norcand disabling the MOTD gives the test a clean shell. The focused owner-loss test now passes in 1.19s with the existing deadline. This is separate from the shared authorized-keys race the isolation change removes.Concurrency evidence: launched four independent
real_sshdtest-binary processes together (PIDs 275699, 275700, 275701, 275702), each with--ignored --nocapture --test-threads=1. All exited 0:test result: ok. 11 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 16.24stest result: ok. 11 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 16.52stest result: ok. 11 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 16.02stest result: ok. 11 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 16.36sReview follow-up evidence and decisions:
setsid(), so the test daemon's original process group cannot clean them up.TestSshd::dropnow stops/reaps the listener first and repeatedly sweeps per-runSetEnvmarkers so no new detached session can escape the scan. Each signal usespidfd_open/pidfd_send_signalafter rechecking the process start time and marker; this avoids the check-to-killpgPID reuse window. See the OpenSSH PTY setup source: https://github.com/openssh/openssh-portable/blob/master/sshpty.c and Rustix process API: https://docs.rs/rustix/latest/rustix/process/fn.pidfd_send_signal.htmlssh.socketcan bind port 22 independently ofsshd_config. Provisioning now masks it before OpenSSH package installation and verifies the service is inactive afterward. The temporary loopback-only config protects a service-start during package setup.Finding from the first four-process run: one process failed
pty_resize_and_replayatassert!(replayed.gap()); its other 11 tests passed. The test slept for one second after sending 2 MiB and could assert before the output had crossed the 1 MiB replay limit under concurrent VM load. Replaced the sleep with a completion file written by the real remote shell after the 2 MiB stream; the test still does not drain PTY output before checking the replay gap. Targeted rerun passed:1 passed; 0 failed; finished in 3.62s. I will repeat the four-process proof after the full gates.Second replay-test finding: after the remote command completed, the SSH client could still be publishing buffered PTY output. Reattaching from offset 0 queued a full replay window plus live output; under four simultaneous suites the new consumer was correctly terminated as
slow_consumerbefore reaching the marker. The test now waits until the SSH stream has produced 2 MiB, reattaches at that tail offset to consume the completion marker, then separately attaches from offset 0 to assert the replay gap. All four concurrent test-binary processes passed: each reported12 passed; 0 failed(16.15s, 16.66s, 16.59s, 16.50s).Finished on
ssh-test-isolation, head855fbe6cb1e3e2969f5cca4719d15b6869635830(base7bda001e5bd8ff42c3768fb1e6cbba317d22789e).Approach. Each SSH test run starts a real foreground
sshd -Don an ephemeral loopback port, with a private host key,AuthorizedKeysFile, config and log in a temp directory. It usesUsePAM noandStrictModes yes. The parent owns teardown; Drop stops the listener and sweeps detached test processes, and suite startup removes stale marked processes. An unprivilegedsshdcan serve the process owner's UID, but cannot switch to a different login UID, so this isolates the test runner's account. I chose it over a container because it uses the installed OpenSSH binary directly and needs no container engine or image. See Debian sshd(8) for foreground mode and Debian sshd_config(5) for the PAM, authorized-keys and strict-mode settings.owner_loss_terminates_shellwas timing out because the runner's interactive.bashrcstarts Zellij and hijacks the PTY. The isolated server usesForceCommand /bin/bash --noprofile --norc; the test passes with its original timeout. No production SSH behavior changed. The sharedsshtargetfixture was removed fromscripts/provision-dev-vm; no SSH test or benchmark uses it.Four concurrent SSH suites: all four processes exited 0, with 12 passed and 0 failed each:
scripts/verify fastoutput:scripts/verify integrationoutput:No push; issue remains open.
Starting independent review and verification on branch
ssh-test-isolation, worktree/home/kayg/wt/ssh-test-isolation, at head855fbe6cb1e3e2969f5cca4719d15b6869635830(job base7bda001e5bd8ff42c3768fb1e6cbba317d22789e). Fetched currentdevatd4d16de95dfe5c2519c48cb251bcf086305268da; merging it before review as requested. I will check isolated real-sshd runs, strict host-key behavior, shell setup, allsshtargetreferences, then runscripts/verify fastandscripts/verify integration.SSH test review — #27
Branch
ssh-test-isolation, final headfc3717b743d46f33ce610955d86e81ca82a76bcd. Merged currentorigin/devatf1bedf994ce5399cf4b53ff0ca693ff0db4cfb9d. No push.15 passed; 0 failed; the contended run began at one-minute load 12.07. Six simultaneously held fixtures had six distinct ports, host-key fingerprints and privateauthorized_keyspaths (mode0600). After SIGKILLing all six fixture owners, no fixture directory or matchingsshdlistener remained. The regular successful suites also left no run directories or listeners. The SIGKILL leak found during review was fixed in54af6d685dab2e28781e6326ac72db5439586667, with a regression test./usr/sbin/sshd(OpenSSH_10.0p2 Debian-7+deb13u4).strict_host_key_changepassed in all six suites: after trusting the first server key, a second real server on the same port with a different host key is refused asHostKeyMismatch.ForceCommand /bin/bash --noprofile --norccontrols shell startup while retaining real OpenSSH, authentication, host-key checks and PTY behavior.owner_loss_terminates_shellpassed in all six suites. Product SSH sessions request a PTY and the user's shell, with no startup suppression. A user's shell startup file that launches Zellij can take over that PTY; track this as a later product issue, not a test defect. OpenSSH ForceCommand, Bash startup files.sshtarget,127.0.0.1:2222orlocalhost:2222references.scripts/verify integrationpassed all 13 steps, including isolated real sshd.scripts/verify fastfailed twice in unchangedpackages/stream: both runs failed the 4108 close-code assertion; the first also failed the 1008 burst-close assertion.git diff origin/dev -- packages/streamis empty. [blocks-merge] The required fast gate is still red on this branch due to that existing stream test failure; no stream files were changed in this SSH-scoped job.Gate output
fast(second run; output lines quoted verbatim):integration(output lines quoted verbatim):Orchestrator: merged to
devatba5f99cand pushed. The merged tree is identical to the reviewed headfc3717b. Gates: integration 13/13 (from the review); fast 15/15 on two re-runs, after the review's redfastcame from the known flaky stream Vitest case (#31). The product follow-up is #32 (a user rc file starting a multiplexer, for 14b).