diff --git a/README.md b/README.md index 741d8be..9f6c264 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,7 @@ cd vfio-native/packaging | Package | What it is | | --- | --- | | `vfio-native` | the three commands, the patches, the ACPI tables, the benchmark, the libvirt hook | -| `vfio-native-qemu` | QEMU 11.1.1 with the platform-identity patches, in `/opt/qemu-native` | +| `vfio-native-qemu` | QEMU 11.1.2 with the platform-identity patches, in `/opt/qemu-native` | | `vfio-native-kvm-dkms` | the patched KVM modules, rebuilt by DKMS for every installed 7.2.x kernel | The modules land in `updates/dkms/`, which modprobe prefers, but nothing reloads them for you. @@ -144,10 +144,18 @@ vm-native-setup -d win11 -p full -c 8 -m 16 -r -u auto -y | `-s` | Secure Boot with enrolled keys, `on` (default) or `off` | | `-u` | USB passthrough: `none`, `auto`, or `vid:pid,...,0000:bb:dd.f` | | `-r` | randomise the hardware identity: serials, MAC, memory module | +| `-S` | refit to a real machine's dumped SMBIOS (Method B) | | `-y` | no prompts | It backs the domain up first and prints the revert command. Re-running it is safe. +To present as a specific machine instead of this host, fill the target keys in the +domain's `identity.env` (`SYS_PRODUCT`, `BIOS_*`, `CPU_*`, ...): the tool then authors +SMBIOS type 0-4/17 as `-smbios type=` field args (Method A), so a new-laptop refit is a +few field edits. `-S ` instead splits a real machine's dumped DMI table into +`-smbios file=` blobs for byte-perfect fidelity (Method B); mixing is per structure type. +Either wires the ICH9 USB set, disables HPET, and needs `-p full`. + ### 5. Check it worked ```sh @@ -274,9 +282,9 @@ and ship with a cover letter and a selftest in `patches/kvm/`. either alone leaves it standing. `EXPERIMENTAL-0006` is built into the DKMS package but off by default; it is not upstream material, because it hands the guest raw host CPUID. -**QEMU** - eight patchsets against v11.1.1: `01-firmware`, `02-disk-identity`, `03-pci-ids`, -`04-fw-cfg`, `05-usb-hid`, `06-audio`, and `07-edid` and `08-cpu-misc`, which clear no check by -themselves and are carried for other detectors. +**QEMU** - ten patchsets against v11.1.2: `01-firmware`, `02-disk-identity`, `03-pci-ids`, +`04-fw-cfg`, `05-usb-hid`, `06-audio`, `09-firmware-chain` and `10-smbios`, and `07-edid` and +`08-cpu-misc`, which clear no check by themselves and are carried for other detectors. --- @@ -366,7 +374,7 @@ the whole trail, including the two wrong turns. ``` patches/kvm/ five upstream-style kernel patches, a cover letter, plus one experimental -patches/qemu/ eight patchsets against QEMU 11.1.1 +patches/qemu/ ten patchsets against QEMU 11.1.2 packaging/ three AUR-shaped packages: tools, patched QEMU, DKMS modules scripts/ setup, verify, GPU, libvirt hook, module install scripts/generate-tables.py raw SMBIOS type 7/26/27/28/29 blobs for -smbios file= diff --git a/packaging/vfio-native/PKGBUILD b/packaging/vfio-native/PKGBUILD index bddc1ea..16956f6 100644 --- a/packaging/vfio-native/PKGBUILD +++ b/packaging/vfio-native/PKGBUILD @@ -5,7 +5,7 @@ # Three packages: # vfio-native scripts, patches, ACPI tables and the benchmark # vfio-native-kvm-dkms the patched KVM modules, rebuilt by DKMS per kernel -# vfio-native-qemu QEMU 11.1.1 with the platform-identity patches, in /opt +# vfio-native-qemu QEMU 11.1.2 with the platform-identity patches, in /opt pkgname=vfio-native pkgver=1.3.3 @@ -37,6 +37,7 @@ 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/split_smbios.py "${share}/scripts/split_smbios.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 diff --git a/scripts/setup-vm.sh b/scripts/setup-vm.sh index c59e107..bfbe419 100755 --- a/scripts/setup-vm.sh +++ b/scripts/setup-vm.sh @@ -11,8 +11,14 @@ # -s on | off Secure Boot with enrolled keys (default on) # -u none | auto | USB passthrough # -r randomize the hardware identity (serials, MAC, memory modules) +# -S refit to a real machine's dumped SMBIOS (Method B) # -y no prompts # +# To present as a specific machine instead of this host, fill the target keys in +# the per-domain identity.env (SYS_PRODUCT, BIOS_*, CPU_*, ...): the tool then +# authors SMBIOS type 0-4/17 as field args (Method A). -S uses a real +# machine's dumped DMI table byte-for-byte instead (Method B). Either needs -p full. +# # Takes a plain libvirt domain to the tuned, corrected state in one pass: # detects the host CPU layout and maps vCPUs onto real SMT pairs within one # cache domain, keeps the emulator off the vCPU cores, moves the disk to @@ -23,10 +29,10 @@ set -euo pipefail # Run bare it is an interview: every choice is asked, with a default. Every flag # answers one question in advance, and -y takes every default. -DOM=win11; PROFILE=""; CORES=""; RAMG=""; ASSUME=0; SECBOOT=""; USBSPEC=""; RANDOMIZE="" -while getopts "d:p:c:m:s:u:ryh" o; do case $o in +DOM=win11; PROFILE=""; CORES=""; RAMG=""; ASSUME=0; SECBOOT=""; USBSPEC=""; RANDOMIZE=""; DMIBLOB="" +while getopts "d:p:c:m:s:u:S:ryh" o; do case $o in d) DOM=$OPTARG;; p) PROFILE=$OPTARG;; c) CORES=$OPTARG;; - m) RAMG=$OPTARG;; s) SECBOOT=$OPTARG;; u) USBSPEC=$OPTARG;; r) RANDOMIZE=1;; y) ASSUME=1;; + m) RAMG=$OPTARG;; s) SECBOOT=$OPTARG;; u) USBSPEC=$OPTARG;; S) DMIBLOB=$OPTARG;; r) RANDOMIZE=1;; y) ASSUME=1;; # print the header comment block, however long it grows h) sed -n '2,/^[^#]/p' "$0" | sed "\$d; s/^# \?//; s|@@SELFNAME@@|$(basename "$0")|g"; exit 0;; esac; done @@ -302,6 +308,24 @@ gen_identity() { echo "MEM_PART='${mem_part:-KF556C40BB-16}'" echo "MEM_SPEED='${mem_speed:-5600}'" echo "MEM_SERIAL='$(rnd 8 'A-F0-9')'" + echo "" + echo "# Target refit (Method A): uncomment and fill to present as a specific machine" + echo "# instead of this host (SYS_UUID becomes the SMBIOS UUID). Needs -p full." + echo "#SYS_MANUFACTURER=''; SYS_PRODUCT=''; SYS_VERSION=''; SYS_UUID=''; SYS_SKU=''; SYS_FAMILY=''" + echo "#BIOS_VENDOR=''; BIOS_VERSION=''; BIOS_DATE=''; BIOS_RELEASE=''" + echo "#BOARD_MANUFACTURER=''; BOARD_PRODUCT=''; BOARD_VERSION=''" + echo "#CHASSIS_MANUFACTURER=''; CHASSIS_TYPE='10'" + echo "#CPU_MANUFACTURER='Intel(R) Corporation'; CPU_VERSION=''; CPU_SOCKET=''; CPU_ID=''; CPU_MAX_SPEED=''; CPU_CURRENT_SPEED=''" + echo "# CPUID identity override, for when the target CPU vendor differs from this host (model-id alone" + echo "# leaves lscpu's vendor/family/model as the host's). Set these to the target's values, e.g. a" + echo "# Raptor Lake-U Intel target on an AMD host: GenuineIntel, family 6, model 186, stepping 2. CPU_DISABLE_FLAGS" + echo "# drops host-only flags that would contradict the vendor (on AMD: svm sse4a 3dnow 3dnowext" + echo "# misalignsse cr8legacy osvw). Host-exclusive features of the real target (Intel vmx/hwp/..) cannot" + echo "# be synthesised and stay absent." + echo "#CPU_VENDOR_ID=''; CPU_FAMILY=''; CPU_MODEL=''; CPU_STEPPING=''; CPU_DISABLE_FLAGS=''" + echo "#MEM_LOCATOR='DIMM 0'; NVME_MODEL=''" + echo "# SECOND_MAC adds a present-but-dead NIC (isolated, link down, anti-spoofed) at that MAC:" + echo "#SECOND_MAC=''" } > "$DATA/identity.env" } mkdir -p "$DATA" @@ -321,6 +345,32 @@ if [ "$RANDOMIZE" = 1 ] || [ ! -f "$DATA/identity.env" ]; then gen_identity; fi # shellcheck disable=SC1091 . "$DATA/identity.env" +# Target identity: present as a specific machine instead of this host. Method A +# (SYS_PRODUCT set in identity.env) authors SMBIOS type 0-4/17 as -smbios type= +# field args; Method B (-S ) splits a real DMI table into -smbios file= +# blobs. Both ride on the patched QEMU's 10-smbios, so they need the full level. +TARGET_MODE="" +if [ -n "$DMIBLOB" ]; then TARGET_MODE=dump +elif [ -n "${SYS_PRODUCT:-}" ]; then TARGET_MODE=fields; fi +if [ -n "$TARGET_MODE" ]; then + [ "$PROFILE" = full ] || { echo "target identity (-S or SYS_PRODUCT) needs -p full"; exit 1; } + [ "$TARGET_MODE" != dump ] || [ -f "$DMIBLOB" ] || { echo "no such DMI blob: $DMIBLOB"; exit 1; } + # a hand-written target identity.env may set only the keys it cares about + for k in SYS_MANUFACTURER SYS_PRODUCT SYS_VERSION SYS_UUID SYS_SKU SYS_FAMILY SYS_SERIAL \ + BIOS_VENDOR BIOS_VERSION BIOS_DATE BIOS_RELEASE BOARD_MANUFACTURER BOARD_PRODUCT \ + BOARD_VERSION BOARD_SERIAL CHASSIS_MANUFACTURER CHASSIS_TYPE CHASSIS_SERIAL \ + CPU_MANUFACTURER CPU_VERSION CPU_SOCKET CPU_ID CPU_MAX_SPEED CPU_CURRENT_SPEED \ + CPU_VENDOR_ID CPU_FAMILY CPU_MODEL CPU_STEPPING CPU_DISABLE_FLAGS \ + MEM_MANUFACTURER MEM_PART MEM_SERIAL MEM_SPEED MEM_LOCATOR \ + NVME_SERIAL NVME_MODEL MAC SECOND_MAC; do + declare -n _k="$k"; : "${_k:=}" + done; unset -n _k + : "${CHASSIS_TYPE:=10}"; : "${MEM_LOCATOR:=DIMM 0}" +fi +# the shared isolated network a SECOND_MAC dead NIC hangs off (created at apply time) +VOID_NET="" +[ -n "${SECOND_MAC:-}" ] && [ "$PROFILE" != tuned ] && VOID_NET=vfio-native-void + # --------------------------------------------------------- USB passthrough --- # Passing a whole controller is cleaner than passing devices one by one: no # emulated hub, no hotplug, nothing in the guest that says "redirected". It is @@ -504,9 +554,14 @@ echo " binary $EMULATOR" echo " memory ${RAMG} GiB" [ -n "$MODEL" ] && [ "$PROFILE" != tuned ] && echo " declares $MODEL" if [ "$PROFILE" != tuned ]; then - [ "$CONVERT" = 1 ] && echo " disk emulated NVMe, serial $NVME_SERIAL; virtio devices replaced" + if [ -n "$TARGET_MODE" ]; then + echo " refit $([ "$TARGET_MODE" = dump ] && echo "dumped SMBIOS from $DMIBLOB (Method B)" || echo "$SYS_PRODUCT (Method A field args)")" + [ -n "$CPU_VERSION" ] && echo " CPU $CPU_VERSION; ICH9 USB set; HPET off" + fi + [ "$CONVERT" = 1 ] && echo " disk emulated NVMe, serial $NVME_SERIAL${NVME_MODEL:+, model $NVME_MODEL}; virtio devices replaced" [ "$SECBOOT" = on ] && echo " firmware Secure Boot, keys enrolled from $NVRAM_TPL" [ "$RANDOMIZE" = 1 ] && echo " identity SMBIOS serials, MAC $MAC and memory module from $DATA/identity.env" + [ -n "${SECOND_MAC:-}" ] && echo " 2nd nic dead NIC $SECOND_MAC on isolated $VOID_NET (link down, anti-spoofed)" [ "$PROFILE" = full ] && echo " display none - the guest console goes dark, use SSH or RDP" fi [ -n "$USB_HOSTDEVS" ] && echo " usb $(echo "$USB_HOSTDEVS" | grep -c '\n" done +# The dead NIC's isolated network: no uplink, no host IP, no DHCP, so it reaches +# nothing; libvirt names the bridge. Created once, reused by every domain. +if [ -n "$VOID_NET" ]; then + if ! "${C[@]}" net-info "$VOID_NET" >/dev/null 2>&1; then + echo "$VOID_NET" > "$BACKUP/$VOID_NET.xml" + "${C[@]}" net-define "$BACKUP/$VOID_NET.xml" >/dev/null + "${C[@]}" net-autostart "$VOID_NET" >/dev/null + fi + "${C[@]}" net-start "$VOID_NET" >/dev/null 2>&1 || true +fi + PROFILE="$PROFILE" VENDOR="$VENDOR" VCPUS="$VCPUS" CORES="$CORES" SMT="$SMT" RAMG="$RAMG" \ MODEL="$MODEL" EMULATOR="$EMULATOR" PINS="$PINS" \ EMUSET="$(pair "$EMU")" IOCSET="$(pair "$IOC")" NUMANODE="$([ "$NNODES" -gt 1 ] && echo "$GNODE")" \ @@ -547,6 +613,7 @@ DMI_SKU="$(dmi product_sku)" DMI_FAMILY="$(dmi product_family)" DMI_BOARD_VENDOR DMI_BOARD="$(dmi board_name)" DMI_BOARD_VERSION="$(dmi board_version)" DMI_CHASSIS_VENDOR="$(dmi chassis_vendor)" \ DMI_CHASSIS_VERSION="$(dmi chassis_version)" DMI_BIOS_VENDOR="$(dmi bios_vendor)" DMI_BIOS_VERSION="$(dmi bios_version)" \ DMI_BIOS_DATE="$(dmi bios_date)" \ +TARGET_MODE="$TARGET_MODE" SECOND_MAC="${SECOND_MAC:-}" VOID_NET="$VOID_NET" \ python3 - "$BACKUP/$DOM.before-setup.xml" "$BACKUP/$DOM.setup.xml" <<'XMLGEN_END' import io, os, re, sys from xml.sax.saxutils import escape @@ -555,6 +622,7 @@ E = os.environ prof, vcpus, cores, smt = E["PROFILE"], int(E["VCPUS"]), int(E["CORES"]), int(E["SMT"]) s = io.open(src, encoding="utf-8").read() conformant = prof != "tuned" +target = E.get("TARGET_MODE") or "" kib = int(E["RAMG"]) * 1024 * 1024 s = re.sub(r"\d+", "%d" % kib, s, count=1) @@ -621,7 +689,7 @@ if "" not in s: s = s.replace(" ", " \n \n" " \n \n ", 1) -if conformant and E["MODEL"]: +if conformant and not target and E["MODEL"]: s = re.sub(r"version=AMD Ryzen [^,]*,", "version=%s," % E["MODEL"], s) if conformant and E["CONVERT"] == "1": @@ -669,7 +737,42 @@ if conformant and E["CONVERT"] == "1": if prof == "full": s = re.sub(r"", "", s, flags=re.S) -if conformant: +if conformant and target: + # the target's SMBIOS rides in as -smbios qemu args, not libvirt + if E.get("MAC"): + s = re.sub(r"", "" % E["MAC"], s, count=1) + s = re.sub(r"\s*.*?", "", s, flags=re.S) + s = re.sub(r"\s*", "", s) + # a real laptop has the ICH9 EHCI+UHCI set at 1d, not a bare qemu-xhci + s = re.sub(r"\s*]*>.*?", "", s, flags=re.S) + s = re.sub(r"\s*]*/>", "", s) + ich9_usb = ( + " \n" + "
\n" + " \n" + " \n" + " \n" + "
\n" + " \n" + " \n" + " \n" + "
\n" + " \n" + " \n" + " \n" + "
\n" + " \n") + s = s.replace(" ", ich9_usb + " ", 1) + if "name='hpet'" in s: + s = re.sub(r"]*/>", "", s, count=1) + elif re.search(r"]*/>", s): + s = re.sub(r"(]*)/>", + r"\1>\n \n ", s, count=1) + else: + s = re.sub(r"(]*>)", + r"\1\n ", s, count=1) + +if conformant and not target: if E["RANDOMIZE"] == "1": s = re.sub(r"", "" % E["MAC"], s, count=1) def ent(name, key): @@ -689,6 +792,7 @@ if conformant: if "", " \n ", 1) +if conformant: if E["SECBOOT"] == "on": s = re.sub(r"", "", s, count=1) s = re.sub(r"\s*.*?", "", s, flags=re.S) @@ -702,6 +806,17 @@ if conformant: if "" not in s: s = s.replace(" ", " \n ", 1) +# SECOND_MAC: a present-but-dead NIC on the isolated void network - shows up in the +# guest but reaches nothing, with its own clean-traffic anti-spoof. Replace a prior one. +if conformant and E.get("SECOND_MAC") and E.get("VOID_NET"): + s = re.sub(r"\s*(?:(?!).)*?(?:(?!).)*" % re.escape(E["VOID_NET"]), "", s, flags=re.S) + nic = (" \n" + " \n" % E["SECOND_MAC"] + + " \n" % E["VOID_NET"] + + " \n \n" + " \n \n") + s = s.replace(" ", nic + " ", 1) + # USB passthrough: replace what this tool put in before, leave other hostdevs alone s = re.sub(r"\s*(?:(?!).)*(?:(?!).)*", "", s, flags=re.S) if E["USB_HOSTDEVS"]: @@ -726,22 +841,27 @@ fi # are replaced; anything else already in the block is left alone. QARGS=() if [ "$PROFILE" != tuned ]; then - kb() { local s; s=$(cat "/sys/devices/system/cpu/cpu0/cache/index$1/size"); echo "${s%K}"; } - SPAN=$(( (CORES + MAXC - 1) / MAXC )) - python3 "$SELF/scripts/generate-tables.py" --output-dir "$DATA" \ - --cache-l1 $(( CORES * ($(kb 0) + $(kb 1)) )) --cache-l2 $(( CORES * $(kb 2) )) \ - --cache-l3 $(( SPAN * $(kb 3) )) >/dev/null - for f in "$DATA"/type*.bin; do QARGS+=("-smbios" "file=$f"); done + # A target refit builds its SMBIOS from field args (Method A) or a real dump + # (Method B), so it skips the synthesised cache/probe blobs; presenting as this + # host keeps them, sized from the declared vCPU cache. + if [ -z "$TARGET_MODE" ]; then + kb() { local s; s=$(cat "/sys/devices/system/cpu/cpu0/cache/index$1/size"); echo "${s%K}"; } + SPAN=$(( (CORES + MAXC - 1) / MAXC )) + python3 "$SELF/scripts/generate-tables.py" --output-dir "$DATA" \ + --cache-l1 $(( CORES * ($(kb 0) + $(kb 1)) )) --cache-l2 $(( CORES * $(kb 2) )) \ + --cache-l3 $(( SPAN * $(kb 3) )) >/dev/null + for f in "$DATA"/type*.bin; do QARGS+=("-smbios" "file=$f"); done + fi if [ -d "$SELF/acpi" ]; then cp "$SELF"/acpi/*.aml "$DATA"/ for f in "$DATA"/*.aml; do QARGS+=("-acpitable" "file=$f"); done fi # A second -cpu REPLACES the first rather than merging, so it is built from # libvirt's own generated line and never hand-written. It is incompatible - # with , which is why the tuned level skips it. - # libvirt's own -cpu comes first; a previous run's override sits at the - # end, quoted because the brand has spaces, so split like a shell would. - if [ -n "$MODEL" ]; then + # with , which is why the tuned level skips it. The brand is the + # target's CPU for a refit, otherwise a matched host SKU. + BRAND="$MODEL"; [ -n "$TARGET_MODE" ] && BRAND="$CPU_VERSION" + if [ -n "$BRAND" ]; then python3 - "$BACKUP/$DOM.setup.xml" "$BACKUP/$DOM.nohostdev.xml" <<'STRIP_END' import io, re, sys s = io.open(sys.argv[1], encoding="utf-8").read() @@ -751,26 +871,52 @@ STRIP_END import shlex, sys a = shlex.split(sys.stdin.read()) print(a[a.index("-cpu") + 1] if "-cpu" in a else "")') - if [ -n "$GEN" ] && [[ "$GEN" != *hv-* ]]; then QARGS+=("-cpu" "$GEN,model-id=$MODEL"); fi - # SMBIOS type 4 defaults to manufacturer "QEMU" and the machine name as - # the version; a comma inside a QEMU option value is written twice. - case "$VENDOR:$FAMILY" in - AuthenticAMD:23) SOCK=AM4; CPUVEND="Advanced Micro Devices,, Inc.";; - AuthenticAMD:*) SOCK=AM5; CPUVEND="Advanced Micro Devices,, Inc.";; - GenuineIntel:*) SOCK=$([ "$MODELNO" -ge 151 ] && echo LGA1700 || echo LGA1200); CPUVEND="Intel(R) Corporation";; - *) SOCK=CPU; CPUVEND="$VENDOR";; - esac - MAXMHZ=$(( $(cat /sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq 2>/dev/null || echo 4500000) / 1000 )) - QARGS+=("-smbios" "type=4,sock_pfx=$SOCK,manufacturer=$CPUVEND,version=$MODEL,max-speed=$MAXMHZ,current-speed=$MAXMHZ") + if [ -n "$GEN" ] && [[ "$GEN" != *hv-* ]]; then + CPUARG="$GEN,model-id=$BRAND" + [ -n "$CPU_VENDOR_ID" ] && CPUARG="$CPUARG,vendor=$CPU_VENDOR_ID" + [ -n "$CPU_FAMILY" ] && CPUARG="$CPUARG,family=$CPU_FAMILY" + [ -n "$CPU_MODEL" ] && CPUARG="$CPUARG,model=$CPU_MODEL" + [ -n "$CPU_STEPPING" ] && CPUARG="$CPUARG,stepping=$CPU_STEPPING" + for _df in $CPU_DISABLE_FLAGS; do CPUARG="$CPUARG,-$_df"; done + QARGS+=("-cpu" "$CPUARG") + fi fi - if [ "$RANDOMIZE" = 1 ]; then - QARGS+=("-smbios" "type=17,loc_pfx=DIMM,bank=P0 CHANNEL A,manufacturer=$MEM_MANUFACTURER,part=$MEM_PART,serial=$MEM_SERIAL,speed=$MEM_SPEED") + # a comma inside a QEMU option value is written twice + esc() { local v="${1//,/,,}"; printf '%s' "$v"; } + if [ "$TARGET_MODE" = fields ]; then + QARGS+=("-smbios" "type=0,vendor=$(esc "$BIOS_VENDOR"),version=$(esc "$BIOS_VERSION"),date=$(esc "$BIOS_DATE"),release=$(esc "$BIOS_RELEASE"),uefi=on") + QARGS+=("-smbios" "type=1,manufacturer=$(esc "$SYS_MANUFACTURER"),product=$(esc "$SYS_PRODUCT"),version=$(esc "$SYS_VERSION"),serial=$(esc "$SYS_SERIAL"),sku=$(esc "$SYS_SKU"),family=$(esc "$SYS_FAMILY")${SYS_UUID:+,uuid=$SYS_UUID}") + QARGS+=("-smbios" "type=2,manufacturer=$(esc "$BOARD_MANUFACTURER"),product=$(esc "$BOARD_PRODUCT"),version=$(esc "$BOARD_VERSION"),serial=$(esc "$BOARD_SERIAL")") + QARGS+=("-smbios" "type=3,manufacturer=$(esc "$CHASSIS_MANUFACTURER"),serial=$(esc "$CHASSIS_SERIAL"),chassis-type=$CHASSIS_TYPE") + QARGS+=("-smbios" "type=4,manufacturer=$(esc "$CPU_MANUFACTURER"),version=$(esc "$CPU_VERSION"),sock_pfx=$(esc "$CPU_SOCKET")${CPU_ID:+,processor-id=$CPU_ID}${CPU_MAX_SPEED:+,max-speed=$CPU_MAX_SPEED}${CPU_CURRENT_SPEED:+,current-speed=$CPU_CURRENT_SPEED}") + QARGS+=("-smbios" "type=17,manufacturer=$(esc "$MEM_MANUFACTURER"),part=$(esc "$MEM_PART"),serial=$(esc "$MEM_SERIAL"),speed=$MEM_SPEED,loc_pfx=$(esc "$MEM_LOCATOR")") + elif [ "$TARGET_MODE" = dump ]; then + rm -rf "$DATA/smbios_real" + while read -r f; do [ "${f#\#}" = "$f" ] && QARGS+=("-smbios" "file=$f"); done \ + < <(python3 "$SELF/scripts/split_smbios.py" "$DMIBLOB" "$DATA/smbios_real") + else + # present as this host: type 4 from the matched SKU, type 17 with -r + if [ -n "$MODEL" ]; then + case "$VENDOR:$FAMILY" in + AuthenticAMD:23) SOCK=AM4; CPUVEND="Advanced Micro Devices,, Inc.";; + AuthenticAMD:*) SOCK=AM5; CPUVEND="Advanced Micro Devices,, Inc.";; + GenuineIntel:*) SOCK=$([ "$MODELNO" -ge 151 ] && echo LGA1700 || echo LGA1200); CPUVEND="Intel(R) Corporation";; + *) SOCK=CPU; CPUVEND="$VENDOR";; + esac + MAXMHZ=$(( $(cat /sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq 2>/dev/null || echo 4500000) / 1000 )) + QARGS+=("-smbios" "type=4,sock_pfx=$SOCK,manufacturer=$CPUVEND,version=$MODEL,max-speed=$MAXMHZ,current-speed=$MAXMHZ") + fi + if [ "$RANDOMIZE" = 1 ]; then + QARGS+=("-smbios" "type=17,loc_pfx=DIMM,bank=P0 CHANNEL A,manufacturer=$MEM_MANUFACTURER,part=$MEM_PART,serial=$MEM_SERIAL,speed=$MEM_SPEED") + fi fi fi -QARGS_NL="$(printf '%s\n' "${QARGS[@]}")" python3 - "$BACKUP/$DOM.setup.xml" <<'QARGS_END' +QARGS_NL="$(printf '%s\n' "${QARGS[@]}")" NVME_MODEL="${NVME_MODEL:-}" \ +python3 - "$BACKUP/$DOM.setup.xml" <<'QARGS_END' import io, os, re, sys p = sys.argv[1] args = [a for a in os.environ["QARGS_NL"].split("\n") if a] +model = os.environ.get("NVME_MODEL", "") s = io.open(p, encoding="utf-8").read() if "xmlns:qemu=" not in s: s = s.replace("", @@ -796,6 +942,14 @@ if vals or envs: + "".join(" %s\n" % e for e in envs) + " \n") s = s.replace("", block + "", 1) +# libvirt's nvme controller has no model attribute, so the Identify Model Number +# rides in as a qemu device override (the serial stays on the controller). +if model: + s = re.sub(r"\s*.*?", "", s, flags=re.S) + ovr = (" \n \n \n" + " \n" % model.replace("'", "'") + + " \n \n \n") + s = s.replace("", ovr + "", 1) io.open(p, "w", encoding="utf-8").write(s) QARGS_END "${C[@]}" define "$BACKUP/$DOM.setup.xml" >/dev/null diff --git a/scripts/split_smbios.py b/scripts/split_smbios.py new file mode 100644 index 0000000..b18e8cf --- /dev/null +++ b/scripts/split_smbios.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 +"""Split a raw SMBIOS structure table into one file per structure for QEMU. + +Reads a dumped DMI table (/sys/firmware/dmi/tables/DMI on the real machine), +writes one `-smbios file=` blob per structure into OUTDIR, and prints the +ordered list of paths. A complete set through the type-127 end marker is what +lets QEMU build a well-formed entry point from a dump. + +Each structure is a 4-byte header (type, length, handle:u16) + (length-4) +formatted bytes + a string set terminated by a double NUL; a structure with no +strings is still followed by 0x0000. +""" +import os +import sys + + +def split(data): + out, i, n = [], 0, len(data) + while i + 4 <= n: + stype, slen = data[i], data[i + 1] + if slen < 4: + break + j = i + slen + while j + 1 < n and not (data[j] == 0 and data[j + 1] == 0): + j += 1 + j += 2 + out.append((stype, data[i:j])) + i = j + if stype == 127: + break + return out + + +def main(): + dmi, outdir = sys.argv[1], sys.argv[2] + data = open(dmi, "rb").read() + os.makedirs(outdir, exist_ok=True) + structs = split(data) + paths = [] + for idx, (stype, blob) in enumerate(structs): + p = os.path.join(outdir, "sm_%02d_type%d.bin" % (idx, stype)) + open(p, "wb").write(blob) + paths.append(p) + print("# %d structures: %s" % (len(structs), ",".join(str(t) for t, _ in structs))) + for p in paths: + print(p) + + +if __name__ == "__main__": + main()