Repository: systemd/mkosi

Issue:
Ability to build images that are directly bootable on hardware with the usual MS signing keys
I've been fiddling around with the new `ShimBootloader=signed` option and was sort of expecting images to be bootable with SecureBoot on machines from the usual vendors. 

After some chatting with @DaanDeMeyer - it became clear that even though all the pieces are available in mkosi, we need a bit more glue-logic to arrange such images.

We were talking about two main areas for improvement:

**mkosi build**
Afaik, what is missing here is the ability to detect and copy the signed bootloader and kernel directly from the distribution - mkosi should not try to build a unified kernel image as these cannot be verified

A solution could be to introduce `Bootloader=grub-signed` that should try copy the signed grub and copy signed kernels/or UKI's from the distribution itself.

**mkosi qemu**
It would be very nice if it was possible to test these images using qemu, i don't know if it should have a flag that makes it magicly use some OVMF vars with the Microsoft keys enrolled. To me the `QemuFirmwareVariables=` and accompanying documentation that mentions `virt-firmware` was enough.


Discussion:
While it seems like a good idea to have a `Bootloader=grub-signed` that tries to lookup the distribution's grub and copy it - it might be a bit strange that it also disables UKI kernels - I don't know :)

Resolving patch:
diff --git a/mkosi/__init__.py b/mkosi/__init__.py
--- a/mkosi/__init__.py
+++ b/mkosi/__init__.py
@@ -844,7 +844,7 @@ def shim_second_stage_binary(context: Context) -> Path:
         return Path(f"efi/EFI/BOOT/grub{arch}.EFI")
 
 
-def sign_efi_binary(context: Context, input: Path, output: Path) -> None:
+def sign_efi_binary(context: Context, input: Path, output: Path) -> Path:
     assert context.config.secure_boot_key
     assert context.config.secure_boot_certificate
 
@@ -905,6 +905,8 @@ def sign_efi_binary(context: Context, input: Path, output: Path) -> None:
     else:
         die("One of sbsign or pesign is required to use SecureBoot=")
 
+    return output
+
 
 def install_systemd_boot(context: Context) -> None:
     if not want_efi(context.config):
@@ -937,7 +939,7 @@ def install_systemd_boot(context: Context) -> None:
     with complete_step("Installing systemd-boot…"):
         run(
             ["bootctl", "install", "--root", context.root, "--all-architectures", "--no-variables"],
-            env={"SYSTEMD_ESP_PATH": "/efi"},
+            env={"SYSTEMD_ESP_PATH": "/efi", "SYSTEMD_XBOOTLDR_PATH": "/boot"},
             sandbox=context.sandbox(options=["--bind", context.root, context.root]),
         )
 
@@ -1131,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")
@@ -1203,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/<distribution>/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/<distribution>/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.
@@ -1362,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
 
@@ -1612,7 +1537,7 @@ def finalize_default_initrd(
         "--acl", str(config.acl),
         *(f"--package={package}" for package in config.initrd_packages),
         *(["--package-directory", str(package_dir)] if package_dir else []),
-        "--output", f"{config.output}-initrd",
+        "--output", "initrd",
         *(["--image-id", config.image_id] if config.image_id else []),
         *(["--image-version", config.image_version] if config.image_version else []),
         *(
@@ -1657,7 +1582,7 @@ def build_default_initrd(context: Context) -> Path:
         context.args,
         context.config,
         resources=context.resources,
-        output_dir=context.workspace / "initrd",
+        output_dir=context.workspace,
         package_dir=context.packages,
     )
 
@@ -1733,7 +1658,7 @@ def build_microcode_initrd(context: Context) -> Optional[Path]:
     if not context.config.architecture.is_x86_variant():
         return None
 
-    microcode = context.workspace / "initrd-microcode.img"
+    microcode = context.workspace / "microcode.initrd"
     if microcode.exists():
         return microcode
 
@@ -1744,7 +1669,7 @@ def build_microcode_initrd(context: Context) -> Optional[Path]:
         logging.warning("/usr/lib/firmware/{amd-ucode,intel-ucode} not found, not adding microcode")
         return None
 
-    root = context.workspace / "initrd-microcode-root"
+    root = context.workspace / "microcode-root"
     destdir = root / "kernel/x86/microcode"
 
     with umask(~0o755):
@@ -1774,7 +1699,7 @@ def build_microcode_initrd(context: Context) -> Optional[Path]:
 
 
 def build_kernel_modules_initrd(context: Context, kver: str) -> Path:
-    kmods = context.workspace / f"initrd-kernel-modules-{kver}.img"
+    kmods = context.workspace / f"kernel-modules-{kver}.initrd"
     if kmods.exists():
         return kmods
 
@@ -1878,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")
@@ -2006,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"],
@@ -2016,72 +1947,205 @@ def find_entry_token(context: Context) -> str:
 
     output = json.loads(run(["kernel-install", "--root", context.root, "--json=pretty", "inspect"],
                             sandbox=context.sandbox(options=["--ro-bind", context.root, context.root]),
-                            stdout=subprocess.PIPE).stdout)
+                            stdout=subprocess.PIPE,
+                            env={"SYSTEMD_ESP_PATH": "/efi", "SYSTEMD_XBOOTLDR_PATH": "/boot"}).stdout)
     logging.debug(json.dumps(output, indent=4))
     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"
 
-        build_uki(context, stub, kver, context.root / kimg, initrds, cmdline, boot_binary, roothash=roothash)
+    microcode = build_microcode_initrd(context)
 
-        print_output_size(boot_binary)
+    initrds = [microcode] if microcode else []
+    initrds += context.config.initrds or [build_default_initrd(context)]
+
+    if context.config.kernel_modules_initrd:
+        initrds += [build_kernel_modules_initrd(context, kver)]
+
+    # 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
@@ -2095,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)
 
@@ -2139,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(
@@ -2409,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",
@@ -2818,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")
 
@@ -3261,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
--- 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 @@ def parse_ini(path: Path, only_sections: Collection[str] = ()) -> Iterator[tuple
         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/qemu.py b/mkosi/qemu.py
--- a/mkosi/qemu.py
+++ b/mkosi/qemu.py
@@ -800,7 +800,9 @@ def run_qemu(args: Args, config: Config) -> None:
         elif config.architecture.supports_smbios(firmware):
             cmdline += [
                 "-smbios",
-                f"type=11,value=io.systemd.stub.kernel-cmdline-extra={' '.join(kcl)}"
+                f"type=11,value=io.systemd.stub.kernel-cmdline-extra={' '.join(kcl)}",
+                "-smbios",
+                f"type=11,value=io.systemd.boot.kernel-cmdline-extra={' '.join(kcl)}",
             ]
 
         if config.output_format == OutputFormat.cpio:


Test patch:
diff --git a/tests/test_json.py b/tests/test_json.py
--- 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,