summaryrefslogtreecommitdiff
path: root/9ns/test/mntgen.sh
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-21 14:23:27 -0300
committerGabriel Schneider <[email protected]>2026-09-21 15:20:27 -0300
commit0d7e295efee1fca0935cf4a8bee9629c007dd2b6 (patch)
treed371eb028c63da5ab5b506bd25ac6579cf8a93fc /9ns/test/mntgen.sh
parent3a23f6a29e47ace901bd4d82b9db4055fcc12bb9 (diff)
downloadcloud9-0d7e295efee1fca0935cf4a8bee9629c007dd2b6.tar.gz
cloud9-0d7e295efee1fca0935cf4a8bee9629c007dd2b6.zip
9ns --mntgen: registry subdirectories are mount points too
A registry entry that is a directory is now served the way the root is: a synthetic directory listing the real one, dialing the sockets inside it on walk and recursing into further directories, to max_synth_depth (8) levels across max_synth_dirs (64) synthetic nodes. That is the plan9port mntgen shape and the layout zmx now posts under, so a live session reads at /mnt/9p/zmx/<name>. Before this a directory in the registry was dialed like a socket and answered EIO for good. post gains the two entry points the traversal needs: postedDir (the registry scan, against any directory) and dialPath (a dial by composed path, no name validation). Hardening, each from an attack that broke the code: - BATCH_FORGET carries entries for many owners and puts 0 in the header nodeid, so routing it by the header dropped all of them: 32 of 64 synthetic slots leaked in one close burst and the subdirectories that held them answered EIO forever. distributeForgets unpacks the body and hands each entry to its owner. - probe() and connectBlocking() copied a caller's path into the kernel address with no bound: a path past sun_path overran the 110-byte stack sockaddr (a panic in Debug, silent corruption in ReleaseFast). Both refuse it now, probe as `.live` so a claim never deletes what it could not inspect. - That bound then caught 9proc's own listener, which handed probe() the whole 108-byte sun_path array instead of the path inside it. The probe reads `.live` for anything it cannot ask about, so every stale socket became AlreadyListening and no server could ever take a dead predecessor's name back. It passes the path now. Suites: 87/87 root (+7 post/serve attack regressions), 48/48 9ns, 51+88 9ns integration (+4 traversal and slot-recycling checks), 213/0 9ns adversarial, 60/60 9proc plus its adversarial suites with a new stale-socket takeover check, freestanding green.
Diffstat (limited to '9ns/test/mntgen.sh')
-rwxr-xr-x9ns/test/mntgen.sh74
1 files changed, 74 insertions, 0 deletions
diff --git a/9ns/test/mntgen.sh b/9ns/test/mntgen.sh
index ea58e79..aade76c 100755
--- a/9ns/test/mntgen.sh
+++ b/9ns/test/mntgen.sh
@@ -2,6 +2,8 @@
# Integration tests for 9ns --mntgen: one FUSE mount whose synthetic root
# lists the posted-9P registry ($XDG_RUNTIME_DIR/9p), servers dialed lazily
# on the first walk into their name, one worker thread per server.
+# Registry entries that are directories are served like the root itself:
+# their sockets dial on walk and their directories recurse (depth-capped).
# Usage: bash 9ns/test/mntgen.sh <9ns> <9proc-demo> (zig build 9ns-itest)
# Exit 0 on success (or when the machine cannot run the tests), 1 on failure.
set -u
@@ -148,6 +150,78 @@ expect_eq "re-dial picks up the fresh post" "$(zig version)" "$(run_in "cat $M/a
echo "# mount defaults and environment"
expect_eq "default mountpoint is /mnt/9p" "$M" "$(timeout 60 "$NS" --mntgen -- sh -c 'echo $NINE_MOUNT')"
expect_contains "default mount is served" "alpha" "$(timeout 60 "$NS" --mntgen -- sh -c 'ls $NINE_MOUNT')"
+
+echo "# registry subdirectories are served like the root"
+mkdir -p "$REG/svc/deep"
+"$PROC" --unix "$REG/svc/delta" & PIDS+=($!)
+wait_socket "$REG/svc/delta"
+"$PROC" --unix "$REG/svc/deep/eps" & PIDS+=($!)
+wait_socket "$REG/svc/deep/eps"
+touch "$REG/svc/plainfile"
+expect_contains "root lists the subdirectory" "svc" "$(run_in "ls $M")"
+SVC_LS=$(run_in "ls $M/svc")
+expect_contains "subdirectory lists its sockets" "delta" "$SVC_LS"
+expect_contains "subdirectory lists nested directories" "deep" "$SVC_LS"
+expect_contains "subdirectory lists a plain file" "plainfile" "$SVC_LS"
+expect_eq "socket inside a directory dials" "$(zig version)" "$(run_in "cat $M/svc/delta/build/zig_version")"
+expect_eq "socket inside a nested directory dials" "$(zig version)" "$(run_in "cat $M/svc/deep/eps/build/zig_version")"
+expect_eq "walk into a plain file inside a directory is EIO, not a crash" "1" "$(run_in "cat $M/svc/plainfile 2>/dev/null; echo \$?")"
+expect_contains "the plain file stays listed" "plainfile" "$(run_in "ls $M/svc")"
+mkdir -p "$REG/a/b/c/d/e/f/g/h/i/j"
+expect_eq "eight levels of nesting serve" "ok" "$(run_in "[ -d $M/a/b/c/d/e/f/g/h ] && echo ok")"
+expect_eq "the ninth level answers EIO" "1" "$(run_in "[ -e $M/a/b/c/d/e/f/g/h/i/j ]; echo \$?")"
+expect_eq "\".\" inside a synthetic subdirectory is the subdirectory" "delta" \
+ "$(run_in "ls $M/svc/. | grep -x delta")"
+expect_eq "\"..\" from a synthetic subdirectory is the mount root" "svc" \
+ "$(run_in "ls $M/svc/.. | grep -x svc")"
+
+# A synthetic subdirectory holds one of 64 slots until the kernel forgets
+# its node. FUSE delivers those forgets in BATCH_FORGET messages whose
+# header nodeid is 0, so a dispatcher that routes a batch by that header
+# drops every entry in it and the slots never come back: subdirectories
+# served once then answer EIO forever. The forgets themselves arrive
+# lazily (the kernel frees dentries on its own schedule), so the check
+# retries rather than sampling once.
+if command -v python3 > /dev/null; then
+ echo "# synthetic subdirectory slots come back after the kernel forgets them"
+ for i in $(seq 1 65); do mkdir -p "$REG/s$(printf %02d "$i")"; done
+ cat > "$TMP/slots.py" <<'PYEOF'
+import os, sys, time
+M = sys.argv[1]
+n = 64
+def opendir(i):
+ return os.open("%s/s%02d" % (M, i), os.O_RDONLY | os.O_DIRECTORY)
+fds = [opendir(i) for i in range(1, n + 1)]
+try:
+ os.close(opendir(n + 1))
+ print("65th=opened") # the cap is not enforced
+except OSError:
+ print("65th=refused")
+for fd in fds:
+ os.close(fd)
+deadline, ok = time.time() + 10, 0
+while True:
+ ok = 0
+ for i in range(1, n + 1):
+ try:
+ os.close(opendir(i))
+ ok += 1
+ except OSError:
+ pass
+ if ok == n or time.time() > deadline:
+ break
+ time.sleep(0.2)
+print("recycled=%d" % ok)
+PYEOF
+ SLOTS=$(run_in "python3 $TMP/slots.py $M")
+ expect_contains "the 65th concurrent subdirectory is refused cleanly" "65th=refused" "$SLOTS"
+ expect_contains "all 64 slots come back after a close burst" "recycled=64" "$SLOTS"
+ rm -rf "$REG"/s[0-9][0-9]
+else
+ echo "SKIP: synthetic-slot recycling check needs python3"
+fi
+
+rm -rf "$REG/svc" "$REG/a"
expect_eq "mount is fuse" "yes" "$(run_in "grep -q \"^9ns $M fuse\" /proc/mounts && echo yes")"
echo "# usage errors"