diff --git a/config/redbear-device-services.toml b/config/redbear-device-services.toml index f4fed815..8aab5e9e 100644 --- a/config/redbear-device-services.toml +++ b/config/redbear-device-services.toml @@ -1,6 +1,6 @@ # Red Bear OS shared device-service wiring # -# Shared by profiles that ship the firmware/input/Wi-Fi/FAT control compatibility stack. +# Shared by profiles that ship the firmware/input/Wi-Fi control compatibility stack. [packages] redbear-quirks = {} @@ -13,6 +13,11 @@ data = "" directory = true mode = 0o755 +[[files]] +path = "/usr/bin/usbctl" +data = "/usr/lib/drivers/usbctl" +symlink = true + [[files]] path = "/lib/pcid.d/intel_gpu.toml" data = """ @@ -107,18 +112,3 @@ requires_weak = [ cmd = "evdevd" type = "oneshot_async" """ - -[[files]] -path = "/usr/lib/init.d/15_fatd.service" -data = """ -[unit] -description = "FAT filesystem scheme daemon" -requires_weak = [ - "00_pcid-spawner.service", -] - -[service] -cmd = "fatd" -args = ["disk/live-nvme", "fat-live"] -type = "oneshot_async" -""" diff --git a/local/scripts/test-xhci-device-lifecycle-qemu.sh b/local/scripts/test-xhci-device-lifecycle-qemu.sh new file mode 100755 index 00000000..32b2fcc7 --- /dev/null +++ b/local/scripts/test-xhci-device-lifecycle-qemu.sh @@ -0,0 +1,343 @@ +#!/usr/bin/env bash +# Validate bounded xHCI device attach/detach lifecycle behavior in QEMU. + +set -euo pipefail + +seed_usb_image() { + local image_path="$1" + python3 - "$image_path" <<'PY' +import base64 +import pathlib +import sys + +path = pathlib.Path(sys.argv[1]) +payload = (b"REDBEAR-XHCID-LIFECYCLE-CHECK\0" * 32)[:512] +payload = payload.ljust(512, b'\0') + +with path.open("r+b") as fh: + fh.seek(0) + fh.write(payload) + +print(base64.b64encode(payload).decode("ascii")) +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-xhci-device-lifecycle-qemu.sh [--check] [config] + +Boot a Red Bear image and exercise bounded xHCI attach/detach behavior via +QEMU monitor hotplug events. Defaults to redbear-mini (mapped to the in-tree redbear-minimal image). +USAGE +} + +config="redbear-mini" +for arg in "$@"; do + case "$arg" in + --help|-h|help) + usage + exit 0 + ;; + --check) + ;; + redbear-*) + config="$arg" + ;; + esac +done + +if [[ "$config" == "redbear-mini" ]]; then + config="redbear-minimal" +fi + +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-lifecycle-storage.img" +log_file="build/$arch/$config/xhci-device-lifecycle.log" + +if [[ ! -f "$image" ]]; then + echo "ERROR: missing image $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 [[ ! -f "$usb_img" ]]; then + truncate -s 64M "$usb_img" +fi +seed_usb_image "$usb_img" >/dev/null + +pkill -f "qemu-system-x86_64.*$image" 2>/dev/null || true +sleep 1 + +rm -f "$log_file" + +expect < hid_port +expect -re {USB HID driver spawned} +expect -re {xhcid: finished attach for port [0-9\.]+} +expect -re {Device on port [0-9\.]+ was attached} +expect -re {USB HID driver spawned with scheme .*, port [0-9\.]+, interface 0} +set hid_spawn \$expect_out(0,string) +regexp {scheme .([^,]+), port} \$hid_spawn -> hid_scheme +set hid_scheme [string range \$hid_scheme 0 end-1] + +send "printf '' > /scheme/\$hid_scheme/port\$hid_port/suspend\r" +expect -re {xhcid: suspended port [0-9\.]+} +send "printf '{}' > /scheme/\$hid_scheme/port\$hid_port/configure && printf 'PM_CONFIG_UNEXPECTED\\n' || printf 'PM_CONFIG_BLOCKED\\n'\r" +expect -re {xhcid: port [0-9\.]+ rejected routable operation while suspended} +expect -re {PM_CONFIG_BLOCKED} +send "printf '' > /scheme/\$hid_scheme/port\$hid_port/resume\r" +expect -re {xhcid: resumed port [0-9\.]+} + +send "\001c" +expect "(qemu)" +send "device_del usbkbd0\r" +expect "(qemu)" +send "\001c" +expect -re {Device on port [0-9\.]+ was detached} + +send "printf 'fail_after_configure_endpoint\n' > /tmp/xhcid-test-hook\r" +after 500 + +send "\001c" +expect "(qemu)" +send "device_add usb-kbd,bus=xhci.0,id=usbkbdcfgpre0\r" +expect "(qemu)" +send "\001c" +expect -re {xhcid: begin attach for port [0-9\.]+} +expect -re {USB HID driver spawned} +expect -re {xhcid: finished attach for port [0-9\.]+} +expect -re {xhcid: test hook injecting failure after CONFIGURE_ENDPOINT for port [0-9\.]+} + +send "\001c" +expect "(qemu)" +send "device_del usbkbdcfgpre0\r" +expect "(qemu)" +send "\001c" +expect -re {Device on port [0-9\.]+ was detached} + +send "\001c" +expect "(qemu)" +send "device_add usb-kbd,bus=xhci.0,id=usbkbd1\r" +expect "(qemu)" +send "\001c" +expect -re {xhcid: begin attach for port [0-9\.]+} +expect -re {USB HID driver spawned} +expect -re {xhcid: finished attach for port [0-9\.]+} +expect -re {Device on port [0-9\.]+ was attached} + +send "\001c" +expect "(qemu)" +send "device_del usbkbd1\r" +expect "(qemu)" +send "\001c" +expect -re {Device on port [0-9\.]+ was detached} + +send "printf 'fail_after_set_configuration\n' > /tmp/xhcid-test-hook\r" +after 500 + +send "\001c" +expect "(qemu)" +send "device_add usb-kbd,bus=xhci.0,id=usbkbdcfg0\r" +expect "(qemu)" +send "\001c" +expect -re {xhcid: begin attach for port [0-9\.]+} +expect -re {xhcid: finished attach for port [0-9\.]+} +expect -re {xhcid: test hook injecting failure after SET_CONFIGURATION for port [0-9\.]+} + +send "\001c" +expect "(qemu)" +send "device_del usbkbdcfg0\r" +expect "(qemu)" +send "\001c" +expect -re {Device on port [0-9\.]+ was detached} + +send "printf 'delay_before_attach_commit_ms=2000\n' > /tmp/xhcid-test-hook\r" +after 500 + +send "\001c" +expect "(qemu)" +send "device_add usb-storage,bus=xhci.0,drive=usbdisk0,id=usbstore_delay\r" +expect "(qemu)" +send "\001c" +expect -re {xhcid: begin attach for port [0-9\.]+} +expect -re {xhcid: test hook delaying attach commit for port [0-9\.]+ by 2000 ms} + +send "\001c" +expect "(qemu)" +send "device_del usbstore_delay\r" +expect "(qemu)" +send "\001c" +expect -re {attach for port [0-9\.]+ completed after detach already started; skipping publication} +expect -re {Device on port [0-9\.]+ was detached} + +send "\001c" +expect "(qemu)" +send "device_add usb-storage,bus=xhci.0,drive=usbdisk0,id=usbstore0\r" +expect "(qemu)" +send "\001c" +expect -re {xhcid: begin attach for port [0-9\.]+} +expect -re {USB SCSI driver spawned} +expect -re {xhcid: finished attach for port [0-9\.]+} +expect -re {Device on port [0-9\.]+ was attached} +after 3000 + +send "\001c" +expect "(qemu)" +send "device_del usbstore0\r" +expect "(qemu)" +send "\001c" +expect -re {Device on port [0-9\.]+ was detached} + +send "shutdown\r" +sleep 2 +EOF + +pkill -f "qemu-system-x86_64.*$image" 2>/dev/null || true + +failures=0 + +echo "--- xHCI Device Lifecycle Validation: $config ---" + +if grep -aq "xhcid: using MSI/MSI-X interrupt delivery\|xhcid: using legacy INTx interrupt delivery" "$log_file"; then + echo " [PASS] xHCI interrupt-driven mode detected" +else + echo " [FAIL] xHCI did not report interrupt-driven mode" >&2 + failures=$((failures + 1)) +fi + +if [[ "$(grep -Eac 'xhcid: begin attach for port [0-9.]+' "$log_file")" -ge 5 ]]; then + echo " [PASS] xHCI attach path observed across repeated hotplug cycles" +else + echo " [FAIL] Missing repeated xHCI attach evidence" >&2 + failures=$((failures + 1)) +fi + +if [[ "$(grep -Eac 'xhcid: finished attach for port [0-9.]+' "$log_file")" -ge 4 ]]; then + echo " [PASS] xHCI attach completion observed for full attach cycles" +else + echo " [FAIL] Missing xHCI attach completion evidence" >&2 + failures=$((failures + 1)) +fi + +if [[ "$(grep -Eac 'Device on port [0-9.]+ was detached' "$log_file")" -ge 4 ]]; then + echo " [PASS] xHCI detach path observed for repeated hot-unplugged devices" +else + echo " [FAIL] Missing xHCI detach evidence" >&2 + failures=$((failures + 1)) +fi + +if [[ "$(grep -Eac 'USB HID driver spawned' "$log_file")" -ge 2 ]]; then + echo " [PASS] USB HID spawn observed across repeated hotplug cycles" +else + echo " [FAIL] USB HID driver did not spawn after hotplug" >&2 + failures=$((failures + 1)) +fi + +if [[ "$(grep -Eac 'USB SCSI driver spawned' "$log_file")" -ge 1 ]]; then + echo " [PASS] USB SCSI spawn observed during hotplug lifecycle proof" +else + echo " [FAIL] USB SCSI driver did not spawn after hotplug" >&2 + failures=$((failures + 1)) +fi + +if grep -aq 'xhcid: test hook injecting failure after SET_CONFIGURATION for port ' "$log_file"; then + echo " [PASS] Configure-failure injection hook fired during lifecycle proof" +else + echo " [FAIL] Missing configure-failure injection evidence" >&2 + failures=$((failures + 1)) +fi + +if grep -aq 'xhcid: test hook injecting failure after CONFIGURE_ENDPOINT for port ' "$log_file"; then + echo " [PASS] Configure-endpoint injection hook fired during lifecycle proof" +else + echo " [FAIL] Missing configure-endpoint injection evidence" >&2 + failures=$((failures + 1)) +fi + +if grep -aq 'xhcid: test hook delaying attach commit for port ' "$log_file"; then + echo " [PASS] Attach-commit timing hook fired during lifecycle proof" +else + echo " [FAIL] Missing attach-commit timing hook evidence" >&2 + failures=$((failures + 1)) +fi + +if grep -Eaq 'attach for port [0-9.]+ completed after detach already started; skipping publication' "$log_file"; then + echo " [PASS] Delayed attach stayed unpublished once detach won the race" +else + echo " [FAIL] Missing delayed attach race outcome evidence" >&2 + failures=$((failures + 1)) +fi + +if grep -aq 'PM_CONFIG_BLOCKED' "$log_file" && [[ "$(grep -Eac '(^|[^[:alpha:]])suspended([^[:alpha:]]|$)' "$log_file")" -ge 1 ]]; then + echo " [PASS] Suspend/resume admission checks blocked configure while suspended" +else + echo " [FAIL] Missing suspend/resume admission evidence" >&2 + failures=$((failures + 1)) +fi + +if grep -aqi "panic\|failed to disable port slot" "$log_file"; then + echo " [FAIL] Lifecycle path hit crash-class or teardown errors" >&2 + failures=$((failures + 1)) +else + echo " [PASS] No crash-class lifecycle errors detected" +fi + +echo "--- Results: $failures failure(s), log: $log_file ---" + +if [[ "$failures" -gt 0 ]]; then + exit 1 +fi + +exit 0