Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f99ff1400a | |||
| 4e0df16271 | |||
| 0c3425e5af | |||
| a956ececfd | |||
| 966a2a6390 | |||
| d3062f5e24 | |||
| 1e1078e16a | |||
| f90745e68e | |||
| 91fd1c45e1 | |||
| 52b18427f2 |
29
README.md
29
README.md
@@ -165,25 +165,32 @@ vm-native-verify
|
||||
|
||||
`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
|
||||
paying it is to not exit. `vm-native-setup` prints the two lines for your declared SKU; on this
|
||||
host they are:
|
||||
paying it is to not exit. That switch has to be off while a guest cold-boots - a booting Windows
|
||||
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
|
||||
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
|
||||
```
|
||||
|
||||
Run them once the guest is up, never before: a booting Windows enumerates CPUID bits KVM
|
||||
synthesises, and hangs if they vanish half way through. Switch it off again (`echo N`) before the
|
||||
next boot. The module applies it only to vCPU threads pinned to exactly one host CPU, so on an
|
||||
unpinned guest it does nothing rather than something wrong.
|
||||
|
||||
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.
|
||||
The module applies it only to vCPU threads pinned to exactly one host CPU, so on an unpinned guest
|
||||
it does nothing rather than something wrong. It is one switch and one brand string for the whole
|
||||
host, so it serves one hardened guest at a time - the hook enforces that and leaves a second guest
|
||||
alone.
|
||||
|
||||
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
|
||||
|
||||
@@ -75,12 +75,6 @@ Enable-NetFirewallRule -DisplayGroup "Remote Desktop"
|
||||
Put your public key in `C:\ProgramData\ssh\administrators_authorized_keys` for an Administrator
|
||||
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
|
||||
|
||||
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
|
||||
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
|
||||
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
|
||||
|
||||
@@ -165,7 +164,7 @@ echo Y | sudo tee /sys/module/kvm_amd/parameters/cpuid_passthrough
|
||||
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
|
||||
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.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
pkgbase = vfio-native
|
||||
pkgdesc = Present a libvirt guest as a self-consistent physical machine, and tune it
|
||||
pkgver = 1.1.0
|
||||
pkgver = 1.3.3
|
||||
pkgrel = 1
|
||||
url = https://git.archworks.co/sandwich/vfio-native
|
||||
install = vfio-native.install
|
||||
@@ -17,7 +17,7 @@ pkgbase = vfio-native
|
||||
optdepends = cpupower: set the host CPU governor
|
||||
optdepends = vfio-native-kvm-dkms: patched KVM modules 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
|
||||
|
||||
pkgname = vfio-native
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
# vfio-native-qemu QEMU 11.1.1 with the platform-identity patches, in /opt
|
||||
|
||||
pkgname=vfio-native
|
||||
pkgver=1.1.0
|
||||
pkgver=1.3.3
|
||||
pkgrel=1
|
||||
pkgdesc="Present a libvirt guest as a self-consistent physical machine, and tune it"
|
||||
arch=('any')
|
||||
@@ -29,6 +29,7 @@ package() {
|
||||
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/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"
|
||||
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/restore-stock-kvm.sh "${share}/scripts/restore-stock-kvm.sh"
|
||||
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 -dm755 "${share}/patches"
|
||||
cp -r patches/. "${share}/patches/"
|
||||
@@ -45,7 +48,6 @@ package() {
|
||||
# this coexists with whatever hook the host already has.
|
||||
install -Dm755 scripts/libvirt-hook-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 -Dm644 scripts/99-vfio-native-vnet-offload.rules \
|
||||
"${pkgdir}/usr/lib/udev/rules.d/99-vfio-native-vnet-offload.rules"
|
||||
install -Dm755 scripts/libvirt-hook-vnet-offload.sh \
|
||||
"${pkgdir}/etc/libvirt/hooks/qemu.d/20-vnet-offload.sh"
|
||||
}
|
||||
|
||||
@@ -49,11 +49,11 @@ post_install() {
|
||||
|
||||
'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
|
||||
libvirt tap as it appears. The emulated e1000e NIC corrupts integrity-checked
|
||||
traffic with them on; SSH to the guest fails with "Corrupted MAC on input".
|
||||
It applies to every VM on the host; the throughput cost on a host<->guest
|
||||
link is not measurable.
|
||||
A hook at /etc/libvirt/hooks/qemu.d/20-vnet-offload.sh turns GSO and GRO on
|
||||
for the guest's tap. With the interface's MTU 9000 that takes 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.
|
||||
|
||||
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,
|
||||
|
||||
@@ -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
71
scripts/cpuid-passthrough-watch
Executable 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
77
scripts/cpuid-passthrough.sh
Executable 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
|
||||
@@ -57,6 +57,24 @@ group_members() {
|
||||
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() {
|
||||
local l
|
||||
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
|
||||
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)
|
||||
# only real functions of the card: skip bridges (class 0x0604xx)
|
||||
case "$cls" in 0x0604*) continue;; esac
|
||||
@@ -219,7 +237,7 @@ prepare)
|
||||
|
||||
# 1. stop whatever is holding the DRM device
|
||||
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
|
||||
echo "$dm" > "$STATE/dm"
|
||||
log "stopping $dm"
|
||||
@@ -303,7 +321,7 @@ apply() {
|
||||
"$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"
|
||||
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
|
||||
IFS=':.' read -r dm bs sl fn <<< "$m"
|
||||
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 "IOMMU group has to go together:"
|
||||
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)
|
||||
case "$cls" in 0x0604*) continue;; esac
|
||||
IFS=':. ' read -r dom bus slot fn <<< "$(echo "$m" | tr ':.' ' ')"
|
||||
|
||||
68
scripts/libvirt-hook-cpuid-passthrough.sh
Executable file
68
scripts/libvirt-hook-cpuid-passthrough.sh
Executable 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
|
||||
15
scripts/libvirt-hook-vnet-offload.sh
Executable file
15
scripts/libvirt-hook-vnet-offload.sh
Executable 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
|
||||
@@ -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):
|
||||
return 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='([^']*)'[^/]*/>",
|
||||
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)
|
||||
if "<serial>" not in d:
|
||||
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"<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"<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":
|
||||
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."
|
||||
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
|
||||
|
||||
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 " sudo install -Dm644 $SELF/scripts/99-vfio-native-vnet-offload.rules /etc/udev/rules.d/ && sudo udevadm control --reload-rules"
|
||||
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" = performance ] || echo "host governor is '$gov' - run: sudo cpupower frequency-set -g performance"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user