EFI direct boot on Debian 10 (EFI stubs)

Background

Unified Extensible Firmware Interface (UEFI) is a specification that describes an interface between a motherboard firmware and an operating system (OS). It follows the Extensible Firmware Interface (EFI), formerly implemented by Intel. It slowly replaces the old Basic Input Output System (BIOS) originally provided on IBM PC compatible computers.

Since few years motherboard manufacturers are providing both UEFI and BIOS compatible firmwares. The Linux kernel provides the appropriate stubs to be booted directly by a compatible UEFI firmware. It removes the need of an intermediate boot loader like GRUB.

If you don’t need an intermediate boot loader, because you have only one boot option for instance, you can setup EFI stubs on your Debian 10 system. It will be booted directly by your UEFI. In the following lines I will describe how to setup this configuration.

Configuration

EFI Boot partition

The first step is to ensure your system is installed with a readable EFI boot partition. Usually it is a small partition of your disk, located at the beginning and formatted using a very simple file system like FAT or EXT2. This partition will be used to store the EFI compatible binaries : either your GRUB or Linux kernel binaries. During the boot phase, the UEFI will traverse the partition and try to find compatible binaries. Here is an example on my workstation:

# lsblk
NAME        MAJ:MIN RM   SIZE RO TYPE MOUNTPOINT
sda           8:0    0 119.2G  0 disk
├─sda1        8:1    0   953M  0 part /boot/efi
└─sda2        8:2    0 118.3G  0 part /

If you don’t have this kind of partition, you will have to create one, maybe while re-installing your system. But this is outside the scope of this article, you can get more information here.

Synchronizing images

To be able to boot directly, the UEFI will need to access your kernel image and its ramdisk. It is possible only if both these images are in the partition mentioned above. On Debian systems, these images are available at the root at the file system, we’ll need to setup something to copy them in the EFI partition, each time it is updated. So that after any regular system update, the kernel and ramdisk images will be updated too in the special partition.

In Debian wiki EFI Stubs, the method used relies on post install hooks available for kernel and ramdisk images. Unfortunately it doesn’t work anymore on Buster release because the post install hooks for initramfs (ramdisk) is not available anymore. Thus we’ll do this update using systemd service units and unit triggers based on file monitoring. The idea is to create a one shot service unit that will copy the file. But instead of launching it at boot (or so), we’ll trigger it using a file monitor. We just have to declare it and systemd will do the job.

Kernel

Here we go. We start by creating the service unit in /etc/systemd/system/uefi-kernel-update.service :

# cat /etc/systemd/system/uefi-kernel-update.service
[Unit]
Description=UEFI Kernel update
After=network.target

[Service]
Type=oneshot
ExecStart=/bin/cp /vmlinuz /boot/efi/EFI/debian/

Then create the file trigger /etc/systemd/system/uefi-kernel-update.path:

# cat /etc/systemd/system/uefi-kernel-update.path
[Path]
PathChanged=/vmlinuz

[Install]
WantedBy=multi-user.target

Finally enable the trigger:

# systemctl enable uefi-kernel-update.path
Created symlink /etc/systemd/system/multi-user.target.wants/uefi-kernel-update.path → /etc/systemd/system/uefi-kernel-update.path.
# systemctl start uefi-kernel-update.path

Finally, you can test it works correctly by running touch /vmlinuz and checking in systemd logs that the unit was triggered:

# touch /vmlinuz
# journalctl -xn
Dec 14 18:14:18 fixe-damien systemd[1]: Starting UEFI Kernel update…
 -- Subject: A start job for unit uefi-kernel-update.service has begun execution
 -- Defined-By: systemd
 -- Support: https://www.debian.org/support
 -- A start job for unit uefi-kernel-update.service has begun execution.
 -- The job identifier is 2611.
 Dec 14 18:14:18 fixe-damien systemd[1]: uefi-kernel-update.service: Succeeded.
 -- Subject: Unit succeeded
 -- Defined-By: systemd
 -- Support: https://www.debian.org/support
 -- The unit uefi-kernel-update.service has successfully entered the 'dead' state.
 Dec 14 18:14:18 fixe-damien systemd[1]: Started UEFI Kernel update.
 -- Subject: A start job for unit uefi-kernel-update.service has finished successfully
 -- Defined-By: systemd
 -- Support: https://www.debian.org/support

