From: Daan De Meyer Date: Sun, 10 Mar 2024 15:45:32 +0000 (+0100) Subject: Add UnifiedKernelImages= X-Git-Tag: v22~21^2 X-Git-Url: http://git.ipfire.org/gitweb/gitweb.cgi?a=commitdiff_plain;h=refs%2Fpull%2F2475%2Fhead;p=thirdparty%2Fmkosi.git Add UnifiedKernelImages= Allows configuring whether we use UKIs or BLS Type 1 entries with systemd-boot and grub on UEFI. The BLS Type 1 logic we already had for grub on BIOS is made generic and reused to implement this feature. Partially fixes #2472. --- diff --git a/mkosi/__init__.py b/mkosi/__init__.py index 6e2a551bc..eb7881f06 100644 --- a/mkosi/__init__.py +++ b/mkosi/__init__.py @@ -1133,15 +1133,12 @@ def find_grub_binary(binary: str, root: Path = Path("/")) -> Optional[Path]: def want_grub_efi(context: Context) -> bool: - if context.config.bootable == ConfigFeature.disabled: + if not want_efi(context.config): return False if context.config.bootloader != Bootloader.grub: return False - if context.config.overlay or context.config.output_format.is_extension_image(): - 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") @@ -1205,97 +1202,23 @@ def prepare_grub_config(context: Context) -> Optional[Path]: with umask(~0o600), config.open("w") as f: f.write("set timeout=0\n") - return config - - -def prepare_grub_efi(context: Context) -> None: - if not want_grub_efi(context): - return - - # Signed EFI grub shipped by distributions reads its configuration from /EFI//grub.cfg in - # the ESP so let's put a shim there to redirect to the actual configuration file. - earlyconfig = context.root / "efi/EFI" / context.config.distribution.name / "grub.cfg" - with umask(~0o700): - earlyconfig.parent.mkdir(parents=True, exist_ok=True) - - # Read the actual config file from the root of the ESP. - earlyconfig.write_text(f"configfile /{context.config.distribution.grub_prefix()}/grub.cfg\n") - - config = prepare_grub_config(context) - assert config + if want_grub_efi(context): + # Signed EFI grub shipped by distributions reads its configuration from /EFI//grub.cfg in + # the ESP so let's put a shim there to redirect to the actual configuration file. + earlyconfig = context.root / "efi/EFI" / context.config.distribution.name / "grub.cfg" + with umask(~0o700): + earlyconfig.parent.mkdir(parents=True, exist_ok=True) - with config.open("a") as f: - f.write('if [ "${grub_platform}" == "efi" ]; then\n') + # Read the actual config file from the root of the ESP. + earlyconfig.write_text(f"configfile /{context.config.distribution.grub_prefix()}/grub.cfg\n") - for uki in (context.root / "boot/EFI/Linux").glob("*.efi"): - f.write( - textwrap.dedent( - f"""\ - menuentry "{uki.stem}" {{ - chainloader /{uki.relative_to(context.root / "boot")} - }} - """ - ) - ) - - f.write("fi\n") + return config -def prepare_grub_bios(context: Context, partitions: Sequence[Partition]) -> None: +def grub_bios_install(context: Context, partitions: Sequence[Partition]) -> None: if not want_grub_bios(context, partitions): return - config = prepare_grub_config(context) - assert config - - root = finalize_root(partitions) - assert root - - token = find_entry_token(context) - - dst = context.root / "boot" / token - with umask(~0o700): - dst.mkdir(exist_ok=True) - - with config.open("a") as f: - f.write('if [ "${grub_platform}" == "pc" ]; then\n') - - for kver, kimg in gen_kernel_images(context): - kdst = dst / kver - with umask(~0o700): - kdst.mkdir(exist_ok=True) - - microcode = build_microcode_initrd(context) - kmods = build_kernel_modules_initrd(context, kver) - - with umask(~0o600): - kimg = Path(shutil.copy2(context.root / kimg, kdst / "vmlinuz")) - initrds = [Path(shutil.copy2(microcode, kdst / "microcode"))] if microcode else [] - initrds += [ - Path(shutil.copy2(initrd, dst / initrd.name)) - for initrd in (context.config.initrds or [build_default_initrd(context)]) - ] - initrds += [Path(shutil.copy2(kmods, kdst / "kmods"))] - - image = Path("/") / kimg.relative_to(context.root / "boot") - cmdline = " ".join(context.config.kernel_command_line) - initrds = " ".join( - [os.fspath(Path("/") / initrd.relative_to(context.root / "boot")) for initrd in initrds] - ) - - f.write( - textwrap.dedent( - f"""\ - menuentry "{token}-{kver}" {{ - linux {image} {root} {cmdline} - initrd {initrds} - }} - """ - ) - ) - - f.write('fi\n') - # 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. @@ -1364,7 +1287,7 @@ def prepare_grub_bios(context: Context, partitions: Sequence[Partition]) -> None shutil.copy2(unicode, dst) -def install_grub_bios(context: Context, partitions: Sequence[Partition]) -> None: +def grub_bios_setup(context: Context, partitions: Sequence[Partition]) -> None: if not want_grub_bios(context, partitions): return @@ -1880,15 +1803,7 @@ def build_uki( initrds: Sequence[Path], cmdline: Sequence[str], output: Path, - roothash: Optional[str] = None, ) -> None: - cmdline = list(cmdline) - - if roothash: - cmdline += [roothash] - - cmdline += context.config.kernel_command_line - # Older versions of systemd-stub expect the cmdline section to be null terminated. We can't embed # nul terminators in argv so let's communicate the cmdline via a file instead. (context.workspace / "cmdline").write_text(f"{' '.join(cmdline).strip()}\x00") @@ -2008,6 +1923,20 @@ def want_efi(config: Config) -> bool: return True +def systemd_stub_binary(context: Context) -> Path: + arch = context.config.architecture.to_efi() + stub = context.root / f"usr/lib/systemd/boot/efi/linux{arch}.efi.stub" + return stub + + +def want_uki(context: Context) -> bool: + return want_efi(context.config) and ( + context.config.bootloader == Bootloader.uki or + context.config.unified_kernel_images == ConfigFeature.enabled or + (context.config.unified_kernel_images == ConfigFeature.auto and systemd_stub_binary(context).exists()) + ) + + def find_entry_token(context: Context) -> str: if ( "--version" not in run(["kernel-install", "--help"], @@ -2024,67 +1953,199 @@ def find_entry_token(context: Context) -> str: return cast(str, output["EntryToken"]) -def install_uki(context: Context, partitions: Sequence[Partition]) -> None: - # Iterates through all kernel versions included in the image and generates a combined - # kernel+initrd+cmdline+osrelease EFI file from it and places it in the /EFI/Linux directory of the ESP. - # sd-boot iterates through them and shows them in the menu. These "unified" single-file images have the - # benefit that they can be signed like normal EFI binaries, and can encode everything necessary to boot a - # specific root device, including the root hash. +def finalize_cmdline(context: Context, roothash: Optional[str]) -> list[str]: + if (context.root / "etc/kernel/cmdline").exists(): + cmdline = [(context.root / "etc/kernel/cmdline").read_text().strip()] + elif (context.root / "usr/lib/kernel/cmdline").exists(): + cmdline = [(context.root / "usr/lib/kernel/cmdline").read_text().strip()] + else: + cmdline = [] - if not want_efi(context.config) or context.config.output_format in (OutputFormat.uki, OutputFormat.esp): - return + if roothash: + cmdline += [roothash] - arch = context.config.architecture.to_efi() - stub = context.root / f"usr/lib/systemd/boot/efi/linux{arch}.efi.stub" - if not stub.exists() and context.config.bootable == ConfigFeature.auto: - return + return cmdline + context.config.kernel_command_line - if context.config.bootable == ConfigFeature.enabled and not gen_kernel_images(context): - die("A bootable image was requested but no kernel was found") - roothash = finalize_roothash(partitions) +def install_type1( + context: Context, + kver: str, + kimg: Path, + token: str, + partitions: Sequence[Partition], +) -> None: + dst = context.root / "boot" / token / kver + entry = context.root / f"boot/loader/entries/{token}-{kver}.conf" + with umask(~0o700): + dst.mkdir(parents=True, exist_ok=True) + entry.parent.mkdir(parents=True, exist_ok=True) - for kver, kimg in gen_kernel_images(context): - # See https://systemd.io/AUTOMATIC_BOOT_ASSESSMENT/#boot-counting - boot_count = "" - if (context.root / "etc/kernel/tries").exists(): - boot_count = f'+{(context.root / "etc/kernel/tries").read_text().strip()}' + microcode = build_microcode_initrd(context) + kmods = build_kernel_modules_initrd(context, kver) + cmdline = finalize_cmdline(context, finalize_roothash(partitions)) - if context.config.bootloader == Bootloader.uki: - if context.config.shim_bootloader != ShimBootloader.none: - boot_binary = context.root / shim_second_stage_binary(context) - else: - boot_binary = context.root / efi_boot_binary(context) + with umask(~0o600): + if ( + want_efi(context.config) and + context.config.secure_boot and + KernelType.identify(context.config, kimg) == KernelType.pe + ): + kimg = sign_efi_binary(context, kimg, dst / "vmlinuz") else: - token = find_entry_token(context) - if roothash: - _, _, h = roothash.partition("=") - boot_binary = context.root / f"boot/EFI/Linux/{token}-{kver}-{h}{boot_count}.efi" - else: - boot_binary = context.root / f"boot/EFI/Linux/{token}-{kver}{boot_count}.efi" + kimg = Path(shutil.copy2(context.root / kimg, dst / "vmlinuz")) - microcode = build_microcode_initrd(context) + initrds = [Path(shutil.copy2(microcode, dst.parent / "microcode.initrd"))] if microcode else [] + initrds += [ + Path(shutil.copy2(initrd, dst.parent / initrd.name)) + for initrd in (context.config.initrds or [build_default_initrd(context)]) + ] + initrds += [Path(shutil.copy2(kmods, dst / "kernel-modules.initrd"))] - initrds = [microcode] if microcode else [] - initrds += context.config.initrds or [build_default_initrd(context)] + with entry.open("w") as f: + f.write( + textwrap.dedent( + f"""\ + title {token} {kver} + version {kver} + linux /{kimg.relative_to(context.root / "boot")} + options {" ".join(cmdline)} + """ + ) + ) - if context.config.kernel_modules_initrd: - initrds += [build_kernel_modules_initrd(context, kver)] + for initrd in initrds: + f.write(f'initrd /{initrd.relative_to(context.root / "boot")}\n') - # Make sure the parent directory where we'll be writing the UKI exists. - with umask(~0o700): - boot_binary.parent.mkdir(parents=True, exist_ok=True) + if want_grub_efi(context) or want_grub_bios(context, partitions): + config = prepare_grub_config(context) + assert config + + root = finalize_root(partitions) + assert root + + with config.open("a") as f: + f.write("if ") + + conditions = [] + if want_grub_efi(context) and not want_uki(context): + conditions += ['[ "${grub_platform}" = efi ]'] + if want_grub_bios(context, partitions): + conditions += ['[ "${grub_platform}" = pc ]'] + + f.write(" || ".join(conditions)) + f.write("; then\n") - if (context.root / "etc/kernel/cmdline").exists(): - cmdline = [(context.root / "etc/kernel/cmdline").read_text().strip()] - elif (context.root / "usr/lib/kernel/cmdline").exists(): - cmdline = [(context.root / "usr/lib/kernel/cmdline").read_text().strip()] + f.write( + textwrap.dedent( + f"""\ + menuentry "{token}-{kver}" {{ + linux /{kimg.relative_to(context.root / "boot")} {root} {" ".join(cmdline)} + initrd {" ".join(os.fspath(Path("/") / i.relative_to(context.root / "boot")) for i in initrds)} + }} + """ + ) + ) + + f.write("fi\n") + + +def install_uki(context: Context, kver: str, kimg: Path, token: str, partitions: Sequence[Partition]) -> None: + roothash = finalize_roothash(partitions) + + boot_count = "" + if (context.root / "etc/kernel/tries").exists(): + boot_count = f'+{(context.root / "etc/kernel/tries").read_text().strip()}' + + if context.config.bootloader == Bootloader.uki: + if context.config.shim_bootloader != ShimBootloader.none: + boot_binary = context.root / shim_second_stage_binary(context) + else: + boot_binary = context.root / efi_boot_binary(context) + else: + if roothash: + _, _, h = roothash.partition("=") + boot_binary = context.root / f"boot/EFI/Linux/{token}-{kver}-{h}{boot_count}.efi" else: - cmdline = [] + boot_binary = context.root / f"boot/EFI/Linux/{token}-{kver}{boot_count}.efi" + + microcode = build_microcode_initrd(context) + + initrds = [microcode] if microcode else [] + initrds += context.config.initrds or [build_default_initrd(context)] - build_uki(context, stub, kver, context.root / kimg, initrds, cmdline, boot_binary, roothash=roothash) + if context.config.kernel_modules_initrd: + initrds += [build_kernel_modules_initrd(context, kver)] - print_output_size(boot_binary) + # Make sure the parent directory where we'll be writing the UKI exists. + with umask(~0o700): + boot_binary.parent.mkdir(parents=True, exist_ok=True) + + build_uki( + context, + systemd_stub_binary(context), + kver, + context.root / kimg, + initrds, + finalize_cmdline(context, roothash), + boot_binary, + ) + + print_output_size(boot_binary) + + if want_grub_efi(context): + config = prepare_grub_config(context) + assert config + + with config.open("a") as f: + f.write('if [ "${grub_platform}" = efi ]; then\n') + + f.write( + textwrap.dedent( + f"""\ + menuentry "{boot_binary.stem}" {{ + chainloader /{boot_binary.relative_to(context.root / "boot")} + }} + """ + ) + ) + + f.write("fi\n") + + +def install_kernel(context: Context, partitions: Sequence[Partition]) -> None: + # Iterates through all kernel versions included in the image and generates a combined + # kernel+initrd+cmdline+osrelease EFI file from it and places it in the /EFI/Linux directory of the ESP. + # sd-boot iterates through them and shows them in the menu. These "unified" single-file images have the + # benefit that they can be signed like normal EFI binaries, and can encode everything necessary to boot a + # specific root device, including the root hash. + + if context.config.output_format in (OutputFormat.uki, OutputFormat.esp): + return + + if context.config.bootable == ConfigFeature.disabled: + return + + if context.config.bootable == ConfigFeature.auto and ( + context.config.output_format == OutputFormat.cpio or + context.config.output_format.is_extension_image() or + context.config.overlay + ): + return + + stub = systemd_stub_binary(context) + if want_uki(context) and not stub.exists(): + die(f"Unified kernel image(s) requested but systemd-stub not found at /{stub.relative_to(context.root)}") + + if context.config.bootable == ConfigFeature.enabled and not gen_kernel_images(context): + die("A bootable image was requested but no kernel was found") + + token = find_entry_token(context) + + for kver, kimg in gen_kernel_images(context): + if want_uki(context): + install_uki(context, kver, kimg, token, partitions) + if not want_uki(context) or want_grub_bios(context, partitions): + install_type1(context, kver, kimg, token, partitions) if context.config.bootloader == Bootloader.uki: break @@ -2098,7 +2159,7 @@ def make_uki(context: Context, stub: Path, kver: str, kimg: Path, output: Path) initrds = [microcode] if microcode else [] initrds += [context.workspace / "initrd"] - build_uki(context, stub, kver, kimg, initrds, [], output) + build_uki(context, stub, kver, kimg, initrds, context.config.kernel_command_line, output) extract_pe_section(context, output, ".linux", context.staging / context.config.output_split_kernel) extract_pe_section(context, output, ".initrd", context.staging / context.config.output_split_initrd) @@ -2142,7 +2203,7 @@ def copy_uki(context: Context) -> None: if (context.staging / context.config.output_split_uki).exists(): return - if not want_efi(context.config): + if not want_efi(context.config) or context.config.unified_kernel_images == ConfigFeature.disabled: return ukis = sorted( @@ -2412,7 +2473,7 @@ def check_systemd_tool( def check_tools(config: Config, verb: Verb) -> None: if verb == Verb.build: - if want_efi(config): + if want_efi(config) and config.unified_kernel_images == ConfigFeature.enabled: check_systemd_tool( config, "ukify", "/usr/lib/systemd/ukify", @@ -2821,10 +2882,10 @@ def save_uki_components(context: Context) -> tuple[Optional[Path], Optional[str] kimg = shutil.copy2(context.root / kimg, context.workspace) - if not (arch := context.config.architecture.to_efi()): + if not context.config.architecture.to_efi(): die(f"Architecture {context.config.architecture} does not support UEFI") - stub = context.root / f"usr/lib/systemd/boot/efi/linux{arch}.efi.stub" + stub = systemd_stub_binary(context) if not stub.exists(): die(f"sd-stub not found at /{stub.relative_to(context.root)} in the image") @@ -3264,13 +3325,12 @@ 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_uki(context, partitions) - prepare_grub_efi(context) - prepare_grub_bios(context, partitions) + 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") - install_grub_bios(context, partitions) + grub_bios_setup(context, partitions) if context.config.split_artifacts: make_disk(context, split=True, msg="Extracting partitions") diff --git a/mkosi/config.py b/mkosi/config.py index 0883ea3c5..4c3743ec1 100644 --- a/mkosi/config.py +++ b/mkosi/config.py @@ -1298,6 +1298,7 @@ class Config: bootloader: Bootloader bios_bootloader: BiosBootloader shim_bootloader: ShimBootloader + unified_kernel_images: ConfigFeature initrds: list[Path] initrd_packages: list[str] microcode_host: bool @@ -2160,6 +2161,13 @@ SETTINGS = ( default=ShimBootloader.none, help="Specify whether to use shim", ), + ConfigSetting( + dest="unified_kernel_images", + metavar="FEATURE", + section="Content", + parse=config_parse_feature, + help="Specify whether to use UKIs with grub/systemd-boot in UEFI mode", + ), ConfigSetting( dest="initrds", long="--initrd", diff --git a/mkosi/resources/mkosi.md b/mkosi/resources/mkosi.md index a550791e5..67a119438 100644 --- a/mkosi/resources/mkosi.md +++ b/mkosi/resources/mkosi.md @@ -1190,6 +1190,17 @@ boolean argument: either `1`, `yes`, or `true` to enable, or `0`, `no`, on UEFI firmware is requested using other options (`Bootable=`, `Bootloader=`). +`UnifiedKernelImages=`, `--unified-kernel-images=` + +: Specifies whether to use unified kernel images or not when + `Bootloader=` is set to `systemd-boot` or `grub`. Takes a boolean + value or `auto`. Defaults to `auto`. If enabled, unified kernel images + are always used and the build will fail if any components required to + build unified kernel images are missing. If set to `auto`, unified + kernel images will be used if all necessary components are available. + Otherwise Type 1 entries as defined by the Boot Loader Specification + will be used instead. If disabled, Type 1 entries will always be used. + `Initrds=`, `--initrd` : Use user-provided initrd(s). Takes a comma separated list of paths to diff --git a/tests/test_json.py b/tests/test_json.py index b1939f5f3..1c0281928 100644 --- a/tests/test_json.py +++ b/tests/test_json.py @@ -303,6 +303,7 @@ def test_config() -> None: "ToolsTreeRepositories": [ "abc" ], + "UnifiedKernelImages": "auto", "UseSubvolumes": "auto", "VerityCertificate": "/path/to/cert", "VerityKey": null, @@ -442,6 +443,7 @@ def test_config() -> None: tools_tree_packages = [], tools_tree_release = None, tools_tree_repositories = ["abc"], + unified_kernel_images = ConfigFeature.auto, use_subvolumes = ConfigFeature.auto, verity_certificate = Path("/path/to/cert"), verity_key = None,