diff --git a/nixos/modules/virtualisation/qemu-vm.nix b/nixos/modules/virtualisation/qemu-vm.nix index 5a502c36180..a927766eabb 100644 --- a/nixos/modules/virtualisation/qemu-vm.nix +++ b/nixos/modules/virtualisation/qemu-vm.nix @@ -81,6 +81,7 @@ let drivesCmdLine = drives: concatStringsSep " " (imap1 driveCmdline drives); + # Creates a device name from a 1-based a numerical index, e.g. # * `driveDeviceName 1` -> `/dev/vda` # * `driveDeviceName 2` -> `/dev/vdb` @@ -99,6 +100,13 @@ let addDeviceNames = imap1 (idx: drive: drive // { device = driveDeviceName idx; }); + efiPrefix = + if (pkgs.stdenv.isi686 || pkgs.stdenv.isx86_64) then "${pkgs.OVMF.fd}/FV/OVMF" + else if pkgs.stdenv.isAarch64 then "${pkgs.OVMF.fd}/FV/AAVMF" + else throw "No EFI firmware available for platform"; + efiFirmware = "${efiPrefix}_CODE.fd"; + efiVarsDefault = "${efiPrefix}_VARS.fd"; + # Shell script to start the VM. startVM = '' @@ -124,10 +132,14 @@ let # A writable boot disk can be booted from automatically. ${qemu}/bin/qemu-img create -f qcow2 -b ${bootDisk}/disk.img $TMPDIR/disk.img || exit 1 + NIX_EFI_VARS=$(readlink -f ''${NIX_EFI_VARS:-${cfg.efiVars}}) + ${if cfg.useEFIBoot then '' - # VM needs a writable flash BIOS. - cp ${bootDisk}/bios.bin $TMPDIR || exit 1 - chmod 0644 $TMPDIR/bios.bin || exit 1 + # VM needs writable EFI vars + if ! test -e "$NIX_EFI_VARS"; then + cp ${bootDisk}/efi-vars.fd "$NIX_EFI_VARS" || exit 1 + chmod 0644 "$NIX_EFI_VARS" || exit 1 + fi '' else '' ''} '' else '' @@ -172,21 +184,22 @@ let '' mkdir $out diskImage=$out/disk.img - bootFlash=$out/bios.bin - ${qemu}/bin/qemu-img create -f qcow2 $diskImage "40M" + ${qemu}/bin/qemu-img create -f qcow2 $diskImage "60M" ${if cfg.useEFIBoot then '' - cp ${pkgs.OVMF-CSM.fd}/FV/OVMF.fd $bootFlash - chmod 0644 $bootFlash + efiVars=$out/efi-vars.fd + cp ${efiVarsDefault} $efiVars + chmod 0644 $efiVars '' else '' ''} ''; buildInputs = [ pkgs.utillinux ]; - QEMU_OPTS = if cfg.useEFIBoot - then "-pflash $out/bios.bin -nographic -serial pty" - else "-nographic -serial pty"; + QEMU_OPTS = "-nographic -serial stdio -monitor none" + + lib.optionalString cfg.useEFIBoot ( + " -drive if=pflash,format=raw,unit=0,readonly=on,file=${efiFirmware}" + + " -drive if=pflash,format=raw,unit=1,file=$efiVars"); } '' - # Create a /boot EFI partition with 40M and arbitrary but fixed GUIDs for reproducibility + # Create a /boot EFI partition with 60M and arbitrary but fixed GUIDs for reproducibility ${pkgs.gptfdisk}/bin/sgdisk \ --set-alignment=1 --new=1:34:2047 --change-name=1:BIOSBootPartition --typecode=1:ef02 \ --set-alignment=512 --largest-new=2 --change-name=2:EFISystem --typecode=2:ef00 \ @@ -210,6 +223,10 @@ let mkdir /boot mount /dev/vda2 /boot + ${optionalString config.boot.loader.efi.canTouchEfiVariables '' + mount -t efivarfs efivarfs /sys/firmware/efi/efivars + ''} + # This is needed for GRUB 0.97, which doesn't know about virtio devices. mkdir /boot/grub echo '(hd0) /dev/vda' > /boot/grub/device.map @@ -467,6 +484,16 @@ in ''; }; + virtualisation.efiVars = + mkOption { + default = "./${vmName}-efi-vars.fd"; + description = + '' + Path to nvram image containing UEFI variables. The will be created + on startup if it does not exist. + ''; + }; + virtualisation.bios = mkOption { default = null; @@ -556,7 +583,8 @@ in ''-append "$(cat ${config.system.build.toplevel}/kernel-params) init=${config.system.build.toplevel}/init regInfo=${regInfo}/registration ${consoles} $QEMU_KERNEL_PARAMS"'' ]) (mkIf cfg.useEFIBoot [ - "-pflash $TMPDIR/bios.bin" + "-drive if=pflash,format=raw,unit=0,readonly,file=${efiFirmware}" + "-drive if=pflash,format=raw,unit=1,file=$NIX_EFI_VARS" ]) (mkIf (cfg.bios != null) [ "-bios ${cfg.bios}/bios.bin" diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index e13a5ee57f6..4eb6849cfc6 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -323,7 +323,7 @@ in systemd = handleTest ./systemd.nix {}; systemd-analyze = handleTest ./systemd-analyze.nix {}; systemd-binfmt = handleTestOn ["x86_64-linux"] ./systemd-binfmt.nix {}; - systemd-boot = handleTestOn ["x86_64-linux"] ./systemd-boot.nix {}; + systemd-boot = handleTest ./systemd-boot.nix {}; systemd-confinement = handleTest ./systemd-confinement.nix {}; systemd-timesyncd = handleTest ./systemd-timesyncd.nix {}; systemd-networkd-vrf = handleTest ./systemd-networkd-vrf.nix {}; diff --git a/nixos/tests/systemd-boot.nix b/nixos/tests/systemd-boot.nix index eba4729d6de..7a663dd9b42 100644 --- a/nixos/tests/systemd-boot.nix +++ b/nixos/tests/systemd-boot.nix @@ -11,6 +11,8 @@ let virtualisation.useBootLoader = true; virtualisation.useEFIBoot = true; boot.loader.systemd-boot.enable = true; + boot.loader.efi.canTouchEfiVariables = true; + environment.systemPackages = [ pkgs.efibootmgr ]; }; in { @@ -31,6 +33,36 @@ in machine.succeed( "test -e /sys/firmware/efi/efivars/LoaderEntrySelected-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f" ) + + # "bootctl install" should have created an EFI entry + machine.succeed('efibootmgr | grep "Linux Boot Manager"') + ''; + }; + + # Boot without having created an EFI entry--instead using default "/EFI/BOOT/BOOTX64.EFI" + fallback = makeTest { + name = "systemd-boot-fallback"; + meta.maintainers = with pkgs.stdenv.lib.maintainers; [ danielfullmer ]; + + machine = { pkgs, lib, ... }: { + imports = [ common ]; + boot.loader.efi.canTouchEfiVariables = mkForce false; + }; + + testScript = '' + machine.start() + machine.wait_for_unit("multi-user.target") + + machine.succeed("test -e /boot/loader/entries/nixos-generation-1.conf") + + # Ensure we actually booted using systemd-boot + # Magic number is the vendor UUID used by systemd-boot. + machine.succeed( + "test -e /sys/firmware/efi/efivars/LoaderEntrySelected-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f" + ) + + # "bootctl install" should _not_ have created an EFI entry + machine.fail('efibootmgr | grep "Linux Boot Manager"') ''; }; diff --git a/pkgs/applications/virtualization/OVMF/default.nix b/pkgs/applications/virtualization/OVMF/default.nix index 19ba8ced497..94d0ae94dbd 100644 --- a/pkgs/applications/virtualization/OVMF/default.nix +++ b/pkgs/applications/virtualization/OVMF/default.nix @@ -41,11 +41,14 @@ edk2.mkDerivation projectDscPath { mkdir -vp $fd/AAVMF mv -v $out/FV/QEMU_{EFI,VARS}.fd $fd/FV - # Uses Fedora dir layout: https://src.fedoraproject.org/cgit/rpms/edk2.git/tree/edk2.spec - # FIXME: why is it different from Debian dir layout? https://salsa.debian.org/qemu-team/edk2/blob/debian/debian/rules - dd of=$fd/AAVMF/QEMU_EFI-pflash.raw if=/dev/zero bs=1M count=64 - dd of=$fd/AAVMF/QEMU_EFI-pflash.raw if=$fd/FV/QEMU_EFI.fd conv=notrunc - dd of=$fd/AAVMF/vars-template-pflash.raw if=/dev/zero bs=1M count=64 + # Use Debian dir layout: https://salsa.debian.org/qemu-team/edk2/blob/debian/debian/rules + dd of=$fd/FV/AAVMF_CODE.fd if=/dev/zero bs=1M count=64 + dd of=$fd/FV/AAVMF_CODE.fd if=$fd/FV/QEMU_EFI.fd conv=notrunc + dd of=$fd/FV/AAVMF_VARS.fd if=/dev/zero bs=1M count=64 + + # Also add symlinks for Fedora dir layout: https://src.fedoraproject.org/cgit/rpms/edk2.git/tree/edk2.spec + ln -s $fd/FV/AAVMF_CODE.fd $fd/AAVMF/QEMU_EFI-pflash.raw + ln -s $fd/FV/AAVMF_VARS.fd $fd/AAVMF/vars-template-pflash.raw '' else '' mkdir -vp $fd/FV mv -v $out/FV/OVMF{,_CODE,_VARS}.fd $fd/FV