summaryrefslogtreecommitdiff
path: root/9ns/test/adv_bridge_hostile.sh
blob: 20c05bfe1401cde10c9434d77d1cfdbe07c5b070 (plain) (blame)
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
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
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" --mount "$M" "$@" -- 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" --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 ]