2.2 · Getting a guest running
Installing the KVM Stack on Raspberry Pi OS: What Each Package Does
The previous article confirmed the kernel side is sound; this one installs the userspace stack on top of it.
Most guides give you an apt install line with nine package names and no explanation. That works, but it leaves you unable to diagnose anything later, because you don’t know which package provides which binary.
Since the previous six articles established what each layer of the stack does, the packages can be mapped onto that structure instead of memorized. Then we run libvirt’s own readiness checker, which produces a wall of warnings and failures on a perfectly healthy Raspberry Pi — and reading that output correctly is a genuinely useful skill.
The install command
sudo apt update
sudo apt install qemu-system-arm qemu-utils qemu-efi-aarch64 \
libvirt-daemon-system libvirt-clients virtinst virt-manager \
bridge-utils dnsmasq
Read the breakdown before running it. One of these packages causes a problem later that’s much easier to understand if you know it’s there.
The QEMU layer: the VM process itself
qemu-system-arm provides /usr/bin/qemu-system-aarch64 — the full-system emulator. This is the binary that becomes each VM. Everything from article 4 and article 5 lives inside it: the device models, the vCPU threads, the KVM_RUN loop, the QMP control socket. One running copy is one virtual machine.
The package name is a persistent annoyance. It says -arm, but it contains the 64-bit qemu-system-aarch64 binary. Don’t go looking for a separate qemu-system-aarch64 package; there isn’t one.
qemu-utils provides qemu-img, the disk image tool. It creates, converts, resizes, and snapshots image files, and it runs on the host without any VM involved. virt-install calls it under the hood to create guest disks.
qemu-efi-aarch64 is the most ARM64-specific package here. As article 2 noted in passing and the firmware discussion will make concrete: ARM64 has no legacy BIOS. Guests boot through UEFI firmware, and this package puts that firmware on disk as AAVMF — the ARM equivalent of x86’s OVMF.
This package matters more than its size suggests. It installs multiple firmware variants, and libvirt’s automatic choice between them causes the most confusing failure in this entire series. That’s article 9.
The libvirt layer: the management brain
libvirt-daemon-system installs libvirtd and registers it as a system service. This is the management brain from article 3: it owns each VM’s domain XML, translates that XML into a QEMU command line, launches the process, and tracks it.
Installing it also creates the libvirt and kvm groups, defines the default NAT network, and sets up the default storage pool at /var/lib/libvirt/images.
Note that newer libvirt versions split the monolithic daemon into modular ones — virtqemud, virtnetworkd, virtstoraged. Same responsibilities, different packaging. Debian Bookworm still uses the traditional libvirtd, which is what the commands below assume.
libvirt-clients provides the tools you actually type:
virsh— the primary CLI, and where this series lives from here onvirt-host-validate— the readiness checker used belowvirt-xml-validate— checks a domain XML file is well-formed
Everything virsh does, it sends to libvirtd over the libvirt API. It never talks to QEMU directly.
The front-ends
virtinst provides virt-install, which creates a new VM from a set of flags — writing the XML and launching the installer in one command. It also provides virt-clone and virt-xml. This is the tool that builds the first guest in article 10.
virt-manager is the GTK desktop GUI. Useful for seeing a VM’s hardware laid out visually, though everything it does is available through virsh. On a headless Pi it’s arguably optional, but it’s small and occasionally handy over X forwarding.
The networking pieces
bridge-utils provides brctl for managing Linux bridges — virtual switches in the kernel. Largely superseded by ip link in modern usage, but still commonly referenced and useful when bridged networking arrives in a later phase.
dnsmasq is a lightweight DHCP and DNS server. libvirt runs its own instance behind the default NAT network, handing IP addresses to guests on virbr0.
This is the package that causes a problem. Installing it as a standalone package also installs and starts it as a system service, bound to port 53 on all interfaces. libvirt’s own scoped instance then can’t bind the port it needs, and the default network fails to start with Address already in use. Full diagnosis and fix in article 9 — but if you’d rather avoid the collision entirely, install dnsmasq-base instead of dnsmasq. That provides the binary libvirt needs without the system service.
The whole stack in one picture
┌──────────────────────────────────────────────────────────────┐
│ USER SPACE │
│ │
│ FRONT-ENDS virtinst ──► virt-install │
│ (you type these) virt-manager ──► virt-manager (GUI) │
│ libvirt-clients ──► virsh │
│ │ │
│ ▼ (libvirt API) │
│ MANAGEMENT BRAIN libvirt-daemon-system ──► libvirtd │
│ │ + dnsmasq │
│ ▼ (builds & launches) │
│ THE VM PROCESS qemu-system-arm ──► qemu-system-aarch64 │
│ + DISK TOOLS qemu-utils ──► qemu-img │
│ + GUEST FIRMWARE qemu-efi-aarch64 ─► AAVMF (UEFI) │
│ │ │
│ │ opens /dev/kvm (ioctl) │
├────────┼──────────────────────────────────────────────────────┤
│ KERNEL SPACE ▼ │
│ KVM (built-in on the Pi) bridge / netfilter │
│ (bridge-utils manages) │
├───────────────────────────────────────────────────────────────┤
│ HARDWARE Cortex-A76 ×4 (EL2/VHE) · 8 GB · eth0 · NVMe/SD │
└───────────────────────────────────────────────────────────────┘
To inspect any of this yourself:
dpkg -L qemu-system-arm # every file the package installed
dpkg -S /usr/bin/qemu-img # which package owns a given binary
virt-host-validate: the readiness check
With the stack installed, libvirt can assess the host itself:
$ virt-host-validate
QEMU: Checking if device /dev/kvm exists : PASS
QEMU: Checking if device /dev/kvm is accessible : PASS
QEMU: Checking if device /dev/vhost-net exists : PASS
QEMU: Checking if device /dev/net/tun exists : PASS
QEMU: Checking for cgroup 'cpu' controller support : PASS
QEMU: Checking for cgroup 'cpuacct' controller support : PASS
QEMU: Checking for cgroup 'cpuset' controller support : PASS
QEMU: Checking for cgroup 'memory' controller support : WARN (Enable 'memory' in kernel Kconfig file or mount/enable cgroup controller in your system)
QEMU: Checking for cgroup 'devices' controller support : WARN (Enable 'devices' in kernel Kconfig file or mount/enable cgroup controller in your system)
QEMU: Checking for cgroup 'blkio' controller support : PASS
QEMU: Checking for device assignment IOMMU support : WARN (Unknown if this platform has IOMMU support)
QEMU: Checking for secure guest support : WARN (Unknown if this platform has Secure Guest support)
LXC: Checking for Linux >= 2.6.26 : PASS
LXC: Checking for namespace ipc : PASS
LXC: Checking for namespace mnt : PASS
LXC: Checking for namespace pid : PASS
LXC: Checking for namespace uts : PASS
LXC: Checking for namespace net : PASS
LXC: Checking for namespace user : PASS
LXC: Checking for cgroup 'cpu' controller support : PASS
LXC: Checking for cgroup 'cpuacct' controller support : PASS
LXC: Checking for cgroup 'cpuset' controller support : PASS
LXC: Checking for cgroup 'memory' controller support : FAIL (Enable 'memory' in kernel Kconfig file or mount/enable cgroup controller in your system)
LXC: Checking for cgroup 'devices' controller support : FAIL (Enable 'devices' in kernel Kconfig file or mount/enable cgroup controller in your system)
LXC: Checking for cgroup 'freezer' controller support : FAIL (Enable 'freezer' in kernel Kconfig file or mount/enable cgroup controller in your system)
LXC: Checking for cgroup 'blkio' controller support : PASS
LXC: Checking if device /sys/fs/fuse/connections exists : PASS
Four warnings and three failures. This host is fine. Here’s how to know that.
The four checks that decide everything
/dev/kvm exists : PASS ← KVM is present
/dev/kvm accessible : PASS ← your user can use it
/dev/vhost-net exists : PASS ← the network fast path is available
/dev/net/tun exists : PASS ← TAP devices work
These four determine whether accelerated VMs are possible. All pass.
Two are worth pausing on. /dev/kvm is accessible confirms group and ACL permissions work for your user — that VMs won’t need sudo. And /dev/vhost-net exists is the kernel-side network fast path from article 6, confirmed present before a single VM has been built.
Triaging the warnings
A WARN means “be aware,” not “broken.”
The cgroup warnings (memory, devices) — expected on Raspberry Pi. cgroups are the kernel mechanism for limiting and accounting resources per process group. libvirt uses them to enforce per-VM limits. Raspberry Pi OS disables the memory cgroup controller by default because it carries a small performance cost on every allocation.
The practical effect: VMs run normally, but libvirt cannot enforce a hard memory ceiling through cgroups. A VM’s XML says how much RAM it gets and QEMU allocates that much. For learning and lab use this changes nothing. Enabling it means adding cgroup_enable=memory cgroup_memory=1 to /boot/firmware/cmdline.txt and rebooting — worth doing if you’re running many VMs under memory pressure, unnecessary otherwise.
The IOMMU warning — expected, and it’s a boundary rather than a fault. An IOMMU is the hardware that makes device passthrough safe, by constraining which memory a device can reach via DMA. Without one, handing a real device to a guest means the guest could direct that device to write anywhere in host memory.
The Pi can’t confirm usable IOMMU support, which is exactly why VFIO passthrough is studied conceptually on this hardware rather than performed. This warning isn’t something to fix; it’s a genuine hardware limitation showing up where it should.
The secure guest warning — irrelevant here. “Secure guests” means hardware memory encryption that protects a VM’s RAM from the host itself — AMD SEV, Intel TDX, IBM Secure Execution. Enterprise features, not present on the Pi, not needed for anything in this series.
The LXC failures are not your problem
This is the part that looks alarming:
LXC: cgroup 'memory' : FAIL
LXC: cgroup 'devices' : FAIL
LXC: cgroup 'freezer' : FAIL
virt-host-validate checks two unrelated technologies. libvirt can manage QEMU/KVM virtual machines and LXC containers, so the tool validates readiness for both. Every line prefixed LXC: is about container support.
You are not using LXC. Mentally delete that entire block.
The severity difference is itself informative. The same missing cgroup controllers are a WARN for QEMU and a FAIL for LXC, because containers depend on cgroups for isolation — without them, containers can’t do their job. Full VMs get isolation from the hypervisor instead, so the same absence is merely a limitation.
The verdict
What matters (QEMU/KVM):
4 core acceleration checks ✓ PASS ← ready
cgroup memory/devices ⚠ WARN ← Pi default, harmless
IOMMU ⚠ WARN ← passthrough boundary, expected
secure guest ⚠ WARN ← irrelevant
What to ignore entirely (LXC):
every LXC line ← not using containers
Nothing here needs fixing.
Two housekeeping steps
Confirm the daemon is running:
systemctl status libvirtd
Look for Active: active (running) and Loaded: ... enabled, the latter meaning it starts at boot. If it isn’t running: sudo systemctl enable --now libvirtd.
Confirm your group membership:
groups
You need libvirt and kvm. If either is missing:
sudo usermod -aG libvirt,kvm $USER
The -a is critical. usermod -G without it replaces your entire group list rather than appending to it, which is a genuinely destructive mistake and a classic one.
Group changes take effect on a fresh login, so log out and back in — or reboot, which is simpler on a Pi.
The final check
$ virsh list --all
Id Name State
--------------------
An empty table, and that’s the desired result. It proves three things at once: libvirtd is running and answering API calls, your user can reach it over the group-owned socket without sudo, and virsh is connected to a working libvirt instance.
An error here — particularly failed to connect to the socket — means group membership hasn’t taken effect and you need to log out and back in.
One caveat that becomes important immediately. virsh connects to a URI, and which URI it picks by default is not always the one you want. The empty table above doesn’t tell you which libvirt instance answered. That ambiguity is the first of four problems in the next article, and it’s the one that wastes the most time when it bites.
Summary
- Nine packages across three layers: QEMU (the VM process), libvirt (management), and networking support.
qemu-system-armcontains the 64-bitqemu-system-aarch64binary despite its name.qemu-efi-aarch64provides AAVMF UEFI firmware, which ARM64 guests need because there’s no legacy BIOS.- Installing
dnsmasqrather thandnsmasq-basestarts a system service that collides with libvirt’s own instance on port 53. virt-host-validatechecks both QEMU/KVM and LXC. Only the QEMU lines matter unless you use containers.- The four
/dev/*checks decide whether acceleration works. Everything else is advisory. - cgroup warnings are a Raspberry Pi OS default and harmless. The IOMMU warning reflects a real hardware limitation around passthrough.
- Use
usermod -aG— omitting-areplaces your groups instead of appending. - An empty
virsh list --allproves the daemon, permissions, and client connection all work.
Comments