1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
|
#!/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 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 ]
|