feat(cpuid): automate the passthrough toggle around enabled guests

This commit is contained in:
2026-09-06 02:46:30 +02:00
parent c1e8676834
commit 52b18427f2
8 changed files with 216 additions and 20 deletions

41
scripts/cpuid-passthrough-watch Executable file
View File

@@ -0,0 +1,41 @@
#!/bin/bash
# Waits for a just-started guest's network to actually come up, then turns
# kvm_amd cpuid_passthrough on.
#
# 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.
#
# 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).
DOMAIN=$1
BRAND=$2
PARAM=/sys/module/kvm_amd/parameters
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
running || exit 0 # guest went away before it came up
printf '%s' "$BRAND" > "$PARAM/brand_string"
echo Y > "$PARAM/cpuid_passthrough"
logger -t vfio-cpuid "$DOMAIN network up: cpuid_passthrough=Y"

77
scripts/cpuid-passthrough.sh Executable file
View File

@@ -0,0 +1,77 @@
#!/bin/bash
# vm-native-cpuid - manage the automatic cpuid_passthrough toggle for hardened guests.
#
# vm-native-cpuid enable <domain> apply the automatic cpuid passthrough to <domain>
# vm-native-cpuid disable <domain> stop applying it to <domain>
# vm-native-cpuid status show the config and the live module state
# vm-native-cpuid revert remove the hook, watcher and config; reset the module
#
# The switch and brand string are one global kvm_amd parameter, so it serves one
# hardened guest at a time. It is off until you enable it here - no domain is
# touched otherwise. Around an enabled guest the libvirt hook forces the switch off
# for the cold boot, a watcher flips it on once the guest's network is up, and it
# goes off again on stop.
set -uo pipefail
CONF=/etc/vfio-native/cpuid-passthrough.conf
HOOK=/etc/libvirt/hooks/qemu.d/30-cpuid-passthrough.sh
WATCH=/usr/lib/vfio-native/cpuid-passthrough-watch
PARAM=/sys/module/kvm_amd/parameters
SELF=$(cd "$(dirname "$0")/.." 2>/dev/null && pwd)
die() { echo "$*" >&2; exit 1; }
need_root() { [ "$(id -u)" = 0 ] || die "run this as root"; }
load() { ENABLED=yes; DOMAINS=""; [ -r "$CONF" ] && . "$CONF"; }
save() { mkdir -p "$(dirname "$CONF")"; printf 'ENABLED=%s\nDOMAINS="%s"\n' "$ENABLED" "$DOMAINS" > "$CONF"; }
# copy the hook and watcher into place from the installed tree or this checkout
install_hook() {
local src=""
for d in /usr/share/vfio-native "$SELF"; do
[ -f "$d/scripts/libvirt-hook-cpuid-passthrough.sh" ] && src="$d"
done
[ -n "$src" ] || die "cannot find the hook source (install vfio-native, or run from a checkout)"
install -Dm755 "$src/scripts/libvirt-hook-cpuid-passthrough.sh" "$HOOK"
install -Dm755 "$src/scripts/cpuid-passthrough-watch" "$WATCH"
}
case "${1:-}" in
enable)
need_root; [ -n "${2:-}" ] || die "usage: vm-native-cpuid enable <domain>"
load; install_hook; ENABLED=yes
case " $DOMAINS " in *" $2 "*) ;; *) DOMAINS="${DOMAINS:+$DOMAINS }$2" ;; esac
save
echo "$2 enabled. cpuid passthrough is now automatic for: $DOMAINS"
echo "on start it forces N for the cold boot, flips Y once the guest is on the network, N again on stop."
;;
disable)
need_root; [ -n "${2:-}" ] || die "usage: vm-native-cpuid disable <domain>"
load; DOMAINS=$(printf ' %s ' "$DOMAINS" | sed "s/ $2 / /g" | xargs || true); save
systemctl stop "vfio-cpuid-watch-$2.service" 2>/dev/null || true
echo "$2 disabled. remaining: ${DOMAINS:-none}"
;;
status)
load
echo "config: $CONF"
echo "enabled: $ENABLED"
echo "domains: ${DOMAINS:-none}"
echo "hook: $([ -f "$HOOK" ] && echo installed || echo 'not installed')"
if [ -r "$PARAM/cpuid_passthrough" ]; then
echo "module: cpuid_passthrough=$(cat "$PARAM/cpuid_passthrough") brand='$(cat "$PARAM/brand_string")'"
else
echo "module: patched kvm_amd not loaded"
fi
;;
revert)
need_root; load
for d in $DOMAINS; do systemctl stop "vfio-cpuid-watch-$d.service" 2>/dev/null || true; done
rm -f "$HOOK" "$WATCH" "$CONF" /run/vfio-native/cpuid-active
[ -w "$PARAM/cpuid_passthrough" ] && echo N > "$PARAM/cpuid_passthrough"
echo "reverted: hook, watcher and config removed; cpuid_passthrough=N."
;;
*)
sed -n '2,13p' "$0" | sed 's/^#\( \|$\)//'
;;
esac

