#!/bin/sh set -eu dut=${1:?DUT path is required} freebsd_src=${2:?FreeBSD source path is required} base=${3:?base image is required} askpass=${4:?askpass path is required} output=${5:?output path is required} protected_pid=26318 protected_port=9222 builder=$dut/tests/pre15/fixtures/B12-build-kld.sh test ! -e "$output" || { printf '%s\n' "B12 QEMU output exists: $output" >&2 exit 20 } mkdir -p "$output" work=$(mktemp -d /tmp/pre15-b12-qemu.XXXXXX) overlay=$work/guest-overlay.qcow2 module=$work/B12-erofs-zstdio0.ko qemu_pid= port= guest_ready=0 module_loaded=0 protected_start=$(awk '{print $22}' /proc/$protected_pid/stat) protected_cmd=$(sha256sum /proc/$protected_pid/cmdline | awk '{print $1}') stat -c 'path=%n size=%s inode=%i mode=%f mtime=%Y ctime=%Z' "$base" \ >"$output/base-metadata-before.txt" ssh_options() { printf '%s\n' \ -q \ -o BatchMode=no \ -o PubkeyAuthentication=no \ -o PreferredAuthentications=keyboard-interactive,password \ -o NumberOfPasswordPrompts=1 \ -o StrictHostKeyChecking=no \ -o UserKnownHostsFile=/dev/null \ -o ConnectTimeout=5 \ -p "$port" } guest() { timeout -k 5 "${B12_GUEST_TIMEOUT:-60}" env DISPLAY=:0 \ SSH_ASKPASS="$askpass" SSH_ASKPASS_REQUIRE=force \ ssh $(ssh_options) root@127.0.0.1 "$@" } copy_to_guest() { timeout -k 5 "${B12_COPY_TIMEOUT:-120}" env DISPLAY=:0 \ SSH_ASKPASS="$askpass" SSH_ASKPASS_REQUIRE=force \ scp -O -q \ -o BatchMode=no \ -o PubkeyAuthentication=no \ -o PreferredAuthentications=keyboard-interactive,password \ -o NumberOfPasswordPrompts=1 \ -o StrictHostKeyChecking=no \ -o UserKnownHostsFile=/dev/null \ -o ConnectTimeout=5 \ -P "$port" "$1" root@127.0.0.1:"$2" } port_free() { python3 - "$1" <<'PY' import socket import sys with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: sock.settimeout(0.2) raise SystemExit(0 if sock.connect_ex(("127.0.0.1", int(sys.argv[1]))) != 0 else 1) PY } cleanup() { set +e if test "$guest_ready" = 1 && test "$module_loaded" = 1; then guest kldunload /root/B12-erofs-zstdio0.ko \ >>"$output/cleanup.log" 2>&1 module_loaded=0 fi if test -n "$qemu_pid" && kill -0 "$qemu_pid" 2>/dev/null; then kill -TERM "$qemu_pid" 2>/dev/null wait_count=0 while kill -0 "$qemu_pid" 2>/dev/null && test "$wait_count" -lt 20; do sleep 1 wait_count=$((wait_count + 1)) done if kill -0 "$qemu_pid" 2>/dev/null; then kill -KILL "$qemu_pid" 2>/dev/null fi wait "$qemu_pid" 2>/dev/null fi rm -rf "$work" } trap cleanup EXIT HUP INT TERM for tool in awk clang file nm python3 qemu-img qemu-system-x86_64 scp \ sha256sum ssh stat timeout; do command -v "$tool" >/dev/null 2>&1 || { printf '%s\n' "missing B12 QEMU tool: $tool" >&2 exit 21 } done test -d "$freebsd_src/sys" test -f "$base" test -x "$askpass" test -r /proc/$protected_pid/stat timeout -k 10 600 /bin/sh "$builder" "$dut" "$freebsd_src" "$module" \ "$work/kld-work" >"$output/build.stdout" 2>"$output/build.stderr" file "$module" >"$output/module.file" sha256sum "$module" >"$output/module.sha256" nm -g "$module" | LC_ALL=C sort >"$output/module.nm-global" nm -u "$module" | LC_ALL=C sort >"$output/module.nm-u" qemu-img create -q -f qcow2 -F qcow2 -b "$base" "$overlay" port=$(python3 - "$protected_port" <<'PY' import socket import sys protected = int(sys.argv[1]) while True: with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: sock.bind(("127.0.0.1", 0)) port = sock.getsockname()[1] if port != protected: print(port) break PY ) printf '%s\n' "$port" >"$output/owned-port.txt" qemu-system-x86_64 -accel tcg,thread=multi -cpu qemu64 \ -m 3072 -smp 2 \ -drive "file=$overlay,if=virtio,format=qcow2,cache=none" \ -netdev "user,id=net0,hostfwd=tcp:127.0.0.1:$port-:22" \ -device virtio-net-pci,netdev=net0 -display none \ -serial "file:$output/serial.log" -monitor none \ -D "$output/qemu.log" >"$output/qemu-process.log" 2>&1 & qemu_pid=$! printf '%s\n' "$qemu_pid" >"$output/owned-pid.txt" printf '%s\n' \ "qemu-system-x86_64 -m 3072 -smp 2 -drive owned-overlay -port $port" \ >"$output/qemu-argv-summary.txt" boot_wait=0 while test "$boot_wait" -lt "${B12_BOOT_TIMEOUT:-300}"; do if ! kill -0 "$qemu_pid" 2>/dev/null; then printf '%s\n' 'owned QEMU exited before SSH readiness' >&2 exit 21 fi if B12_GUEST_TIMEOUT=10 guest true >/dev/null 2>&1; then guest_ready=1 break fi sleep 2 boot_wait=$((boot_wait + 2)) done test "$guest_ready" = 1 || { printf '%s\n' 'owned QEMU SSH boot deadline expired' >&2 exit 21 } guest uname -a >"$output/guest-uname.txt" guest sysctl -n kern.osreldate >"$output/guest-osreldate.txt" test "$(tr -d '\r\n' < "$output/guest-osreldate.txt")" = 1500068 || { printf '%s\n' 'guest OSREL is not exact FreeBSD 15 ABI 1500068' >&2 exit 10 } if guest kldstat -q -m erofs >/dev/null 2>&1; then printf '%s\n' 'guest already has an EROFS module loaded' >&2 exit 21 fi copy_to_guest "$module" /root/B12-erofs-zstdio0.ko guest sha256 -q /root/B12-erofs-zstdio0.ko >"$output/guest-module.sha256" test "$(awk '{print $1}' "$output/module.sha256")" = \ "$(tr -d '\r\n' < "$output/guest-module.sha256")" guest dmesg >"$output/dmesg-before.txt" guest kldload /root/B12-erofs-zstdio0.ko >"$output/kldload.stdout" \ 2>"$output/kldload.stderr" module_loaded=1 guest kldstat -n B12-erofs-zstdio0.ko >"$output/kldstat-loaded.txt" guest kldunload /root/B12-erofs-zstdio0.ko >"$output/kldunload.stdout" \ 2>"$output/kldunload.stderr" module_loaded=0 guest dmesg >"$output/dmesg-after.txt" diff -u "$output/dmesg-before.txt" "$output/dmesg-after.txt" \ >"$output/dmesg.diff" || true sed -n '/^+++ /d; /^+/s/^+//p' "$output/dmesg.diff" \ >"$output/dmesg-added.txt" if grep -Eqi 'panic:|lock order reversal|witness.*warning|use-after-free|link_elf_obj: symbol .* undefined' \ "$output/dmesg-added.txt"; then printf '%s\n' 'B12 exact-ABI load produced a kernel diagnostic' >&2 exit 10 fi cleanup trap - EXIT HUP INT TERM stat -c 'path=%n size=%s inode=%i mode=%f mtime=%Y ctime=%Z' "$base" \ >"$output/base-metadata-after.txt" cmp "$output/base-metadata-before.txt" "$output/base-metadata-after.txt" test -r /proc/$protected_pid/stat test "$(awk '{print $22}' /proc/$protected_pid/stat)" = "$protected_start" test "$(sha256sum /proc/$protected_pid/cmdline | awk '{print $1}')" = "$protected_cmd" port_free "$port" cat >"$output/result.json" <