diff --git a/party b/party index 752ee6b..d42e7aa 100755 --- a/party +++ b/party @@ -312,34 +312,6 @@ roster_list() { # Liveness # -------- -# Liveness check: the socket is a live tmux server we can speak the tmux -# protocol to. `tmux -S list-clients` exits 0 only against a real tmux -# server bound to that exact socket path, which simultaneously rules out -# stale roster pointers (server gone), AF_UNIX impostors (a same-group -# user planting a hand-rolled listener via `nc -lU`), and PID-reuse -# leftovers, none of those answer the tmux handshake. Stronger than any -# PID check, and it works regardless of who owns the server process. -# -# Earlier revisions also gated on `kill -0 $pid` as a cheap pre-filter. -# That broke cross-user discovery on every non-Linux POSIX target: per -# POSIX, kill(2) signal 0 may return EPERM when the caller lacks -# send-permission, and illumos, all the BSDs, and macOS honor that. -# Linux is the outlier that returns 0 for "exists, even if not -# signalable," and Linux ABI environments (LX-branded zones) inherit -# that behavior, which is why the bug hid in the test matrix until a -# native illumos run surfaced it. The PID arg is kept in the function -# signature for caller compatibility and as a numeric-validation guard, -# but is no longer probed. Local var names are prefixed to avoid -# clobbering caller-side `pid` / `sock` (POSIX sh has no real locals). -is_party_alive() { - _pa_pid="$1" - _pa_sock="$2" - case "$_pa_pid" in - '' | *[!0-9]* ) return 1 ;; - esac - "$PARTY_TMUX" -S "$_pa_sock" list-clients >/dev/null 2>&1 -} - # Classify the caller's relationship to a party socket. Prints exactly # one of: # ok : live tmux server, the caller is authorized. @@ -360,6 +332,20 @@ is_party_alive() { # to a hidden party, never to a false "ok". # dead : nothing speaking the tmux protocol at that socket # (stale roster, killed server, AF_UNIX impostor). +# +# This probe is the ONLY liveness primitive. `tmux -S +# list-clients` exits 0 only against a real tmux server bound to that +# exact socket path, which rules out stale roster pointers, AF_UNIX +# impostors (a same-group user planting a hand-rolled listener via +# `nc -lU`), and PID-reuse leftovers in one shot. Earlier revisions had +# a separate is_party_alive() that also format-checked the roster's +# SERVER_PID, a vestige of a `kill -0` pre-filter that broke cross-user +# discovery on every non-Linux POSIX target: per POSIX, kill(2) signal +# 0 may return EPERM when the caller lacks send-permission, and +# illumos, all the BSDs, and macOS honor that. Linux (and LX-branded +# zones) return 0 for "exists, even if not signalable", which is why +# the bug hid in the test matrix until a native illumos run surfaced +# it. SERVER_PID stays in the roster as display-only metadata. party_conn_state() { _cs_err=$("$PARTY_TMUX" -S "$1" list-clients 2>&1 >/dev/null) \ && _cs_rc=0 || _cs_rc=$? @@ -712,8 +698,9 @@ EOF fi if [ -d "$party_dir" ] && [ -f "$rec" ]; then roster_read "$rec" 2>/dev/null || true - if is_party_alive "${RR_SERVER_PID:-}" "${RR_SOCKET:-}"; then - echo "party: '$name' is already running (pid $RR_SERVER_PID)" >&2 + if [ -n "${RR_SOCKET:-}" ] \ + && [ "$(party_conn_state "$RR_SOCKET")" != dead ]; then + echo "party: '$name' is already running" >&2 exit 1 fi fi @@ -1078,7 +1065,7 @@ resolve_authoritative_party() { [ -L "$d" ] && continue [ -O "$d" ] || continue roster_read "$rec" || continue - is_party_alive "$RR_SERVER_PID" "$RR_SOCKET" || continue + [ "$(party_conn_state "$RR_SOCKET")" != dead ] || continue [ "$RR_HOST_USER" = "$USER" ] || continue [ -z "$target" ] || [ "$target" = "$RR_PARTY_NAME" ] || continue set -- "$@" "$rec" @@ -1391,7 +1378,7 @@ EOF tmux_sock="${TMUX%%,*}" for rec in $(roster_list); do roster_read "$rec" || continue - is_party_alive "$RR_SERVER_PID" "$RR_SOCKET" || continue + [ "$(party_conn_state "$RR_SOCKET")" != dead ] || continue [ "$RR_SOCKET" = "$tmux_sock" ] || continue sock="$RR_SOCKET"; host_sess="$RR_PARTY_NAME"; break done @@ -1399,7 +1386,7 @@ EOF if [ -z "$sock" ]; then for rec in $(roster_list); do roster_read "$rec" || continue - is_party_alive "$RR_SERVER_PID" "$RR_SOCKET" || continue + [ "$(party_conn_state "$RR_SOCKET")" != dead ] || continue "$PARTY_TMUX" -S "$RR_SOCKET" list-clients -F '#{client_user}' 2>/dev/null \ | grep -qx "$USER" || continue sock="$RR_SOCKET"; host_sess="$RR_PARTY_NAME"; break diff --git a/tests/20-helpers.bats b/tests/20-helpers.bats index c89b5b6..fc6ec52 100644 --- a/tests/20-helpers.bats +++ b/tests/20-helpers.bats @@ -82,32 +82,22 @@ setup() { # liveness # ======== # -# is_party_alive must not probe the PID with kill -0. POSIX kill(2) is +# party_conn_state must not probe PIDs with kill -0. POSIX kill(2) is # allowed to return EPERM when the caller can't signal the target, and -# illumos/BSD/macOS honor that — so kill -0 against another user's PID +# illumos/BSD/macOS honor that, so kill -0 against another user's PID # returns nonzero even when the process exists. Linux and Linux-ABI # zones (LX-branded) are the outliers that return 0; that's why the # bug hid in the test matrix until a native illumos run surfaced it. -# Discovery now relies on `tmux -S list-clients` alone, which is -# strictly stronger (filters PID-reuse, dead servers, AF_UNIX impostors). +# Liveness rides on `tmux -S list-clients` alone, which is strictly +# stronger (filters PID-reuse, dead servers, AF_UNIX impostors). -@test "is_party_alive does not reach for kill -0 (cross-user discovery)" { - body=$(declare -f is_party_alive) +@test "party_conn_state does not reach for kill -0 (cross-user discovery)" { + body=$(declare -f party_conn_state) [[ "$body" != *"kill "* ]] || { echo "$body"; false; } } -@test "is_party_alive rejects non-numeric PID without invoking tmux" { - ! PARTY_TMUX=/nonexistent/should-not-be-called \ - is_party_alive "" /tmp/should-not-matter - ! PARTY_TMUX=/nonexistent/should-not-be-called \ - is_party_alive "abc" /tmp/should-not-matter -} - -@test "is_party_alive returns nonzero when tmux server is absent" { - # Numeric PID, but no live tmux server at the socket path: tmux call - # fails the handshake and the function returns nonzero. Uses PID 1 - # to also confirm a foreign-uid PID is not preventing the result. - ! is_party_alive 1 "$PARTY_TMP/no-such-sock" +@test "party_conn_state answers dead when no tmux server is at the socket" { + [ "$(party_conn_state "$PARTY_TMP/no-such-sock")" = dead ] } @test "validate_socket_dir_parent refuses whitespace in PARTY_SOCKET_DIR" { diff --git a/tests/30-roster.bats b/tests/30-roster.bats index 0bef0ff..9aa94a1 100644 --- a/tests/30-roster.bats +++ b/tests/30-roster.bats @@ -61,7 +61,7 @@ setup() { } # Helper: write a $PARTY_TMUX stub that exits with the given code on -# every invocation. Used to test is_party_alive's tmux probe in isolation. +# every invocation. Used to test party_conn_state's probe in isolation. _stub_tmux() { cat > "$PARTY_TMP/tmux-stub" </dev/null - wait "$pid" 2>/dev/null || true - # Same PID, now reaped — tmux stub still says yes, so still alive. - is_party_alive "$pid" /dev/null - # And a PID we never owned (init/launchd, root) — likewise alive. - is_party_alive 1 /dev/null + [ "$(party_conn_state /dev/null)" = ok ] } -@test "is_party_alive: empty/non-numeric PID → false" { - _stub_tmux 0 - ! is_party_alive "" /dev/null - ! is_party_alive "abc" /dev/null -} - -@test "is_party_alive: tmux probe fails → false" { - # PID-reuse / planted nc -lU socket scenario: the socket isn't a real - # tmux server, so the tmux handshake fails. is_party_alive rejects - # regardless of PID state (regression seen cross-user on illumos - # native). +@test "party_conn_state: failing probe answers dead" { + # PID-reuse / planted nc -lU socket scenario: nothing at the socket + # speaks the tmux protocol, so the probe fails and the party is dead, + # regardless of any PID recorded in the roster. _stub_tmux 1 - ( sleep 30 ) & - pid=$! - ! is_party_alive "$pid" /dev/null - kill "$pid" 2>/dev/null - wait "$pid" 2>/dev/null || true - ! is_party_alive 1 /dev/null + [ "$(party_conn_state /dev/null)" = dead ] +} + +@test "party_conn_state: rc 0 with stderr output answers unauthorized" { + cat > "$PARTY_TMP/tmux-stub" <<'EOF' +#!/bin/sh +echo "access not allowed" >&2 +exit 0 +EOF + chmod +x "$PARTY_TMP/tmux-stub" + PARTY_TMUX="$PARTY_TMP/tmux-stub" + [ "$(party_conn_state /dev/null)" = unauthorized ] } @test "roster_list returns full record paths under PARTY_SOCKET_DIR" { diff --git a/tests/60-list-join-leave.bats b/tests/60-list-join-leave.bats index 4d59107..6642988 100644 --- a/tests/60-list-join-leave.bats +++ b/tests/60-list-join-leave.bats @@ -45,7 +45,7 @@ EOF [[ "$output" == *"no parties"* ]] } -# Cross-user discovery regression. Earlier revisions of is_party_alive +# Cross-user discovery regression. Earlier revisions of party_conn_state # probed the PID with `kill -0`, which returns EPERM under POSIX semantics # when the caller can't signal the target — illumos, all BSDs, and macOS # honor that, so guests couldn't discover parties hosted by other users diff --git a/tests/95-stub-roundtrip.bats b/tests/95-stub-roundtrip.bats index e327073..60e1fb6 100644 --- a/tests/95-stub-roundtrip.bats +++ b/tests/95-stub-roundtrip.bats @@ -15,7 +15,7 @@ setup() { # default 'party' group existing on the host. export TMUX_PARTY_GROUP="$(id -gn)" - # Spawn a long-lived sleeper so cmd_list's is_party_alive check sees a + # Spawn a long-lived sleeper so cmd_list's party_conn_state check sees a # live PID for the "fake server". setup_party_sandbox provides PARTY_TMP. ( exec sleep 60 ) & export FAKE_SERVER_PID=$! @@ -23,7 +23,7 @@ setup() { # Recorder: appends every invocation to TMUX_LOG, fakes the few tmux # subcommands cmd_host depends on (creating a placeholder socket file, # answering display-message -p '#{pid}' with the sleeper's PID so - # is_party_alive sees it as live), and returns 0 for the rest. + # party_conn_state sees it as live), and returns 0 for the rest. export TMUX_LOG="$PARTY_TMP/tmux.log" : > "$TMUX_LOG" cat > "$PARTY_TMP/tmux-stub" <<'STUB' @@ -711,7 +711,7 @@ STUB # Parity with test 76 ('cmd_list skips parties...'), but for the join # resolver. A roster can have a live SERVER_PID (PID reuse, init's pid 1) # and an [ -S ]-passing socket (planted via `nc -lU`) yet not be a real - # tmux server. is_party_alive must probe list-clients on every resolver, + # tmux server. party_conn_state must probe list-clients on every resolver, # not just cmd_list — otherwise `party join # imposter` would resolve to the imposter and try to attach. legit_dir="$PARTY_SOCKET_DIR/party-$USER:legit2.d"