mirror of https://github.com/wolfSSL/wolfssh.git
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 portpull/1256/head
parent
347046f8fd
commit
0a21854b0a
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in New Issue