From ee1da17617291fbea367f72a38ae4b6cef2fb8e6 Mon Sep 17 00:00:00 2001 From: vasilito Date: Mon, 20 Jul 2026 10:04:56 +0900 Subject: [PATCH] tests: add USB UHCI/OHCI/EHCI-autospawn/error-recovery QEMU proofs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Create the four USB validation scripts that local/docs/USB-VALIDATION-RUNBOOK.md references but which did not exist on disk, closing the P8-B runtime-proof gap for the legacy host controllers and the P8-C error-injection gap for xHCI: - test-uhci-runtime-qemu.sh (P1-B): -machine pc + piix3-usb-uhci, serial-log proof that uhcid binds the controller and detects the attached usb-kbd. - test-ohci-runtime-qemu.sh (P1-B): -machine pc + pci-ohci, serial-log proof that ohcid binds the controller and detects the attached usb-kbd. - test-ehci-class-autospawn-qemu.sh (P1-A): -machine q35 + usb-ehci + usb-kbd, serial-log proof that ehcid enumerates the keyboard and usbhidd auto-spawns via the unified UsbHostController trait. The existing test-ehci-qemu.sh is a coarse usb-tablet smoke test and does NOT cover this path, so this is a full script rather than an alias. - test-usb-error-recovery-qemu.sh (P8-C): hot-unplug usb-storage mid-transfer via the QEMU monitor device_del (chardev stdio mux, Ctrl-A c toggle — the same pattern as test-xhci-device-lifecycle-qemu.sh, since qemu-login-expect.py drives only serial and cannot reach the monitor). Proves graceful detach + usbscsid IO-error handling with no panic. test-xhci-device-lifecycle-qemu.sh already existed and is unchanged. test-usb-uas-qemu.sh is intentionally NOT created: UAS is in flight in a separate workstream and its test belongs to that change. Proof style and harness fidelity: - UHCI/OHCI/EHCI proofs follow the test-xhci-irq-qemu.sh serial-log grep pattern (dual --check + interactive mode, ISO-preferred boot_args, 180s timeout, no panic fail-marker). No guest-side checker exists that validates legacy-controller enumeration specifically, so per the runbook's serial-log-evidence rule they grep driver log lines instead of inventing a guest binary. - The error-recovery proof follows the test-xhci-device-lifecycle-qemu.sh expect/Tcl monitor-mux pattern and reuses the existing redbear-usb-storage-check guest binary (redbear-hwutils) to drive an active transfer — no new guest binary is invented. All proof markers are sourced directly from the real driver trees: uhcid main.rs:487/535/545 (UHCI USB 1.1 at / controller initialized / connect) ohcid main.rs:305/341/348 (OHCI USB 1.1 at / controller initialized / connect) ehcid main.rs:154/294/560 (EHCI USB 2.0 at / controller initialized / port device) usbhidd main.rs:221 (USB HID driver spawned with scheme) usbscsid main.rs:190/200/163/156 (READ/WRITE IO ERROR / scheme tick / event error) No existing scripts, qemu-login-expect.py, config/*.toml, recipes, or the runbook were modified. The runbook's script names and --check invocations already matched exactly, so no runbook edits were required. Validation: bash -n passes on all four scripts. shellcheck is not installed on this host. NO QEMU runs were performed — the host is contended by another workload that kills QEMU processes, so validation is syntax check + pattern fidelity review only. Runtime pass/fail is unverified. --- .../scripts/test-ehci-class-autospawn-qemu.sh | 236 +++++++++++++++ local/scripts/test-ohci-runtime-qemu.sh | 203 +++++++++++++ local/scripts/test-uhci-runtime-qemu.sh | 203 +++++++++++++ local/scripts/test-usb-error-recovery-qemu.sh | 286 ++++++++++++++++++ 4 files changed, 928 insertions(+) create mode 100755 local/scripts/test-ehci-class-autospawn-qemu.sh create mode 100755 local/scripts/test-ohci-runtime-qemu.sh create mode 100755 local/scripts/test-uhci-runtime-qemu.sh create mode 100755 local/scripts/test-usb-error-recovery-qemu.sh diff --git a/local/scripts/test-ehci-class-autospawn-qemu.sh b/local/scripts/test-ehci-class-autospawn-qemu.sh new file mode 100755 index 0000000000..e0adea12b8 --- /dev/null +++ b/local/scripts/test-ehci-class-autospawn-qemu.sh @@ -0,0 +1,236 @@ +#!/usr/bin/env bash +# Launch or validate the EHCI class-driver autospawn path (P1-A) in QEMU. +# +# Topology under test: +# -machine q35 → -device usb-ehci,id=ehci +# └─ bus=ehci.0 → usb-kbd +# +# Validates the P1-A claim that a USB keyboard on the EHCI route reaches the +# input stack via the unified `UsbHostController` trait: +# 1. pcid auto-spawns ehcid for the EHCI PCI device (class 0c0320, see +# local/config/drivers.d/20-usb.toml). +# 2. ehcid maps MMIO, initializes ports, and enumerates the usb-kbd. +# 3. ehcid calls usb_core::spawn::spawn_class_driver_for_port() (ehcid +# main.rs:576), which selects the HID driver for class 0x03 (usb-core +# spawn.rs:18) and spawns `/usr/bin/usbhidd`. +# 4. usbhidd starts and registers itself as an `inputd::ProducerHandle` +# holder (usbhidd main.rs:4), wiring the keyboard into the input stack. +# +# This is the autospawn half of the P1-A proof ("all four controllers +# auto-spawn via the unified trait; class daemons can connect to all four via +# the same scheme"). The existing local/scripts/test-ehci-qemu.sh is a coarse +# 17-line smoke test (usb-tablet, grep "ehci|EHCI") that does NOT exercise the +# keyboard-autospawn path; this script is the rigorous P1-A proof and does not +# duplicate that smoke test's logic. +# +# Evidence is read from the serial log. Proof markers come straight from the +# driver sources: +# "EHCI USB 2.0 at " (ehcid main.rs:154) +# "ehcid: controller initialized, ports, ..." (ehcid main.rs:294) +# "ehcid: port device : address " (ehcid main.rs:560) +# "USB HID driver spawned with scheme `...`..." (usbhidd main.rs:221) +# +# Note on "reaches inputd": usbhidd is an inputd producer by construction +# (`use inputd::ProducerHandle`). There is no guest-side checker that validates +# an inputd keypress event for a USB keyboard, so the autospawn-to-usbhidd +# marker is the strongest serial-log evidence available; a dedicated +# keypress-to-inputd proof would require an inputd guest checker that does not +# exist yet (serial-log-evidence rule per the USB validation runbook). + +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-ehci-class-autospawn-qemu.sh [--check] [config] + +Boot or validate the EHCI USB-keyboard class-driver autospawn path (P1-A) on a +Red Bear image in QEMU. Uses `-machine q35` + `usb-ehci` + `usb-kbd`, matching +the controller selection of the existing test-ehci-qemu.sh smoke test but +adding the keyboard and the autospawn evidence checks. Defaults to +redbear-mini (mapped to the in-tree redbear-mini image). +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 build-redbear.sh artifact); harddrive.img is +# only produced by older make-all flows and goes stale, which would test the +# wrong tree. extra.img stays as a data drive in both modes. +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 ($iso or $image)" >&2 + echo "Build it first with: ./local/scripts/build-redbear.sh $config" >&2 + exit 1 +fi + +if [[ ! -f "$extra" ]]; then + truncate -s 1g "$extra" +fi + +if [[ "$boot_media" == "iso" ]]; then + boot_args=(-cdrom "$iso") +else + boot_args=(-drive file="$image",format=raw,if=none,id=drv0,snapshot=on -device nvme,drive=drv0,serial=NVME_SERIAL) +fi + +pkill -f "qemu-system-x86_64.*$config" 2>/dev/null || true +sleep 1 + +if [[ "$check_mode" -eq 1 ]]; then + log_file="build/$arch/$config/ehci-autospawn-check.log" + rm -f "$log_file" + set +e + # 180s mirrors the xHCI interrupt proof budget. The autospawn chain + # (ehcid enumerate → usb-core spawn → usbhidd init) is bounded but the + # guest clock runs several times slower than wall under boot load. + timeout 180s qemu-system-x86_64 \ + -name "Red Bear OS x86_64" \ + -machine q35 \ + -device usb-ehci,id=ehci \ + -device usb-kbd,bus=ehci.0 \ + -smp 4 \ + -m 2048 \ + -bios "$firmware" \ + -chardev stdio,id=debug,signal=off,mux=on \ + -serial chardev:debug \ + -mon chardev=debug \ + -device ich9-intel-hda -device hda-output \ + -device virtio-net,netdev=net0 \ + -netdev user,id=net0 \ + -object filter-dump,id=f1,netdev=net0,file="build/$arch/$config/network.pcap" \ + -nographic -vga none \ + "${boot_args[@]}" \ + -drive file="$extra",format=raw,if=none,id=drv1,snapshot=on \ + -device nvme,drive=drv1,serial=NVME_EXTRA \ + -enable-kvm -cpu host \ + > "$log_file" 2>&1 + status=$? + set -e + + # 1. ehcid bound the controller. This proves pcid matched the EHCI PCI + # class (0c0320) and spawned ehcid. + if ! grep -q "EHCI USB 2.0 at " "$log_file"; then + echo "ERROR: ehcid did not bind the EHCI controller" >&2 + echo " Expected: 'EHCI USB 2.0 at '" >&2 + grep -i "ehcid\|EHCI" "$log_file" | tail -10 >&2 || true + echo " See $log_file" >&2 + exit 1 + fi + + # 2. Controller initialized (ports detected, async queue armed). + if ! grep -q "ehcid: controller initialized," "$log_file"; then + echo "ERROR: ehcid did not finish controller initialization" >&2 + echo " Expected: 'ehcid: controller initialized, ports, ...'" >&2 + echo " See $log_file" >&2 + exit 1 + fi + + # 3. usb-kbd enumerated on the EHCI port. This proves the EHCI transfer + # path (descriptor fetch + SET_ADDRESS + config) reached a fully + # attached device. + if ! grep -Eq "ehcid: port [0-9]+ device [0-9a-fA-F]{4}:[0-9a-fA-F]{4} address [0-9]+" "$log_file"; then + echo "ERROR: ehcid did not enumerate the usb-kbd" >&2 + echo " Expected: 'ehcid: port device : address '" >&2 + echo " See $log_file" >&2 + exit 1 + fi + + # 4. usbhidd auto-spawned via the unified trait. This is the P1-A core + # claim: spawn_class_driver_for_port selected the HID driver for the + # enumerated keyboard and launched usbhidd, which is the inputd + # producer for the keyboard. Without this marker the autospawn path + # is not proven even if enumeration succeeded. + if ! grep -q "USB HID driver spawned with scheme" "$log_file"; then + echo "ERROR: usbhidd did not auto-spawn for the EHCI keyboard" >&2 + echo " Expected: 'USB HID driver spawned with scheme \`...\` ...'" >&2 + grep -i "usbhidd\|HID driver" "$log_file" | tail -10 >&2 || true + echo " See $log_file" >&2 + exit 1 + fi + + # 5. 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 "EHCI_DRIVER=ehcid" + echo "EHCI_CONTROLLER=usb-ehci" + echo "EHCI_MACHINE=q35" + echo "EHCI_AUTOSPAWN=usbhidd" + echo "EHCI_LOG=$log_file" + echo "EHCI keyboard class-driver autospawn confirmed in $log_file" + exit 0 +fi + +exec qemu-system-x86_64 \ + -name "Red Bear OS x86_64" \ + -machine q35 \ + -device usb-ehci,id=ehci \ + -device usb-kbd,bus=ehci.0 \ + -smp 4 \ + -m 2048 \ + -bios "$firmware" \ + -chardev stdio,id=debug,signal=off,mux=on \ + -serial chardev:debug \ + -mon chardev=debug \ + -device ich9-intel-hda -device hda-output \ + -device virtio-net,netdev=net0 \ + -netdev user,id=net0 \ + -object filter-dump,id=f1,netdev=net0,file="build/$arch/$config/network.pcap" \ + -vga std \ + "${boot_args[@]}" \ + -drive file="$extra",format=raw,if=none,id=drv1,snapshot=on \ + -device nvme,drive=drv1,serial=NVME_EXTRA \ + -enable-kvm -cpu host diff --git a/local/scripts/test-ohci-runtime-qemu.sh b/local/scripts/test-ohci-runtime-qemu.sh new file mode 100755 index 0000000000..e93d202e6a --- /dev/null +++ b/local/scripts/test-ohci-runtime-qemu.sh @@ -0,0 +1,203 @@ +#!/usr/bin/env bash +# Launch or validate OHCI controller enumeration (P1-B) in QEMU. +# +# Topology under test: +# -machine pc → -device pci-ohci,id=ohci +# └─ bus=ohci.0 → usb-kbd +# +# Validates: pcid auto-spawns ohcid for the QEMU OHCI PCI device (class 0c0310, +# see local/config/drivers.d/20-usb.toml), ohcid maps the MMIO registers, +# initializes the controller, and detects the attached usb-kbd (port connect + +# enumeration descriptor read). This is the P1-B OHCI runtime-enumeration proof +# that closes the P8-B gap for OHCI. +# +# Evidence is read from the serial log (no guest-side checker exists that +# validates OHCI enumeration specifically), mirroring the test-xhci-irq-qemu.sh +# proof style. Proof markers come straight from +# local/recipes/drivers/ohcid/source/src/main.rs: +# "OHCI USB 1.1 at " (main.rs:305 — controller bound) +# "ohcid: controller initialized, polling ports" (main.rs:341 — init done) +# "ohcid: port connect detected" (main.rs:348 — port event) +# "ohcid: port device : class " (main.rs:352 — enumerated) + +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-ohci-runtime-qemu.sh [--check] [config] + +Boot or validate OHCI (USB 1.1) controller enumeration on a Red Bear image in +QEMU. `pci-ohci` is a standalone PCI OHCI controller; this proof uses +`-machine pc` to match the runbook's "legacy QEMU machine" framing for both +P1-B companion-controller proofs. Defaults to redbear-mini (mapped to the +in-tree redbear-mini image). +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 build-redbear.sh artifact); harddrive.img is +# only produced by older make-all flows and goes stale, which would test the +# wrong tree. extra.img stays as a data drive in both modes. +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 ($iso or $image)" >&2 + echo "Build it first with: ./local/scripts/build-redbear.sh $config" >&2 + exit 1 +fi + +if [[ ! -f "$extra" ]]; then + truncate -s 1g "$extra" +fi + +if [[ "$boot_media" == "iso" ]]; then + boot_args=(-cdrom "$iso") +else + boot_args=(-drive file="$image",format=raw,if=none,id=drv0,snapshot=on -device nvme,drive=drv0,serial=NVME_SERIAL) +fi + +pkill -f "qemu-system-x86_64.*$config" 2>/dev/null || true +sleep 1 + +if [[ "$check_mode" -eq 1 ]]; then + log_file="build/$arch/$config/ohci-runtime-check.log" + rm -f "$log_file" + set +e + # 180s mirrors the xHCI interrupt proof budget. OHCI enumeration on QEMU's + # emulated pci-ohci is quick once ohcid spawns, but the guest clock runs + # several times slower than wall under boot load. + timeout 180s qemu-system-x86_64 \ + -name "Red Bear OS x86_64" \ + -machine pc \ + -device pci-ohci,id=ohci \ + -device usb-kbd,bus=ohci.0 \ + -smp 4 \ + -m 2048 \ + -bios "$firmware" \ + -chardev stdio,id=debug,signal=off,mux=on \ + -serial chardev:debug \ + -mon chardev=debug \ + -device virtio-net,netdev=net0 \ + -netdev user,id=net0 \ + -object filter-dump,id=f1,netdev=net0,file="build/$arch/$config/network.pcap" \ + -nographic -vga none \ + "${boot_args[@]}" \ + -drive file="$extra",format=raw,if=none,id=drv1,snapshot=on \ + -device nvme,drive=drv1,serial=NVME_EXTRA \ + -enable-kvm -cpu host \ + > "$log_file" 2>&1 + status=$? + set -e + + # 1. ohcid bound the controller and logged its PCI path. This proves pcid + # matched the OHCI PCI class (0c0310) and spawned ohcid. + if ! grep -q "OHCI USB 1.1 at " "$log_file"; then + echo "ERROR: ohcid did not bind the OHCI controller" >&2 + echo " Expected: 'OHCI USB 1.1 at '" >&2 + grep -i "ohcid\|OHCI" "$log_file" | tail -10 >&2 || true + echo " See $log_file" >&2 + exit 1 + fi + + # 2. MMIO mapped and the polling loop started. + if ! grep -q "ohcid: controller initialized, polling ports" "$log_file"; then + echo "ERROR: ohcid did not finish controller initialization" >&2 + echo " Expected: 'ohcid: controller initialized, polling ports'" >&2 + echo " See $log_file" >&2 + exit 1 + fi + + # 3. Port connect detected for the attached usb-kbd. This is the + # enumeration-proof marker: the controller saw a device and ran the + # descriptor-read path. + if ! grep -Eq "ohcid: port [0-9]+ connect detected" "$log_file"; then + echo "ERROR: ohcid did not detect a port connect event" >&2 + echo " Expected: 'ohcid: port connect detected'" >&2 + echo " See $log_file" >&2 + exit 1 + fi + + # 4. 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 "OHCI_DRIVER=ohcid" + echo "OHCI_CONTROLLER=pci-ohci" + echo "OHCI_MACHINE=pc" + echo "OHCI_LOG=$log_file" + echo "OHCI controller enumeration confirmed in $log_file" + exit 0 +fi + +exec qemu-system-x86_64 \ + -name "Red Bear OS x86_64" \ + -machine pc \ + -device pci-ohci,id=ohci \ + -device usb-kbd,bus=ohci.0 \ + -smp 4 \ + -m 2048 \ + -bios "$firmware" \ + -chardev stdio,id=debug,signal=off,mux=on \ + -serial chardev:debug \ + -mon chardev=debug \ + -device virtio-net,netdev=net0 \ + -netdev user,id=net0 \ + -object filter-dump,id=f1,netdev=net0,file="build/$arch/$config/network.pcap" \ + -vga std \ + "${boot_args[@]}" \ + -drive file="$extra",format=raw,if=none,id=drv1,snapshot=on \ + -device nvme,drive=drv1,serial=NVME_EXTRA \ + -enable-kvm -cpu host diff --git a/local/scripts/test-uhci-runtime-qemu.sh b/local/scripts/test-uhci-runtime-qemu.sh new file mode 100755 index 0000000000..fecc4ca0ec --- /dev/null +++ b/local/scripts/test-uhci-runtime-qemu.sh @@ -0,0 +1,203 @@ +#!/usr/bin/env bash +# Launch or validate UHCI controller enumeration (P1-B) in QEMU. +# +# Topology under test: +# -machine pc (i440fx/PIIX3 chipset) → -device piix3-usb-uhci,id=uhci +# └─ bus=uhci.0 → usb-kbd +# +# Validates: pcid auto-spawns uhcid for the PIIX3 UHCI PCI device (class 0c0300, +# see local/config/drivers.d/20-usb.toml), uhcid maps the I/O base, initializes +# the frame list, and detects the attached usb-kbd (port connect + enumeration +# descriptor read). This is the P1-B UHCI runtime-enumeration proof that closes +# the P8-B gap for UHCI. +# +# Evidence is read from the serial log (no guest-side checker exists that +# validates UHCI enumeration specifically), mirroring the test-xhci-irq-qemu.sh +# proof style. Proof markers come straight from +# local/recipes/drivers/uhcid/source/src/main.rs: +# "UHCI USB 1.1 at " (main.rs:487 — controller bound) +# "uhcid: controller initialized, polling ports" (main.rs:535 — init done) +# "uhcid: port connect detected" (main.rs:545 — port event) +# "uhcid: port device : class " (main.rs:551 — enumerated) + +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-uhci-runtime-qemu.sh [--check] [config] + +Boot or validate UHCI (USB 1.1) controller enumeration on a Red Bear image in +QEMU. The PIIX3 UHCI companion controller only exists on the i440fx machine +type, so this proof uses `-machine pc` rather than the q35 machine used by the +xHCI suite. Defaults to redbear-mini (mapped to the in-tree redbear-mini image). +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 build-redbear.sh artifact); harddrive.img is +# only produced by older make-all flows and goes stale, which would test the +# wrong tree. extra.img stays as a data drive in both modes. +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 ($iso or $image)" >&2 + echo "Build it first with: ./local/scripts/build-redbear.sh $config" >&2 + exit 1 +fi + +if [[ ! -f "$extra" ]]; then + truncate -s 1g "$extra" +fi + +if [[ "$boot_media" == "iso" ]]; then + boot_args=(-cdrom "$iso") +else + boot_args=(-drive file="$image",format=raw,if=none,id=drv0,snapshot=on -device nvme,drive=drv0,serial=NVME_SERIAL) +fi + +pkill -f "qemu-system-x86_64.*$config" 2>/dev/null || true +sleep 1 + +if [[ "$check_mode" -eq 1 ]]; then + log_file="build/$arch/$config/uhci-runtime-check.log" + rm -f "$log_file" + set +e + # 180s mirrors the xHCI interrupt proof budget. UHCI enumeration on QEMU's + # emulated PIIX3 is quick once uhcid spawns, but the guest clock runs several + # times slower than wall under boot load (same characteristic latency noted + # in test-usb-hub-qemu.sh). + timeout 180s qemu-system-x86_64 \ + -name "Red Bear OS x86_64" \ + -machine pc \ + -device piix3-usb-uhci,id=uhci \ + -device usb-kbd,bus=uhci.0 \ + -smp 4 \ + -m 2048 \ + -bios "$firmware" \ + -chardev stdio,id=debug,signal=off,mux=on \ + -serial chardev:debug \ + -mon chardev=debug \ + -device virtio-net,netdev=net0 \ + -netdev user,id=net0 \ + -object filter-dump,id=f1,netdev=net0,file="build/$arch/$config/network.pcap" \ + -nographic -vga none \ + "${boot_args[@]}" \ + -drive file="$extra",format=raw,if=none,id=drv1,snapshot=on \ + -device nvme,drive=drv1,serial=NVME_EXTRA \ + -enable-kvm -cpu host \ + > "$log_file" 2>&1 + status=$? + set -e + + # 1. uhcid bound the controller and logged its PCI path. This proves pcid + # matched the PIIX3 UHCI PCI class (0c0300) and spawned uhcid. + if ! grep -q "UHCI USB 1.1 at " "$log_file"; then + echo "ERROR: uhcid did not bind the UHCI controller" >&2 + echo " Expected: 'UHCI USB 1.1 at '" >&2 + grep -i "uhcid\|UHCI" "$log_file" | tail -10 >&2 || true + echo " See $log_file" >&2 + exit 1 + fi + + # 2. Frame list allocated and the polling loop started. + if ! grep -q "uhcid: controller initialized, polling ports" "$log_file"; then + echo "ERROR: uhcid did not finish controller initialization" >&2 + echo " Expected: 'uhcid: controller initialized, polling ports'" >&2 + echo " See $log_file" >&2 + exit 1 + fi + + # 3. Port connect detected for the attached usb-kbd. This is the + # enumeration-proof marker: the controller saw a device and ran the + # descriptor-read path. + if ! grep -Eq "uhcid: port [0-9]+ connect detected" "$log_file"; then + echo "ERROR: uhcid did not detect a port connect event" >&2 + echo " Expected: 'uhcid: port connect detected'" >&2 + echo " See $log_file" >&2 + exit 1 + fi + + # 4. 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 "UHCI_DRIVER=uhcid" + echo "UHCI_CONTROLLER=piix3-usb-uhci" + echo "UHCI_MACHINE=pc" + echo "UHCI_LOG=$log_file" + echo "UHCI controller enumeration confirmed in $log_file" + exit 0 +fi + +exec qemu-system-x86_64 \ + -name "Red Bear OS x86_64" \ + -machine pc \ + -device piix3-usb-uhci,id=uhci \ + -device usb-kbd,bus=uhci.0 \ + -smp 4 \ + -m 2048 \ + -bios "$firmware" \ + -chardev stdio,id=debug,signal=off,mux=on \ + -serial chardev:debug \ + -mon chardev=debug \ + -device virtio-net,netdev=net0 \ + -netdev user,id=net0 \ + -object filter-dump,id=f1,netdev=net0,file="build/$arch/$config/network.pcap" \ + -vga std \ + "${boot_args[@]}" \ + -drive file="$extra",format=raw,if=none,id=drv1,snapshot=on \ + -device nvme,drive=drv1,serial=NVME_EXTRA \ + -enable-kvm -cpu host diff --git a/local/scripts/test-usb-error-recovery-qemu.sh b/local/scripts/test-usb-error-recovery-qemu.sh new file mode 100755 index 0000000000..27fe6a7ea5 --- /dev/null +++ b/local/scripts/test-usb-error-recovery-qemu.sh @@ -0,0 +1,286 @@ +#!/usr/bin/env bash +# Validate bounded USB error recovery under hot-unplug mid-transfer (P8-C). +# +# Topology under test: +# qemu-xhci root port → usb-storage (backed by a seeded raw image) +# +# Proof: hot-unplug the usb-storage device via the QEMU monitor (`device_del`) +# while usbscsid is actively serving reads, then verify xhcid and usbscsid +# handle the loss gracefully — a logged detach + a logged IO/scheme error is +# acceptable; a panic, abort, or RUST_BACKTRACE is not. This is the runtime +# error-injection half of P2-C (core complete) and the P8-C exit criterion: +# "hot-unplug during transfer, verify graceful error (not panic)". +# +# Why the QEMU monitor and not the python login harness: +# qemu-login-expect.py drives only the guest serial console (send/expect). It +# cannot toggle to the QEMU monitor to issue `device_del`, so this script uses +# the same `-chardev stdio,mux=on -mon chardev=debug` monitor-mux pattern as +# test-xhci-device-lifecycle-qemu.sh, switching between serial and monitor with +# the Ctrl-A c (`\001c`) escape. +# +# The guest-side transfer is driven by `redbear-usb-storage-check` (in the +# redbear-hwutils package), which performs STORAGE_DISCOVERY + WRITE + READBACK +# + RESTORE — a sequence of real bulk transfers that usbscsid is mid-way +# through when the unplug lands. No guest-side checker is invented; the +# existing one is reused, exactly as test-usb-storage-qemu.sh does. +# +# Graceful-handling markers (real driver source): +# "Device on port was detached" (xhcid detach path) +# "usbscsid: READ IO ERROR: ..." (usbscsid main.rs:190) +# "usbscsid: WRITE IO ERROR: ..." (usbscsid main.rs:200) +# "usbscsid: scheme tick error: ..." (usbscsid main.rs:163) +# "usbscsid: event error: ..." (usbscsid main.rs:156) +# Failure markers (prove NOT graceful): +# "panic" / "RUST_BACKTRACE" / "usbscsid: startup failed" + +set -euo pipefail + +seed_usb_image() { + local image_path="$1" + python3 - "$image_path" <<'PY' +import pathlib +import sys + +path = pathlib.Path(sys.argv[1]) +payload = (b"REDBEAR-USB-ERROR-RECOVERY-CHECK\0" * 32)[:512] +payload = payload.ljust(512, b'\0') + +with path.open("r+b") as fh: + fh.seek(0) + fh.write(payload) +PY +} + +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-error-recovery-qemu.sh [--check] [config] + +Boot a Red Bear image, attach a usb-storage device, drive an active transfer +via redbear-usb-storage-check, and hot-unplug the device mid-transfer through +the QEMU monitor. Verifies xhcid/usbscsid handle the loss gracefully (logged +detach + logged error, no panic). Defaults to redbear-mini (mapped to the +in-tree redbear-mini image). + +The --check flag is accepted for runbook/matriz consistency with the other USB +proofs; the monitor-driven sequence runs identically in either mode (mirroring +test-xhci-device-lifecycle-qemu.sh, which treats --check the same way). +USAGE +} + +config="redbear-mini" +for arg in "$@"; do + case "$arg" in + --help|-h|help) + usage + exit 0 + ;; + --check) + # Accepted for consistency; behavior is identical (see usage). + ;; + 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)}" +image="build/$arch/$config/harddrive.img" +extra="build/$arch/$config/extra.img" +usb_img="build/$arch/$config/usb-error-recovery-storage.img" +log_file="build/$arch/$config/usb-error-recovery.log" +session_tag="Red Bear OS USB Error Recovery Test $$" +session_image="/tmp/redbear-usb-error-recovery-$$-harddrive.img" +session_extra="/tmp/redbear-usb-error-recovery-$$-extra.img" +session_usb_img="/tmp/redbear-usb-error-recovery-$$-usb.img" + +# Prefer the live ISO (canonical build-redbear.sh artifact); fall back to +# harddrive.img for older make-all flows. The lifecycle/error-recovery proofs +# drive the QEMU monitor, which requires a harddrive-style -drive (snapshot=on) +# rather than -cdrom, so when only the ISO is present we still boot the ISO but +# attach the USB image as a separate snapshot drive (the storage device under +# test is the usb-storage, not the boot disk). +iso="build/$arch/$config.iso" +if [[ ! -f "$image" && ! -f "$iso" ]]; then + echo "ERROR: no bootable image found ($iso or $image)" >&2 + echo "Build it first with: ./local/scripts/build-redbear.sh $config" >&2 + exit 1 +fi +# Resolve to an existing boot medium, preferring the ISO. +if [[ -f "$iso" ]]; then + boot_src="iso" +else + boot_src="harddrive" +fi + +if [[ ! -f "$extra" ]]; then + truncate -s 1g "$extra" +fi + +if [[ ! -f "$usb_img" ]]; then + truncate -s 64M "$usb_img" +fi +seed_usb_image "$usb_img" + +image_real="$(realpath "$image" 2>/dev/null || echo "$image")" +iso_real="$(realpath "$iso" 2>/dev/null || echo "$iso")" +extra_real="$(realpath "$extra")" +usb_img_real="$(realpath "$usb_img")" +log_file="$(realpath -m "$log_file")" + +ln -sf "$extra_real" "$session_extra" +ln -sf "$usb_img_real" "$session_usb_img" +if [[ "$boot_src" == "harddrive" ]]; then + ln -sf "$image_real" "$session_image" +fi + +pkill -f "qemu-system-x86_64.*$session_tag" 2>/dev/null || true +sleep 1 + +rm -f "$log_file" + +# Assemble the boot-drive argument. The monitor-mux lifecycle pattern uses a +# snapshot harddrive drive; when we have an ISO we attach it via -cdrom instead +# (the usb-storage device under test is independent of the boot medium). +if [[ "$boot_src" == "harddrive" ]]; then + boot_drive_args=(-drive "file=$session_image,format=raw,if=none,id=drv0,snapshot=on" -device nvme,drive=drv0,serial=NVME_SERIAL) +else + boot_drive_args=(-cdrom "$iso_real") +fi + +expect </dev/null || true +rm -f "$session_image" "$session_extra" "$session_usb_img" + +failures=0 + +echo "--- USB Error Recovery Validation: $config ---" + +# 1. The usb-storage device attached and usbscsid spawned (precondition: there +# was a live transfer to interrupt). +if grep -aq "USB SCSI driver spawned with scheme" "$log_file"; then + echo " [PASS] usbscsid spawned for the hotplugged usb-storage device" +else + echo " [FAIL] usbscsid did not spawn; no live transfer to interrupt" >&2 + failures=$((failures + 1)) +fi + +# 2. The hot-unplug was observed by xhcid (detach path fired). +if grep -Eaq 'Device on port [0-9.]+ was detached' "$log_file"; then + echo " [PASS] xhcid observed the mid-transfer hot-unplug" +else + echo " [FAIL] xhcid did not log the detach for the unplugged device" >&2 + failures=$((failures + 1)) +fi + +# 3. Graceful error handling: usbscsid logged an IO/scheme error from its +# transfer/tick loop rather than hanging silently. Any one of the +# documented error markers satisfies this (they are the catch-and-log +# sites that replaced the panic! calls in P1-B). +if grep -Eaq 'usbscsid: (READ|WRITE) IO ERROR:|usbscsid: scheme tick error:|usbscsid: event error:' "$log_file"; then + echo " [PASS] usbscsid logged a graceful transfer error (no silent hang)" +else + echo " [WARN] no usbscsid transfer-error marker observed (transfer may have completed before the unplug landed)" >&2 +fi + +# 4. The critical P8-C guarantee: NO panic / abort / backtrace anywhere in the +# boot. This is the single hard failure that invalidates the proof. +if grep -qiE "panic|RUST_BACKTRACE|usbscsid: startup failed" "$log_file"; then + echo " [FAIL] panic-class failure detected during mid-transfer hot-unplug" >&2 + grep -iE "panic|RUST_BACKTRACE|usbscsid: startup failed" "$log_file" | head -5 >&2 + failures=$((failures + 1)) +else + echo " [PASS] no panic/abort/backtrace during mid-transfer hot-unplug" +fi + +echo "--- Results: $failures failure(s), log: $log_file ---" + +if [[ "$failures" -gt 0 ]]; then + exit 1 +fi + +exit 0