Skip to content

Harness fixes: ready-file budget and run scoping - #1256

Merged
philljj merged 14 commits into
wolfSSL:masterfrom
ejohnstown:cleanup-5
Sep 17, 2026
Merged

philljj merged 14 commits into
wolfSSL:masterfrom
ejohnstown:cleanup-5

Conversation

@ejohnstown

@ejohnstown ejohnstown commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Two concurrent runs of the wolfsshd suite could not both finish: a fixed port, a host-wide teardown, and pid detection by process-table diff. Three test scripts also gave the echoserver only two seconds to publish its port.

  • widen the ready-file wait to ten seconds in sftp.test, scp.test and get-put.test, matching sshclient.test, and test -s after the loop so a file caught between creation and the port write cannot pass as ready
  • give each suite run its own block of ports in place of 22222 and the five private-daemon constants, and make --port work for a local run
  • read the daemon's pid from a PidFile in its generated config, so a concurrent start is not mistaken for this one
  • stop only the daemons a run recorded, instead of every wolfsshd on the machine
  • name the tmux session and the OpenSSH-certificate teardown pattern per run

The sftp, scp and get-put scripts wait for the echoserver to publish its
port before connecting. Two seconds is not enough for a libtool re-exec,
the dynamic linker and the sample-key parse with a dozen sibling test
jobs on the machine, so a parallel make check failed them at their first
scenario. Ten seconds matches what sshclient.test already allows.

- raise the three wait loops from 20 to 100 iterations of 0.1 seconds
- test -s, not -e, after the loop in scp.test and get-put.test: the
  ready file is created empty and the port written afterward, so -e can
  take a file caught mid-write and yield an empty port
Two runs of the suite on one machine no longer collide. Each run takes a
block of ports, identifies the daemons it starts by the PID file in
their generated config, and at exit stops only those. wolfSSHd writes
that file after it has finished daemonizing, so the pid no longer has to
be guessed from what appeared in the process table.

- take a free block of ports per run, replacing the fixed 22222 and the
  constants the private daemons used
- honour --port for a local run, and pass the port to
  create_sshd_config.sh rather than baking it into the four configs
- read the daemon pid from a PidFile placed at the top of the generated
  config, ahead of any Match block, where it is applied
- stop only the daemons recorded during this run, not every wolfsshd on
  the machine
The OpenSSH certificate test kills its daemon with a pkill pattern. That
pattern now carries $WORK, the mktemp directory this invocation created,
so it names one run's daemon. "sshd_config_ossh" appears on every
concurrent run's command line, so the old pattern took their daemons
down too.
sshd_term_size_test.sh drives the client through a tmux session named
for the port it was given. The name used to be the constant "test",
which is shared across everything the user runs, so two concurrent runs
fought over one session and each EXIT trap killed the other's.
Copilot AI lite review requested due to automatic review settings September 15, 2026 17:51

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

Unresolved port allocation, certificate isolation, and PID/cleanup safety issues remain.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

This PR improves wolfsshd test reliability for slower startup and concurrent runs.

Changes:

  • Extends readiness waits and validates non-empty ready files.
  • Allocates per-run ports and supports local --port.
  • Scopes daemon, tmux, and certificate cleanup.
File summaries
File Summary
scripts/sftp.test Extends readiness polling.
scripts/scp.test Extends and validates readiness.
scripts/get-put.test Extends and validates readiness.
apps/wolfsshd/test/start_sshd.sh Adds PID tracking and scoped teardown.
apps/wolfsshd/test/sshd_term_size_test.sh Names tmux sessions per run.
apps/wolfsshd/test/sshd_privdrop_fail_test.sh Derives a private daemon port.
apps/wolfsshd/test/sshd_ossh_cert_test.sh Scopes certificate daemon cleanup.
apps/wolfsshd/test/run_all_sshd_tests.sh Allocates port blocks and handles local ports.
apps/wolfsshd/test/create_sshd_config.sh Parameterizes generated config ports.
Review details

Suppressed comments (2)

