feat(quickemu): add ARM64 (aarch64) guest support

- Allow arch override via config (arch="${arch:-x86_64}") and set
  ARCH_VM accordingly so aarch64 VMs can be selected from configs.
- Re-detect qemu-system-${ARCH_VM} after sourcing the VM config and
  fail fast with a clear error if the appropriate QEMU binary is
  missing (e.g. qemu-system-aarch64).
- Use virt machine for ARM64 and enable highmem when required
  (MACHINE_TYPE="virt,highmem=on,pflash0=rom,pflash1=efivars").
  pflash0/pflash1 reference named blockdev nodes instead of -drive if
  using OVMF-style pflash on x86.
- Set CPU selection for ARM64 to "max" when available; fall back to
  TCG accel when cross-arch emulation is required (ensures guests
  boot on non-ARM hosts).
- Omit x86-only machine options (smm, vmport) for aarch64 builds to
  avoid passing unsupported flags to QEMU.
- Add AAVMF/ARM64 firmware search paths and keep OVMF logic for
  x86_64 (preserve existing secureboot behaviour for x86 guests).
- Use virtio-gpu-pci for ARM64 (no VGA/virtio-vga on ARM) and add a
  ramfb device to provide an early UEFI framebuffer on ARM64 UEFI
  boot.
- Use virtio-scsi for CD-ROM on ARM64 (virt has no IDE controller) and
  set CD-ROM bootindex=1 so ISO boots before disk when provided.
  Set disk bootindex=2 when an ISO is present so disk remains second.
- Implement EFI boot configuration for ARM64 using -blockdev with
  named nodes (pflash handled via blockdev) rather than the x86
  -drive/secure global approach which is SMM/x86-specific.
- Use the ARM-compatible TPM device (tpm-tis-device) for aarch64
  instead of the x86 tpm-tis device where appropriate.
- Fix EFI_CODE condition bug by using -z instead of -n when checking
  for empty variables (pre-existing bug surfaced while testing ARM64).

IMPACT:
- Enables running aarch64 guests with proper firmware, machine type,
  devices and boot order on both native ARM hosts and non-ARM hosts
  (via TCG emulation).
- Maintainers should note the different pflash/blockdev handling and
  that -global secure pflash settings used for x86 must NOT be used
  for ARM64 virt machines.

Signed-off-by: Martin Wimpress <martin@wimpress.org>
This commit is contained in:
Martin Wimpress 2026-01-24 16:26:03 +00:00 committed by Martin Wimpress
parent b0e643d1c3
commit 6e0b4981ce
2 changed files with 157 additions and 55 deletions

View File

@ -59,8 +59,10 @@ mkShell {
''
}${lib.optionalString stdenv.isDarwin ''
-e 's|local SHARE_PATH="/usr/share"|local SHARE_PATH="${pkgs.qemu_full}/share"|' \
-e '/command -v brew/,/^[[:space:]]*fi$/{ /command -v brew/s/.*/ : # Nix provides QEMU, skip brew check/; /SHARE_PATH.*brew/d; /^[[:space:]]*fi$/d; }' \
-e 's|ovmfs=("[$][{]SHARE_PATH}/OVMF/OVMF_CODE_4M.secboot.fd"|ovmfs=("${pkgs.qemu_full}/share/qemu/edk2-x86_64-secure-code.fd","${pkgs.qemu_full}/share/qemu/edk2-i386-vars.fd" "''${SHARE_PATH}/OVMF/OVMF_CODE_4M.secboot.fd"|' \
-e 's|ovmfs=("[$][{]SHARE_PATH}/OVMF/OVMF_CODE_4M.fd"|ovmfs=("${pkgs.qemu_full}/share/qemu/edk2-x86_64-code.fd","${pkgs.qemu_full}/share/qemu/edk2-i386-vars.fd" "''${SHARE_PATH}/OVMF/OVMF_CODE_4M.fd"|' \
-e 's|ovmfs=("/usr/share/AAVMF/AAVMF_CODE.fd"|ovmfs=("${pkgs.qemu_full}/share/qemu/edk2-aarch64-code.fd","${pkgs.qemu_full}/share/qemu/edk2-arm-vars.fd" "/usr/share/AAVMF/AAVMF_CODE.fd"|' \
''} \
-e '/cp "''${VARS_IN}" "''${VARS_OUT}"/a chmod +w "''${VARS_OUT}"' \
-e 's,\$(command -v smbd),${pkgs.samba}/bin/smbd,' \

