From 5f267f4ddab4e1e461cdb00cf1f41c8041afa2d7 Mon Sep 17 00:00:00 2001 From: r-vdp Date: Thu, 25 Jun 2026 22:46:22 +0200 Subject: [PATCH] nixos/boot: allow passing extra initrd archives to the bootloader This is for instance useful on Asahi where an additional initrd archive containing firmware blobs and per-device calibration files is placed on the ESP and updated by the Asahi Linux Installer. These need to be loaded alongside the NixOS initrd. Implemented for systemd-boot and Limine. Grub is left out since its install script does not use bootspec yet. Co-authored-by: Florian Klink --- nixos/modules/module-list.nix | 1 + nixos/modules/system/boot/extra-initrd.nix | 21 ++++ .../boot/loader/limine/limine-install.py | 13 +++ .../systemd-boot/systemd-boot-builder.py | 36 ++++--- nixos/tests/all-tests.nix | 3 + nixos/tests/extra-initrd.nix | 96 +++++++++++++++++++ 6 files changed, 157 insertions(+), 13 deletions(-) create mode 100644 nixos/modules/system/boot/extra-initrd.nix create mode 100644 nixos/tests/extra-initrd.nix diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 0fd01c232583..334ebc3de609 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1911,6 +1911,7 @@ ./system/boot/clevis-luks-askpass.nix ./system/boot/clevis.nix ./system/boot/emergency-mode.nix + ./system/boot/extra-initrd.nix ./system/boot/grow-partition.nix ./system/boot/initrd-network.nix ./system/boot/initrd-openvpn.nix diff --git a/nixos/modules/system/boot/extra-initrd.nix b/nixos/modules/system/boot/extra-initrd.nix new file mode 100644 index 000000000000..22451c64da7d --- /dev/null +++ b/nixos/modules/system/boot/extra-initrd.nix @@ -0,0 +1,21 @@ +{ config, lib, ... }: +{ + options.system.boot.extraInitrd = { + paths = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = '' + List of paths relative to the ESP that are combined with the NixOS + main initrd before being passed to the kernel. + ''; + }; + }; + + config = { + boot.bootspec.extensions = lib.mkIf (config.system.boot.extraInitrd.paths != [ ]) { + "org.nixos.extra-initrd.v1" = { + inherit (config.system.boot.extraInitrd) paths; + }; + }; + }; +} diff --git a/nixos/modules/system/boot/loader/limine/limine-install.py b/nixos/modules/system/boot/loader/limine/limine-install.py index 629a2fef6c76..0ddbd8a9f29b 100644 --- a/nixos/modules/system/boot/loader/limine/limine-install.py +++ b/nixos/modules/system/boot/loader/limine/limine-install.py @@ -36,6 +36,7 @@ class BootSpec: label: str toplevel: str specialisations: Dict[str, "BootSpec"] + extraInitrdPaths: list[str] xen: XenBootSpec | None initrd: str | None = None initrdSecrets: str | None = None @@ -173,10 +174,15 @@ def bootjson_to_bootspec(bootjson: dict) -> BootSpec: xen = None if "org.xenproject.bootspec.v2" in bootjson: xen = bootjson["org.xenproject.bootspec.v2"] + + extraInitrdExtension = bootjson.get("org.nixos.extra-initrd.v1", {}) + extraInitrdPaths = extraInitrdExtension.get("paths", []) + return BootSpec( **bootjson["org.nixos.bootspec.v1"], specialisations=specialisations, xen=xen, + extraInitrdPaths=extraInitrdPaths, ) @@ -299,6 +305,10 @@ def xen_config_entry( if bootspec.initrd: # the final module is the initrd entry += "module_path: " + get_kernel_uri(bootspec.initrd) + "\n" + + for p in bootspec.extraInitrdPaths: + entry += f"module_path: boot():/{p}\n" + return entry @@ -321,6 +331,9 @@ def config_entry(levels: int, bootspec: BootSpec, label: str, time: str) -> str: if bootspec.initrd: entry += f"module_path: " + get_kernel_uri(bootspec.initrd) + "\n" + for p in bootspec.extraInitrdPaths: + entry += f"module_path: boot():/{p}\n" + if bootspec.initrdSecrets: base_path = str(limine_install_dir) + "/kernels/" initrd_secrets_path = ( diff --git a/nixos/modules/system/boot/loader/systemd-boot/systemd-boot-builder.py b/nixos/modules/system/boot/loader/systemd-boot/systemd-boot-builder.py index 06c15ee6674b..534efa8877e2 100644 --- a/nixos/modules/system/boot/loader/systemd-boot/systemd-boot-builder.py +++ b/nixos/modules/system/boot/loader/systemd-boot/systemd-boot-builder.py @@ -45,6 +45,7 @@ BOOT_COUNTING = "@bootCounting@" == "True" class BootSpec: init: Path initrd: Path + extraInitrdPaths: list[Path] kernel: Path kernelParams: list[str] # noqa: N815 label: str @@ -306,9 +307,7 @@ def get_bootspec(profile: str | None, generation: int) -> BootSpec | None: try: bootspec_json = json.load(f) except ValueError as e: - print( - f"error: Malformed Json: {e}, in {boot_json_path}", file=sys.stderr - ) + print(f"error: Malformed Json: {e}, in {boot_json_path}", file=sys.stderr) sys.exit(1) return bootspec_from_json(bootspec_json) @@ -320,6 +319,11 @@ def bootspec_from_json(bootspec_json: dict[str, Any]) -> BootSpec: sortKey = systemdBootExtension.get("sortKey", "nixos") devicetree = systemdBootExtension.get("devicetree") + extraInitrdExtension = bootspec_json.get("org.nixos.extra-initrd.v1", {}) + extraInitrdPaths = list( + map(lambda path: Path(path), extraInitrdExtension.get("paths", [])) + ) + if devicetree: devicetree = Path(devicetree) @@ -332,6 +336,7 @@ def bootspec_from_json(bootspec_json: dict[str, Any]) -> BootSpec: specialisations=specialisations, sortKey=sortKey, devicetree=devicetree, + extraInitrdPaths=extraInitrdPaths, ) @@ -374,16 +379,21 @@ def boot_file( specialisation=" (%s)" % specialisation if specialisation else "", ) description = f"Generation {generation} {bootspec.label}, built on {build_date}" - boot_entry = [ - f"title {title}", - f"version {description}", - f"linux /{str(kernel.path)}", - f"initrd /{str(initrd.path)}", - f"options {kernel_params}", - f"machine-id {machine_id}" if machine_id is not None else None, - f"devicetree /{str(devicetree.path)}" if devicetree is not None else None, - f"sort-key {bootspec.sortKey}", - ] + boot_entry = ( + [ + f"title {title}", + f"version {description}", + f"linux /{str(kernel.path)}", + f"initrd /{str(initrd.path)}", + ] + + list(map(lambda initrd: f"initrd /{initrd}", bootspec.extraInitrdPaths)) + + [ + f"options {kernel_params}", + f"machine-id {machine_id}" if machine_id is not None else None, + f"devicetree /{str(devicetree.path)}" if devicetree is not None else None, + f"sort-key {bootspec.sortKey}", + ] + ) contents = "\n".join(filter(None, boot_entry)) entry, bootctl_id = BootFile.from_entry(contents.encode("utf-8")) return (list(filter(None, [kernel, initrd, devicetree, entry])), bootctl_id) diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 24b3d1216af7..9651010181ca 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -554,6 +554,9 @@ in etebase-server = runTest ./etebase-server.nix; etesync-dav = runTest ./etesync-dav.nix; evcc = runTest ./evcc.nix; + extra-initrd = import ./extra-initrd.nix { + inherit runTest pkgs; + }; facter = runTest ./facter; fail2ban = runTest ./fail2ban.nix; fakeroute = runTest ./fakeroute.nix; diff --git a/nixos/tests/extra-initrd.nix b/nixos/tests/extra-initrd.nix new file mode 100644 index 000000000000..20da3ad8d11b --- /dev/null +++ b/nixos/tests/extra-initrd.nix @@ -0,0 +1,96 @@ +{ + runTest, + ... +}: +let + common = + { config, pkgs, ... }: + { + virtualisation.useBootLoader = true; + virtualisation.useEFIBoot = true; + system.boot.extraInitrd.paths = [ + extraInitrdPath + ]; + + boot.loader.timeout = 2; + + boot.initrd.systemd.mounts = [ + { + what = "/canary.txt"; + where = "/sysroot/run/canary.txt"; + type = "none"; + options = "bind"; + unitConfig = { + DefaultDependencies = false; + }; + requiredBy = [ "initrd-fs.target" ]; + before = [ "initrd-fs.target" ]; + } + ]; + }; + + extraInitrdPath = "custom.cpio"; + canaryCpio = + pkgs: + pkgs.runCommand "canary.cpio" + { + nativeBuildInputs = [ pkgs.cpio ]; + } + '' + echo canary > canary.txt + find . -print0 | cpio --null -o --format=newc > $out + ''; + + testScript = + # python + '' + machine.wait_for_unit("multi-user.target") + + # Check that the extra cpio archive is on the ESP + machine.succeed("test -e /boot/custom.cpio") + + # Check that the initrd that we booted with contained the file from + # the extra initrd and our initrd mount unit bound it into sysroot + assert machine.succeed("cat /run/canary.txt").strip() == "canary" + ''; +in +{ + systemd-boot = runTest ( + { pkgs, ... }: { + name = "systemd-boot-extra-initrd"; + + nodes.machine = { config, ... }: { + imports = [ common ]; + + boot.loader.systemd-boot = { + enable = true; + extraInstallCommands = '' + cp ${canaryCpio pkgs} ${config.boot.loader.efi.efiSysMountPoint}/${extraInitrdPath} + ''; + }; + }; + + inherit testScript; + } + ); + + limine = runTest { + name = "limine-extra-initrd"; + + nodes.machine = { pkgs, config, ... }: { + imports = [ common ]; + + boot.loader.limine = { + enable = true; + efiSupport = true; + enableEditor = true; + extraInstallCommands = '' + cp ${canaryCpio pkgs} ${config.boot.loader.efi.efiSysMountPoint}/${extraInitrdPath} + ''; + }; + + }; + + inherit testScript; + }; +}