From 7e6bacc1b709e07522e3e85b6322ea1549e24e3d Mon Sep 17 00:00:00 2001 From: Daan De Meyer Date: Sun, 10 Mar 2024 22:15:39 +0100 Subject: [PATCH] Add grub for EFI support We also rework the grub setup to not copy the grub modules into the ESP anymore. We do this as grub for EFI booted in secure boot mode does not load any unsigned modules for security reasons so we opt to include all necessary modules into the grub image itself. --- mkosi.conf.d/20-opensuse.conf | 1 + .../mkosi.conf.d/20-uefi.conf | 1 + .../mkosi.conf.d/20-x86-64.conf | 3 +- mkosi/__init__.py | 79 ++++++++++--------- tests/test_boot.py | 28 ++++++- 5 files changed, 73 insertions(+), 39 deletions(-) diff --git a/mkosi.conf.d/20-opensuse.conf b/mkosi.conf.d/20-opensuse.conf index f3d21cea1..cfc37d9b1 100644 --- a/mkosi.conf.d/20-opensuse.conf +++ b/mkosi.conf.d/20-opensuse.conf @@ -22,6 +22,7 @@ Packages= erofs-utils grep grub2-i386-pc + grub2-x86_64-efi iproute iputils kernel-kvmsmall diff --git a/mkosi.conf.d/30-centos-fedora/mkosi.conf.d/20-uefi.conf b/mkosi.conf.d/30-centos-fedora/mkosi.conf.d/20-uefi.conf index 7ce68d647..400758906 100644 --- a/mkosi.conf.d/30-centos-fedora/mkosi.conf.d/20-uefi.conf +++ b/mkosi.conf.d/30-centos-fedora/mkosi.conf.d/20-uefi.conf @@ -10,3 +10,4 @@ Packages= pesign edk2-ovmf shim + grub2-efi-x64-modules diff --git a/mkosi.conf.d/30-debian-ubuntu/mkosi.conf.d/20-x86-64.conf b/mkosi.conf.d/30-debian-ubuntu/mkosi.conf.d/20-x86-64.conf index 57c275e66..48b054758 100644 --- a/mkosi.conf.d/30-debian-ubuntu/mkosi.conf.d/20-x86-64.conf +++ b/mkosi.conf.d/30-debian-ubuntu/mkosi.conf.d/20-x86-64.conf @@ -6,5 +6,6 @@ Architecture=x86-64 [Content] Packages= amd64-microcode - grub-pc + grub-pc-bin + grub-efi-amd64 intel-microcode diff --git a/mkosi/__init__.py b/mkosi/__init__.py index 64d675da5..0c010d8af 100644 --- a/mkosi/__init__.py +++ b/mkosi/__init__.py @@ -1136,9 +1136,9 @@ def install_shim(context: Context) -> None: find_and_install_shim_binary(context, "mok", signed, unsigned, dst.parent) -def find_grub_bios_directory(context: Context) -> Optional[Path]: - for d in ("usr/lib/grub/i386-pc", "usr/share/grub2/i386-pc"): - if (p := context.root / d).exists() and any(p.iterdir()): +def find_grub_directory(context: Context, *, target: str) -> Optional[Path]: + for d in ("usr/lib/grub", "usr/share/grub2"): + if (p := context.root / d / target).exists() and any(p.iterdir()): return p return None @@ -1156,11 +1156,9 @@ def want_grub_efi(context: Context) -> bool: if context.config.bootloader != Bootloader.grub: return False - if not any((context.root / "efi").rglob("grub*.efi")): - if context.config.bootable == ConfigFeature.enabled: - die("A bootable EFI image with grub was requested but grub for EFI is not installed in /efi") - - return False + have = find_grub_directory(context, target="x86_64-efi") is not None + if not have and context.config.bootable == ConfigFeature.enabled: + die("An EFI bootable image with grub was requested but grub for EFI is not installed") return True @@ -1178,7 +1176,7 @@ def want_grub_bios(context: Context, partitions: Sequence[Partition] = ()) -> bo if context.config.overlay: return False - have = find_grub_bios_directory(context) is not None + have = find_grub_directory(context, target="i386-pc") is not None if not have and context.config.bootable == ConfigFeature.enabled: die("A BIOS bootable image with grub was requested but grub for BIOS is not installed") @@ -1232,24 +1230,17 @@ def prepare_grub_config(context: Context) -> Optional[Path]: return config -def grub_bios_install(context: Context, partitions: Sequence[Partition]) -> None: - if not want_grub_bios(context, partitions): - return - - # grub-install insists on opening the root partition device to probe it's filesystem which requires root - # so we're forced to reimplement its functionality. Luckily that's pretty simple, run grub-mkimage to - # generate the required core.img and copy the relevant files to the ESP. - +def grub_mkimage(context: Context, *, target: str, modules: Sequence[str] = (), output: Optional[Path] = None) -> None: mkimage = find_grub_binary("mkimage", root=context.config.tools()) assert mkimage - directory = find_grub_bios_directory(context) + directory = find_grub_directory(context, target=target) assert directory - dst = context.root / "efi" / context.config.distribution.grub_prefix() / "i386-pc" - dst.mkdir(parents=True, exist_ok=True) - - with tempfile.NamedTemporaryFile("w", prefix="grub-early-config") as earlyconfig: + with ( + complete_step(f"Generating grub image for {target}"), + tempfile.NamedTemporaryFile("w", prefix="grub-early-config") as earlyconfig + ): earlyconfig.write( textwrap.dedent( f"""\ @@ -1267,15 +1258,16 @@ def grub_bios_install(context: Context, partitions: Sequence[Partition]) -> None "--directory", directory, "--config", earlyconfig.name, "--prefix", f"/{context.config.distribution.grub_prefix()}", - "--output", dst / "core.img", - "--format", "i386-pc", + "--output", output or (directory / "core.img"), + "--format", target, *(["--verbose"] if ARG_DEBUG.get() else []), - # Modules required to find and read from the XBOOTLDR partition which has all the other modules. "fat", "part_gpt", - "biosdisk", "search", "search_fs_file", + "normal", + "linux", + *modules, ], sandbox=context.sandbox( options=[ @@ -1285,18 +1277,30 @@ def grub_bios_install(context: Context, partitions: Sequence[Partition]) -> None ), ) - for p in directory.glob("*.mod"): - shutil.copy2(p, dst) - for p in directory.glob("*.lst"): - shutil.copy2(p, dst) +def install_grub(context: Context) -> None: + if not want_grub_bios(context) and not want_grub_efi(context): + return - shutil.copy2(directory / "modinfo.sh", dst) - shutil.copy2(directory / "boot.img", dst) + if want_grub_bios(context): + grub_mkimage(context, target="i386-pc", modules=("biosdisk",)) + + if want_grub_efi(context): + if context.config.shim_bootloader != ShimBootloader.none: + output = context.root / shim_second_stage_binary(context) + else: + output = context.root / efi_boot_binary(context) + + with umask(~0o700): + output.parent.mkdir(parents=True, exist_ok=True) + + grub_mkimage(context, target="x86_64-efi", output=output, modules=("chain",)) + if context.config.secure_boot: + sign_efi_binary(context, output, output) dst = context.root / "efi" / context.config.distribution.grub_prefix() / "fonts" with umask(~0o700): - dst.mkdir(exist_ok=True) + dst.mkdir(parents=True, exist_ok=True) for d in ("grub", "grub2"): unicode = context.root / "usr/share" / d / "unicode.pf2" @@ -1311,8 +1315,11 @@ def grub_bios_setup(context: Context, partitions: Sequence[Partition]) -> None: setup = find_grub_binary("bios-setup", root=context.config.tools()) assert setup + directory = find_grub_directory(context, target="i386-pc") + assert directory + with ( - complete_step("Installing grub boot loader…"), + complete_step("Installing grub boot loader for BIOS…"), tempfile.NamedTemporaryFile(mode="w") as mountinfo, ): # grub-bios-setup insists on being able to open the root device that --directory is located on, which @@ -1329,7 +1336,7 @@ def grub_bios_setup(context: Context, partitions: Sequence[Partition]) -> None: [ "sh", "-c", f"mount --bind {mountinfo.name} /proc/$$/mountinfo && exec $0 \"$@\"", setup, - "--directory", context.root / "efi" / context.config.distribution.grub_prefix() / "i386-pc", + "--directory", directory, *(["--verbose"] if ARG_DEBUG.get() else []), context.staging / context.config.output_with_format, ], @@ -3319,6 +3326,7 @@ def build_image(context: Context) -> None: configure_clock(context) install_systemd_boot(context) + install_grub(context) install_shim(context) run_sysusers(context) run_tmpfiles(context) @@ -3344,7 +3352,6 @@ def build_image(context: Context) -> None: normalize_mtime(context.root, context.config.source_date_epoch) partitions = make_disk(context, skip=("esp", "xbootldr"), tabs=True, msg="Generating disk image") install_kernel(context, partitions) - grub_bios_install(context, partitions) normalize_mtime(context.root, context.config.source_date_epoch, directory=Path("boot")) normalize_mtime(context.root, context.config.source_date_epoch, directory=Path("efi")) partitions = make_disk(context, msg="Formatting ESP/XBOOTLDR partitions") diff --git a/tests/test_boot.py b/tests/test_boot.py index 0ec42b7c7..21614a12c 100644 --- a/tests/test_boot.py +++ b/tests/test_boot.py @@ -5,7 +5,7 @@ import subprocess import pytest -from mkosi.config import OutputFormat +from mkosi.config import Bootloader, OutputFormat, QemuFirmware from mkosi.distributions import Distribution from mkosi.qemu import find_virtiofsd from mkosi.run import find_binary, run @@ -25,7 +25,7 @@ def have_vmspawn() -> bool: @pytest.mark.parametrize("format", OutputFormat) -def test_boot(config: Image.Config, format: OutputFormat) -> None: +def test_format(config: Image.Config, format: OutputFormat) -> None: with Image( config, options=[ @@ -76,3 +76,27 @@ def test_boot(config: Image.Config, format: OutputFormat) -> None: return image.qemu(options=options + ["--qemu-firmware=bios"]) + + +@pytest.mark.parametrize("bootloader", Bootloader) +def test_bootloader(config: Image.Config, bootloader: Bootloader) -> None: + if config.distribution == Distribution.rhel_ubi: + return + + firmware = QemuFirmware.linux if bootloader == Bootloader.none else QemuFirmware.uefi + + with Image( + config, + options=[ + "--kernel-command-line=systemd.unit=mkosi-check-and-shutdown.service", + "--incremental", + "--ephemeral", + "--format=disk", + "--bootloader", str(bootloader), + "--qemu-firmware", str(firmware) + ], + ) as image: + image.summary() + image.genkey() + image.build() + image.qemu() -- 2.47.2