210
quickemu
View File

@ -439,12 +439,20 @@ function configure_cpu() {
QEMU_ACCEL="kvm"
fi
if [ "${ARCH_VM}" == "aarch64" ]; then
# Support to run aarch64 VMs (best guess; untested)
if [ "${ARCH_VM}" == "aarch64" ]; then
# ARM64 guest support
# https://qemu-project.gitlab.io/qemu/system/arm/virt.html
# highmem=on allows RAM above 4GB (required for VMs with >3GB RAM)
# pflash0/pflash1 reference the blockdev nodes for AAVMF firmware
MACHINE_TYPE="virt,highmem=on,pflash0=rom,pflash1=efivars"
case ${ARCH_HOST} in
arm64|aarch64) CPU_MODEL="max"
MACHINE_TYPE="virt,highmem=off";;
arm64|aarch64)
# Native ARM64 host running ARM64 guest - use hardware acceleration
CPU_MODEL="max";;
*)
# Cross-architecture emulation (e.g., x86_64 host running ARM64 guest)
CPU_MODEL="max"
QEMU_ACCEL="tcg";;
esac
elif [ "${ARCH_VM}" != "${ARCH_HOST}" ]; then
# If the architecture of the VM is different from the host, disable acceleration
@ -819,32 +827,46 @@ function configure_bios() {
# https://bugzilla.redhat.com/show_bug.cgi?id=1929357#c5
# TODO: Check if macOS should use 'edk2-i386-vars.fd'
if [ -n "${EFI_CODE}" ] || [ ! -e "${EFI_CODE}" ]; then
case ${secureboot} in
on) # shellcheck disable=SC2054,SC2140
ovmfs=("${SHARE_PATH}/OVMF/OVMF_CODE_4M.secboot.fd","${SHARE_PATH}/OVMF/OVMF_VARS_4M.ms.fd" \
"${SHARE_PATH}/edk2/ovmf/OVMF_CODE.secboot.fd","${SHARE_PATH}/edk2/ovmf/OVMF_VARS.secboot.fd" \
"${SHARE_PATH}/OVMF/x64/OVMF_CODE.secboot.fd","${SHARE_PATH}/OVMF/x64/OVMF_VARS.fd" \
"${SHARE_PATH}/edk2-ovmf/OVMF_CODE.secboot.fd","${SHARE_PATH}/edk2-ovmf/OVMF_VARS.fd" \
"${SHARE_PATH}/qemu/ovmf-x86_64-smm-ms-code.bin","${SHARE_PATH}/qemu/ovmf-x86_64-smm-ms-vars.bin" \
"${SHARE_PATH}/qemu/edk2-x86_64-secure-code.fd","${SHARE_PATH}/qemu/edk2-x86_64-code.fd" \
"${SHARE_PATH}/edk2-ovmf/x64/OVMF_CODE.secboot.fd","${SHARE_PATH}/edk2-ovmf/x64/OVMF_VARS.fd" \
"${SHARE_PATH}/edk2/x64/OVMF_CODE.secboot.4m.fd","${SHARE_PATH}/edk2/x64/OVMF_VARS.4m.fd" \
"${SHARE_PATH}/edk2/ovmf/OVMF_CODE_4M.secboot.qcow2","${SHARE_PATH}/edk2/ovmf/OVMF_VARS_4M.secboot.qcow2"
);;
*) # shellcheck disable=SC2054,SC2140
ovmfs=("${SHARE_PATH}/OVMF/OVMF_CODE_4M.fd","${SHARE_PATH}/OVMF/OVMF_VARS_4M.fd" \
"${SHARE_PATH}/edk2/ovmf/OVMF_CODE.fd","${SHARE_PATH}/edk2/ovmf/OVMF_VARS.fd" \
"${SHARE_PATH}/OVMF/OVMF_CODE.fd","${SHARE_PATH}/OVMF/OVMF_VARS.fd" \
"${SHARE_PATH}/OVMF/x64/OVMF_CODE.fd","${SHARE_PATH}/OVMF/x64/OVMF_VARS.fd" \
"${SHARE_PATH}/edk2-ovmf/OVMF_CODE.fd","${SHARE_PATH}/edk2-ovmf/OVMF_VARS.fd" \
"${SHARE_PATH}/qemu/ovmf-x86_64-4m-code.bin","${SHARE_PATH}/qemu/ovmf-x86_64-4m-vars.bin" \
"${SHARE_PATH}/qemu/edk2-x86_64-code.fd","${SHARE_PATH}/qemu/edk2-x86_64-code.fd" \
"${SHARE_PATH}/edk2-ovmf/x64/OVMF_CODE.fd","${SHARE_PATH}/edk2-ovmf/x64/OVMF_VARS.fd" \
"${SHARE_PATH}/edk2/x64/OVMF_CODE.4m.fd","${SHARE_PATH}/edk2/x64/OVMF_VARS.4m.fd" \
"${SHARE_PATH}/edk2/ovmf/OVMF_CODE_4M.qcow2","${SHARE_PATH}/edk2/ovmf/OVMF_VARS_4M.qcow2"
);;
esac
# Search for firmware if EFI_CODE is not set or the specified file doesn't exist
if [ -z "${EFI_CODE}" ] || [ ! -e "${EFI_CODE}" ]; then
if [ "${ARCH_VM}" == "aarch64" ]; then
# AAVMF firmware paths for ARM64 guests
# SecureBoot is not commonly supported on ARM64, use standard firmware
# shellcheck disable=SC2054,SC2140
ovmfs=("/usr/share/AAVMF/AAVMF_CODE.fd","/usr/share/AAVMF/AAVMF_VARS.fd" \
"${SHARE_PATH}/edk2/aarch64/QEMU_CODE.fd","${SHARE_PATH}/edk2/aarch64/QEMU_VARS.fd" \
"${SHARE_PATH}/edk2/aarch64/QEMU_EFI-pflash.raw","${SHARE_PATH}/edk2/aarch64/vars-template-pflash.raw" \
"${SHARE_PATH}/qemu/edk2-aarch64-code.fd","${SHARE_PATH}/qemu/edk2-arm-vars.fd" \
"${SHARE_PATH}/AAVMF/AAVMF_CODE.fd","${SHARE_PATH}/AAVMF/AAVMF_VARS.fd"
)
else
# x86_64 OVMF firmware paths
case ${secureboot} in
on) # shellcheck disable=SC2054,SC2140
ovmfs=("${SHARE_PATH}/OVMF/OVMF_CODE_4M.secboot.fd","${SHARE_PATH}/OVMF/OVMF_VARS_4M.ms.fd" \
"${SHARE_PATH}/edk2/ovmf/OVMF_CODE.secboot.fd","${SHARE_PATH}/edk2/ovmf/OVMF_VARS.secboot.fd" \
"${SHARE_PATH}/OVMF/x64/OVMF_CODE.secboot.fd","${SHARE_PATH}/OVMF/x64/OVMF_VARS.fd" \
"${SHARE_PATH}/edk2-ovmf/OVMF_CODE.secboot.fd","${SHARE_PATH}/edk2-ovmf/OVMF_VARS.fd" \
"${SHARE_PATH}/qemu/ovmf-x86_64-smm-ms-code.bin","${SHARE_PATH}/qemu/ovmf-x86_64-smm-ms-vars.bin" \
"${SHARE_PATH}/qemu/edk2-x86_64-secure-code.fd","${SHARE_PATH}/qemu/edk2-x86_64-code.fd" \
"${SHARE_PATH}/edk2-ovmf/x64/OVMF_CODE.secboot.fd","${SHARE_PATH}/edk2-ovmf/x64/OVMF_VARS.fd" \
"${SHARE_PATH}/edk2/x64/OVMF_CODE.secboot.4m.fd","${SHARE_PATH}/edk2/x64/OVMF_VARS.4m.fd" \
"${SHARE_PATH}/edk2/ovmf/OVMF_CODE_4M.secboot.qcow2","${SHARE_PATH}/edk2/ovmf/OVMF_VARS_4M.secboot.qcow2"
);;
*) # shellcheck disable=SC2054,SC2140
ovmfs=("${SHARE_PATH}/OVMF/OVMF_CODE_4M.fd","${SHARE_PATH}/OVMF/OVMF_VARS_4M.fd" \
"${SHARE_PATH}/edk2/ovmf/OVMF_CODE.fd","${SHARE_PATH}/edk2/ovmf/OVMF_VARS.fd" \
"${SHARE_PATH}/OVMF/OVMF_CODE.fd","${SHARE_PATH}/OVMF/OVMF_VARS.fd" \
"${SHARE_PATH}/OVMF/x64/OVMF_CODE.fd","${SHARE_PATH}/OVMF/x64/OVMF_VARS.fd" \
"${SHARE_PATH}/edk2-ovmf/OVMF_CODE.fd","${SHARE_PATH}/edk2-ovmf/OVMF_VARS.fd" \
"${SHARE_PATH}/qemu/ovmf-x86_64-4m-code.bin","${SHARE_PATH}/qemu/ovmf-x86_64-4m-vars.bin" \
"${SHARE_PATH}/qemu/edk2-x86_64-code.fd","${SHARE_PATH}/qemu/edk2-x86_64-code.fd" \
"${SHARE_PATH}/edk2-ovmf/x64/OVMF_CODE.fd","${SHARE_PATH}/edk2-ovmf/x64/OVMF_VARS.fd" \
"${SHARE_PATH}/edk2/x64/OVMF_CODE.4m.fd","${SHARE_PATH}/edk2/x64/OVMF_VARS.4m.fd" \
"${SHARE_PATH}/edk2/ovmf/OVMF_CODE_4M.qcow2","${SHARE_PATH}/edk2/ovmf/OVMF_VARS_4M.qcow2"
);;
esac
fi
# Attempt each EFI_CODE file one by one, selecting the corresponding code and vars
# when an existing file is found.
_IFS=$IFS
@ -1129,10 +1151,15 @@ function configure_display() {
*bsd) DISPLAY_DEVICE="VGA";;
linux_old|solaris) DISPLAY_DEVICE="vmware-svga";;
linux)
case ${display} in
none|spice|spice-app) DISPLAY_DEVICE="virtio-gpu";;
*) DISPLAY_DEVICE="virtio-vga";;
esac;;
# ARM64 does not have VGA hardware - use virtio-gpu-pci instead of virtio-vga
if [ "${ARCH_VM}" == "aarch64" ]; then
DISPLAY_DEVICE="virtio-gpu-pci"
else
case ${display} in
none|spice|spice-app) DISPLAY_DEVICE="virtio-gpu";;
*) DISPLAY_DEVICE="virtio-vga";;
esac
fi;;
macos)
# qxl-vga and VGA supports seamless mouse and sane resolutions if only
# one scanout is used. '-vga none' is added to the QEMU command line
@ -1185,6 +1212,11 @@ function configure_display() {
# Build the video configuration
VIDEO="-device ${DISPLAY_DEVICE}"
# ARM64 needs ramfb for UEFI boot display before virtio-gpu driver loads
if [ "${ARCH_VM}" == "aarch64" ]; then
VIDEO="-device ramfb ${VIDEO}"
fi
# Try and coerce the display resolution for Linux guests only.
if [ "${DISPLAY_DEVICE}" != "vmware-svga" ]; then
VIDEO="${VIDEO},xres=${X_RES},yres=${Y_RES}"
@ -1468,11 +1500,23 @@ function vm_boot() {
# shellcheck disable=SC2054,SC2206,SC2140
args+=(-name ${VMNAME},process=${VMNAME},debug-threads=on)
fi
# shellcheck disable=SC2054,SC2206,SC2140
args+=(-machine ${MACHINE_TYPE},smm=${SMM},vmport=off,accel=${QEMU_ACCEL} ${GUEST_TWEAKS}
${CPU} ${SMP}
-m ${RAM_VM} ${BALLOON}
-pidfile "${VMDIR}/${VMNAME}.pid")
# Build machine arguments - SMM and vmport are x86-only options
if [ "${ARCH_VM}" == "aarch64" ]; then
# ARM64 uses 'virt' machine type without x86-specific options
# shellcheck disable=SC2054,SC2206,SC2140
args+=(-machine ${MACHINE_TYPE},accel=${QEMU_ACCEL} ${GUEST_TWEAKS}
${CPU} ${SMP}
-m ${RAM_VM} ${BALLOON}
-pidfile "${VMDIR}/${VMNAME}.pid")
else
# x86_64 includes SMM (System Management Mode) and vmport options
# shellcheck disable=SC2054,SC2206,SC2140
args+=(-machine ${MACHINE_TYPE},smm=${SMM},vmport=off,accel=${QEMU_ACCEL} ${GUEST_TWEAKS}
${CPU} ${SMP}
-m ${RAM_VM} ${BALLOON}
-pidfile "${VMDIR}/${VMNAME}.pid")
fi
if [ "${guest_os}" == "windows" ] || [ "${guest_os}" == "windows-server" ] || [ "${guest_os}" == "reactos" ] || [ "${guest_os}" == "freedos" ]; then
# shellcheck disable=SC2054
@ -1646,10 +1690,20 @@ function vm_boot() {
QCOW2VARS=$(is_firmware_qcow2 "${EFI_VARS}")
if [ "${QCOW2CODE}" = "true" ]; then EFI_CODE_FORMAT="qcow2"; else EFI_CODE_FORMAT="raw"; fi
if [ "${QCOW2VARS}" = "true" ]; then EFI_VARS_FORMAT="qcow2"; else EFI_VARS_FORMAT="raw"; fi
# shellcheck disable=SC2054
args+=(-global driver=cfi.pflash01,property=secure,value=on
-drive if=pflash,format="${EFI_CODE_FORMAT}",unit=0,file="${EFI_CODE}",readonly=on
-drive if=pflash,format="${EFI_VARS_FORMAT}",unit=1,file="${EFI_VARS}")
if [ "${ARCH_VM}" == "aarch64" ]; then
# ARM64 uses blockdev with named nodes referenced by machine pflash parameters
# Do NOT use -global cfi.pflash01 secure property - that's x86 SMM-specific
# shellcheck disable=SC2054
args+=(-blockdev node-name=rom,driver=file,filename="${EFI_CODE}",read-only=true
-blockdev node-name=efivars,driver=file,filename="${EFI_VARS}")
else
# x86 uses traditional pflash drives with secure boot support
# shellcheck disable=SC2054
args+=(-global driver=cfi.pflash01,property=secure,value=on
-drive if=pflash,format="${EFI_CODE_FORMAT}",unit=0,file="${EFI_CODE}",readonly=on
-drive if=pflash,format="${EFI_VARS_FORMAT}",unit=1,file="${EFI_VARS}")
fi
fi
if [ -n "${iso}" ] && [ "${guest_os}" == "freedos" ]; then
@ -1678,14 +1732,35 @@ function vm_boot() {
args+=(-drive if=floppy,format=raw,file="${floppy}")
fi
if [ -n "${iso}" ]; then
# ARM64: create virtio-scsi controller if any CD-ROM ISOs are present
# (virt machine has no IDE controller)
if [ "${ARCH_VM}" == "aarch64" ] && { [ -n "${iso}" ] || [ -n "${fixed_iso}" ]; }; then
# shellcheck disable=SC2054
args+=(-drive media=cdrom,index=0,file="${iso}")
args+=(-device virtio-scsi-pci,id=scsi0)
fi
if [ -n "${iso}" ]; then
if [ "${ARCH_VM}" == "aarch64" ]; then
# ARM64: bootindex=1 ensures UEFI boots from CD-ROM first during installation
# shellcheck disable=SC2054
args+=(-device scsi-cd,drive=cd0,bus=scsi0.0,bootindex=1
-drive id=cd0,if=none,format=raw,media=cdrom,readonly=on,file="${iso}")
else
# shellcheck disable=SC2054
args+=(-drive media=cdrom,index=0,file="${iso}")
fi
fi
if [ -n "${fixed_iso}" ]; then
# shellcheck disable=SC2054
args+=(-drive media=cdrom,index=1,file="${fixed_iso}")
if [ "${ARCH_VM}" == "aarch64" ]; then
# ARM64: attach second ISO to virtio-scsi controller
# shellcheck disable=SC2054
args+=(-device scsi-cd,drive=cd1,bus=scsi0.0,bootindex=3
-drive id=cd1,if=none,format=raw,media=cdrom,readonly=on,file="${fixed_iso}")
else
# shellcheck disable=SC2054
args+=(-drive media=cdrom,index=1,file="${fixed_iso}")
fi
fi
if [ "${guest_os}" == "macos" ]; then
@ -1744,9 +1819,16 @@ function vm_boot() {
-drive id=SystemDisk,if=none,format=qcow2,file="${disk_img}" ${STATUS_QUO})
else
# shellcheck disable=SC2054,SC2206
args+=(-device virtio-blk-pci,drive=SystemDisk
-drive id=SystemDisk,if=none,format=${disk_format},file="${disk_img}" ${STATUS_QUO})
if [ "${ARCH_VM}" == "aarch64" ]; then
# ARM64: bootindex=2 ensures disk boots after CD-ROM (bootindex=1) during installation
# shellcheck disable=SC2054,SC2206
args+=(-device virtio-blk-pci,drive=SystemDisk,bootindex=2
-drive id=SystemDisk,if=none,format=${disk_format},file="${disk_img}" ${STATUS_QUO})
else
# shellcheck disable=SC2054,SC2206
args+=(-device virtio-blk-pci,drive=SystemDisk
-drive id=SystemDisk,if=none,format=${disk_format},file="${disk_img}" ${STATUS_QUO})
fi
fi
# https://wiki.qemu.org/Documentation/9psetup
@ -1765,9 +1847,16 @@ function vm_boot() {
if [ "${tpm}" == "on" ] && [ -S "${VMDIR}/${VMNAME}.swtpm-sock" ]; then
# shellcheck disable=SC2054
args+=(-chardev socket,id=chrtpm,path="${VMDIR}/${VMNAME}.swtpm-sock"
-tpmdev emulator,id=tpm0,chardev=chrtpm
-device tpm-tis,tpmdev=tpm0)
if [ "${ARCH_VM}" == "aarch64" ]; then
# ARM64 uses tpm-tis-device (system bus) instead of tpm-tis (ISA/LPC bus)
args+=(-chardev socket,id=chrtpm,path="${VMDIR}/${VMNAME}.swtpm-sock"
-tpmdev emulator,id=tpm0,chardev=chrtpm
-device tpm-tis-device,tpmdev=tpm0)
else
args+=(-chardev socket,id=chrtpm,path="${VMDIR}/${VMNAME}.swtpm-sock"
-tpmdev emulator,id=tpm0,chardev=chrtpm
-device tpm-tis,tpmdev=tpm0)
fi
fi
if [ "${monitor}" == "none" ]; then
@ -2224,8 +2313,9 @@ readonly LAUNCHER=$(basename "${0}")
readonly DISK_MIN_SIZE=$((197632 * 8))
readonly VERSION="4.9.9"
# TODO: Make this run the native architecture binary
ARCH_VM="x86_64"
# Default architecture is x86_64, can be overridden by config file (arch="aarch64")
arch="${arch:-x86_64}"
ARCH_VM="${arch}"
ARCH_HOST=$(uname -m)
QEMU=$(command -v qemu-system-${ARCH_VM})
QEMU_IMG=$(command -v qemu-img)
@ -2427,6 +2517,16 @@ if [ -n "${VM}" ] && [ -e "${VM}" ]; then
source "${VM}"
PUBLIC="${public_dir:-${PUBLIC}}"
# Re-detect architecture and QEMU binary after sourcing config
# Config file can set arch="aarch64" to override the default
ARCH_VM="${arch:-x86_64}"
QEMU=$(command -v qemu-system-${ARCH_VM})
if [ ! -x "${QEMU}" ]; then
echo "ERROR! qemu-system-${ARCH_VM} not found."
echo " Please install QEMU for ${ARCH_VM} architecture."
exit 1
fi
VMDIR=$(dirname "${disk_img}") # directory the VM disk and state files are stored
VMNAME=$(basename "${VM}" .conf) # name of the VM
VMPATH=$(realpath "$(dirname "${VM}")") # path to the top-level VM directory