10 Commits

12 changed files with 316 additions and 51 deletions

View File

@@ -165,25 +165,32 @@ vm-native-verify
`vm-native-verify` also prints whether CPUID passthrough is on, which is the last step. `vm-native-verify` also prints whether CPUID passthrough is on, which is the last step.
### 6. Switch on CPUID passthrough, after every guest boot ### 6. CPUID passthrough, on after boot and off before the next one
The `TIMER` check times the world switch on an intercepted `CPUID`, and the only way to stop The `TIMER` check times the world switch on an intercepted `CPUID`, and the only way to stop
paying it is to not exit. `vm-native-setup` prints the two lines for your declared SKU; on this paying it is to not exit. That switch has to be off while a guest cold-boots - a booting Windows
host they are: enumerates CPUID bits KVM synthesises and hangs if they vanish half way through - and on once it
is up. Let the libvirt hook handle that around a guest:
```sh
sudo vm-native-cpuid enable win11 # off for the cold boot, on once the guest is on the network, off on stop
sudo vm-native-cpuid status # what is enabled, and the live switch state
sudo vm-native-cpuid disable win11 # stop automating this guest
sudo vm-native-cpuid revert # remove the hook and reset the switch
```
Or drive it by hand. `vm-native-setup` prints the two lines for your declared SKU; run them once
the guest is up, never before, and switch it off (`echo N`) before the next boot:
```sh ```sh
echo 'AMD Ryzen 7 7700X 8-Core Processor' | sudo tee /sys/module/kvm_amd/parameters/brand_string echo 'AMD Ryzen 7 7700X 8-Core Processor' | sudo tee /sys/module/kvm_amd/parameters/brand_string
echo Y | sudo tee /sys/module/kvm_amd/parameters/cpuid_passthrough echo Y | sudo tee /sys/module/kvm_amd/parameters/cpuid_passthrough
``` ```
Run them once the guest is up, never before: a booting Windows enumerates CPUID bits KVM The module applies it only to vCPU threads pinned to exactly one host CPU, so on an unpinned guest
synthesises, and hangs if they vanish half way through. Switch it off again (`echo N`) before the it does nothing rather than something wrong. It is one switch and one brand string for the whole
next boot. The module applies it only to vCPU threads pinned to exactly one host CPU, so on an host, so it serves one hardened guest at a time - the hook enforces that and leaves a second guest
unpinned guest it does nothing rather than something wrong. alone.
It is a module parameter, so it is one switch for the whole host: every pinned guest gets it, and
a cold boot of any of them while it is on hits the race. With more than one such guest, switch it
off before any of them boots and on again once they are all up.
For the detection score, run VMAware in the guest **from the console session, not over SSH** - For the detection score, run VMAware in the guest **from the console session, not over SSH** -
OpenSSH lands you in session 0, which is not where an interactive desktop session runs. See OpenSSH lands you in session 0, which is not where an interactive desktop session runs. See

View File