apps/wolfsshd/test/run_all_sshd_tests.sh:127

  • The requested local --port is not excluded from the block selected above. For example, --port 22303 can select base 22300, then the shared daemon uses +3 while run_hostkey_perm_check also starts on +3, causing a valid local run to fail with EADDRINUSE. Choose a block that excludes the requested port before assigning the private offsets.
LOCAL_PORT="${TEST_PORT:-$PORT_BASE}"
STRICTMODES_PORT=$((PORT_BASE + 1))
UPN_PORT=$((PORT_BASE + 2))
HOSTKEY_PERM_PORT=$((PORT_BASE + 3))

apps/wolfsshd/test/sshd_ossh_cert_test.sh:56

  • The daemon kill is now scoped to $WORK, but the certificates used by this test are still global: both runs invoke keys/renew-ossh-certs.sh and overwrite the same $LOGINUSER-ossh-*-cert.pub files. In particular, the force-command certificate embeds each run's MARKERDIR, so one run can authenticate with the other run's certificate and fail its marker assertion. Generate certificates under the per-run work directory (or serialize renewal) and use those paths for the clients.
trap 'pkill -f "wolfsshd .*$WORK" 2>/dev/null; rm -rf "$WORK"' EXIT
  • Files reviewed: 9/9 changed files
  • Comments generated: 5
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread apps/wolfsshd/test/sshd_ossh_cert_test.sh
Comment thread apps/wolfsshd/test/start_sshd.sh
Comment thread apps/wolfsshd/test/start_sshd.sh
Comment thread apps/wolfsshd/test/run_all_sshd_tests.sh Outdated
Comment thread apps/wolfsshd/test/sshd_privdrop_fail_test.sh Outdated
The registry now holds only daemons that are still running, and it is
cleaned up however the run ends. Both private-daemon ports come from the
runner, so they stay inside the block it probed even when --port moves
the shared daemon off it.

- drop a pid from the registry once stop_wolfsshd has stopped it, so the
  end-of-run sweep cannot reach a pid since recycled by another run
- run the sweep and the registry cleanup from an EXIT trap: every early
  exit used to skip them and leave the file in /tmp
- export WOLFSSHD_PRIVDROP_PORT rather than re-deriving the offset in
  sshd_privdrop_fail_test.sh, and correct that script's usage message
- quote the arguments to create_sshd_config.sh: an empty USER shifted
  the port into $1, silently leaving the daemon on 22222
- name the three tests that still read the whole process table, which
  are what keeps two concurrent runs from being fully independent
start_wolfsshd returns once the daemon accepts connections, not
once it has written its pid: wolfSSHd saves the PID file just
before tcp_listen(), so a caller connecting straight away could be
refused while the daemon's log showed no connection at all. A run
also claims its port block rather than only probing it.

- wait for the daemon's own "Listening on port" line, matched on its pid
  so a previous daemon's line in the appended log cannot satisfy it
- take a block by creating its lock directory, which mkdir makes atomic,
  and release it in the teardown; probing alone let two runners pick the
  same block, and a claim whose owner is gone is treated as stale
- check that a pid from the PID file is still a wolfsshd, since kill -0
  answers only whether some process owns the number
- fail the OpenSSH cert test when mktemp gives it no work dir: an
  empty one reduced its teardown pattern to every wolfsshd present

@wolfSSL-Fenrir-bot wolfSSL-Fenrir-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.

Fenrir Automated Review — PR #1256

No scan targets match the changed files in this PR. Review skipped.

@philljj philljj 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.

Looks good, just a few suggestions / questions on some loops

Comment thread apps/wolfsshd/test/start_sshd.sh
Comment thread apps/wolfsshd/test/start_sshd.sh
@philljj philljj assigned ejohnstown and unassigned wolfSSL-Bot Sep 16, 2026
A run's port block is leased through a directory named for the block
and for the pid holding it, in a host-wide pool under /tmp. A run
creates and removes only its own lease, and ps decides whether an
existing one is still held. The allocator moves to port_lease.sh.