Ramdisk (initrd)

Repeat the operation by creating a service unit and a path trigger to update /initrd.img file. Unit files are provided below:

# cat /etc/systemd/system/uefi-initrd-update.service
[Unit]
Description=UEFI ignited update
After=network.target

[Service]
Type=oneshot
ExecStart=/bin/cp /initrd.img /boot/efi/EFI/debian/
# cat /etc/systemd/system/uefi-initrd-update.path
[Path]
PathChanged=/initrd.img

[Install]
WantedBy=multi-user.target

Finally enable the unit and the trigger.

# systemctl enable uefi-initrd-update.path
Created symlink /etc/systemd/system/multi-user.target.wants/uefi-initrd-update.path → /etc/systemd/system/uefi-initrd-update.path.
# systemctl start uefi-initrd-update.path

Don’t forget to test it works with touch and journalctl.

Adding an UEFI boot entry

Now that we’re sure the kernel and the ramdisk will be correctly deployed and updated, we have to add a boot entry to the UEFI to let him know we can boot the kernel directly.

First we have to gather the kernel command line from GRUB configuration. In the file /boot/grub/grub.cfg, find the menuentry ... { ... } block that matches the entry to use in GRUB to boot. Then read the block and find the linux /boot/vmlinuz... entry, it provides the kernel command line parameters. For me it looks like:

menuentry 'Debian GNU/Linux' ... {
  ...
  echo   'Loading Linux 4.19.0-6-amd64 ...'
  linux  /boot/vmlinuz-4.19.0-6-amd64 root=UUID=3c51b884-79b9-46f5-aff9-a2d8f68cd308 ro quiet
  echo   'Loading initial ramdisk ...'
  initrd /boot/initrd.img-4.19.0-6-amd64
}

The kernel command line parameters are root=UUID=3c51b884-79b9-46f5-aff9-a2d8f68cd308 ro quiet. Then we can use efibootmgr to add an entry to the UEFI, don’t forget to update the command with your kernel parameters:

# efibootmgr -c -g -L "Debian (EFI stubs)" -l '\EFI\debian\vmlinuz' -u "root=UUID=3c51b884-79b9-46f5-aff9-a2d8f68cd308 ro quiet rootfstype=ext4 add_efi_memmap initrd=\\EFI\\debian\\initrd.img"

If the command fails, you may need to add an option to help to find the EFI partition like: --disk /dev/nvme0n1.

Finally you can check it is added correctly:

# efibootmgr
 BootCurrent: 0008
 Timeout: 1 seconds
 BootOrder: 0008,0000,0009,0003,0001,0002,0004,0005
 Boot0000* debian
 Boot0001* Hard Drive
 Boot0002* UEFI:CD/DVD Drive
 Boot0003* CD/DVD Drive
 Boot0004* UEFI:Removable Device
 Boot0005* UEFI:Network Device
 Boot0008* Debian (EFI stubs)
 Boot0009* debian

The next step is: reboot! If everything is fine, your computer should restart and skip GRUB. In case the configuration is not correct you may not able to reboot correctly. If it happens, you can still chose the boot entry manually in you UEFI menu (early computer startup) and boot on the standard debian entry. Then you can troubleshoot the problem.

Conclusion

Obviously, this is mainly a technical achievement because going through GRUB at boot time is not so long or expensive. But It was important for me to write a complete procedure to implement this as I’m using this configuration every day. I hope it will help some of you.

By the way, there’s some possible improvements. For instance it would be good to regenerate the UEFI loader boot entry automatically when the kernel command line parameters are updated. Even if it is not updated so often it would ensure not to miss any configuration change.

Sources