3.3 · Inside the VM definition

Why a VM Needs Two Firmware Files: AAVMF and OVMF Explained

UEFI · libvirt · Virtualization · KVM

The previous post explained how UEFI firmware finds and starts a bootloader, and ended with a question: a physical computer keeps its firmware in a chip on the motherboard, so where does a virtual machine’s firmware come from?

The answer is two files, and the reason there are two explains a whole category of boot failure. It also explains the Secure Boot error that stops many people building their first ARM64 guest.

The firmware is a file

A VM’s firmware is supplied as a file. When the VM starts, the hypervisor loads that file into the guest’s memory, at the address where firmware would sit on real hardware, and the guest’s virtual processor starts running it, just as a physical processor would.

The firmware comes from edk2, an open-source project also known as TianoCore, which provides a complete implementation of UEFI. It is the real thing, with the same start-up phases, boot manager and built-in shell described in the previous post, not a simplified stand-in.

edk2 is built separately for each processor architecture, and the builds have different names:

NameStands forBuilt for
OVMFOpen Virtual Machine Firmwarex86-64 guests
AAVMFARM Architecture Virtual Machine FirmwareARM64 (aarch64) guests

These are the same software, compiled for different processors. If you know OVMF from x86 virtualization, you already know AAVMF.

On Debian and Raspberry Pi OS, AAVMF comes in the qemu-efi-aarch64 package and is installed into /usr/share/AAVMF/. The two files that matter most are AAVMF_CODE.fd and AAVMF_VARS.fd, and the package also includes variants with .ms in their names, which this post returns to at the end.

Why there are two files

Firmware on a physical computer has two parts, which happen to live in the same chip:

  • The program: the firmware code itself, which is the same on every computer of the same model and never changes.
  • The settings: the boot entries, the Secure Boot keys and other options, stored in a small writable area. These are different on every individual computer.

For virtual machines, edk2 deliberately splits these into two files:

FileHoldsShared or per VMChanges?
AAVMF_CODE.fdThe firmware programShared by every VM on the hostNever, it is read-only
AAVMF_VARS.fdThe firmware settings (its NVRAM)Each VM gets its own copyYes, the firmware writes to it

Splitting them is both efficient and safe. Every VM runs exactly the same firmware code, so one read-only copy can serve them all, and no VM can damage the code another one uses. But every VM needs its own settings, because each one has its own boot entries.

That means /usr/share/AAVMF/AAVMF_VARS.fd does not belong to any VM. It is a template: a clean, empty set of settings. When libvirt creates a VM, it copies the template to a private file for that VM:

   /usr/share/AAVMF/AAVMF_VARS.fd               the template: empty, shared, never written
              │
              │  copied once, when the VM is created
              ▼
   /var/lib/libvirt/qemu/nvram/lab1_VARS.fd     lab1's own settings, written by its firmware

The VM’s operating system installer writes its boot entry into that private copy, and Secure Boot keeps its state there too. In every way that matters, it is the VM’s equivalent of the settings chip on a motherboard.

How the two files appear in the XML

In a libvirt domain’s XML, the two files appear inside the <os> block. Here it is from lab1, as captured with virsh dumpxml lab1:

  <os>
    <type arch='aarch64' machine='virt-7.2'>hvm</type>
    <loader readonly='yes' type='pflash'>/usr/share/AAVMF/AAVMF_CODE.fd</loader>
    <nvram template='/usr/share/AAVMF/AAVMF_VARS.fd'>/var/lib/libvirt/qemu/nvram/lab1_VARS.fd</nvram>
    <boot dev='hd'/>
  </os>

Element by element:

  • <loader> is the firmware program. readonly='yes' says it is shared and never written. type='pflash' describes how the guest sees it: as flash memory, which is where UEFI expects to find itself.
  • <nvram> is the settings file, and it contains two paths. The template attribute names the clean original, and the text between the tags is this VM’s private copy. The copy described above is recorded right here in the XML.
  • <boot dev='hd'/> tells libvirt that the VM should boot from its hard disk.

If you create VMs with virt-install rather than writing XML, the same two paths are given with the --boot option, as described in post 2.4.

The failure this causes

The split between the two files leads to one specific failure, and it is very confusing the first time you see it, because the key fact goes against intuition:

A VM’s boot entry is stored in its settings file, not on its disk.

