wolfsshd: lease a port block to one run only

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
pull/1256/head
John Safranek 2026-09-16 15:49:05 -07:00
parent 347046f8fd
commit 0a21854b0a
2 changed files with 135 additions and 74 deletions

View File

@ -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
}

View File

@ -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"