diff --git a/apps/wolfsshd/test/port_lease.sh b/apps/wolfsshd/test/port_lease.sh new file mode 100755 index 00000000..317bae18 --- /dev/null +++ b/apps/wolfsshd/test/port_lease.sh @@ -0,0 +1,126 @@ +#!/bin/bash + +# Port-block leases for one run of the wolfSSHd test suite. +# +# Sourced by run_all_sshd_tests.sh, which takes a block for its run, and by +# sshd_port_lease_test.sh, which races several processes through these +# functions to check that a block is never handed to two runs at once. Kept +# apart from the runner so that test can load the allocator without running +# the suite. + +: "${PORT_BLOCK_FIRST:=28000}" +: "${PORT_BLOCK_SIZE:=8}" +: "${PORT_BLOCK_COUNT:=64}" + +# True when something is already listening. A shell built without /dev/tcp +# fails here exactly as a refused connection does, which degrades to taking +# the pid-derived block unprobed -- still per run, just unverified. +port_in_use() { + (exec 3<>"/dev/tcp/127.0.0.1/$1") 2>/dev/null +} + +# Where blocks are claimed. A port is host wide, so its lease has to be too: +# under $TMPDIR this was not, because TMPDIR is per user on macOS +# (/var/folders/...) and sudo's env_reset drops it, so an invoking-user run and +# a sudo run kept private pools and could lease the same block while each +# believed it held it alone. A fixed path in /tmp is the one namespace both +# see. Sticky and world writable like /tmp itself so both can create in it. +# The sticky bit is not what keeps the two apart -- it still lets the pool's +# own owner unlink another user's entry, and the first run to arrive creates +# the pool -- so what keeps a live lease safe is the liveness test below. +# Overridable only so sshd_port_lease_test.sh can race against a scratch pool. +: "${PORT_LOCK_ROOT:=/tmp/wolfssh-sshd-ports}" + +port_lease_init() { + mkdir -p "$PORT_LOCK_ROOT" 2>/dev/null + chmod 1777 "$PORT_LOCK_ROOT" 2>/dev/null || true + if [ ! -d "$PORT_LOCK_ROOT" ]; then + echo "Error: cannot create the port lease directory $PORT_LOCK_ROOT." + return 1 + fi + return 0 +} + +# A lease on a block is a directory named for the block and for the pid that +# holds it. A run creates and removes only its own, so no run can delete a +# lease another still believes it holds. Reclaiming a stale lease in place +# could not manage that: whether it removed the directory or renamed it aside, +# the act was authorized by an earlier read of the owner, so a second runner +# that had read the same dead owner went on to displace the live claim the +# first had just made, and both used the block. A lease whose owner is gone is +# simply ignored instead, which costs a run killed before its teardown nothing +# but a directory entry -- and any run may delete those, since a dead owner's +# lease is one nothing is relying on. +claim_port_block() { + local base mine d owner + base=$1 + mine="$PORT_LOCK_ROOT/$base.$$" + mkdir "$mine" 2>/dev/null || return 1 + # The block is ours only if no other live lease on it exists. Two runners + # that reach this at once each see the other and both stand down, which + # costs a block rather than handing one to both; the caller moves on to + # the next. Neither can see the other as absent: each creates its lease + # before it looks, so the later look always finds the earlier lease. + for d in "$PORT_LOCK_ROOT/$base".*; do + [ -d "$d" ] || continue + [ "$d" = "$mine" ] && continue + owner=${d##*.} + # "ps -p", not "kill -0": kill reports failure both for a pid that is + # gone and for one the caller may not signal, and those are opposite + # answers here. A non-root run reading a lease held by a live root run + # -- which is CI, where sshd-test.yml runs the suite under sudo and + # code-coverage.yml does not -- took the EPERM for "owner gone", swept + # the lease and took a block that run was using. ps -p selects by pid + # whatever owns it. + if ps -p "$owner" >/dev/null 2>&1; then + rmdir "$mine" 2>/dev/null + return 1 + fi + rmdir "$d" 2>/dev/null + done + return 0 +} + +release_port_block() { + [ -n "$1" ] && rmdir "$PORT_LOCK_ROOT/$1.$$" 2>/dev/null + return 0 +} + +find_port_block() { + local start i n base busy + # Pid-derived so two runs rarely probe the same candidate first. + # PORT_BLOCK_START pins it for the self-test, which has to be able to aim + # the scan at a chosen block to exercise the --port skip below. + start=${PORT_BLOCK_START:-$(( $$ % PORT_BLOCK_COUNT ))} + for (( i = 0; i < PORT_BLOCK_COUNT; i++ )); do + base=$(( PORT_BLOCK_FIRST \ + + ((start + i) % PORT_BLOCK_COUNT) * PORT_BLOCK_SIZE )) + # A --port inside the block would be handed to the shared daemon while + # the private offsets are still assigned from the same block, so the + # caller's own port could be one of them: --port 22303 against base + # 22300 put the host key test on the port the shared daemon had. Skip + # any block the requested port falls in and the two never overlap. + if [ -n "$TEST_PORT" ] \ + && [ "$TEST_PORT" -ge "$base" ] 2>/dev/null \ + && [ "$TEST_PORT" -lt $(( base + PORT_BLOCK_SIZE )) ]; then + continue + fi + claim_port_block "$base" || continue + busy=0 + for (( n = 0; n < PORT_BLOCK_SIZE; n++ )); do + if port_in_use $(( base + n )); then + busy=1 + break + fi + done + if [ "$busy" -eq 0 ]; then + printf '%s' "$base" + return 0 + fi + # Claimed but unusable: something outside the suite holds a port in + # it. Give the lease back rather than sit on a block we cannot use. + release_port_block "$base" + done + return 1 +} + diff --git a/apps/wolfsshd/test/run_all_sshd_tests.sh b/apps/wolfsshd/test/run_all_sshd_tests.sh index 0a182c01..e516ff8f 100755 --- a/apps/wolfsshd/test/run_all_sshd_tests.sh +++ b/apps/wolfsshd/test/run_all_sshd_tests.sh @@ -75,9 +75,11 @@ done # +1 StrictModes negative test +4 OpenSSH certificate test # +2 AuthorizedUPNDomains negative +5 privilege-drop test # -# The range deliberately starts above 22226: other CI steps in this repo bind -# 22222, 22225 and 22226 while the suite is not running, and a daemon this -# suite leaks must not be able to sit on one of them. +# The range deliberately starts above everything else in the repo that binds +# a port: CI steps bind 22222, 22225 and 22226, and scripts/fwd-bulk.test +# takes 22000 + attempt * 1000 + (pid % 1000) over six attempts, so it can +# land anywhere in 22000-27999 and hard-fails if the port is taken. It picks +# blindly, so only this side can stay out of the way. # # Not yet per run: sshd_term_close_test.sh counts "pgrep wolfsshd" before and # after its connection, sshd_sftp_idle_cpu_test.sh picks the first wolfsshd @@ -85,76 +87,10 @@ done # ticks over every wolfsshd on the machine. All three read the whole process # table, so a second run's connection children can perturb them. Two runs at # once otherwise pass; these are the remaining single-run-per-host tests. -PORT_BLOCK_FIRST=22300 -PORT_BLOCK_SIZE=8 -PORT_BLOCK_COUNT=64 - -# True when something is already listening. A shell built without /dev/tcp -# fails here exactly as a refused connection does, which degrades to taking -# the pid-derived block unprobed -- still per run, just unverified. -port_in_use() { - (exec 3<>"/dev/tcp/127.0.0.1/$1") 2>/dev/null -} - -# Where blocks are claimed. Sticky and world-writable like /tmp itself, so a -# run as the invoking user and a run under sudo can claim from the same pool. -PORT_LOCK_ROOT="${TMPDIR:-/tmp}/wolfssh-sshd-ports" -mkdir -p "$PORT_LOCK_ROOT" 2>/dev/null -chmod 1777 "$PORT_LOCK_ROOT" 2>/dev/null || true - -# Take a block by creating its directory. mkdir is atomic, so of two runners -# racing for one block exactly one wins; probing alone could not do this, -# because both could see the same eight ports free before either had bound -# one. The claim is held for the whole run and released in run_teardown. -claim_port_block() { - local lock owner - lock="$PORT_LOCK_ROOT/$1" - if mkdir "$lock" 2>/dev/null; then - echo $$ > "$lock/pid" 2>/dev/null - return 0 - fi - # A claim whose owner is gone is stale. Without this a run killed before - # its teardown would take its block out of circulation permanently. Two - # runners can reach this at once; the loser's mkdir just fails and it - # moves on to the next block. - owner=`cat "$lock/pid" 2>/dev/null` - if [ -n "$owner" ] && ! kill -0 "$owner" 2>/dev/null; then - rm -rf "$lock" 2>/dev/null - if mkdir "$lock" 2>/dev/null; then - echo $$ > "$lock/pid" 2>/dev/null - return 0 - fi - fi - return 1 -} - -find_port_block() { - local start i n base busy - start=$(( $$ % PORT_BLOCK_COUNT )) - for (( i = 0; i < PORT_BLOCK_COUNT; i++ )); do - base=$(( PORT_BLOCK_FIRST \ - + ((start + i) % PORT_BLOCK_COUNT) * PORT_BLOCK_SIZE )) - claim_port_block "$base" || continue - busy=0 - for (( n = 0; n < PORT_BLOCK_SIZE; n++ )); do - if port_in_use $(( base + n )); then - busy=1 - break - fi - done - if [ "$busy" -eq 0 ]; then - printf '%s' "$base" - return 0 - fi - # Claimed but unusable: something outside the suite holds a port in - # it. Give the claim back rather than sit on a block we cannot use. - rm -rf "$PORT_LOCK_ROOT/$base" 2>/dev/null - done - return 1 -} +. ./port_lease.sh +port_lease_init || exit 1 PORT_BASE=`find_port_block` -PORT_LOCK_DIR="$PORT_LOCK_ROOT/$PORT_BASE" if [ -z "$PORT_BASE" ]; then echo "Error: no free block of $PORT_BLOCK_SIZE ports found starting at" \ "$PORT_BLOCK_FIRST." @@ -208,9 +144,7 @@ run_teardown() { stop_all_wolfsshd || true fi rm -f "$WOLFSSHD_TEST_PIDFILE" || true - if [ -n "$PORT_BASE" ]; then - rm -rf "$PORT_LOCK_DIR" || true - fi + release_port_block "$PORT_BASE" || true } trap run_teardown EXIT @@ -597,6 +531,7 @@ if [[ -n "$MATCH" ]]; then RUN_COMPLETE=1 else echo "Running all tests..." + for test in "${test_cases[@]}"; do if [[ "$test" != "$EXCLUDE" ]]; then echo "Running test: $test"