diff options
Diffstat (limited to '9ns/test/adv_bridge_interrupt.sh')
| -rwxr-xr-x | 9ns/test/adv_bridge_interrupt.sh | 136 |
1 files changed, 136 insertions, 0 deletions
diff --git a/9ns/test/adv_bridge_interrupt.sh b/9ns/test/adv_bridge_interrupt.sh new file mode 100755 index 0000000..e364143 --- /dev/null +++ b/9ns/test/adv_bridge_interrupt.sh @@ -0,0 +1,136 @@ +#!/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 </dev/null & + SRVPID=$! + for _ in $(seq 1 100); do [ -S "$SOCK" ] && return 0; sleep 0.02; done + echo "server for $1 did not start"; cat "$SRVLOG"; exit 1 +} + +# run MODE SCRIPT [extra 9ns args...]: starts the server, runs 9ns; sets OUT, RC, STDERR. +run() { + local mode=$1 script=$2; shift 2 + start_server "$mode" + OUT=$(timeout 60 "$NS" --unix "$SOCK" --mount "$M" "$@" -- sh -c "$script" 2>"$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 stays blocked, SIGTERM to 9ns still ends the session" +start_server never +timeout -s TERM 3 "$NS" --unix "$SOCK" --mount "$M" -- sh -c "timeout -s INT 1 cat $M/f; echo unreachable-rc=\$?" >"$TMP/never.out" 2>"$TMP/never.err" & +TPID=$! +sleep 4 +if kill -0 "$TPID" 2>/dev/null; then + fail "never: SIGTERM did not end 9ns while the reader was stuck"; kill -9 "$TPID" +else + pass "never: SIGTERM ends 9ns even though the server ignores the Tflush" +fi +wait "$TPID" 2>/dev/null +expect_eq "never: the reader never came back (server ignores Tflush)" "" "$(grep unreachable "$TMP/never.out")" + +echo +echo "passed=$PASSED failed=$FAILED" +[ "$FAILED" -eq 0 ] |
