#!/usr/bin/env bash # Launch or validate USB hub enumeration (P3-A state machine) in QEMU. # # Topology under test: # qemu-xhci root port 1 → usb-hub (USB 2.0, 8 ports) # └─ hub port 2 → usb-kbd # qemu-xhci root port 2 → usb-tablet (control case, direct attach) # # Validates: hub descriptor read, usbhubd spawn, EP1 change detection or # polling fallback, power-on, debounce, reset-wait, attach delegation to # xhcid, child enumeration behind the hub (usbhidd spawn), no panics. set -euo pipefail find_uefi_firmware() { local candidates=( "/usr/share/ovmf/x64/OVMF.4m.fd" "/usr/share/OVMF/x64/OVMF.4m.fd" "/usr/share/ovmf/x64/OVMF_CODE.4m.fd" "/usr/share/OVMF/x64/OVMF_CODE.4m.fd" "/usr/share/ovmf/OVMF.fd" "/usr/share/OVMF/OVMF_CODE.fd" "/usr/share/qemu/edk2-x86_64-code.fd" ) local path for path in "${candidates[@]}"; do if [[ -f "$path" ]]; then printf '%s\n' "$path" return 0 fi done return 1 } usage() { cat <<'USAGE' Usage: test-usb-hub-qemu.sh [--check] [config] Boot or validate USB hub enumeration (usbhubd P3-A state machine) on a Red Bear image in QEMU. Defaults to redbear-mini. USAGE } check_mode=0 config="redbear-mini" for arg in "$@"; do case "$arg" in --help|-h|help) usage exit 0 ;; --check) check_mode=1 ;; redbear-*) config="$arg" ;; esac done firmware="$(find_uefi_firmware)" || { echo "ERROR: no usable x86_64 UEFI firmware found" >&2 exit 1 } arch="${ARCH:-$(uname -m)}" # Prefer the live ISO (canonical artifact of build-redbear.sh). harddrive.img # is only produced by older `make all`-style flows and can go stale — booting # it tests the wrong tree (this happened in practice: a stale Jul-17 image # with a pre-version-sync bootloader masked the current build). iso="build/$arch/$config.iso" image="build/$arch/$config/harddrive.img" extra="build/$arch/$config/extra.img" boot_media="" if [[ -f "$iso" ]]; then boot_media="iso" elif [[ -f "$image" ]]; then boot_media="harddrive" else echo "ERROR: no bootable image found" >&2 echo "Build it first with: ./local/scripts/build-redbear.sh $config" >&2 exit 1 fi pkill -f "qemu-system-x86_64.*$config" 2>/dev/null || true sleep 1 # Assemble the boot-media args once so check and interactive modes share it. if [[ "$boot_media" == "iso" ]]; then media_args=(-cdrom "$iso") else if [[ ! -f "$extra" ]]; then truncate -s 1g "$extra" fi media_args=( -drive "file=$image,format=raw,if=none,id=drv0,snapshot=on" -device nvme,drive=drv0,serial=NVME_SERIAL -drive "file=$extra,format=raw,if=none,id=drv1,snapshot=on" -device nvme,drive=drv1,serial=NVME_EXTRA ) fi if [[ "$check_mode" -eq 1 ]]; then log_file="build/$arch/$config/usb-hub-check.log" rm -f "$log_file" set +e # Timeout budget: hub enumeration itself is quick, but a hub-child's # class driver then performs its own descriptor/control transfers on the # (slow, emulated) TT path, and the guest clock runs several times # slower than wall time under load. 180s was not enough for a hub-child # HID driver to finish registering after being spawned. timeout 360s qemu-system-x86_64 \ -name "Red Bear OS x86_64" \ -device qemu-xhci,id=xhci \ -device usb-hub,bus=xhci.0,port=1 \ -device usb-kbd,bus=xhci.0,port=1.2 \ -device usb-tablet,bus=xhci.0,port=2 \ -smp 4 \ -m 2048 \ -bios "$firmware" \ -chardev stdio,id=debug,signal=off,mux=on \ -serial chardev:debug \ -mon chardev=debug \ -machine q35 \ -device ich9-intel-hda -device hda-output \ -device virtio-net,netdev=net0 \ -netdev user,id=net0 \ -nographic -vga none \ "${media_args[@]}" \ -enable-kvm -cpu host \ > "$log_file" 2>&1 status=$? set -e fail=0 # 1. xhcid detected the controller. if ! grep -q "^.*XHCI " "$log_file"; then echo "ERROR: xHCI controller not detected in boot log; see $log_file" >&2 exit 1 fi # 2. usbhubd spawned for the hub device. The "USB HUB driver spawned" # log line is emitted before setup_logging and never reaches the # serial console, so the reliable spawn proof is the hub-descriptor # read. Serial output from many daemons interleaves mid-line, so # match a short fragment rather than the full sentence. if ! grep -q "port(s) detected" "$log_file"; then echo "ERROR: usbhubd did not spawn or hub descriptor read failed" >&2 echo " Expected fragment: 'port(s) detected'" >&2 grep -i "usbhubd" "$log_file" | tail -10 >&2 || true echo " See $log_file" >&2 exit 1 fi # 4. Change detection is active (interrupt EP or polling fallback). if ! grep -Eq "change detection|using polling|falling back to polling" "$log_file"; then echo "ERROR: usbhubd did not report a change-detection mode" >&2 echo " See $log_file" >&2 exit 1 fi # 5. Hub-child enumeration: the usb-kbd sits BEHIND the hub (QEMU port # 1.2, a dotted PortId). Requiring the hub-child subdriver line proves # the full chain (power-on → debounce → reset-wait → enable → attach → # xhcid enumerate → class match). A root-port HID (the tablet on port 2) # must NOT satisfy this — the match is on the hub-child "5.2" form. if ! grep -Eq 'Loading subdriver "USB HID" for port [0-9]+\.[0-9]+' "$log_file"; then echo "ERROR: hub-child (usb-kbd behind hub) did not enumerate to a HID driver" >&2 echo " Expected: 'Loading subdriver \"USB HID\" for port .'" >&2 echo " See $log_file" >&2 exit 1 fi # 6. No panics anywhere in the boot. if grep -qiE "panic|RUST_BACKTRACE" "$log_file"; then echo "ERROR: panic detected in boot log" >&2 grep -iE "panic|RUST_BACKTRACE" "$log_file" | head -5 >&2 echo " See $log_file" >&2 exit 1 fi echo "HUB_ENUMERATION=ok" echo "HUB_LOG=$log_file" echo "USB hub enumeration confirmed in $log_file" exit 0 fi exec qemu-system-x86_64 \ -name "Red Bear OS x86_64" \ -device qemu-xhci,id=xhci \ -device usb-hub,bus=xhci.0,port=1 \ -device usb-kbd,bus=xhci.0,port=1.2 \ -device usb-tablet,bus=xhci.0,port=2 \ -smp 4 \ -m 2048 \ -bios "$firmware" \ -chardev stdio,id=debug,signal=off,mux=on \ -serial chardev:debug \ -mon chardev=debug \ -machine q35 \ -device ich9-intel-hda -device hda-output \ -device virtio-net,netdev=net0 \ -netdev user,id=net0 \ -vga std \ "${media_args[@]}" \ -enable-kvm -cpu host