#!/usr/bin/env bash # Interrupt tests: a reader blocked in a 9P read the server never answers must # be releasable. Killing or Ctrl-C-ing it makes the kernel send FUSE_INTERRUPT, # which the bridge turns into a Tflush; a server that answers Rflush (the # hostile server's `never_flush` mode) unblocks the reader with EINTR and the # mount stays usable; a server that ignores everything (`never`) still blocks # the mount, but SIGTERM to 9ns ends the session as before. # Usage: bash 9ns/test/adv_bridge_interrupt.sh <9ns> <9proc-demo> (9proc unused; part of zig build 9ns-adv) set -u NS=$(realpath "${1:?path to 9ns}") HERE=$(cd "$(dirname "$0")" && pwd) SRV=$HERE/adv_bridge_hostile.py TMP=$(mktemp -d "${TMPDIR:-/tmp}/9ns-int.XXXXXX") M=/mnt/9p FAILED=0 PASSED=0 SRVPID= cleanup() { [ -n "$SRVPID" ] && kill "$SRVPID" 2>/dev/null; pkill -f "adv_bridge_hostile.py $TMP" 2>/dev/null; rm -rf "$TMP"; } trap cleanup EXIT if ! unshare -Urm true 2>/dev/null || [ ! -c /dev/fuse ]; then echo "SKIP: no user namespaces or /dev/fuse"; exit 0; fi pass() { PASSED=$((PASSED + 1)); echo "ok - $1"; } fail() { FAILED=$((FAILED + 1)); echo "FAIL - $1"; shift; [ $# -gt 0 ] && printf ' %s\n' "$@"; } expect_eq() { if [ "$2" = "$3" ]; then pass "$1"; else fail "$1" "expected: $(printf %q "$2")" "actual: $(printf %q "$3")"; fi; } expect_contains() { case "$3" in *"$2"*) pass "$1" ;; *) fail "$1" "missing: $(printf %q "$2")" "in: $(printf %q "$3")" ;; esac; } # expect_lt NAME ACTUAL LIMIT expect_lt() { if [ "$2" -lt "$3" ]; then pass "$1 ($2 < $3)"; else fail "$1" "expected < $3, got $2"; fi; } start_server() { # mode [ -n "$SRVPID" ] && { kill "$SRVPID" 2>/dev/null; wait "$SRVPID" 2>/dev/null; } SOCK=$TMP/$1.sock SRVLOG=$TMP/$1.srv.out rm -f "$SOCK" python3 "$SRV" "$SOCK" "$1" >"$SRVLOG" 2>&1 "$TMP/stderr") RC=$? STDERR=$(cat "$TMP/stderr") } no_crash() { # name if [ "$RC" -ge 128 ] || [ "$RC" -eq 124 ]; then fail "$1: 9ns exit $RC" "$STDERR"; return; fi case "$STDERR" in *panic*|*"Segmentation"*|*"integer overflow"*|*"reached unreachable"*|*"index out of bounds"*) fail "$1: crash text in stderr" "$STDERR";; *) pass "$1: no crash (exit $RC)";; esac } # The shell snippet that times a command: prints "rc=N" and "ms=N". timed() { # command... printf 's=$(date +%%s%%N); %s; echo rc=$?; e=$(date +%%s%%N); echo ms=$(( (e - s) / 1000000 ))' "$*" } field() { printf '%s\n' "$2" | sed -n "s/^$1=//p" | tail -1; } # Server-side fid count before and after the interrupted operation, measured in # the same run. Everything the scripts touch is looked up first, so the inode # fids the kernel keeps for f, d and g are in both samples and the comparison # is exact: any difference is a fid an interrupted operation left behind. BEFORE="stat $M/f $M/d/g >/dev/null; ls $M >/dev/null; echo before=\$(cat $M/fids)" AFTER="sleep 0.2; echo after=\$(cat $M/fids)" fids_same() { # name local b a; b=$(field before "$OUT"); a=$(field after "$OUT") case "$b" in ''|*[!0-9]*) fail "$1: fid count before is not numeric: '$b'"; return;; esac expect_eq "$1: server-side fid count unchanged ($b)" "$b" "$a" } # Same for the bridge's own counter (--debug prints fids=N per request): the # first and the last READ traced are the two `cat fids`. bridge_fids_same() { # name local reads; reads=$(printf '%s\n' "$STDERR" | sed -n 's/^9ns: <- read .*(fids=\([0-9]*\) .*/\1/p') expect_eq "$1: bridge fid counter unchanged ($(printf '%s\n' "$reads" | head -1))" "$(printf '%s\n' "$reads" | head -1)" "$(printf '%s\n' "$reads" | tail -1)" } echo "# (a) SIGINT to a reader blocked in a read the server never answers" run never_flush "$BEFORE; $(timed "timeout -s INT 2 cat $M/f"); ls $M | tr '\n' ' '; echo; cat $M/d/g; $AFTER" --debug no_crash "never_flush/SIGINT" expect_eq "never_flush/SIGINT: cat was killed by the signal (timeout reports 124)" "124" "$(field rc "$OUT")" expect_lt "never_flush/SIGINT: the reader was released promptly" "$(field ms "$OUT")" 2500 expect_contains "never_flush/SIGINT: the mount is still usable (ls)" "big d f fids" "$OUT" expect_contains "never_flush/SIGINT: the mount is still usable (read another file)" "in d" "$OUT" fids_same "never_flush/SIGINT" hung_tag=$(sed -n 's/^Tread tag=\([0-9]*\) .*hang$/\1/p' "$SRVLOG" | head -1) flush_oldtag=$(sed -n 's/^Tflush tag=[0-9]* oldtag=\([0-9]*\)$/\1/p' "$SRVLOG" | head -1) expect_eq "never_flush/SIGINT: exactly one Tflush reached the server" "1" "$(grep -c '^Tflush ' "$SRVLOG")" expect_eq "never_flush/SIGINT: Tflush.oldtag is the hung Tread's tag ($hung_tag)" "$hung_tag" "$flush_oldtag" expect_contains "never_flush/SIGINT: --debug shows the interrupt being forwarded" "sending Tflush" "$STDERR" expect_contains "never_flush/SIGINT: --debug shows EINTR going back to the kernel" "error EINTR" "$STDERR" bridge_fids_same "never_flush/SIGINT" echo "# (c) SIGKILL to the blocked reader" run never_flush "$BEFORE; $(timed "cat $M/f & p=\$!; sleep 0.5; kill -9 \$p; wait \$p"); cat $M/d/g; $AFTER" no_crash "never_flush/SIGKILL" expect_eq "never_flush/SIGKILL: reader died of SIGKILL (137)" "137" "$(field rc "$OUT")" expect_lt "never_flush/SIGKILL: released promptly" "$(field ms "$OUT")" 2000 expect_contains "never_flush/SIGKILL: mount still usable" "in d" "$OUT" fids_same "never_flush/SIGKILL" expect_eq "never_flush/SIGKILL: one Tflush" "1" "$(grep -c '^Tflush ' "$SRVLOG")" echo "# a second blocked read after the first was interrupted" run never_flush "$BEFORE; $(timed "timeout -s INT 1 cat $M/f"); $(timed "timeout -s INT 1 cat $M/f"); cat $M/d/g; $AFTER" no_crash "never_flush/twice" expect_eq "never_flush/twice: both readers killed" "124 124" "$(printf '%s\n' "$OUT" | sed -n 's/^rc=//p' | tr '\n' ' ' | sed 's/ $//')" expect_eq "never_flush/twice: two Tflush, no tag confusion" "2" "$(grep -c '^Tflush ' "$SRVLOG")" expect_contains "never_flush/twice: mount still usable" "in d" "$OUT" fids_same "never_flush/twice" echo "# an interrupted open+read through a lookup (walk+stat) leaves no fid behind" # `f` is looked up fresh each time (cache 0), so the LOOKUP's walk+stat and the # OPEN's clone+open all run before the read blocks; all their fids must go. run never_flush "$BEFORE; $(timed "timeout -s INT 1 cat $M/f"); $AFTER" --cache 0 --debug no_crash "never_flush/cache0" expect_eq "never_flush/cache0: reader killed" "124" "$(field rc "$OUT")" fids_same "never_flush/cache0" bridge_fids_same "never_flush/cache0" echo "# (b) a server that ignores Tflush too: the reader comes back after the flush grace, and the session is gone with it" # `never` stops reading once the Tread hangs, so the Tflush is never even # seen. The protocol says wait for the Rflush; a server that has not managed # one in 3s (nine.Session.flush_grace_ms) is not going to, and the reader # behind the interrupt is unkillable until we stop waiting. So the read fails # EINTR after the grace, the session is wedged, and 9ns ends the mount: the # next access answers ENOTCONN instead of parking another process forever. run never "$(timed "timeout -s INT 1 cat $M/f"); cat $M/d/g 2>&1; echo after-rc=\$?" no_crash "never/grace" expect_eq "never/grace: reader killed by the signal (124)" "124" "$(field rc "$OUT")" NEVER_MS=$(field ms "$OUT") if [ "$NEVER_MS" -ge 3500 ] && [ "$NEVER_MS" -lt 9000 ]; then pass "never/grace: released after the 3s flush grace (${NEVER_MS}ms)"; else fail "never/grace: release time out of range" "ms=$NEVER_MS (expected 3500..9000)"; fi expect_eq "never/grace: the mount is gone afterwards (not a hang)" "after-rc=1" "$(printf '%s\n' "$OUT" | grep '^after-rc=')" expect_contains "never/grace: 9ns reports the closed session" "connection closed" "$STDERR" echo echo "passed=$PASSED failed=$FAILED" [ "$FAILED" -eq 0 ]