- a pool under $TMPDIR was not host wide: TMPDIR is per user on macOS
  and sudo's env_reset drops it, so two runs kept private pools for one
  set of ports
- a stale lease cannot be reclaimed in place: whatever does the
  removing is authorized by an earlier read of the owner, so a second
  runner displaces the live claim the first just made
- ps -p rather than kill -0, which reports failure both for a pid that
  is gone and for one the caller may not signal -- opposite answers
  when a non-root run reads a lease held by a live root run, as in CI
- the range starts at 28000, clear of the 22000-27999 that
  scripts/fwd-bulk.test picks from and fails outright when taken
- a block holding the port passed to --port is skipped, so the shared
  daemon and a private one cannot be assigned the same port
sshd_port_lease_test.sh forks processes that contend for real leases
and checks that a block is never held by two at once. It is the first
entry in test_cases, so a broken allocator is named there rather than
surfacing as a bind collision in an unrelated test much later.

- covers a contended stale block, parallel allocation, a live lease, a
  live owner this run cannot signal, release and reuse, and --port
- a winner holds its lease until the parent releases it, so a straggler
  cannot take a block already counted and read as a second winner
- the cross-user case drops to $SUDO_USER, the one arrangement that
  tells "owner gone" from "owner not mine"
- contends over 29000-29511, below the ephemeral range and away from
  the suite's daemon, with the scan pinned so --port has a block to skip
start_wolfsshd now records whether the daemon's listening line appeared
in the ten seconds it waits for it. Without one it kills the daemon and
clears PID, which puts the start through the empty-PID check every
caller already has, so the run stops and names the daemon.

- the pid reaches the run registry before that check, so a daemon that
  came up but never bound is still reaped by the runner's teardown
stop_wolfsshd escalates to SIGKILL when SIGTERM has left the daemon
running after five seconds, and drops the pid from the run registry
only once it is gone. wolfsshd_alive answers whether the pid is still a
wolfsshd, which is what each of those steps turns on.

- the entry was dropped unconditionally, so a stuck daemon lost its
  last handle and held its port for the rest of the run
- kill -0 says only that some process holds the pid, and this sends
  SIGKILL as root, so a recycled pid was killed outright
- a recycled pid now counts as stopped and leaves the registry
Dropping a stopped daemon from the run registry now installs the
rewritten file only when grep either kept lines or matched none. Any
other status is grep failing, and the empty file it leaves behind would
become the registry, losing every other daemon's pid.

- that registry is what the end-of-run sweep works from, so blanking it
  strands whatever else the run started
The exit teardown stops the shared daemon before sweeping the registry.
stop_wolfsshd is what removes the per-daemon temp directory, which
holds the rewritten config and root-owned copies of the trust anchors,
and it is idempotent, so a run that stopped already is unaffected.

- a run that exits early, on a daemon that will not start or on a
  failed test, reaches no stop of its own and left the directory in
  /tmp with a copy of the host key in it
sshd_ossh_cert_test.sh generates its OpenSSH certificates into its own
work directory, through the OSSH_CERT_DIR that renew-ossh-certs.sh now
reads. Only the keypairs stay shared, and those are committed.

- the names carry just the login user, so two runs in one checkout
  reissued each other's certificates and the force-command case failed
  on a marker written to the other run's directory
sshd_large_sftp_test.sh puts its pid in both the local and the remote
file name, and removes them from an EXIT trap.

- the remote name was fixed in the daemon user's home, one directory
  for the whole host, so two runs uploaded to the same path and each
  compared its own file against the other's upload
- 4.4G apiece, and the transfer runs under "set -e", so a failed run
  left both behind
@ejohnstown
ejohnstown requested a review from philljj September 16, 2026 22:55
@ejohnstown ejohnstown assigned wolfSSL-Bot and unassigned ejohnstown Sep 16, 2026
@philljj
philljj merged commit 4ca96ec into wolfSSL:master Sep 17, 2026
185 checks passed
@ejohnstown
ejohnstown deleted the cleanup-5 branch September 17, 2026 15:56
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.

5 participants