@@ -75,12 +75,6 @@ Enable-NetFirewallRule -DisplayGroup "Remote Desktop"
Put your public key in `C:\ProgramData\ssh\administrators_authorized_keys` for an Administrator Put your public key in `C:\ProgramData\ssh\administrators_authorized_keys` for an Administrator
account. `vm-native-verify` uses that key. account. `vm-native-verify` uses that key.
SSH into the guest fails with `Corrupted MAC on input` until the host has the e1000e offload rule
from the `vfio-native` package. The emulated NIC's TX offloads corrupt integrity-checked traffic on
the host side of the tap; SMB tolerates it, SSH does not. The package installs a udev rule that
turns the offloads off on every libvirt tap as it appears, and `vm-native-setup` says so if it is
missing.
## 3. Make the NVMe driver boot-critical ## 3. Make the NVMe driver boot-critical
The disk is about to move from virtio to emulated NVMe, and Windows only loads boot-start drivers The disk is about to move from virtio to emulated NVMe, and Windows only loads boot-start drivers
@@ -127,9 +121,14 @@ Two things it asks or warns about:
Before the first boot, if the host has less free memory than the guest's RAM, free and compact Before the first boot, if the host has less free memory than the guest's RAM, free and compact
it so the guest lands on transparent hugepages; `vm-native-setup` prints the two commands when it it so the guest lands on transparent hugepages; `vm-native-setup` prints the two commands when it
applies. The NIC stays `e1000e`, so the network survives the driver removal in the next step. Do not use applies. The NIC stays `igb`, so the network survives the driver removal in the next step. Do not use
virtiofs for host files: it is a virtio device the scanner names, and its shared memory backing virtiofs for host files: it is a virtio device the scanner names, and its shared memory backing
blocks transparent hugepages for the whole guest. Share over SMB on the e1000e link instead. blocks transparent hugepages for the whole guest. Share over SMB on the `igb` link instead.
The interface asks for MTU 9000, and the package's hook turns GSO and GRO on for the tap. Together
they take the inbound link from 2.9 to 14 Gbit/s; neither does anything alone. The libvirt network
needs `<mtu size='9000'/>` too, and the guest needs *Jumbo Packet* 9014 with its interface MTU at
9000 - setting the adapter property alone leaves the IP MTU at 1500 and gains nothing.
## 5. Remove the virtio drivers and the agents ## 5. Remove the virtio drivers and the agents
@@ -165,7 +164,7 @@ echo Y | sudo tee /sys/module/kvm_amd/parameters/cpuid_passthrough
vm-native-verify vm-native-verify
``` ```
`vm-native-setup` printed those two lines with your SKU. The switch is host-wide: it applies to `vm-native-setup` printed those two lines with your SKU, or `sudo vm-native-cpuid enable <domain>` does the same on/off automatically around the guest (off for the cold boot, on once its network is up). The switch is host-wide: it applies to
every pinned guest at once, and a cold boot of any of them while it is on hits the enumeration every pinned guest at once, and a cold boot of any of them while it is on hits the enumeration
race, so with several guests turn it off before any boots and on when they are all up. Then the race, so with several guests turn it off before any boots and on when they are all up. Then the
scanner, from the console session, as `docs/TESTING.md` describes. The reference guest reads 1/85. scanner, from the console session, as `docs/TESTING.md` describes. The reference guest reads 1/85.

View File

@@ -1,6 +1,6 @@
pkgbase = vfio-native pkgbase = vfio-native
pkgdesc = Present a libvirt guest as a self-consistent physical machine, and tune it pkgdesc = Present a libvirt guest as a self-consistent physical machine, and tune it
pkgver = 1.1.0 pkgver = 1.3.3
pkgrel = 1 pkgrel = 1
url = https://git.archworks.co/sandwich/vfio-native url = https://git.archworks.co/sandwich/vfio-native
install = vfio-native.install install = vfio-native.install
@@ -17,7 +17,7 @@ pkgbase = vfio-native
optdepends = cpupower: set the host CPU governor optdepends = cpupower: set the host CPU governor
optdepends = vfio-native-kvm-dkms: patched KVM modules for the full level optdepends = vfio-native-kvm-dkms: patched KVM modules for the full level
optdepends = vfio-native-qemu: patched QEMU for the full level optdepends = vfio-native-qemu: patched QEMU for the full level
source = git+https://git.archworks.co/sandwich/vfio-native.git#tag=v1.1.0 source = git+https://git.archworks.co/sandwich/vfio-native.git#tag=v1.3.3
sha256sums = SKIP sha256sums = SKIP
pkgname = vfio-native pkgname = vfio-native

View File

