diff options
Diffstat (limited to '9ns/test')
| -rwxr-xr-x | 9ns/test/adv_bridge_hostile.py | 11 | ||||
| -rwxr-xr-x | 9ns/test/adv_bridge_hostile.sh | 6 | ||||
| -rwxr-xr-x | 9ns/test/adv_bridge_interrupt.sh | 136 | ||||
| -rwxr-xr-x | 9ns/test/adv_bridge_semantics.sh | 7 | ||||
| -rwxr-xr-x | 9ns/test/adv_bridge_stress.sh | 2 | ||||
| -rwxr-xr-x | 9ns/test/adversarial.sh | 2 | ||||
| -rwxr-xr-x | 9ns/test/integration.sh | 27 |
7 files changed, 180 insertions, 11 deletions
diff --git a/9ns/test/adv_bridge_hostile.py b/9ns/test/adv_bridge_hostile.py index d353541..f676d2f 100755 --- a/9ns/test/adv_bridge_hostile.py +++ b/9ns/test/adv_bridge_hostile.py @@ -62,7 +62,8 @@ name_huge / has an entry with a 60000-byte name name_dots / lists "." and ".." too rerror_big Twalk to nope: Rerror with 65535 bytes of text extra_reply Rread on /f: an unsolicited Rclunk (tag 9) precedes the real reply -never Tread on /f: never reply (hang) +never Tread on /f: never reply (hang); the server stops reading, so a Tflush is never seen +never_flush Tread on /f: never reply, but keep serving: log every Tflush and answer Rflush close_mid Tread on /f: close the socket without replying renegotiate Tread on /f: an unsolicited Rversion precedes the real reply length_max Tstat on /f: length = 2**64-1 @@ -142,6 +143,9 @@ class Server: parent.children[name] = n return n + def log(self, text): + print(text, flush=True) + # -- framing --------------------------------------------------------- def frame(self, typ, tag, body): return s32(7 + len(body)) + s8(typ) + s16(tag) + body @@ -196,6 +200,8 @@ class Server: self.fids[fid] = [self.root, False] return self.frame(Rattach, tag, self.root.qid(self)) if typ == Tflush: + oldtag = r.u16() + self.log('Tflush tag=%d oldtag=%d' % (tag, oldtag)) return self.frame(Rflush, tag, b'') if typ == Twalk: fid, newfid, nw = r.u32(), r.u32(), r.u16() @@ -292,6 +298,9 @@ class Server: if m == 'never': time.sleep(3600) return None + if m == 'never_flush': + self.log('Tread tag=%d fid=%d hang' % (tag, fid)) + return b'' if m == 'close_mid': return None if m == 'renegotiate': diff --git a/9ns/test/adv_bridge_hostile.sh b/9ns/test/adv_bridge_hostile.sh index f5ba871..20c05bf 100755 --- a/9ns/test/adv_bridge_hostile.sh +++ b/9ns/test/adv_bridge_hostile.sh @@ -37,7 +37,7 @@ start_server() { # mode run() { local mode=$1 script=$2; shift 2 start_server "$mode" - OUT=$(timeout 30 "$NS" --unix "$SOCK" "$@" -- sh -c "$script" 2>"$TMP/stderr") + OUT=$(timeout 30 "$NS" --unix "$SOCK" --mount "$M" "$@" -- sh -c "$script" 2>"$TMP/stderr") RC=$? STDERR=$(cat "$TMP/stderr") } @@ -146,7 +146,7 @@ echo "# a server that never replies" start_server never # SIGTERM is forwarded to the child; once the child is gone 9ns must leave the # pending 9P reply behind and exit even though the server stays silent. -timeout -s TERM 3 "$NS" --unix "$SOCK" -- sh -c "cat $M/f; echo unreachable" >"$TMP/never.out" 2>"$TMP/never.err" & +timeout -s TERM 3 "$NS" --unix "$SOCK" --mount "$M" -- sh -c "cat $M/f; echo unreachable" >"$TMP/never.out" 2>"$TMP/never.err" & TPID=$! sleep 4 if kill -0 "$TPID" 2>/dev/null; then @@ -156,7 +156,7 @@ else fi wait "$TPID" 2>/dev/null # Without a signal the mount hangs (documented v1 limitation) until the server dies. -timeout 30 "$NS" --unix "$SOCK" -- sh -c "cat $M/f; echo unreachable" >"$TMP/never.out" 2>"$TMP/never.err" & +timeout 30 "$NS" --unix "$SOCK" --mount "$M" -- sh -c "cat $M/f; echo unreachable" >"$TMP/never.out" 2>"$TMP/never.err" & TPID=$! sleep 1.5 if kill -0 "$TPID" 2>/dev/null; then 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 ] diff --git a/9ns/test/adv_bridge_semantics.sh b/9ns/test/adv_bridge_semantics.sh index b38ce9c..526ebc8 100755 --- a/9ns/test/adv_bridge_semantics.sh +++ b/9ns/test/adv_bridge_semantics.sh @@ -24,7 +24,7 @@ SOCK=$TMP/i.sock SRVPID=$! for _ in $(seq 1 100); do [ -S "$SOCK" ] && break; sleep 0.02; done # Each run is a fresh session; state persists in the server, so tests clean up after themselves. -run() { OUT=$(timeout 120 "$NS" --unix "$SOCK" "${EXTRA[@]}" -- sh -c "$1" 2>"$TMP/stderr"); RC=$?; STDERR=$(cat "$TMP/stderr"); } +run() { OUT=$(timeout 120 "$NS" --unix "$SOCK" --mount "$M" "${EXTRA[@]}" -- sh -c "$1" 2>"$TMP/stderr"); RC=$?; STDERR=$(cat "$TMP/stderr"); } EXTRA=() py() { run "python3 - <<'PYEOF' $1 @@ -141,8 +141,9 @@ for n in os.listdir(d): os.unlink(f'{d}/{n}') os.rmdir(d) " expect_eq "readdir of a directory that changes mid-iteration" "a True True" "$OUT" -run "ls $M/.. > /dev/null && echo ok; stat -c %i $M $M/. $M/scratch/..; cd $M/scratch && ls .. | grep -c scratch" -expect_eq ".. of the root and of a subdir" $'ok\n1\n1\n1\n1' "$OUT" +# The root's inode number is its 9P qid.path (not a fixed 1): all three views must agree. +run "ls $M/.. > /dev/null && echo ok; stat -c %i $M $M/. $M/scratch/.. | sort -u | wc -l; cd $M/scratch && ls .. | grep -c scratch" +expect_eq ".. of the root and of a subdir" $'ok\n1\n1' "$OUT" run "cd $M && find . -type d | wc -l && find . -type f | head -1 && find $S -type f | wc -l" expect_contains "find -type works" "./README" "$OUT" run "stat -f -c '%T %S %l' $M; df -P $M | tail -1 | awk '{print \$1}'; sync -f $M && echo synced; sync && echo synced2" diff --git a/9ns/test/adv_bridge_stress.sh b/9ns/test/adv_bridge_stress.sh index b306b28..d49f453 100755 --- a/9ns/test/adv_bridge_stress.sh +++ b/9ns/test/adv_bridge_stress.sh @@ -21,7 +21,7 @@ SOCK=$TMP/i.sock "$PROC" --unix "$SOCK" >"$TMP/srv.out" 2>&1 & SRVPID=$! for _ in $(seq 1 100); do [ -S "$SOCK" ] && break; sleep 0.02; done -run() { OUT=$(timeout 600 "$NS" --unix "$SOCK" -- sh -c "$1" 2>"$TMP/stderr"); RC=$?; STDERR=$(cat "$TMP/stderr"); } +run() { OUT=$(timeout 600 "$NS" --unix "$SOCK" --mount "$M" -- sh -c "$1" 2>"$TMP/stderr"); RC=$?; STDERR=$(cat "$TMP/stderr"); } echo "# 100k+ 9P operations in one session; RSS must plateau" # Each iteration: create+write+close, open+read+close, unlink, plus a failing lookup: ~15 RPCs. diff --git a/9ns/test/adversarial.sh b/9ns/test/adversarial.sh index 71c6f44..8575dcf 100755 --- a/9ns/test/adversarial.sh +++ b/9ns/test/adversarial.sh @@ -8,7 +8,7 @@ NS=${1:?path to 9ns} PROC=${2:?path to 9proc-demo} HERE=$(cd "$(dirname "$0")" && pwd) status=0 -for suite in adv_ns_process adv_bridge_hostile adv_bridge_semantics adv_bridge_stress; do +for suite in adv_ns_process adv_bridge_hostile adv_bridge_interrupt adv_bridge_semantics adv_bridge_stress; do echo "### $suite" if bash "$HERE/$suite.sh" "$NS" "$PROC"; then echo "### $suite: ok"; else echo "### $suite: FAILED"; status=1; fi done diff --git a/9ns/test/integration.sh b/9ns/test/integration.sh index 5e0ad13..32d132a 100755 --- a/9ns/test/integration.sh +++ b/9ns/test/integration.sh @@ -10,6 +10,8 @@ TMP=$(mktemp -d "${TMPDIR:-/tmp}/9ns-itest.XXXXXX") PIDS=() FAILED=0 PASSED=0 +# The suites pin the mountpoint with --mount; the default is /mnt/9p/<name> +# (see the "# mount names" section below). M=/mnt/9p cleanup() { @@ -39,8 +41,8 @@ wait_socket() { # path return 1 } -# run_in "<shell script>" — run inside a namespace with the current transport ($TRANSPORT array). -run_in() { timeout 60 "$NS" "${TRANSPORT[@]}" -- sh -c "$1" 2>"$TMP/stderr"; } +# run_in "<shell script>" — run inside a namespace with the current transport ($TRANSPORT array), mounted on $M. +run_in() { timeout 60 "$NS" "${TRANSPORT[@]}" --mount "$M" -- sh -c "$1" 2>"$TMP/stderr"; } # --- scratch battery: works against any writable 9P tree rooted at $1 (relative to mount) --- scratch_battery() { # label scratchdir @@ -111,6 +113,26 @@ mkdir -p "$TMP/mnt" expect_eq "--mount existing dir" "ok" "$(timeout 60 "$NS" --unix "$SOCK" --mount "$TMP/mnt" -- sh -c "[ -f $TMP/mnt/README ] && echo ok")" expect_eq "--mount relative" "ok" "$(cd "$TMP" && timeout 60 "$NS" --unix "$SOCK" --mount rel -- sh -c "[ -f $TMP/rel/README ] && echo ok")" expect_eq "--mount missing under /" "125" "$(timeout 60 "$NS" --unix "$SOCK" --mount /nonexistent-9ns-dir -- true 2>/dev/null; echo $?)" +expect_eq "--mount beats --name" "$TMP/mnt" "$(timeout 60 "$NS" --unix "$SOCK" --name zz --mount "$TMP/mnt" -- sh -c 'echo $NINE_MOUNT')" + +echo "# mount names (default mountpoint /mnt/9p/<name>)" +mkdir -p "$TMP/n" +"$PROC" --unix "$TMP/n/foo.sock" & +PIDS+=($!) +wait_socket "$TMP/n/foo.sock" || echo "foo.sock missing" +named() { timeout 60 "$NS" --unix "$TMP/n/foo.sock" "$@"; } +expect_eq "unix socket foo.sock -> /mnt/9p/foo" "/mnt/9p/foo" "$(named -- sh -c 'echo $NINE_MOUNT')" +expect_eq "default mount is served" "ok" "$(named -- sh -c '[ -f /mnt/9p/foo/README ] && grep -q "^9ns /mnt/9p/foo fuse" /proc/self/mounts && echo ok')" +expect_eq "default mount keeps /mnt entries" "$(ls -A /mnt | sort | tr '\n' ' ')" "$(named -- sh -c "ls -A /mnt | grep -v '^9p\$' | sort | tr '\n' ' '")" +expect_eq "--name bar -> /mnt/9p/bar" "/mnt/9p/bar ok" "$(named --name bar -- sh -c 'echo $NINE_MOUNT; [ -f /mnt/9p/bar/README ] && echo ok' | tr '\n' ' ' | sed 's/ $//')" +expect_eq "--name=bar form" "/mnt/9p/bar" "$(named --name=bar -- sh -c 'echo $NINE_MOUNT')" +expect_eq "--name a/b is a usage error" "125" "$(named --name a/b -- true 2>/dev/null; echo $?)" +expect_eq "--name . is a usage error" "125" "$(named --name . -- true 2>/dev/null; echo $?)" +expect_eq "--name '' is a usage error" "125" "$(named --name '' -- true 2>/dev/null; echo $?)" +expect_eq "nested, two names: both under /mnt/9p" "a b both /mnt/9p/b" "$(named --name a -- "$NS" --unix "$TMP/n/foo.sock" --name b -- sh -c 'ls /mnt/9p | tr "\n" " "; [ -f /mnt/9p/a/README ] && [ -f /mnt/9p/b/README ] && echo both; echo $NINE_MOUNT' 2>/dev/null | tr '\n' ' ' | sed 's/ $//')" +expect_eq "nested, same name: inner shadows outer" "2 /mnt/9p/a" "$(named --name a -- "$NS" --unix "$TMP/n/foo.sock" --name a -- sh -c 'grep -c "^9ns /mnt/9p/a fuse" /proc/self/mounts; [ -f /mnt/9p/a/README ] && echo $NINE_MOUNT' 2>/dev/null | tr '\n' ' ' | sed 's/ $//')" +expect_eq "--spawn default name is the command basename" "/mnt/9p/9proc-demo" "$(timeout 60 "$NS" --spawn "$PROC --stdio" -- sh -c 'echo $NINE_MOUNT')" +expect_eq "host mount table untouched by named mounts" "no" "$(grep -q " /mnt/9p" /proc/self/mountinfo && echo yes || echo no)" echo "# lifecycle" START=$(date +%s) @@ -131,6 +153,7 @@ PIDS+=($!) sleep 0.3 TRANSPORT=(--tcp "127.0.0.1:$PORT") expect_eq "tcp: zig_version" "$(zig version)" "$(run_in "cat $M/build/zig_version")" +expect_eq "tcp: default name" "/mnt/9p/tcp-127.0.0.1-$PORT" "$(timeout 60 "$NS" --tcp "127.0.0.1:$PORT" -- sh -c 'echo $NINE_MOUNT')" expect_eq "tcp: scratch" "tcp" "$(run_in "echo tcp > $M/scratch/t && cat $M/scratch/t && rm $M/scratch/t")" echo "# server death" |
