2.3 · Getting a guest running

Fixing virt-install on arm64: Security Violation, dnsmasq Port 53, and the qemu:///session Trap

KVM · libvirt · QEMU · Raspberry Pi

The previous article installed the stack; this one covers what breaks when you first use it.

The virt-install command in most ARM64 tutorials is correct. It still fails, for four reasons that have nothing to do with the command itself and everything to do with defaults — libvirt’s, Debian’s, and your shell’s.

Each of these produces an error message that points somewhere other than the cause, which is why they eat hours. Taken in order, they’re all quick to fix.

Pre-flight: check before you launch

Four checks that take thirty seconds and save considerably more:

ls -l /dev/kvm                 # acceleration available
virsh version                  # libvirt stack responding
ls /usr/share/AAVMF/           # ARM64 UEFI firmware present
virsh net-list --all           # is there a network to attach to?

The fourth one is where trouble usually starts.

Problem 1: virsh is talking to the wrong libvirt

$ virsh net-list --all
 Name   State   Autostart   Persistent
----------------------------------------

Empty. But the previous article said installing libvirt-daemon-system defines a default NAT network, and it does. So where is it?

virsh connects to a libvirt instance identified by a URI, and there are two on every system:

   qemu:///system   → system-wide VMs, managed by libvirtd running as root
                      the 'default' network lives here
                      guest disks in /var/lib/libvirt/images
                      this is what you want

   qemu:///session  → a per-user libvirt instance
                      no system networks, no shared storage pools
                      useful for unprivileged desktop VMs

They are entirely separate worlds. A VM defined in one is invisible from the other. So is a network.

Check which one you’re on:

virsh uri

If that reports qemu:///session, every command so far has queried a private per-user instance that legitimately has no networks and no VMs. Nothing was broken — you were looking in the wrong place.

The fix, permanently:

echo "export LIBVIRT_DEFAULT_URI=qemu:///system" >> ~/.bashrc
source ~/.bashrc

Or explicitly per-command with -c qemu:///system.

This matters more than it first appears, because the URI determines where a VM lives for its entire life. Build a guest under session and it won’t appear in virsh list --all under system, won’t have access to the default network, and will store its disk somewhere else. The mismatch surfaces later as “my VM disappeared,” which is a considerably more confusing symptom than an empty table.

This is why the virt-install command in this series carries --connect qemu:///system explicitly. Even with the environment variable set, being explicit in the one command that determines a VM’s permanent home is worth the extra line.

Problem 2: the default network won’t start

Now querying the right instance:

$ sudo virsh -c qemu:///system net-list --all
 Name      State      Autostart   Persistent
----------------------------------------------
 default   inactive   no          yes

The network exists but is inactive. Start it:

$ sudo virsh -c qemu:///system net-start default
error: Failed to start network default
error: internal error: Child process (dnsmasq ...) unexpected exit status 2:
dnsmasq: failed to create listening socket for 192.168.122.1: Address already in use

Port 53 is taken. The cause is the dnsmasq package from the previous article.

Installing dnsmasq as a standalone package installs it and starts it as a system service, bound to port 53 on all interfaces. libvirt runs its own dnsmasq instance scoped to virbr0, and it cannot bind a port the system service has already claimed.

The fix — libvirt doesn’t need the standalone service:

sudo systemctl stop dnsmasq
sudo systemctl disable dnsmasq
sudo virsh -c qemu:///system net-start default
sudo virsh -c qemu:///system net-autostart default

That last line makes the network start automatically at boot, so this is a one-time fix rather than something to repeat after every reboot.

Verify:

$ ip addr show virbr0
3: virbr0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 ...
    inet 192.168.122.1/24 brd 192.168.122.255 scope global virbr0

virbr0 now holds 192.168.122.1, the gateway address guests will use. The NO-CARRIER state is normal and not a fault — a bridge with no attached interfaces reports no carrier. It comes up when a VM connects.

The underlying lesson generalizes: libvirt manages its own dnsmasq. The standalone service only collides with it. Installing dnsmasq-base instead provides the binary without the service, which avoids this entirely.

Problem 3: os-variant not found

$ virt-install --os-variant debian12 ...
ERROR    Unknown OS name 'debian12'. See `--osinfo list` for valid values.

--os-variant tells libvirt which OS is being installed so it can choose sensible device defaults. The valid names come from osinfo-db, a database packaged separately and frequently out of date relative to current releases.

Check what yours knows:

virt-install --osinfo list | grep debian

On Bookworm this typically stops at debian11, predating Debian 12 entirely.

The fix: use the newest value your database recognizes.

--os-variant debian11

This is safe, and understanding why is the point. --os-variant only supplies hints — which disk bus, which NIC model, which defaults suit that OS family. The actual operating system being installed comes from --location. Declaring debian11 while installing Debian 12 gets you Debian 12 with device defaults tuned for Debian 11, and since both use the same virtio drivers, nothing differs.

--os-variant generic is the always-available fallback. It works; it just picks conservative defaults.

You can update the database with sudo osinfo-db-import --local against the latest upstream release, but for a lab it isn’t worth the step.

Problem 4: Security Violation

This is the one that stops people, because the error appears at the firmware level before any recognizable software has run.

