summaryrefslogtreecommitdiff
path: root/9ns/test/adv_bridge_interrupt.sh
diff options
context:
space:
mode:
Diffstat (limited to '9ns/test/adv_bridge_interrupt.sh')
-rwxr-xr-x9ns/test/adv_bridge_interrupt.sh136
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 ]