summaryrefslogtreecommitdiff
path: root/9player/test/adv_bridge_hostile.sh
diff options
context:
space:
mode:
Diffstat (limited to '9player/test/adv_bridge_hostile.sh')
-rwxr-xr-x9player/test/adv_bridge_hostile.sh181
1 files changed, 181 insertions, 0 deletions
diff --git a/9player/test/adv_bridge_hostile.sh b/9player/test/adv_bridge_hostile.sh
new file mode 100755
index 0000000..f1ee292
--- /dev/null
+++ b/9player/test/adv_bridge_hostile.sh
@@ -0,0 +1,181 @@
+#!/usr/bin/env bash
+# Hostile-server tests: 9player against 9player/test/adv_bridge_hostile.py in every
+# misbehaviour mode. 9player must never crash (panic/segfault) and must turn
+# each misbehaviour into an errno for the child.
+# Usage: bash 9player/test/adv_bridge_hostile.sh <9player> <introspect> (introspect unused; part of zig build 9player-adv)
+set -u
+PLAYER=$(realpath "${1:?path to 9player}")
+HERE=$(cd "$(dirname "$0")" && pwd)
+SRV=$HERE/adv_bridge_hostile.py
+TMP=$(mktemp -d "${TMPDIR:-/tmp}/9player-adv.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; }
+
+start_server() { # mode
+ [ -n "$SRVPID" ] && { kill "$SRVPID" 2>/dev/null; wait "$SRVPID" 2>/dev/null; }
+ SOCK=$TMP/$1.sock
+ rm -f "$SOCK"
+ python3 "$SRV" "$SOCK" "$1" >"$TMP/$1.srv.out" 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 "$TMP/$1.srv.out"; exit 1
+}
+
+# run MODE SCRIPT [extra 9player args...]: starts the server, runs 9player; sets OUT, RC, STDERR.
+run() {
+ local mode=$1 script=$2; shift 2
+ start_server "$mode"
+ OUT=$(timeout 30 "$PLAYER" --unix "$SOCK" "$@" -- sh -c "$script" 2>"$TMP/stderr")
+ RC=$?
+ STDERR=$(cat "$TMP/stderr")
+}
+
+# 9player must not die of a signal or panic. RC 124 = timeout(1) fired.
+no_crash() { # name
+ if [ "$RC" -ge 128 ] || [ "$RC" -eq 124 ]; then fail "$1: 9player 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
+}
+
+echo "# sanity: the hostile server behaves in 'ok' mode"
+run ok "cat $M/f"; expect_eq "ok: cat f" "hello world" "$OUT"
+run ok "cat $M/d/g"; expect_eq "ok: nested" "in d" "$OUT"
+run ok "cat $M/nope 2>&1 | sed 's/.*: //'"; expect_eq "ok: ENOENT" "No such file or directory" "$OUT"
+run ok "head -c 1048576 $M/big | wc -c | grep -q 1048576 && echo yes"; expect_eq "ok: 1 MiB read matches" "yes" "$OUT"
+
+echo "# unlink + recreate with a recycled qid.path"
+run ok "echo 1 > $M/a; rm $M/a; echo 2 > $M/a; cat $M/a; rm $M/a"; expect_eq "qid reuse: new content, not stale" "2" "$OUT"
+run ok "echo 1 > $M/x; rm $M/x; mkdir $M/x; stat -c %F $M/x; rmdir $M/x"; expect_eq "qid reuse: file→dir on the same path" "directory" "$OUT"
+
+echo "# fids do not grow with the number of operations"
+loop='i=0; while [ $i -lt N ]; do echo hi > M/t; cat M/t >/dev/null; mkdir M/dd; rmdir M/dd; rm M/t; i=$((i+1)); done; cat M/fids'
+run ok "$(echo "$loop" | sed "s|N|20|; s|M/|$M/|g")"; a=$OUT
+run ok "$(echo "$loop" | sed "s|N|200|; s|M/|$M/|g")"; b=$OUT
+expect_eq "fids after 20 == after 200 iterations ($a)" "$a" "$b"
+floop='i=0; while [ $i -lt N ]; do cat M/nope 2>/dev/null; echo x > M/fids 2>/dev/null; mkdir M/f 2>/dev/null; rm M/d 2>/dev/null; mv M/f M/d 2>/dev/null; i=$((i+1)); done; cat M/fids'
+run ok "$(echo "$floop" | sed "s|N|20|; s|M/|$M/|g")"; a=$OUT
+run ok "$(echo "$floop" | sed "s|N|200|; s|M/|$M/|g")"; b=$OUT
+expect_eq "fids after 20 == after 200 failing iterations ($a)" "$a" "$b"
+
+echo "# rename over an existing file must not lose the target when the rename fails"
+run rename_fail "echo A > $M/a; echo B > $M/b; mv $M/a $M/b 2>/dev/null; echo mv=\$?; cat $M/b; cat $M/a"
+expect_contains "rename_fail: mv reports failure" "mv=1" "$OUT"
+expect_contains "rename_fail: target b still has its content" "B" "$OUT"
+expect_contains "rename_fail: source a still has its content" "A" "$OUT"
+
+echo "# protocol violations on a data read must yield an error, not a crash"
+for mode in trunc short_frame huge_frame wrong_tag rread_big extra_reply close_mid renegotiate rwrite_big; do
+ if [ "$mode" = rwrite_big ]; then script="dd if=/dev/zero of=$M/f bs=10 count=1 2>&1; echo status=\$?"; else script="cat $M/f 2>&1; echo status=\$?"; fi
+ run "$mode" "$script"
+ no_crash "$mode"
+ expect_contains "$mode: child sees an error" "status=1" "$OUT"
+ case "$OUT" in *"Input/output error"*|*"not connected"*) pass "$mode: EIO/ENOTCONN";; *) fail "$mode: errno text" "$OUT";; esac
+done
+
+echo "# protocol violations on lookup/stat"
+for mode in wrong_type rwalk_many rstat_garbage rstat_overlong; do
+ run "$mode" "stat -c %s $M/f 2>&1; echo status=\$?"
+ no_crash "$mode"
+ expect_contains "$mode: child sees an error" "status=1" "$OUT"
+done
+run rwalk_zero "cat $M/nope 2>&1; echo status=\$?; cat $M/f 2>&1"
+no_crash "rwalk_zero"
+expect_contains "rwalk_zero: child sees an error" "status=1" "$OUT"
+run rerror_big "cat $M/nope 2>&1; echo status=\$?; cat $M/f"
+no_crash "rerror_big"
+expect_contains "rerror_big: child sees an error" "status=1" "$OUT"
+expect_contains "rerror_big: session survives a 64 KiB Rerror" "hello world" "$OUT"
+
+echo "# hostile stat contents"
+run length_max "stat -c '%s %b' $M/f 2>&1; echo status=\$?"
+no_crash "length_max"
+expect_contains "length_max: stat succeeds with a saturated block count" "status=0" "$OUT"
+run iounit_one "cat $M/f; head -c 3000 $M/big | wc -c"
+no_crash "iounit_one"
+expect_contains "iounit_one: read still complete" "hello world" "$OUT"
+expect_contains "iounit_one: 3000 bytes" "3000" "$OUT"
+
+echo "# hostile directory listings"
+run dir_split "ls $M 2>&1; echo status=\$?; cat $M/f"
+no_crash "dir_split"
+expect_contains "dir_split: readdir fails with EIO" "Input/output error" "$OUT"
+expect_contains "dir_split: session survives" "hello world" "$OUT"
+run dir_forever "ls $M 2>&1 | tail -c 200; echo status=\$?; cat $M/f; grep VmRSS /proc/\$PPID/status"
+no_crash "dir_forever"
+expect_contains "dir_forever: infinite directory is cut off with EIO" "Input/output error" "$OUT"
+expect_contains "dir_forever: session survives" "hello world" "$OUT"
+for mode in name_slash name_empty name_huge name_dots; do
+ run "$mode" "ls -a $M | tr '\n' ' '; echo; cat $M/f"
+ no_crash "$mode"
+ expect_contains "$mode: listing still works" "big d f fids" "$OUT"
+ expect_contains "$mode: file readable" "hello world" "$OUT"
+ case "$mode" in
+ name_slash) expect_eq "$mode: slash entry dropped" "" "$(printf '%s' "$OUT" | grep -o 'a/b')";;
+ name_dots) expect_eq "$mode: exactly one . and one .." "1 1" "$(printf '%s %s' "$(printf '%s\n' "$OUT" | head -1 | tr ' ' '\n' | grep -c '^\.$')" "$(printf '%s\n' "$OUT" | head -1 | tr ' ' '\n' | grep -c '^\.\.$')")";;
+ esac
+done
+
+echo "# qid collisions"
+run qid_collide "cat $M/f; cat $M/d/g; ls $M/d; stat -c %i $M/f $M/d 2>&1; echo status=\$?"
+no_crash "qid_collide"
+expect_contains "qid_collide: reads work" "hello world" "$OUT"
+run qid_zero "cat $M/f; ls $M | tr '\n' ' '; echo; cat $M/d/g; echo status=\$?"
+no_crash "qid_zero"
+expect_contains "qid_zero: file with root's qid.path is still a readable file" "hello world" "$OUT"
+expect_contains "qid_zero: root still lists" "big d f fids" "$OUT"
+expect_contains "qid_zero: nested file readable" "in d" "$OUT"
+
+echo "# version negotiation"
+run version_unknown "echo ran"
+expect_eq "version_unknown: 9player refuses (125)" "125" "$RC"
+run msize_tiny "cat $M/f 2>&1; echo status=\$?"
+no_crash "msize_tiny"
+
+echo "# a server that never replies"
+start_server never
+# SIGTERM is forwarded to the child; once the child is gone 9player must leave the
+# pending 9P reply behind and exit even though the server stays silent.
+timeout -s TERM 3 "$PLAYER" --unix "$SOCK" -- 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
+ fail "never: SIGTERM did not end 9player while a reply was outstanding"; kill -9 "$TPID"
+else
+ pass "never: SIGTERM ends 9player even while the server is silent"
+fi
+wait "$TPID" 2>/dev/null
+# Without a signal the mount hangs (documented v1 limitation) until the server dies.
+timeout 30 "$PLAYER" --unix "$SOCK" -- 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
+ pass "never: mount hangs while the server is silent (documented v1 limitation)"
+ kill "$SRVPID"; wait "$SRVPID" 2>/dev/null; SRVPID=
+ for _ in $(seq 1 50); do kill -0 "$TPID" 2>/dev/null || break; sleep 0.1; done
+ if kill -0 "$TPID" 2>/dev/null; then fail "never: 9player still alive after its server died"; kill -9 "$TPID"; else pass "never: killing the server unblocks 9player"; fi
+else
+ wait "$TPID"; fail "never: 9player exited early ($?)" "$(cat "$TMP/never.err")"
+fi
+
+echo "# interrupting a slow read (INTERRUPT must not confuse reply matching)"
+run slow "cat $M/big > /dev/null; cat $M/f; cat $M/fids" --msize 8192
+base=$(printf '%s\n' "$OUT" | tail -1)
+run slow "(cat $M/big > /dev/null & sleep 0.3; kill -INT \$!; wait \$!) 2>/dev/null; cat $M/f; cat $M/fids" --msize 8192
+no_crash "slow"
+expect_contains "slow: read after interrupted read works" "hello world" "$OUT"
+expect_eq "slow: fids after an interrupted read == after a complete one ($base)" "$base" "$(printf '%s\n' "$OUT" | tail -1)"
+
+echo
+echo "passed=$PASSED failed=$FAILED"
+[ "$FAILED" -eq 0 ]