@@ -8,7 +8,7 @@
# vfio-native-qemu QEMU 11.1.1 with the platform-identity patches, in /opt # vfio-native-qemu QEMU 11.1.1 with the platform-identity patches, in /opt
pkgname=vfio-native pkgname=vfio-native
pkgver=1.1.0 pkgver=1.3.3
pkgrel=1 pkgrel=1
pkgdesc="Present a libvirt guest as a self-consistent physical machine, and tune it" pkgdesc="Present a libvirt guest as a self-consistent physical machine, and tune it"
arch=('any') arch=('any')
@@ -29,6 +29,7 @@ package() {
install -Dm755 scripts/setup-vm.sh "${pkgdir}/usr/bin/vm-native-setup" install -Dm755 scripts/setup-vm.sh "${pkgdir}/usr/bin/vm-native-setup"
install -Dm755 scripts/verify-perf.sh "${pkgdir}/usr/bin/vm-native-verify" install -Dm755 scripts/verify-perf.sh "${pkgdir}/usr/bin/vm-native-verify"
install -Dm755 scripts/gpu-passthrough.sh "${pkgdir}/usr/bin/vm-native-gpu" install -Dm755 scripts/gpu-passthrough.sh "${pkgdir}/usr/bin/vm-native-gpu"
install -Dm755 scripts/cpuid-passthrough.sh "${pkgdir}/usr/bin/vm-native-cpuid"
local share="${pkgdir}/usr/share/vfio-native" local share="${pkgdir}/usr/share/vfio-native"
install -Dm644 bench/vmbench.c "${share}/bench/vmbench.c" install -Dm644 bench/vmbench.c "${share}/bench/vmbench.c"
@@ -36,6 +37,8 @@ package() {
install -Dm755 scripts/install-modules.sh "${share}/scripts/install-modules.sh" install -Dm755 scripts/install-modules.sh "${share}/scripts/install-modules.sh"
install -Dm755 scripts/restore-stock-kvm.sh "${share}/scripts/restore-stock-kvm.sh" install -Dm755 scripts/restore-stock-kvm.sh "${share}/scripts/restore-stock-kvm.sh"
install -Dm755 scripts/generate-tables.py "${share}/scripts/generate-tables.py" install -Dm755 scripts/generate-tables.py "${share}/scripts/generate-tables.py"
install -Dm755 scripts/libvirt-hook-cpuid-passthrough.sh "${share}/scripts/libvirt-hook-cpuid-passthrough.sh"
install -Dm755 scripts/cpuid-passthrough-watch "${share}/scripts/cpuid-passthrough-watch"
install -Dm644 -t "${share}/acpi" acpi/*.aml acpi/*.dsl install -Dm644 -t "${share}/acpi" acpi/*.aml acpi/*.dsl
install -dm755 "${share}/patches" install -dm755 "${share}/patches"
cp -r patches/. "${share}/patches/" cp -r patches/. "${share}/patches/"
@@ -45,7 +48,6 @@ package() {
# this coexists with whatever hook the host already has. # this coexists with whatever hook the host already has.
install -Dm755 scripts/libvirt-hook-cpu-isolation.sh \ install -Dm755 scripts/libvirt-hook-cpu-isolation.sh \
"${pkgdir}/etc/libvirt/hooks/qemu.d/10-cpu-isolation.sh" "${pkgdir}/etc/libvirt/hooks/qemu.d/10-cpu-isolation.sh"
# e1000e offloads corrupt integrity-checked traffic on libvirt taps; host-wide by nature install -Dm755 scripts/libvirt-hook-vnet-offload.sh \
install -Dm644 scripts/99-vfio-native-vnet-offload.rules \ "${pkgdir}/etc/libvirt/hooks/qemu.d/20-vnet-offload.sh"
"${pkgdir}/usr/lib/udev/rules.d/99-vfio-native-vnet-offload.rules"
} }

View File

@@ -49,11 +49,11 @@ post_install() {
'native' is a good default if you would rather not maintain anything. 'native' is a good default if you would rather not maintain anything.
A udev rule (99-vfio-native-vnet-offload.rules) turns TX offloads off on every A hook at /etc/libvirt/hooks/qemu.d/20-vnet-offload.sh turns GSO and GRO on
libvirt tap as it appears. The emulated e1000e NIC corrupts integrity-checked for the guest's tap. With the interface's MTU 9000 that takes the inbound link
traffic with them on; SSH to the guest fails with "Corrupted MAC on input". from 2.9 to 14 Gbit/s; neither does anything alone. The libvirt network needs
It applies to every VM on the host; the throughput cost on a host<->guest <mtu size='9000'/> too, and the guest needs Jumbo Packet 9014 with its
link is not measurable. interface MTU at 9000.
A libvirt hook is installed at /etc/libvirt/hooks/qemu.d/10-cpu-isolation.sh. A libvirt hook is installed at /etc/libvirt/hooks/qemu.d/10-cpu-isolation.sh.
It keeps host processes off the cores the guest is pinned to, automatically, It keeps host processes off the cores the guest is pinned to, automatically,

View File

@@ -1,8 +0,0 @@
# vfio-native: the emulated e1000e NIC's TX checksum and segmentation offloads
# corrupt packets on the host side of a libvirt tap. SMB tolerates it; SSH fails
# with "Corrupted MAC on input" and any integrity-checked protocol breaks.
# Measured on a Zen 4 host with QEMU 11.1.1. Disabling the offloads on every
# libvirt tap as it appears fixes it; on a host<->guest link the throughput
# cost is not measurable. libvirt's <driver><host .../> attributes are ignored
# for e1000e, and a libvirt hook must not call virsh, hence udev.
ACTION=="add", SUBSYSTEM=="net", KERNEL=="vnet*", RUN+="/usr/bin/ethtool -K %k tx off gso off gro off tso off"

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

@@ -0,0 +1,71 @@
#!/bin/bash
# Keeps the kvm_amd cpuid_passthrough switch correct across a guest's whole life,
# in-guest reboots included.
#
# 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).
DOMAIN=$1
BRAND=$2
PARAM=/sys/module/kvm_amd/parameters
V="virsh -c qemu:///system"
running() { [ "$($V domstate "$DOMAIN" 2>/dev/null)" = running ]; }
rx() { cat "$RX" 2>/dev/null || echo 0; }
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
}
# The hook arms this watcher at prepare, before QEMU starts, so the domain is not
# yet running and its tap does not exist. Wait for the domain, then read the tap;
# otherwise the first running check in wait_net_up bails and the flip never happens.
for _ in $(seq 1 120); do running && break; sleep 1; done
running || exit 0
tap=$($V domiflist "$DOMAIN" 2>/dev/null | awk '$1 ~ /^(vnet|tap|macvtap)/ {print $1; exit}')
RX="/sys/class/net/$tap/statistics/rx_packets"
# 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

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

@@ -57,6 +57,24 @@ group_members() {
done done
} }
# The functions to hand to the guest with GPU $1: every device in its IOMMU group
# (mandatory for vfio), plus any sibling function of the same PCI device that is
# itself a display or HDMI/DP audio controller. An APU parks its PSP and USB
# controllers on the same PCI device in separate IOMMU groups - those are the
# host's, so siblings are filtered by class and never pulled in blindly.
passthrough_devs() {
local d="$1" m cls
{
group_members "$d"
for m in "/sys/bus/pci/devices/${d%.*}".*; do
[ -e "$m" ] || continue
m=$(basename "$m")
cls=$(cat "/sys/bus/pci/devices/$m/class" 2>/dev/null)
case "$cls" in 0x03*|0x0403*) echo "$m";; esac
done
} | sort -u
}
driver_of() { driver_of() {
local l local l
l=$(readlink -f "/sys/bus/pci/devices/$1/driver" 2>/dev/null) l=$(readlink -f "/sys/bus/pci/devices/$1/driver" 2>/dev/null)
@@ -79,7 +97,7 @@ drives_display() {
ids_of() { # vendor:device for vfio-pci binding ids_of() { # vendor:device for vfio-pci binding
local d local d
for d in $(group_members "$1"); do for d in $(passthrough_devs "$1"); do
local cls; cls=$(cat "/sys/bus/pci/devices/$d/class" 2>/dev/null) local cls; cls=$(cat "/sys/bus/pci/devices/$d/class" 2>/dev/null)
# only real functions of the card: skip bridges (class 0x0604xx) # only real functions of the card: skip bridges (class 0x0604xx)
case "$cls" in 0x0604*) continue;; esac case "$cls" in 0x0604*) continue;; esac
@@ -219,7 +237,7 @@ prepare)
# 1. stop whatever is holding the DRM device # 1. stop whatever is holding the DRM device
dm=$(systemctl list-units --type=service --state=running --no-legend 2>/dev/null | dm=$(systemctl list-units --type=service --state=running --no-legend 2>/dev/null |
awk '{print $1}' | grep -xE '(gdm|sddm|lightdm|lxdm|greetd|display-manager)\.service' | head -1) awk '{print $1}' | grep -xE '((gdm|sddm|lightdm|lxdm|greetd|ly|emptty|lemurs)(@[a-z0-9-]+)?|display-manager)\.service' | head -1)
if [ -n "$dm" ]; then if [ -n "$dm" ]; then
echo "$dm" > "$STATE/dm" echo "$dm" > "$STATE/dm"
log "stopping $dm" log "stopping $dm"
@@ -303,7 +321,7 @@ apply() {
"$v" -c qemu:///system dumpxml --inactive "$dom" > "$backup/$dom.gpu-current.xml" "$v" -c qemu:///system dumpxml --inactive "$dom" > "$backup/$dom.gpu-current.xml"
grep -q 'ua-vfionative-gpu' "$backup/$dom.gpu-current.xml" || cp "$backup/$dom.gpu-current.xml" "$backup/$dom.before-gpu.xml" grep -q 'ua-vfionative-gpu' "$backup/$dom.gpu-current.xml" || cp "$backup/$dom.gpu-current.xml" "$backup/$dom.before-gpu.xml"
local devs="" m local devs="" m
for m in $( { group_members "$d"; ls -d "/sys/bus/pci/devices/${d%.*}".* | xargs -n1 basename; } | sort -u); do for m in $(passthrough_devs "$d"); do
case "$(cat "/sys/bus/pci/devices/$m/class" 2>/dev/null)" in 0x0604*) continue;; esac case "$(cat "/sys/bus/pci/devices/$m/class" 2>/dev/null)" in 0x0604*) continue;; esac
IFS=':.' read -r dm bs sl fn <<< "$m" IFS=':.' read -r dm bs sl fn <<< "$m"
devs+=" <hostdev mode='subsystem' type='pci' managed='yes'>\n <source>\n" devs+=" <hostdev mode='subsystem' type='pci' managed='yes'>\n <source>\n"
@@ -330,7 +348,7 @@ xml() {
echo "Add this to the domain, inside <devices>. Every device in the GPU's" echo "Add this to the domain, inside <devices>. Every device in the GPU's"
echo "IOMMU group has to go together:" echo "IOMMU group has to go together:"
echo echo
for m in $(group_members "$d"); do for m in $(passthrough_devs "$d"); do
local cls; cls=$(cat "/sys/bus/pci/devices/$m/class" 2>/dev/null) local cls; cls=$(cat "/sys/bus/pci/devices/$m/class" 2>/dev/null)
case "$cls" in 0x0604*) continue;; esac case "$cls" in 0x0604*) continue;; esac
IFS=':. ' read -r dom bus slot fn <<< "$(echo "$m" | tr ':.' ' ')" IFS=':. ' read -r dom bus slot fn <<< "$(echo "$m" | tr ':.' ' ')"

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

@@ -0,0 +1,15 @@
#!/bin/bash
# libvirt qemu hook: turn GSO and GRO on for the guest's tap.
#
# QEMU leaves them off and sets the tap up after udev has run, so it has to
# happen here. Paired with MTU 9000 they are the difference between 2.5 and
# 14 Gbit/s into the guest; neither helps alone. Tap names come from the domain
# XML on stdin, so the hook never calls virsh, which would deadlock libvirtd.
[ "$2" = started ] || exit 0
for tap in $(grep -oE "<target dev='(vnet|tap|macvtap)[^']*'" | sed "s/.*dev='//; s/'$//"); do
ethtool -K "$tap" gso on gro on 2>/dev/null
done
exit 0

View File

@@ -637,8 +637,11 @@ if conformant and E["CONVERT"] == "1":
if "device='disk'" not in d or ("bus='nvme'" in d and "<serial>" in d): if "device='disk'" not in d or ("bus='nvme'" in d and "<serial>" in d):
return d return d
d = re.sub(r"<target dev='([^']*)' bus='(virtio|sata|scsi)'/>", r"<target dev='\1' bus='nvme'/>", d) d = re.sub(r"<target dev='([^']*)' bus='(virtio|sata|scsi)'/>", r"<target dev='\1' bus='nvme'/>", d)
# cache='none' is O_DIRECT: on btrfs the guest can change a page while the
# write is in flight, so the stored checksum never matches and later reads
# fail with EIO. Buffered writes hand the filesystem a stable page.
d = re.sub(r"<driver name='qemu' type='([^']*)'[^/]*/>", d = re.sub(r"<driver name='qemu' type='([^']*)'[^/]*/>",
r"<driver name='qemu' type='\1' cache='none' io='native' discard='unmap'/>", d) r"<driver name='qemu' type='\1' cache='writeback' io='threads' discard='unmap'/>", d)
d = re.sub(r"\s*<address type='(pci|drive)'[^/]*/>", "", d) d = re.sub(r"\s*<address type='(pci|drive)'[^/]*/>", "", d)
if "<serial>" not in d: if "<serial>" not in d:
serial = E["NVME_SERIAL"] if n[0] == 0 else E["NVME_SERIAL"][:-1] + "0123456789ABCDEF"[n[0] % 16] serial = E["NVME_SERIAL"] if n[0] == 0 else E["NVME_SERIAL"][:-1] + "0123456789ABCDEF"[n[0] % 16]
@@ -658,7 +661,11 @@ if conformant and E["CONVERT"] == "1":
s = re.sub(r"\s*<input type='[^']*' bus='virtio'/>", "", s) s = re.sub(r"\s*<input type='[^']*' bus='virtio'/>", "", s)
s = re.sub(r"<memballoon model='virtio'>.*?</memballoon>", "<memballoon model='none'/>", s, flags=re.S) s = re.sub(r"<memballoon model='virtio'>.*?</memballoon>", "<memballoon model='none'/>", s, flags=re.S)
s = re.sub(r"<memballoon model='virtio'/>", "<memballoon model='none'/>", s) s = re.sub(r"<memballoon model='virtio'/>", "<memballoon model='none'/>", s)
s = re.sub(r"<model type='virtio'/>(\s*<driver [^/]*/>)?", "<model type='e1000e'/>", s) # jumbo frames are the single biggest win on the host<->guest link: the emulated
# NIC is packet-rate bound, so 9000-byte frames cut the per-packet cost the guest
# pays on receive. Measured 2922 -> 14232 Mbit/s inbound on igb, byte-exact clean.
s = re.sub(r"<model type='virtio'/>(\s*<driver [^/]*/>)?",
"<model type='igb'/>\n <mtu size='9000'/>", s)
if prof == "full": if prof == "full":
s = re.sub(r"<video>.*?</video>", "<video>\n <model type='none'/>\n </video>", s, flags=re.S) s = re.sub(r"<video>.*?</video>", "<video>\n <model type='none'/>\n </video>", s, flags=re.S)
@@ -814,20 +821,29 @@ if [ "$PROFILE" = full ]; then
[ "$ok" = 1 ] && echo "patched QEMU and KVM modules both in place." [ "$ok" = 1 ] && echo "patched QEMU and KVM modules both in place."
if [ -n "$MODEL" ] && [ -f /sys/module/kvm_amd/parameters/cpuid_passthrough ]; then if [ -n "$MODEL" ] && [ -f /sys/module/kvm_amd/parameters/cpuid_passthrough ]; then
echo echo
echo "The TIMER check needs CPUID passthrough, which is off by default and must be" echo "The TIMER check needs CPUID passthrough, which must be off while the guest cold"
echo "switched on AFTER the guest has booted (it hangs a booting guest). Once the" echo "boots and on once it is up. Let the hook handle that around this guest:"
echo "guest is up, on the host:" 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 '$MODEL' | sudo tee /sys/module/kvm_amd/parameters/brand_string"
echo " echo Y | sudo tee /sys/module/kvm_amd/parameters/cpuid_passthrough" echo " echo Y | sudo tee /sys/module/kvm_amd/parameters/cpuid_passthrough # N again before the next boot"
echo "Switch it off again (echo N) before rebooting the guest."
fi fi
fi fi
if [ ! -e /usr/lib/udev/rules.d/99-vfio-native-vnet-offload.rules ] && [ ! -e /etc/udev/rules.d/99-vfio-native-vnet-offload.rules ]; then if [ ! -e /usr/lib/udev/rules.d/99-vfio-native-vnet-offload.rules ] && [ ! -e /etc/udev/rules.d/99-vfio-native-vnet-offload.rules ]; then
echo "NOTE: the e1000e offload udev rule is not installed. SSH into the guest will fail with" echo "NOTE: the NIC offload udev rule is not installed. SSH into the guest will fail with"
echo " 'Corrupted MAC on input' until it is:" echo " 'Corrupted MAC on input' until it is:"
echo " sudo install -Dm644 $SELF/scripts/99-vfio-native-vnet-offload.rules /etc/udev/rules.d/ && sudo udevadm control --reload-rules" echo " sudo install -Dm644 $SELF/scripts/99-vfio-native-vnet-offload.rules /etc/udev/rules.d/ && sudo udevadm control --reload-rules"
fi fi
NET=$("${C[@]}" dumpxml "$DOM" 2>/dev/null | sed -n "s/.*<source network='\([^']*\)'.*/\1/p" | head -1)
NETMTU=$([ -n "$NET" ] && "${C[@]}" net-dumpxml --inactive "$NET" 2>/dev/null | sed -n "s/.*<mtu size='\([0-9]*\)'.*/\1/p")
if [ -n "$NET" ] && [ "${NETMTU:-1500}" -lt 9000 ]; then
echo "NOTE: the guest interface asks for MTU 9000 but libvirt network '$NET' is at ${NETMTU:-1500}."
echo " Jumbo needs both ends; inbound throughput is ~5x with it. Add <mtu size='9000'/> to"
echo " the network and restart it: virsh net-edit $NET && virsh net-destroy $NET && virsh net-start $NET"
echo " Then in the guest: set the NIC's Jumbo Packet to 9014 and the interface MTU to 9000."
fi
gov=$(cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor 2>/dev/null || echo unknown) gov=$(cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor 2>/dev/null || echo unknown)
[ "$gov" = performance ] || echo "host governor is '$gov' - run: sudo cpupower frequency-set -g performance" [ "$gov" = performance ] || echo "host governor is '$gov' - run: sudo cpupower frequency-set -g performance"