The command launches, virt-install retrieves the installer kernel and initrd successfully, and then:

   BdsDxe: failed to load Boot0001 "UEFI Misc Device" ... Security Violation
   ...
   No bootable option or device was found.

The download worked, so networking is fine. The disk was created. And yet the firmware refuses to boot what it just fetched.

Inspect what libvirt actually configured:

$ sudo virsh -c qemu:///system dumpxml lab1 | grep -A2 loader
    <loader readonly='yes' type='pflash'>/usr/share/AAVMF/AAVMF_CODE.ms.fd</loader>
    <nvram template='/usr/share/AAVMF/AAVMF_VARS.ms.fd'>...</nvram>

There it is: AAVMF_CODE.ms.fd. The .ms suffix means Microsoft-keyed — firmware built with Microsoft’s Secure Boot certificates enrolled, which enforces Secure Boot and refuses to execute anything not signed by a trusted key.

Debian’s arm64 netboot installer isn’t signed with those keys. The firmware does exactly what Secure Boot firmware is supposed to do: it refuses, and reports a security violation.

The qemu-efi-aarch64 package installs several firmware variants, and libvirt’s automatic selection picked the enforcing one.

   AAVMF_CODE.fd      → plain UEFI, no Secure Boot enforcement
   AAVMF_CODE.ms.fd   → Microsoft-keyed, enforces Secure Boot

The fix has two parts, and the first is easy to miss.

First, tear down the failed VM properly:

sudo virsh -c qemu:///system destroy lab1
sudo virsh -c qemu:///system undefine lab1 --nvram

--nvram is essential. UEFI firmware has two components: read-only code, and a writable variables store holding boot entries and Secure Boot state. That variables file persists independently of the VM definition. Undefine without --nvram and the tainted Secure Boot state survives to contaminate the next attempt — producing the identical error and the strong impression that the fix didn’t work.

Second, relaunch with the non-.ms firmware forced explicitly:

--boot loader=/usr/share/AAVMF/AAVMF_CODE.fd,loader.readonly=yes,loader.type=pflash,nvram.template=/usr/share/AAVMF/AAVMF_VARS.fd

Reading that line as four separate statements:

  • loader= — the firmware code, the plain non-Secure-Boot variant
  • loader.readonly=yes — firmware code is shared read-only across VMs
  • loader.type=pflash — loaded as flash memory, which is how UEFI expects to be mapped
  • nvram.template= — the template from which this VM’s private writable variables file is created

That single flag is what turns a Security Violation into a booting installer.

Worth noting that this is an ARM64-specific fight. On x86, libvirt’s OVMF selection is generally sensible and this flag is rarely needed. On ARM64 with an unsigned installer, it’s the line that matters.

Bonus: the GRUB rescue prompt

One more that isn’t a configuration error but looks like disaster.

The Debian install completes, the VM reboots, and instead of a login prompt:

   grub>

An interactive GRUB command prompt. GRUB installed correctly and the firmware found it — it simply didn’t proceed to boot the installed system, typically because the reboot happened directly out of the installer with residual state.

The fix is usually just a clean restart:

sudo virsh -c qemu:///system destroy lab1
sudo virsh -c qemu:///system start lab1
sudo virsh -c qemu:///system console lab1

That normally lands at debian login:. If grub> recurs across clean boots, the bootloader genuinely didn’t install to a location the firmware searches, and reinstalling with the installer’s rescue mode is the path — but try the restart first.

The checklist

For a fresh ARM64 host:

  1. Check virsh uri. Use qemu:///system, and set LIBVIRT_DEFAULT_URI to make it permanent.
  2. Confirm the default network is active. If port 53 collides, disable the standalone dnsmasq service and net-autostart default.
  3. Pick a valid --os-variant from virt-install --osinfo list. Older names are fine; they only supply hints.
  4. Force the non-.ms AAVMF firmware via --boot to avoid the Secure Boot rejection.
  5. When tearing down a failed attempt, always pass --nvram.
  6. Ctrl + ] detaches from a serial console without stopping the VM.

Three of these — the session/system split, the dnsmasq collision, and the .ms firmware — are absent from most documentation and produce error messages that point away from their causes. They’re the ones worth remembering.

With them resolved, the virt-install command works, and it’s worth understanding rather than copying. Every flag maps to a concept from earlier in this series, and the next article takes the command apart field by field.

Summary

  • qemu:///session vs qemu:///system: separate libvirt instances with separate VMs and networks. Check virsh uri; set LIBVIRT_DEFAULT_URI=qemu:///system.
  • Port 53 collision: the standalone dnsmasq service blocks libvirt’s own instance. Stop and disable it, then net-start and net-autostart the default network.
  • Unknown --os-variant: osinfo-db lags current releases. Use the newest name it knows, or generic. The flag only supplies device hints.
  • Security Violation: libvirt auto-selected Microsoft-keyed AAVMF_CODE.ms.fd, which rejects Debian’s unsigned installer. Force plain AAVMF_CODE.fd via --boot.
  • undefine without --nvram leaves tainted Secure Boot state that reproduces the same failure on the next attempt.
  • A grub> prompt after install usually clears with a clean destroy and start.

Comments

get new posts

About one email a week, and only when there is something new.

Subscribe →