diff options
Diffstat (limited to '9ns/test/adv_bridge_hostile.sh')
| -rwxr-xr-x | 9ns/test/adv_bridge_hostile.sh | 181 |
1 files changed, 181 insertions, 0 deletions
diff --git a/9ns/test/adv_bridge_hostile.sh b/9ns/test/adv_bridge_hostile.sh new file mode 100755 index 0000000..f5ba871 --- /dev/null +++ b/9ns/test/adv_bridge_hostile.sh @@ -0,0 +1,181 @@ +#!/usr/bin/env bash +# Hostile-server tests: 9ns against 9ns/test/adv_bridge_hostile.py in every +# misbehaviour mode. 9ns must never crash (panic/segfault) and must turn +# each misbehaviour into an errno for the child. +# Usage: bash 9ns/test/adv_bridge_hostile.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-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 9ns args...]: starts the server, runs 9ns; sets OUT, RC, STDERR. +run() { + local mode=$1 script=$2; shift 2 + start_server "$mode" + OUT=$(timeout 30 "$NS" --unix "$SOCK" "$@" -- sh -c "$script" 2>"$TMP/stderr") + RC=$? + STDERR=$(cat "$TMP/stderr") +} + +# 9ns 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: 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 +} + +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: 9ns 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 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" & +TPID=$! +sleep 4 +if kill -0 "$TPID" 2>/dev/null; then + fail "never: SIGTERM did not end 9ns while a reply was outstanding"; kill -9 "$TPID" +else + pass "never: SIGTERM ends 9ns 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 "$NS" --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: 9ns still alive after its server died"; kill -9 "$TPID"; else pass "never: killing the server unblocks 9ns"; fi +else + wait "$TPID"; fail "never: 9ns 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 ] |