Intuition says that a disk with an operating system installed on it can be booted. But a UEFI firmware does not search disks for operating systems. It reads its list of boot entries and starts what the list names, and that list lives in the settings file.

So if a VM starts with an empty settings file, the firmware starts perfectly, finds an empty list, and has no idea that the disk can be booted, even though the bootloader is sitting on the disk’s EFI System Partition.

This happened to me. In post 3.9, I start lab1’s disk with a QEMU command written by hand, without libvirt, and the command needed a writable copy of the settings file. I made that copy from the template:

cp /usr/share/AAVMF/AAVMF_VARS.fd /tmp/lab1_VARS.fd

This was the result:

UEFI firmware (version  built at 13:10:49 on Nov  5 2024)

BdsDxe: failed to load Boot0001 "UEFI Misc Device" from PciRoot(0x0)/Pci(0x1,0x0): Not Found

>>Start PXE over IPv4.

The firmware loaded correctly. It reached the boot selection phase, looked for boot entry Boot0001, did not find the device it pointed to, and fell back to booting over the network, its last resort when nothing local works.

The cause was the copy. The template is empty by design, so my copy had no boot entry for Debian. Both files are called AAVMF_VARS.fd, and only their folders differ, which makes this an easy mistake.

The fix is to copy the VM’s own settings file instead, the one that the Debian installer wrote its boot entry into:

sudo cp /var/lib/libvirt/qemu/nvram/lab1_VARS.fd /tmp/lab1_VARS.fd

In my case, I did not apply this fix straight away. I left the firmware to carry on, and it eventually dropped into its built-in UEFI shell, where it is possible to boot the system by hand. Post 3.10 follows that path, and it turns out to show a lot about how booting works.

The general lesson is worth remembering: a UEFI boot entry is a saved shortcut, and it lives in the firmware’s settings. Lose the shortcut, and a perfectly good operating system cannot be booted until something recreates it.

The .ms variants and Secure Boot

The variants with .ms in their names are the second thing this split explains.

FileWhat it is
AAVMF_CODE.fdPlain UEFI firmware, which does not enforce Secure Boot
AAVMF_CODE.ms.fdFirmware that enforces Secure Boot
AAVMF_VARS.ms.fdThe matching settings template, with Microsoft’s Secure Boot keys already enrolled

Firmware set up this way refuses to run any bootloader that is not signed by a key it trusts. That is correct behaviour, and for a guest that boots through a Microsoft-signed first stage, it is what you want. But Debian’s ARM64 network installer is not signed that way, so this firmware refuses it with a Security Violation message.

The catch is that libvirt sometimes chooses the .ms variant automatically, so a VM can end up enforcing Secure Boot without anyone asking for it. There is also a trap when you try again: because Secure Boot’s state lives in the settings file, deleting the failed VM without its settings file leaves that state behind, and the next attempt fails in exactly the same way. That is why virsh undefine needs the --nvram option.

Post 2.3 works through this failure on a real install, including the fix: naming the plain firmware explicitly.

Depending on your version of the package, you may also see files with .snakeoil in their names. These are test builds, signed with a key that is deliberately public, for testing Secure Boot itself. They are not meant for normal use.

Summary

  • A VM has no motherboard, so its UEFI firmware is supplied as a file that the hypervisor loads.
  • The firmware comes from edk2 (TianoCore). OVMF is the x86-64 build and AAVMF is the ARM64 build of the same software.
  • It is split into two files: CODE, the program, which is read-only and shared by every VM, and VARS, the settings, which every VM has its own copy of.
  • /usr/share/AAVMF/AAVMF_VARS.fd is an empty template. libvirt copies it for each VM, usually into /var/lib/libvirt/qemu/nvram/.
  • In the domain XML, the two files appear as <loader> and <nvram>. The <nvram> element records both the template and the VM’s own copy.
  • A boot entry lives in the settings file, not on the disk. Start a VM with an empty settings file, and it cannot find its operating system, even though the operating system is installed.
  • The .ms variants enforce Secure Boot with Microsoft’s keys, and refuse unsigned bootloaders. Because that state lives in the settings file, removing a failed VM needs virsh undefine --nvram.

The next post goes back to lab1’s XML and reads all of it, block by block, to find out which lines were decisions and which were defaults.

Comments

get new posts

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

Subscribe →