#!/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 "$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" --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 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" --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 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 ]