From a956ececfd90049e6b6329d3d98d14c2458e1cab Mon Sep 17 00:00:00 2001 From: Sandwich Date: Sun, 6 Sep 2026 10:07:57 +0200 Subject: [PATCH] fix: keep cpuid passthrough correct across in-guest reboots --- scripts/cpuid-passthrough-watch | 77 ++++++++++++++++++++++----------- 1 file changed, 51 insertions(+), 26 deletions(-) diff --git a/scripts/cpuid-passthrough-watch b/scripts/cpuid-passthrough-watch index 7d9301a..ea60fa5 100755 --- a/scripts/cpuid-passthrough-watch +++ b/scripts/cpuid-passthrough-watch @@ -1,12 +1,18 @@ #!/bin/bash -# Waits for a just-started guest's network to actually come up, then turns -# kvm_amd cpuid_passthrough on. +# Keeps the kvm_amd cpuid_passthrough switch correct across a guest's whole life, +# in-guest reboots included. # -# The readiness signal is the guest's own tap RX counter: it is fresh every boot, -# it needs nothing enabled inside the guest (no SSH, no RDP, no agent), and it only -# moves once the guest's NIC driver has really loaded - which is well past the CPU -# enumeration that the switch must not change under. A DHCP lease left over from a -# previous boot cannot trip it early. +# The switch must be OFF (N) while the guest enumerates CPUID at boot or Windows +# hangs, and ON (Y) once it is up, where it clears the timer detection. The libvirt +# hook only fires at VM start and stop, so a guest-initiated reboot would otherwise +# re-enumerate with the switch still Y and hang. This watcher drops it to N on every +# QEMU RESET and raises it again once the guest's NIC is back up. +# +# Readiness signal: the guest tap's rx_packets counter growing past a baseline. It +# only moves once the guest NIC driver has loaded, well past CPU enumeration, and it +# needs nothing enabled inside the guest. The counter is cumulative and does NOT +# reset on an in-guest reboot, so readiness is growth past the value captured at the +# reset, not an absolute threshold. # # Launched as a transient systemd unit by the cpuid-passthrough hook, so it is free # to call virsh (the hook itself must not - that deadlocks libvirtd). @@ -18,24 +24,43 @@ V="virsh -c qemu:///system" running() { [ "$($V domstate "$DOMAIN" 2>/dev/null)" = running ]; } -tap="" -i=0 -for _ in $(seq 1 65); do # ~195 s cap, then flip anyway if still up - i=$((i + 1)) - running || { sleep 3; continue; } # not "running" yet at prepare time - wait - [ -z "$tap" ] && tap=$($V domiflist "$DOMAIN" 2>/dev/null | - awk '$1 ~ /^(vnet|tap|macvtap)/ {print $1; exit}') - rx="/sys/class/net/$tap/statistics/rx_packets" - # the iteration floor keeps a stray pre-OS packet (a UEFI netboot attempt) from - # tripping the flip before the guest is even past its interrupt and timer setup - if [ "$i" -ge 4 ] && [ -n "$tap" ] && [ -r "$rx" ] && - [ "$(cat "$rx" 2>/dev/null || echo 0)" -ge 4 ]; then - break - fi - sleep 3 -done +tap=$($V domiflist "$DOMAIN" 2>/dev/null | awk '$1 ~ /^(vnet|tap|macvtap)/ {print $1; exit}') +RX="/sys/class/net/$tap/statistics/rx_packets" +rx() { cat "$RX" 2>/dev/null || echo 0; } -running || exit 0 # guest went away before it came up -printf '%s' "$BRAND" > "$PARAM/brand_string" -echo Y > "$PARAM/cpuid_passthrough" +set_N() { echo N > "$PARAM/cpuid_passthrough"; } +set_Y() { printf '%s' "$BRAND" > "$PARAM/brand_string"; echo Y > "$PARAM/cpuid_passthrough"; } + +# Wait until the tap rx counter grows at least 4 past $1 (guest NIC driver up again). +# The iteration floor keeps a stray pre-OS packet (a UEFI netboot attempt) from +# tripping the flip before the guest is even past its interrupt and timer setup. +wait_net_up() { + local base=$1 i=0 + for _ in $(seq 1 100); do # ~300 s cap, then raise anyway if still up + i=$((i + 1)) + running || return 1 + [ "$i" -ge 4 ] && [ -n "$tap" ] && [ -r "$RX" ] && + [ "$(rx)" -ge "$((base + 4))" ] && return 0 + sleep 3 + done + return 0 +} + +# initial cold boot: wait for the network, then harden +wait_net_up 0 +running || exit 0 +set_Y logger -t vfio-cpuid "$DOMAIN network up: cpuid_passthrough=Y" + +# every in-guest reboot fires a QEMU RESET: drop to N for the re-enumeration, then +# raise it again once the guest's NIC is back. --loop streams one line per reset. +$V qemu-monitor-event --domain "$DOMAIN" --event RESET --loop 2>/dev/null | while read -r _; do + running || continue + base=$(rx) + set_N + logger -t vfio-cpuid "$DOMAIN reset: cpuid_passthrough=N for re-enumeration" + wait_net_up "$base" || continue + running || continue + set_Y + logger -t vfio-cpuid "$DOMAIN back up: cpuid_passthrough=Y" +done