diff --git a/CHANGES.md b/CHANGES.md index 90f6f47..5ec8670 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,7 @@ ## Unreleased +- Instrument diod `t0010` first `access=` mount (stderr/dmesg/diod log), pin `version=9p2000.L`, pre-create export, `Defaults !requiretty`, and force a fresh patched build stamp so CI can triage residual non-root mount failures. +- Fix non-root `ACCESS_SINGLE` mounts across diod sharness: `scripts/v9fs-mount-9p` + trash on `/tmp`; t0010 runasuser now expected PASS including root-negatives. - Run diod sharness as non-root user `v9fs` (so ACCESS_SINGLE / `--runas` negatives work); per-test `timeout` via automake `LOG_COMPILER` instead of one outer suite timeout. - Refresh diod XFAIL baseline for tip `v7.2` (access/umount residuals); tee `make check` so timeout still yields a parseable suite log for XFAIL eval. - Build-in `NET_9P_FD` (and `UNIX`) on published Images so diod-regression `trans=unix` mounts work; arm64 defconfig left FD as `=m` while virtio was forced `=y`. diff --git a/diod/xfail.txt b/diod/xfail.txt index 91f746a..2c19613 100644 --- a/diod/xfail.txt +++ b/diod/xfail.txt @@ -6,25 +6,27 @@ # # :: # -# Example: -# t0010-v9fs-runasuser.t :: root cannot read file -# # Keep this list minimal and update it intentionally when behavior changes. # -# Note: t0010 "root cannot …" / uname=root negatives are NOT XFAIL'd here; -# the suite must run as non-root user `v9fs` so those assertions are meaningful. +# t0010 runasuser ACCESS_SINGLE / root-negative assertions are expected to PASS +# when the suite runs as non-root `v9fs` with v9fs-mount-9p. +# allsquash identity mapping gaps under guest harness t0011-v9fs-allsquash.t :: root can create a directory, mode 755 t0011-v9fs-allsquash.t :: nobody can create a directory, mode 755 t0011-v9fs-allsquash.t :: nobody cannot create a file in nobody directory +t0011-v9fs-allsquash.t :: user can create a file in rootdir +t0011-v9fs-allsquash.t :: user can create a file in nobodydir +t0011-v9fs-allsquash.t :: user can chmod 777 nobody directory +t0011-v9fs-allsquash.t :: exited with status 1 +t0011-v9fs-allsquash :: automake error/timeout +# posixacl / setfacl residuals on tip t0013-v9fs-acl.t :: create a test file and setfacl -m u:root:r t0013-v9fs-acl.t :: getfacl confirms ACL is set t0013-v9fs-acl.t :: it is set on the server too t0013-v9fs-acl.t :: getfacl confirms ACL is unset t0013-v9fs-acl.t :: it is unset on the server too t0013-v9fs-acl.t :: create a test directory and setfacl -d -m u:root:r -t0013-v9fs-acl.t :: getfacl confirms ACL is set -t0013-v9fs-acl.t :: it is set on the server too -t0013-v9fs-acl.t :: getfacl confirms ACL is unset -t0013-v9fs-acl.t :: it is unset on the server too +t0013-v9fs-acl.t :: exited with status 1 +t0013-v9fs-acl :: automake error/timeout diff --git a/scripts/v9fs-build-initrd b/scripts/v9fs-build-initrd index fd0a8e8..5714a1f 100755 --- a/scripts/v9fs-build-initrd +++ b/scripts/v9fs-build-initrd @@ -42,6 +42,11 @@ echo "init: bootver=${bootver}" if mount -t 9p -o "trans=virtio,version=${bootver},cache=loose" hostshare /mnt/9; then echo "init: mounted hostshare at /mnt/9" + # Ensure the Debian chroot has real /proc /sys /dev (image dirs alone are not enough). + mkdir -p /mnt/9/proc /mnt/9/sys /mnt/9/dev + mount -t proc proc /mnt/9/proc 2>/dev/null || mount --bind /proc /mnt/9/proc || true + mount -t sysfs sysfs /mnt/9/sys 2>/dev/null || mount --bind /sys /mnt/9/sys || true + mount --bind /dev /mnt/9/dev 2>/dev/null || true if [ -x /mnt/9/usr/sbin/chroot ] && [ -x /mnt/9/usr/bin/bash ]; then echo "init: chroot -> /mnt/9, running container bash+ls" rc=0 @@ -92,9 +97,13 @@ if mount -t 9p -o "trans=virtio,version=${bootver},cache=loose" hostshare /mnt/9 # Run as non-root so --runas=$(id -u) / access=$(id -u) match diod intent. # Per-test timeout (LOG_COMPILER) so a hung script FAILs that test instead of # stalling the whole suite under a single outer timeout. + # Install per-test timeout wrapper into /tmp (writable, not on 9p). + cp -f /home/v9fs-test/test/scripts/v9fs-diod-test-timeout /tmp/v9fs-diod-test-timeout + chmod 755 /tmp/v9fs-diod-test-timeout + set +e - sudo -u v9fs -H make -C t check \ - LOG_COMPILER=timeout \ + timeout 1500 sudo -u v9fs -H stdbuf -oL -eL make -C t check \ + LOG_COMPILER=/tmp/v9fs-diod-test-timeout \ AM_LOG_FLAGS=300 \ TESTS="\ t0010-v9fs-runasuser.t \ @@ -106,6 +115,16 @@ if mount -t 9p -o "trans=virtio,version=${bootver},cache=loose" hostshare /mnt/9 " 2>&1 | tee "$make_out" make_rc="${PIPESTATUS[0]}" set -e + if [ -r /proc/self/mountinfo ]; then + while read -r _ _ _ _ mnt _; do + case "$mnt" in + /tmp/*) umount -l "$mnt" 2>/dev/null || true ;; + esac + done < /proc/self/mountinfo + fi + pkill -x diod 2>/dev/null || true + pkill -x test_diodrun 2>/dev/null || true + sleep 1 # Prefer automake combined log; synthesize from tee if missing. suite="$build/t/test-suite.log" @@ -116,6 +135,14 @@ if mount -t 9p -o "trans=virtio,version=${bootver},cache=loose" hostshare /mnt/9 fi cp -f "$suite" /home/v9fs-test/test/logs/diod-test-suite.log 2>/dev/null || true + # Scoop t0010 mount instrumentation + diod server logs for CI artifacts. + cp -f /home/v9fs-test/test/logs/diod-t0010-mount-debug.txt \ + /home/v9fs-test/test/logs/ 2>/dev/null || true + find "$build/t" -maxdepth 3 \( -name '*.diod.log' -o -name 'diod-t0010-mount-debug.txt' \) \ + -exec cp -f {} /home/v9fs-test/test/logs/ \; 2>/dev/null || true + cp -f /tmp/diod-t0010-mount.stderr /tmp/diod-t0010.diod.log \ + /home/v9fs-test/test/logs/ 2>/dev/null || true + # Evaluate failures against the harness XFAIL baseline so we can track them # without failing the overall CI stage unless a regression appears. eval_out="/home/v9fs-test/test/logs/diod-xfail" @@ -123,6 +150,12 @@ if mount -t 9p -o "trans=virtio,version=${bootver},cache=loose" hostshare /mnt/9 /home/v9fs-test/test/scripts/v9fs-diod-regression-eval "$suite" "$xfail" "$eval_out" eval_rc="$?" + # timeout(1) exits 124; signals often yield 143/137. Never treat an + # aborted suite with zero FAIL lines as an XFAIL pass. + if [ "$make_rc" -eq 124 ] || [ "$make_rc" -ge 128 ]; then + echo "diod-regression: make aborted rc=$make_rc (timeout/signal)" + exit "$make_rc" + fi if [ "$eval_rc" = "0" ]; then exit 0 fi diff --git a/scripts/v9fs-diod-test-timeout b/scripts/v9fs-diod-test-timeout new file mode 100755 index 0000000..aa44a20 --- /dev/null +++ b/scripts/v9fs-diod-test-timeout @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +# Per-test wrapper: line-buffered timeout around sharness, then drop leftover +# unix 9p mounts / diod so test_diodrun --socktest cannot wedge make check. +set -u +secs="${1:-300}" +shift +set +e +# stdbuf so breadcrumbs appear before a subsequent D-state hang. +stdbuf -oL -eL timeout --foreground -k 10 "$secs" stdbuf -oL -eL "$@" +rc=$? +set -e +if [ -r /proc/self/mountinfo ]; then + while read -r _ _ _ _ mnt _; do + case "$mnt" in + /tmp/sharness-trash/*) + umount -l "$mnt" 2>/dev/null || true + ;; + esac + done < /proc/self/mountinfo +fi +pkill -x diod 2>/dev/null || true +sleep 1 +exit "$rc" diff --git a/scripts/v9fs-mount-9p.c b/scripts/v9fs-mount-9p.c new file mode 100644 index 0000000..f2194e8 --- /dev/null +++ b/scripts/v9fs-mount-9p.c @@ -0,0 +1,121 @@ +/* + * Minimal 9p mount helper for diod sharness under ACCESS_SINGLE. + * util-linux mount(8) post-checks as euid 0 and fails when access=. + * This calls mount(2) directly with no post-check. + * + * If options contain access=, setfsuid to that uid before mount so the + * kernel session matches the ACCESS_SINGLE user (mounter may be root via sudo). + * + * Usage: v9fs-mount-9p [-n] -t 9p -o OPTS SOURCE MNT + * -n is accepted and ignored (compatibility with sharness mountcmd). + */ + +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include + +/* setfsuid(2) is Linux-only; declare if headers omit it. */ +extern int setfsuid(uid_t fsuid); + +static void +usage(const char *prog) +{ + fprintf(stderr, + "usage: %s [-n] -t 9p -o OPTS SOURCE MNT\n", prog); + exit(2); +} + +static uid_t +parse_access_uid(const char *opts) +{ + const char *p = opts; + + while (p && *p) { + if (!strncmp(p, "access=", 7)) { + p += 7; + if (*p >= '0' && *p <= '9') + return (uid_t)strtoul(p, NULL, 10); + return (uid_t)-1; + } + p = strchr(p, ','); + if (p) + p++; + } + return (uid_t)-1; +} + +int +main(int argc, char **argv) +{ + const char *opts = NULL; + const char *source = NULL; + const char *target = NULL; + unsigned long flags = 0; + uid_t access_uid; + int i; + + for (i = 1; i < argc; i++) { + if (!strcmp(argv[i], "-n")) + continue; + if (!strcmp(argv[i], "-t") || !strncmp(argv[i], "-t", 2)) { + const char *fstype = argv[i][2] ? argv[i] + 2 : NULL; + if (!fstype) { + if (++i >= argc) + usage(argv[0]); + fstype = argv[i]; + } + if (strcmp(fstype, "9p") != 0) + usage(argv[0]); + continue; + } + if (!strcmp(argv[i], "-o") || !strncmp(argv[i], "-o", 2)) { + opts = argv[i][2] ? argv[i] + 2 : NULL; + if (!opts) { + if (++i >= argc) + usage(argv[0]); + opts = argv[i]; + } + continue; + } + if (!source) + source = argv[i]; + else if (!target) + target = argv[i]; + else + usage(argv[0]); + } + + if (!opts || !source || !target) + usage(argv[0]); + + if (geteuid() != 0) { + fprintf(stderr, "%s: must run as root (use sudo)\n", argv[0]); + return 1; + } + + access_uid = parse_access_uid(opts); + if (access_uid != (uid_t)-1) { + /* Keep CAP_SYS_ADMIN as root euid; match ACCESS_SINGLE for VFS. + * setfsuid returns the previous fsuid (not -1) even on success. + */ + setfsuid(access_uid); + if ((uid_t)setfsuid(access_uid) != access_uid) { + fprintf(stderr, "%s: setfsuid(%u) did not stick\n", + argv[0], (unsigned)access_uid); + return 1; + } + } + + if (mount(source, target, "9p", flags, opts) < 0) { + fprintf(stderr, "%s: mount %s -> %s (%s): %s\n", + argv[0], source, target, opts, strerror(errno)); + return 1; + } + + return 0; +} diff --git a/scripts/v9fs-prepare-diod-regression b/scripts/v9fs-prepare-diod-regression index 740e696..3428ec0 100755 --- a/scripts/v9fs-prepare-diod-regression +++ b/scripts/v9fs-prepare-diod-regression @@ -8,6 +8,8 @@ src="${DIOD_SRC:-/workspaces/tmp/diod-src}" build_link="${DIOD_BUILD:-/workspaces/tmp/diod-build}" repo="${DIOD_REPO:-https://github.com/chaos/diod}" ref="${DIOD_REF:-master}" +# Bump when harness patches change so CI does not reuse a stale content-addressed build. +patch_stamp="${DIOD_PATCH_STAMP:-mount-helper-v12}" export DEBIAN_FRONTEND=noninteractive apt-get update -y @@ -35,11 +37,14 @@ if ! grep -q 'v9fs-guest' /etc/hosts 2>/dev/null; then fi cat >/etc/sudoers.d/v9fs-nopasswd <<'EOF' Defaults !fqdn +Defaults !requiretty v9fs ALL=(ALL) NOPASSWD:ALL EOF chmod 440 /etc/sudoers.d/v9fs-nopasswd -# Validate passwordless sudo before the guest boots. +# Validate passwordless sudo before the guest boots (no tty, like diodrun setsid). sudo -u v9fs sudo -n true +# Avoid util-linux "Too many levels of symbolic links" on broken /etc/mtab. +ln -sfn /proc/self/mounts /etc/mtab # Clone/update source. if [ ! -d "${src}/.git" ]; then @@ -49,29 +54,171 @@ fi git -C "${src}" fetch --depth 1 origin "${ref}" || true git -C "${src}" checkout -q "${ref}" || true -# Start diod as root then drop to --runas (production path). Non-root -# `unixsocket` left us with flaky mounts (ECONNREFUSED / cascading FAILs) -# even though waitsock passed; `unixsocketroot` matches diod's own root→runas flow. +# Reset patched files so re-prepare is idempotent. +git -C "${src}" checkout -q -- \ + t/t0010-v9fs-runasuser.t \ + t/t0011-v9fs-allsquash.t \ + t/t0012-v9fs-multiuser.t \ + t/t0013-v9fs-acl.t \ + t/t0020-dbench.t \ + t/t0021-postmark.t \ + t/sharness.d/20-diod.sh \ + t/sharness.sh \ + || true + +# Start diod as root then drop to --runas (production path). find "${src}/t" -type f -name 't*.t' -print0 \ | xargs -0 sed -i 's/test_under_diod unixsocket /test_under_diod unixsocketroot /g' +python3 - <<'PY' "${src}" +import pathlib, sys +src = pathlib.Path(sys.argv[1]) + +old_cmds = ( + 'umountcmd="$SUDO umount -l"\n' + 'mountcmd="$SUDO mount -n -t 9p"\n' + 'mountopts="trans=unix,uname=$(id -un)"\n' +) +new_cmds = ( + 'if test "$(id -u)" = "0"; then\n' + '\tumountcmd="$SUDO umount -l"\n' + '\tmountcmd="$SUDO mount -n -t 9p"\n' + 'else\n' + '\t# util-linux post-mount check as euid 0 fails ACCESS_SINGLE when access=.\n' + '\tumountcmd="v9fs_access_umount -l"\n' + '\tmountcmd="v9fs_access_mount -n -t 9p"\n' + 'fi\n' + 'mountopts="trans=unix,version=9p2000.L,uname=$(id -un)"\n' +) + +patched = 0 +for path in sorted((src / "t").glob("t*.t")): + text = path.read_text() + orig = text + if old_cmds in text: + text = text.replace(old_cmds, new_cmds, 1) + if path.name == "t0010-v9fs-runasuser.t": + old_under = ( + "exportdir=$SHARNESS_TRASH_DIRECTORY/exp\n" + "test_under_diod unixsocketroot \\\n" + " --config-file=/dev/null \\\n" + " --debug=0x1 \\\n" + " --runas=$(id -u) \\\n" + " --no-auth \\\n" + " --export=$exportdir\n" + ) + new_under = ( + "exportdir=$SHARNESS_TRASH_DIRECTORY/exp\n" + 'mkdir -p "$exportdir"\n' + "test_under_diod unixsocketroot \\\n" + " --config-file=/dev/null \\\n" + " --debug=0x1 \\\n" + " --runas=$(id -u) \\\n" + " --no-auth \\\n" + " --export=$exportdir\n" + ) + if old_under in text: + text = text.replace(old_under, new_under, 1) + if text != orig: + path.write_text(text) + patched += 1 + print(f"patched {path}") +if patched < 1: + raise SystemExit("no t*.t files patched for mountcmd") + +helper = src / "t" / "sharness.d" / "20-diod.sh" +h = helper.read_text() +mount_helpers = """ +V9FS_MOUNT_9P=/workspaces/tmp/v9fs-mount-9p + +# Minimal mount(2) helper avoids util-linux post-check as root under ACCESS_SINGLE. +v9fs_access_mount() { + if test "$(id -u)" = "0"; then + "$V9FS_MOUNT_9P" "$@" + else + $SUDO "$V9FS_MOUNT_9P" "$@" + fi +} + +v9fs_access_umount() { + if test "$(id -u)" = "0"; then + umount "$@" + else + $SUDO umount "$@" + fi +} + +""" +if "v9fs_access_mount()" not in h: + anchor = "# Usage: waitsock sockpath [retries]\nwaitsock() {" + if anchor not in h: + raise SystemExit("20-diod.sh: waitsock anchor not found") + h = h.replace(anchor, mount_helpers + anchor, 1) + +old_cleanup = ( + '\tif test -n "$TEST_UNDER_DIOD_ACTIVE"; then\n' + '\t\ttest "$debug" = "t" || cleanup rm -f "${SHARNESS_TEST_DIRECTORY:-..}/$log_file"\n' + '\t\treturn\n' + '\tfi\n' +) +new_cleanup = ( + '\tif test -n "$TEST_UNDER_DIOD_ACTIVE"; then\n' + '\t\t# Keep $log_file for harness triage (was removed unless --debug).\n' + '\t\treturn\n' + '\tfi\n' +) +if old_cleanup in h: + h = h.replace(old_cleanup, new_cleanup, 1) +elif "Keep $log_file for harness triage" not in h: + raise SystemExit("20-diod.sh: cleanup block not found") +helper.write_text(h) +print(f"patched {helper}") + +sharness = src / "t" / "sharness.sh" +s = sharness.read_text() +old_trash = 'SHARNESS_TRASH_DIRECTORY="trash-directory.$SHARNESS_TEST_NAME"' +new_trash = 'SHARNESS_TRASH_DIRECTORY="/tmp/sharness-trash/$SHARNESS_TEST_NAME"' +if old_trash not in s: + raise SystemExit("sharness.sh: trash directory assignment not found") +sharness.write_text(s.replace(old_trash, new_trash, 1)) +print(f"patched {sharness}") +PY + +n_root=$(grep -rh 'test_under_diod unixsocketroot' "${src}/t" --include='*.t' 2>/dev/null | wc -l | tr -d ' ' || true) +echo "diod harness patch: unixsocketroot=${n_root} stamp=${patch_stamp}" +if grep -rhq 'test_under_diod unixsocket ' "${src}/t" --include='*.t' 2>/dev/null; then + echo "ERROR: remaining test_under_diod unixsocket references after patch" >&2 + grep -rn 'test_under_diod unixsocket ' "${src}/t" --include='*.t' >&2 || true + exit 1 +fi +grep -q 'v9fs_access_mount' "${src}/t/t0010-v9fs-runasuser.t" +grep -q 'v9fs_access_mount' "${src}/t/t0020-dbench.t" +grep -q 'V9FS_MOUNT_9P' "${src}/t/sharness.d/20-diod.sh" +grep -q '/tmp/sharness-trash' "${src}/t/sharness.sh" + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +gcc -O2 -o /workspaces/tmp/v9fs-mount-9p "${script_dir}/v9fs-mount-9p.c" +chmod 755 /workspaces/tmp/v9fs-mount-9p +if ! findmnt -t tmpfs /tmp >/dev/null 2>&1; then + mount -t tmpfs -o mode=1777,size=512M tmpfs /tmp +fi +mkdir -p /tmp/sharness-trash +chmod 1777 /tmp /tmp/sharness-trash + # Autotools bootstrap. (cd "${src}" && ./autogen.sh) -# Build in a content-addressed directory (avoids cleanup issues with mixed uid mappings). -build_real="/workspaces/tmp/diod-build-$(git -C "${src}" rev-parse --short HEAD)" +build_real="/workspaces/tmp/diod-build-$(git -C "${src}" rev-parse --short HEAD)-${patch_stamp}" +rm -rf "${build_real}" mkdir -p "${build_real}" (cd "${build_real}" && "${src}/configure") (cd "${build_real}" && make -j"$(nproc)") -# Record the build directory for the guest (symlinks can be awkward across 9p+bind mounts). echo "${build_real}" > /workspaces/tmp/diod-build.LATEST +ln -sfn "${build_real}" "${build_link}" 2>/dev/null || true -# The guest writes test logs into the build tree; ensure it's writable over 9p -# regardless of uid mapping. chmod -R a+rwX "${src}" "${build_real}" || true echo "Prepared diod build at ${build_real}" ls -la "${build_real}/t" | head -n 50 || true -