In an ideal world, we would be able to boot any operating system from any firmware. Sadly, this is not the case with the predominant industry standards, hence we have to improvise solutions.
The Unified Extensible Firmware Interface (UEFI) specification is widely adopted by independent BIOS (IBV) and OS vendors. Mostly because it is the only specification out there, but broad adoption does not necessarily mean without drawbacks: Runtime services and UEFI device drivers are being kept alive in memory to allow the OS to interact with certain devices over UEFI protocols.
Outdated, poorly maintained or non-audited UEFI drivers may pose security risks. This is certainly the case with closed-source UEFI implementations, as the past has shown multiple times. Most recently, LogoFAIL.
The UEFI specification is criticized for being complex. It requires the system management mode (SMM), the Advanced Configuration and Power Interface (ACPI) for power management, UEFI runtime and drivers to persist in memory, even after the actual job of hardware initialization is complete. These components have more control over the hardware than the actual OS. We have to trust the UEFI implementation to not do any harm. Or, in case of Intel's reference implementation EDK2, we can review the code and decide ourselves, if we can trust it.
Further more UEFI describes interfaces only, which allows the provisioning of device drivers as blobs by original device manufacturers (ODMs) as long as they satisfy the interfaces. ODMs are not required to provide documentation about hardware functionality anymore, hence it hinders understanding of hardware functionality for the purpose of securing intellectual property. The utilization of blobs is still required not just by EDK2, but also coreboot, an alternative open-source multi-architecture firmware. Especially blobs for hardware initialization like Intel Firmware Support Packages (FSP), but this is also true for AMD hardware initialization. This interview with Ron Minnich really is worth a read here and provides further insight.
Now imagine the following use case: Operators want as much control over the platform with non-UEFI firmware, but still require the ability to boot an UEFI-compliant operating system.
Since we want to use non-UEFI firmware, the natural choice for hardware initialization is coreboot. As stated above, using EDK2 as payload for coreboot is out of the question. But what about [Linuxboot/u-root] solution stack? It provides a reliable, audited and vetted boot runtime using the Linux kernel. The initramfs is generated by u-root and is written in Golang. With this setup, we're unable to run UEFI bootloaders, because their requirements are not met by u-root.
What about starting a virtual machine (VM) from that setup? We have a Linux kernel, so technically we have access to its KVM capability, right? We just have to compile the kernel with its support. Do we have a program written in Golang, which can start a VM based on Linux KVM? Indeed, there is! It's called gokvm.
The concept boils down to the following:
- coreboot for hardware initialization
- Linuxboot/u-root as the boot runtime
- Linux kernel compiled with KVM support
- gokvm to execute EDK2 with Linux KVM.
Inside the virtual machine, the execution of EDK2 shall take place and any further bootloader and operating system shall be executed in it.
This has some advantages:
- no UEFI implementation in coreboot nor u-root required
- isolation of UEFI runtime services and drivers in VM
- hardware-agnostic EDK2 firmware
- executed on real hardware pure Open Source
but also a disadvantage:
We lose runtime efficiency, because the VM blocks some ressources.
Proof of concept
To prove the validity of the concept, we had to try to run a VM from firmware first. With a Supermicro X11SCH-F at hand and coreboot support in a "Work in progress" state, we built an image with u-root as payload and gokvm integrated. The only issue we faced, is the size of the Linux kernel.
The X11SCH-F uses a 32 MiB flash chip, but the Intel Firmware Descriptor (IFD) only allows for 10 MiB for the coreboot filesystem. Fiddling with Linux kernel config to reduce its size as much as possible was the step to go. Basically, we ended up with a kernel which supports Linux KVM for Intel-VT, only XFS filesystem and block device support for SATA drives.
The SATA drive partition was used to store a second kernel and initramfs, which is loaded into gokvm for execution.
State of gokvm
The main hurdle with gokvm was the fact that it didn't support the loading of firmware of any kind. We had to do some research to figure out how a solution to load firmware in a virtual machine could look like. [QEMU], the reference implementation for Linux KVM, implements flash chip devices and is more on the emulation side of things. Then we took a look into Cloud-Hypervisor, which focuses on para virtualization to keep resource consumption minimal. Instead of emulating a flash chip device, Cloud-Hypervisor implements the paravirtualised hardware ([PVH]) Boot Protocol. This mechanism origins from the XEN project and dictates a configuration for virtual CPUs and describes data structures to pass configuration data into the virtual machine for the protocol to process.
In addition, an adaption of EDK2/OVMF called CloudHV is maintained by the Cloud-Hypervisor project. CloudHV is already built with PVH Boot Protocol support. Furthermore, the support for VirtIO devices and EFI-Shell is given.
Implementing the PVH Boot Protocol in gokvm came with some hassle. The firmware file is an ELF-file with a XEN/PVH-specific
PT_NOTE field. It requires parsing and checking for validation. This capability isn't implemented in the Golang
std-library, so we had to implement the parsing specific for the PVH Boot Protocol in gokvm.
Structures required by the PVH Boot Protocol had to be implemented as well. Proper setup of these structures is dependent on the configuration of the VM that the user desires. So a new code path is implemented for ELF-files which have the XEN
First tests showed that the CloudHV image is loaded and executed, but several IO devices were missing. Investigating further revealed that a CMOS/RTC device on IO ports
0x71, serial output devices on
0x402 and the ACPI Power Management Timer on port
0x608 are missing in gokvm. In addition, the device ID for the PCI-bridge used in gokvm is unknown by EDK2.
After implementing the missing devices and renaming the PCI-bridge device ID in gokvm, it finally booted to EFI-Shell without changes to EDK2/CloudHv firmware. Great success! :)
Ok, now that we're able to run EDK2 until EFI-Shell in gokvm, but we have to integrate it into u-root.
A simple configuration of gokvm and loading the firmware image from a block device is implemented as a small demonstration. It only mounts XFS block devices which show the firmware file. This can be a hard or thumb drive. It loads the firmware image, configures the VM statically and executes.
VMBoot requires a different Linux Kernel configuration, hence some changes are done. The kernel needs to be built with Intel-VT and KVM support.
Stitching together the pieces
Now we build the Linux kernel, u-root initramfs and lastly coreboot to test the concept on hardware again. Does it run to EFI-Shell? Indeed, it does!
Limitations and Outlook
Ok, the proof of concept actually works. What now? There still is a lot of work to do! Booting into EFI-Shell is a huge step, but we're far from booting an operating system with EDK2 inside the virtual machine. Most VirtIO devices for proper device pass-through are missing as well as the generation of the required ACPI tables.
The Linux kernel used, lacks drivers for network, storage, filesystems and other capabilities. Activating these may increase kernel size, which means we have to be careful about activating kernel capabilities, for the kernel to fit into flash.
This project is funded through the NGI Assure Fund, a fund established by NLnet with financial support from the European Commission's Next Generation Internet program. Learn more at the NLnet project page.