1 # This module builds the initial ramdisk, which contains an init
2 # script that performs the first stage of booting the system: it loads
3 # the modules necessary to mount the root file system, then calls the
4 # init in the root file system to start the second boot stage.
6 { config, options, lib, utils, pkgs, ... }:
12 udev = config.systemd.package;
14 kernel-name = config.boot.kernelPackages.kernel.name or "kernel";
16 # Determine the set of modules that we need to mount the root FS.
17 modulesClosure = pkgs.makeModulesClosure {
18 rootModules = config.boot.initrd.availableKernelModules ++ config.boot.initrd.kernelModules;
19 kernel = config.system.modulesTree;
20 firmware = config.hardware.firmware;
25 # The initrd only has to mount `/` or any FS marked as necessary for
26 # booting (such as the FS containing `/nix/store`, or an FS needed for
27 # mounting `/`, like `/` on a loopback).
28 fileSystems = filter utils.fsNeededForBoot config.system.build.fileSystems;
30 # Determine whether zfs-mount(8) is needed.
31 zfsRequiresMountHelper = any (fs: lib.elem "zfsutil" fs.options) fileSystems;
33 # A utility for enumerating the shared-library dependencies of a program
34 findLibs = pkgs.buildPackages.writeShellScriptBin "find-libs" ''
40 patchelf="${pkgs.buildPackages.patchelf}/bin/patchelf"
43 rpath="$($patchelf --print-rpath $1)"
45 for lib in $($patchelf --print-needed $1); do
46 left+=("$lib" "$rpath" "$dir")
52 while [ ''${#left[@]} -ne 0 ]; do
56 left=("''${left[@]:3}")
57 if [ -z ''${seen[$next]+x} ]; then
60 # Ignore the dynamic linker which for some reason appears as a DT_NEEDED of glibc but isn't in glibc's RPATH.
65 IFS=: read -ra paths <<< $rpath
67 for path in "''${paths[@]}"; do
68 path=$(eval "echo $path")
69 if [ -f "$path/$next" ]; then
76 if [ -z "$res" ]; then
77 echo "Couldn't satisfy dependency $next" >&2
84 # Some additional utilities needed in stage 1, like mount, lvm, fsck
85 # etc. We don't want to bring in all of those packages, so we just
86 # copy what we need. Instead of using statically linked binaries,
87 # we just copy what we need from Glibc and use patchelf to make it
89 extraUtils = pkgs.runCommand "extra-utils"
90 { nativeBuildInputs = with pkgs.buildPackages; [ nukeReferences bintools ];
91 allowedReferences = [ "out" ]; # prevent accidents like glibc being included in the initrd
96 mkdir -p $out/bin $out/lib
97 ln -s $out/bin $out/sbin
99 copy_bin_and_libs () {
100 [ -f "$out/bin/$(basename $1)" ] && rm "$out/bin/$(basename $1)"
105 for BIN in ${pkgs.busybox}/{s,}bin/*; do
106 copy_bin_and_libs $BIN
109 ${optionalString zfsRequiresMountHelper ''
110 # Filesystems using the "zfsutil" option are mounted regardless of the
111 # mount.zfs(8) helper, but it is required to ensure that ZFS properties
112 # are used as mount options.
114 # BusyBox does not use the ZFS helper in the first place.
115 # util-linux searches /sbin/ as last path for helpers (stage-1-init.sh
116 # must symlink it to the store PATH).
117 # Without helper program, both `mount`s silently fails back to internal
118 # code, using default options and effectively ignore security relevant
119 # ZFS properties such as `setuid=off` and `exec=off` (unless manually
120 # duplicated in `fileSystems.*.options`, defeating "zfsutil"'s purpose).
121 copy_bin_and_libs ${lib.getOutput "mount" pkgs.util-linux}/bin/mount
122 copy_bin_and_libs ${config.boot.zfs.package}/bin/mount.zfs
125 # Copy some util-linux stuff.
126 copy_bin_and_libs ${pkgs.util-linux}/sbin/blkid
128 # Copy dmsetup and lvm.
129 copy_bin_and_libs ${getBin pkgs.lvm2}/bin/dmsetup
130 copy_bin_and_libs ${getBin pkgs.lvm2}/bin/lvm
133 copy_bin_and_libs ${udev}/bin/udevadm
134 cp ${lib.getLib udev.kmod}/lib/libkmod.so* $out/lib
135 copy_bin_and_libs ${udev}/lib/systemd/systemd-sysctl
136 for BIN in ${udev}/lib/udev/*_id; do
137 copy_bin_and_libs $BIN
139 # systemd-udevd is only a symlink to udevadm these days
140 ln -sf udevadm $out/bin/systemd-udevd
143 copy_bin_and_libs ${pkgs.kmod}/bin/kmod
144 ln -sf kmod $out/bin/modprobe
147 ${optionalString config.services.multipath.enable ''
148 copy_bin_and_libs ${config.services.multipath.package}/bin/multipath
149 copy_bin_and_libs ${config.services.multipath.package}/bin/multipathd
150 # Copy lib/multipath manually.
151 cp -rpv ${config.services.multipath.package}/lib/multipath $out/lib
154 # Copy secrets if needed.
156 # TODO: move out to a separate script; see #85000.
157 ${optionalString (!config.boot.loader.supportsInitrdSecrets)
158 (concatStringsSep "\n" (mapAttrsToList (dest: source:
159 let source' = if source == null then dest else source; in
161 mkdir -p $(dirname "$out/secrets/${dest}")
162 # Some programs (e.g. ssh) doesn't like secrets to be
163 # symlinks, so we use `cp -L` here to match the
164 # behaviour when secrets are natively supported.
165 cp -Lr ${source'} "$out/secrets/${dest}"
167 ) config.boot.initrd.secrets))
170 ${config.boot.initrd.extraUtilsCommands}
172 # Copy ld manually since it isn't detected correctly
173 cp -pv ${pkgs.stdenv.cc.libc.out}/lib/ld*.so.? $out/lib
175 # Copy all of the needed libraries in a consistent order so
176 # duplicates are resolved the same way.
177 find $out/bin $out/lib -type f | sort | while read BIN; do
178 echo "Copying libs for executable $BIN"
179 for LIB in $(${findLibs}/bin/find-libs $BIN); do
180 TGT="$out/lib/$(basename $LIB)"
181 if [ ! -f "$TGT" ]; then
182 SRC="$(readlink -e $LIB)"
183 cp -pdv "$SRC" "$TGT"
188 # Strip binaries further than normal.
190 stripDirs "$STRIP" "$RANLIB" "lib bin" "-s"
192 # Run patchelf to make the programs refer to the copied libraries.
193 find $out/bin $out/lib -type f | while read i; do
197 find $out/bin -type f | while read i; do
198 echo "patching $i..."
199 patchelf --set-interpreter $out/lib/ld*.so.? --set-rpath $out/lib $i || true
202 find $out/lib -type f \! -name 'ld*.so.?' | while read i; do
203 echo "patching $i..."
204 patchelf --set-rpath $out/lib $i
207 if [ -z "${toString (pkgs.stdenv.hostPlatform != pkgs.stdenv.buildPlatform)}" ]; then
208 # Make sure that the patchelf'ed binaries still work.
209 echo "testing patched programs..."
210 $out/bin/ash -c 'echo hello world' | grep "hello world"
211 ${if zfsRequiresMountHelper then ''
212 $out/bin/mount -V 1>&1 | grep -q "mount from util-linux"
213 $out/bin/mount.zfs -h 2>&1 | grep -q "Usage: mount.zfs"
215 $out/bin/mount --help 2>&1 | grep -q "BusyBox"
217 $out/bin/blkid -V 2>&1 | grep -q 'libblkid'
218 $out/bin/udevadm --version
219 $out/bin/dmsetup --version 2>&1 | tee -a log | grep -q "version:"
220 LVM_SYSTEM_DIR=$out $out/bin/lvm version 2>&1 | tee -a log | grep -q "LVM"
221 ${optionalString config.services.multipath.enable ''
222 ($out/bin/multipath || true) 2>&1 | grep -q 'need to be root'
223 ($out/bin/multipathd || true) 2>&1 | grep -q 'need to be root'
226 ${config.boot.initrd.extraUtilsCommandsTest}
231 # Networkd link files are used early by udev to set up interfaces early.
232 # This must be done in stage 1 to avoid race conditions between udev and
234 linkUnits = pkgs.runCommand "link-units" {
235 allowedReferences = [ extraUtils ];
236 preferLocalBuild = true;
239 cp -v ${udev}/lib/systemd/network/*.link $out/
242 links = filterAttrs (n: v: hasSuffix ".link" n) config.systemd.network.units;
243 files = mapAttrsToList (n: v: "${v.unit}/${n}") links;
245 concatMapStringsSep "\n" (file: "cp -v ${file} $out/") files
248 udevRules = pkgs.runCommand "udev-rules" {
249 allowedReferences = [ extraUtils ];
250 preferLocalBuild = true;
254 cp -v ${udev}/lib/udev/rules.d/60-cdrom_id.rules $out/
255 cp -v ${udev}/lib/udev/rules.d/60-persistent-storage.rules $out/
256 cp -v ${udev}/lib/udev/rules.d/75-net-description.rules $out/
257 cp -v ${udev}/lib/udev/rules.d/80-drivers.rules $out/
258 cp -v ${udev}/lib/udev/rules.d/80-net-setup-link.rules $out/
259 cp -v ${pkgs.lvm2}/lib/udev/rules.d/*.rules $out/
260 ${config.boot.initrd.extraUdevRulesCommands}
262 for i in $out/*.rules; do
263 substituteInPlace $i \
264 --replace ata_id ${extraUtils}/bin/ata_id \
265 --replace scsi_id ${extraUtils}/bin/scsi_id \
266 --replace cdrom_id ${extraUtils}/bin/cdrom_id \
267 --replace ${pkgs.coreutils}/bin/basename ${extraUtils}/bin/basename \
268 --replace ${pkgs.util-linux}/bin/blkid ${extraUtils}/bin/blkid \
269 --replace ${getBin pkgs.lvm2}/bin ${extraUtils}/bin \
270 --replace ${pkgs.mdadm}/sbin ${extraUtils}/sbin \
271 --replace ${pkgs.bash}/bin/sh ${extraUtils}/bin/sh \
272 --replace ${udev} ${extraUtils}
275 # Work around a bug in QEMU, which doesn't implement the "READ
276 # DISC INFORMATION" SCSI command:
277 # https://bugzilla.redhat.com/show_bug.cgi?id=609049
278 # As a result, `cdrom_id' doesn't print
279 # ID_CDROM_MEDIA_TRACK_COUNT_DATA, which in turn prevents the
280 # /dev/disk/by-label symlinks from being created. We need these
281 # in the NixOS installation CD, so use ID_CDROM_MEDIA in the
282 # corresponding udev rules for now. This was the behaviour in
283 # udev <= 154. See also
284 # https://www.spinics.net/lists/hotplug/msg03935.html
285 substituteInPlace $out/60-persistent-storage.rules \
286 --replace ID_CDROM_MEDIA_TRACK_COUNT_DATA ID_CDROM_MEDIA
290 # The init script of boot stage 1 (loading kernel modules for
291 # mounting the root FS).
292 bootStage1 = pkgs.substituteAll {
293 src = ./stage-1-init.sh;
295 shell = "${extraUtils}/bin/ash";
301 # check both with bash
302 ${pkgs.buildPackages.bash}/bin/sh -n $target
303 # and with ash shell, just in case
304 ${pkgs.buildPackages.busybox}/bin/ash -n $target
307 inherit linkUnits udevRules extraUtils;
309 inherit (config.boot) resumeDevice;
311 inherit (config.system.nixos) distroName;
313 inherit (config.system.build) earlyMountScript;
315 inherit (config.boot.initrd) checkJournalingFS verbose
316 preLVMCommands preDeviceCommands postDeviceCommands postResumeCommands postMountCommands preFailCommands kernelModules;
318 resumeDevices = map (sd: if sd ? device then sd.device else "/dev/disk/by-label/${sd.label}")
319 (filter (sd: hasPrefix "/dev/" sd.device && !sd.randomEncryption.enable
320 # Don't include zram devices
321 && !(hasPrefix "/dev/zram" sd.device)
322 ) config.swapDevices);
325 let f = fs: [ fs.mountPoint (if fs.device != null then fs.device else "/dev/disk/by-label/${fs.label}") fs.fsType (builtins.concatStringsSep "," fs.options) ];
326 in pkgs.writeText "initrd-fsinfo" (concatStringsSep "\n" (concatMap f fileSystems));
328 setHostId = optionalString (config.networking.hostId != null) ''
329 hi="${config.networking.hostId}"
330 ${if pkgs.stdenv.hostPlatform.isBigEndian then ''
331 echo -ne "\x''${hi:0:2}\x''${hi:2:2}\x''${hi:4:2}\x''${hi:6:2}" > /etc/hostid
333 echo -ne "\x''${hi:6:2}\x''${hi:4:2}\x''${hi:2:2}\x''${hi:0:2}" > /etc/hostid
339 # The closure of the init script of boot stage 1 is what we put in
340 # the initial RAM disk.
341 initialRamdisk = pkgs.makeInitrd {
342 name = "initrd-${kernel-name}";
343 inherit (config.boot.initrd) compressor compressorArgs prepend;
346 [ { object = bootStage1;
349 { object = "${modulesClosure}/lib";
352 { object = "${pkgs.kmod-blacklist-ubuntu}/modprobe.conf";
353 symlink = "/etc/modprobe.d/ubuntu.conf";
355 { object = config.environment.etc."modprobe.d/nixos.conf".source;
356 symlink = "/etc/modprobe.d/nixos.conf";
358 { object = pkgs.kmod-debian-aliases;
359 symlink = "/etc/modprobe.d/debian.conf";
361 ] ++ lib.optionals config.services.multipath.enable [
362 { object = pkgs.runCommand "multipath.conf" {
363 src = config.environment.etc."multipath.conf".text;
364 preferLocalBuild = true;
368 substituteInPlace $out \
369 --replace ${config.services.multipath.package}/lib ${extraUtils}/lib
371 symlink = "/etc/multipath.conf";
373 ] ++ (lib.mapAttrsToList
377 object = options.source;
380 config.boot.initrd.extraFiles);
383 # Script to add secret files to the initrd at bootloader update time
384 initialRamdiskSecretAppender =
386 compressorExe = initialRamdisk.compressorExecutableFunction pkgs;
387 in pkgs.writeScriptBin "append-initrd-secrets"
389 #!${pkgs.bash}/bin/bash -e
391 echo "USAGE: $0 INITRD_FILE" >&2
392 echo "Appends this configuration's secrets to INITRD_FILE" >&2
395 if [ $# -ne 1 ]; then
400 if [ "$1"x = "--helpx" ]; then
405 ${lib.optionalString (config.boot.initrd.secrets == {})
408 export PATH=${pkgs.coreutils}/bin:${pkgs.cpio}/bin:${pkgs.gzip}/bin:${pkgs.findutils}/bin
411 if [ -n "$tmp" -a -d "$tmp" ]; then
417 tmp=$(mktemp -d ''${TMPDIR:-/tmp}/initrd-secrets.XXXXXXXXXX)
419 ${lib.concatStringsSep "\n" (mapAttrsToList (dest: source:
420 let source' = if source == null then dest else toString source; in
422 mkdir -p $(dirname "$tmp/.initrd-secrets/${dest}")
423 cp -a ${source'} "$tmp/.initrd-secrets/${dest}"
425 ) config.boot.initrd.secrets)
428 # mindepth 1 so that we don't change the mode of /
429 (cd "$tmp" && find . -mindepth 1 | xargs touch -amt 197001010000 && find . -mindepth 1 -print0 | sort -z | cpio --quiet -o -H newc -R +0:+0 --reproducible --null) | \
430 ${compressorExe} ${lib.escapeShellArgs initialRamdisk.compressorArgs} >> "$1"
438 boot.resumeDevice = mkOption {
441 example = "/dev/sda3";
443 Device for manual resume attempt during boot. This should be used primarily
444 if you want to resume from file. If left empty, the swap partitions are used.
445 Specify here the device where the file resides.
446 You should also use {var}`boot.kernelParams` to specify
451 boot.initrd.enable = mkOption {
453 default = !config.boot.isContainer;
454 defaultText = literalExpression "!config.boot.isContainer";
456 Whether to enable the NixOS initial RAM disk (initrd). This may be
457 needed to perform some initialisation tasks (like mounting
458 network/encrypted file systems) before continuing the boot process.
462 boot.initrd.extraFiles = mkOption {
468 type = types.package;
469 description = "The object to make available inside the initrd.";
474 Extra files to link and copy in to the initrd.
478 boot.initrd.prepend = mkOption {
480 type = types.listOf types.str;
482 Other initrd files to prepend to the final initrd we are building.
486 boot.initrd.checkJournalingFS = mkOption {
490 Whether to run {command}`fsck` on journaling filesystems such as ext3.
494 boot.initrd.preLVMCommands = mkOption {
498 Shell commands to be executed immediately before LVM discovery.
502 boot.initrd.preDeviceCommands = mkOption {
506 Shell commands to be executed before udev is started to create
511 boot.initrd.postDeviceCommands = mkOption {
515 Shell commands to be executed immediately after stage 1 of the
516 boot has loaded kernel modules and created device nodes in
521 boot.initrd.postResumeCommands = mkOption {
525 Shell commands to be executed immediately after attempting to resume.
529 boot.initrd.postMountCommands = mkOption {
533 Shell commands to be executed immediately after the stage 1
534 filesystems have been mounted.
538 boot.initrd.preFailCommands = mkOption {
542 Shell commands to be executed before the failure prompt is shown.
546 boot.initrd.extraUtilsCommands = mkOption {
551 Shell commands to be executed in the builder of the
552 extra-utils derivation. This can be used to provide
553 additional utilities in the initial ramdisk.
557 boot.initrd.extraUtilsCommandsTest = mkOption {
562 Shell commands to be executed in the builder of the
563 extra-utils derivation after patchelf has done its
564 job. This can be used to test additional utilities
565 copied in extraUtilsCommands.
569 boot.initrd.extraUdevRulesCommands = mkOption {
574 Shell commands to be executed in the builder of the
575 udev-rules derivation. This can be used to add
576 additional udev rules in the initial ramdisk.
580 boot.initrd.compressor = mkOption {
582 if lib.versionAtLeast config.boot.kernelPackages.kernel.version "5.9"
586 defaultText = literalMD "`zstd` if the kernel supports it (5.9+), `gzip` if not";
587 type = types.either types.str (types.functionTo types.str);
589 The compressor to use on the initrd image. May be any of:
591 - The name of one of the predefined compressors, see {file}`pkgs/build-support/kernel/initrd-compressor-meta.nix` for the definitions.
592 - A function which, given the nixpkgs package set, returns the path to a compressor tool, e.g. `pkgs: "''${pkgs.pigz}/bin/pigz"`
593 - (not recommended, because it does not work when cross-compiling) the full path to a compressor tool, e.g. `"''${pkgs.pigz}/bin/pigz"`
595 The given program should read data from stdin and write it to stdout compressed.
600 boot.initrd.compressorArgs = mkOption {
602 type = types.nullOr (types.listOf types.str);
603 description = "Arguments to pass to the compressor for the initrd image, or null to use the compressor's defaults.";
606 boot.initrd.secrets = mkOption
608 type = types.attrsOf (types.nullOr types.path);
610 Secrets to append to the initrd. The attribute name is the
611 path the secret should have inside the initrd, the value
612 is the path it should be copied from (or null for the same
613 path inside and out).
615 Note that `nixos-rebuild switch` will generate the initrd
616 also for past generations, so if secrets are moved or deleted
617 you will also have to garbage collect the generations that
620 example = literalExpression
622 { "/etc/dropbear/dropbear_rsa_host_key" =
623 ./secret-dropbear-key;
628 boot.initrd.supportedFilesystems = mkOption {
630 inherit (options.boot.supportedFilesystems) example type description;
633 boot.initrd.verbose = mkOption {
637 Verbosity of the initrd. Please note that disabling verbosity removes
638 only the mandatory messages generated by the NixOS scripts. For a
639 completely silent boot, you might also want to set the two following
640 configuration options:
642 - `boot.consoleLogLevel = 0;`
643 - `boot.kernelParams = [ "quiet" "udev.log_level=3" ];`
647 boot.loader.supportsInitrdSecrets = mkOption
652 Whether the bootloader setup runs append-initrd-secrets.
653 If not, any needed secrets must be copied into the initrd
654 and thus added to the store.
658 fileSystems = mkOption {
659 type = with lib.types; attrsOf (submodule {
660 options.neededForBoot = mkOption {
664 If set, this file system will be mounted in the initial ramdisk.
665 Note that the file system will always be mounted in the initial
666 ramdisk if its mount point is one of the following:
667 ${concatStringsSep ", " (
668 forEach utils.pathsNeededForBoot (i: "{file}`${i}`")
677 config = mkIf config.boot.initrd.enable {
679 { assertion = !config.boot.initrd.systemd.enable -> any (fs: fs.mountPoint == "/") fileSystems;
680 message = "The ‘fileSystems’ option does not specify your root file system.";
682 { assertion = let inherit (config.boot) resumeDevice; in
683 resumeDevice == "" || builtins.substring 0 1 resumeDevice == "/";
684 message = "boot.resumeDevice has to be an absolute path."
685 + " Old \"x:y\" style is no longer supported.";
687 # TODO: remove when #85000 is fixed
688 { assertion = !config.boot.loader.supportsInitrdSecrets ->
690 builtins.isPath source ||
691 (builtins.isString source && hasPrefix builtins.storeDir source))
692 (attrValues config.boot.initrd.secrets);
694 boot.loader.initrd.secrets values must be unquoted paths when
695 using a bootloader that doesn't natively support initrd
698 boot.initrd.secrets = {
699 "/etc/secret" = /path/to/secret;
702 Note that this will result in all secrets being stored
703 world-readable in the Nix store!
708 system.build = mkMerge [
709 { inherit bootStage1 initialRamdiskSecretAppender extraUtils; }
711 # generated in nixos/modules/system/boot/systemd/initrd.nix
712 (mkIf (!config.boot.initrd.systemd.enable) { inherit initialRamdisk; })
715 system.requiredKernelConfig = with config.lib.kernelConfig; [
717 (isYes "BLK_DEV_INITRD")
720 boot.initrd.supportedFilesystems = map (fs: fs.fsType) fileSystems;
724 (mkRenamedOptionModule [ "boot" "initrd" "mdadmConf" ] [ "boot" "swraid" "mdadmConf" ])