View File

@@ -0,0 +1,68 @@
#!/bin/bash
# libvirt qemu hook: drive the kvm_amd cpuid_passthrough switch around a guest's life.
#
# The switch must be OFF while a guest cold-boots - raw CPUID changes shape mid
# enumeration and Windows hangs - and ON once the guest is up, where it clears the
# TIMER detection. This applies only to the domains you opt in with
# `vm-native-cpuid enable <domain>`; it does nothing to any other guest.
#
# On start it sets the brand string from the guest's declared SKU and forces N,
# then a detached watcher flips Y once the guest's network is up; on stop it sets N.
# cpuid_passthrough and brand_string are one global kvm_amd parameter, so a single
# hardened guest is served at a time; a second is logged and left alone.
#
# Exits 0 on every path. The hook never calls virsh (that deadlocks libvirtd); the
# watcher that does is a transient systemd unit and runs after this returns.
CONF=/etc/vfio-native/cpuid-passthrough.conf
ENABLED=yes
DOMAINS=""
[ -r "$CONF" ] && . "$CONF"
[ "$ENABLED" = yes ] || exit 0
PARAM=/sys/module/kvm_amd/parameters
[ -w "$PARAM/cpuid_passthrough" ] || exit 0 # stock module, or not an AMD host
DOMAIN=$1
OPERATION=$2
case " $DOMAINS " in *" $DOMAIN "*) ;; *) exit 0 ;; esac # not an opted-in domain
RUN=/run/vfio-native
ACTIVE=$RUN/cpuid-active
WATCH=/usr/lib/vfio-native/cpuid-passthrough-watch
case "$OPERATION" in
prepare)
# the guest's declared SKU, straight from the -cpu model-id in the XML on stdin
BRAND=$(grep -oE "model-id=[^'\"]+" | head -1 | sed 's/^model-id=//')
[ -n "$BRAND" ] || exit 0 # not a full-fidelity guest
mkdir -p "$RUN"
held=$( [ -e "$ACTIVE" ] && cut -d: -f1 "$ACTIVE" )
if [ -n "$held" ] && [ "$held" != "$DOMAIN" ]; then
logger -t vfio-cpuid "cpuid passthrough held by $held; $DOMAIN left unhardened (one guest at a time)"
exit 0
fi
printf '%s' "$BRAND" > "$PARAM/brand_string"
echo N > "$PARAM/cpuid_passthrough" # safe for the cold boot
printf '%s:%s' "$DOMAIN" "$BRAND" > "$ACTIVE"
logger -t vfio-cpuid "$DOMAIN start: brand='$BRAND' cpuid_passthrough=N, watcher armed"
# transient systemd unit, not a bare background job: libvirt reaps anything left
# in the hook's own process tree; systemd-run escapes it
[ -x "$WATCH" ] && systemd-run --collect --quiet \
--unit="vfio-cpuid-watch-${DOMAIN}" "$WATCH" "$DOMAIN" "$BRAND"
;;
release|stopped)
held=$( [ -e "$ACTIVE" ] && cut -d: -f1 "$ACTIVE" )
[ "$held" = "$DOMAIN" ] || exit 0
systemctl stop "vfio-cpuid-watch-${DOMAIN}.service" 2>/dev/null
echo N > "$PARAM/cpuid_passthrough"
rm -f "$ACTIVE"
logger -t vfio-cpuid "$DOMAIN $OPERATION: cpuid_passthrough=N"
;;
esac
exit 0

View File

@@ -814,12 +814,12 @@ if [ "$PROFILE" = full ]; then
[ "$ok" = 1 ] && echo "patched QEMU and KVM modules both in place."
if [ -n "$MODEL" ] && [ -f /sys/module/kvm_amd/parameters/cpuid_passthrough ]; then
echo
echo "The TIMER check needs CPUID passthrough, which is off by default and must be"
echo "switched on AFTER the guest has booted (it hangs a booting guest). Once the"
echo "guest is up, on the host:"
echo "The TIMER check needs CPUID passthrough, which must be off while the guest cold"
echo "boots and on once it is up. Let the hook handle that around this guest:"
echo " sudo vm-native-cpuid enable $DOM"
echo "Or drive it by hand, after the guest has booted, on the host:"
echo " echo '$MODEL' | sudo tee /sys/module/kvm_amd/parameters/brand_string"
echo " echo Y | sudo tee /sys/module/kvm_amd/parameters/cpuid_passthrough"
echo "Switch it off again (echo N) before rebooting the guest."
echo " echo Y | sudo tee /sys/module/kvm_amd/parameters/cpuid_passthrough # N again before the next boot"
fi
fi