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.runCommand "initrd-kmod-blacklist-ubuntu" {
353 src = "${pkgs.kmod-blacklist-ubuntu}/modprobe.conf";
354 preferLocalBuild = true;
357 ${pkgs.buildPackages.perl}/bin/perl -0pe 's/## file: iwlwifi.conf(.+?)##/##/s;' $src > $out
359 symlink = "/etc/modprobe.d/ubuntu.conf";
361 { object = config.environment.etc."modprobe.d/nixos.conf".source;
362 symlink = "/etc/modprobe.d/nixos.conf";
364 { object = pkgs.kmod-debian-aliases;
365 symlink = "/etc/modprobe.d/debian.conf";
367 ] ++ lib.optionals config.services.multipath.enable [
368 { object = pkgs.runCommand "multipath.conf" {
369 src = config.environment.etc."multipath.conf".text;
370 preferLocalBuild = true;
374 substituteInPlace $out \
375 --replace ${config.services.multipath.package}/lib ${extraUtils}/lib
377 symlink = "/etc/multipath.conf";
379 ] ++ (lib.mapAttrsToList
383 object = options.source;
386 config.boot.initrd.extraFiles);
389 # Script to add secret files to the initrd at bootloader update time
390 initialRamdiskSecretAppender =
392 compressorExe = initialRamdisk.compressorExecutableFunction pkgs;
393 in pkgs.writeScriptBin "append-initrd-secrets"
395 #!${pkgs.bash}/bin/bash -e
397 echo "USAGE: $0 INITRD_FILE" >&2
398 echo "Appends this configuration's secrets to INITRD_FILE" >&2
401 if [ $# -ne 1 ]; then
406 if [ "$1"x = "--helpx" ]; then
411 ${lib.optionalString (config.boot.initrd.secrets == {})
414 export PATH=${pkgs.coreutils}/bin:${pkgs.libarchive}/bin:${pkgs.gzip}/bin:${pkgs.findutils}/bin
417 if [ -n "$tmp" -a -d "$tmp" ]; then
423 tmp=$(mktemp -d ''${TMPDIR:-/tmp}/initrd-secrets.XXXXXXXXXX)
425 ${lib.concatStringsSep "\n" (mapAttrsToList (dest: source:
426 let source' = if source == null then dest else toString source; in
428 mkdir -p $(dirname "$tmp/.initrd-secrets/${dest}")
429 cp -a ${source'} "$tmp/.initrd-secrets/${dest}"
431 ) config.boot.initrd.secrets)
434 # mindepth 1 so that we don't change the mode of /
435 (cd "$tmp" && find . -mindepth 1 | xargs touch -amt 197001010000 && find . -mindepth 1 -print0 | sort -z | bsdtar --uid 0 --gid 0 -cnf - -T - | bsdtar --null -cf - --format=newc @-) | \
436 ${compressorExe} ${lib.escapeShellArgs initialRamdisk.compressorArgs} >> "$1"
444 boot.resumeDevice = mkOption {
447 example = "/dev/sda3";
449 Device for manual resume attempt during boot. This should be used primarily
450 if you want to resume from file. If left empty, the swap partitions are used.
451 Specify here the device where the file resides.
452 You should also use {var}`boot.kernelParams` to specify
457 boot.initrd.enable = mkOption {
459 default = !config.boot.isContainer;
460 defaultText = literalExpression "!config.boot.isContainer";
462 Whether to enable the NixOS initial RAM disk (initrd). This may be
463 needed to perform some initialisation tasks (like mounting
464 network/encrypted file systems) before continuing the boot process.
468 boot.initrd.extraFiles = mkOption {
474 type = types.package;
475 description = "The object to make available inside the initrd.";
480 Extra files to link and copy in to the initrd.
484 boot.initrd.prepend = mkOption {
486 type = types.listOf types.str;
488 Other initrd files to prepend to the final initrd we are building.
492 boot.initrd.checkJournalingFS = mkOption {
496 Whether to run {command}`fsck` on journaling filesystems such as ext3.
500 boot.initrd.preLVMCommands = mkOption {
504 Shell commands to be executed immediately before LVM discovery.
508 boot.initrd.preDeviceCommands = mkOption {
512 Shell commands to be executed before udev is started to create
517 boot.initrd.postDeviceCommands = mkOption {
521 Shell commands to be executed immediately after stage 1 of the
522 boot has loaded kernel modules and created device nodes in
527 boot.initrd.postResumeCommands = mkOption {
531 Shell commands to be executed immediately after attempting to resume.
535 boot.initrd.postMountCommands = mkOption {
539 Shell commands to be executed immediately after the stage 1
540 filesystems have been mounted.
544 boot.initrd.preFailCommands = mkOption {
548 Shell commands to be executed before the failure prompt is shown.
552 boot.initrd.extraUtilsCommands = mkOption {
557 Shell commands to be executed in the builder of the
558 extra-utils derivation. This can be used to provide
559 additional utilities in the initial ramdisk.
563 boot.initrd.extraUtilsCommandsTest = mkOption {
568 Shell commands to be executed in the builder of the
569 extra-utils derivation after patchelf has done its
570 job. This can be used to test additional utilities
571 copied in extraUtilsCommands.
575 boot.initrd.extraUdevRulesCommands = mkOption {
580 Shell commands to be executed in the builder of the
581 udev-rules derivation. This can be used to add
582 additional udev rules in the initial ramdisk.
586 boot.initrd.compressor = mkOption {
588 if lib.versionAtLeast config.boot.kernelPackages.kernel.version "5.9"
592 defaultText = literalMD "`zstd` if the kernel supports it (5.9+), `gzip` if not";
593 type = types.either types.str (types.functionTo types.str);
595 The compressor to use on the initrd image. May be any of:
597 - The name of one of the predefined compressors, see {file}`pkgs/build-support/kernel/initrd-compressor-meta.nix` for the definitions.
598 - A function which, given the nixpkgs package set, returns the path to a compressor tool, e.g. `pkgs: "''${pkgs.pigz}/bin/pigz"`
599 - (not recommended, because it does not work when cross-compiling) the full path to a compressor tool, e.g. `"''${pkgs.pigz}/bin/pigz"`
601 The given program should read data from stdin and write it to stdout compressed.
606 boot.initrd.compressorArgs = mkOption {
608 type = types.nullOr (types.listOf types.str);
609 description = "Arguments to pass to the compressor for the initrd image, or null to use the compressor's defaults.";
612 boot.initrd.secrets = mkOption
614 type = types.attrsOf (types.nullOr types.path);
616 Secrets to append to the initrd. The attribute name is the
617 path the secret should have inside the initrd, the value
618 is the path it should be copied from (or null for the same
619 path inside and out).
621 Note that `nixos-rebuild switch` will generate the initrd
622 also for past generations, so if secrets are moved or deleted
623 you will also have to garbage collect the generations that
626 example = literalExpression
628 { "/etc/dropbear/dropbear_rsa_host_key" =
629 ./secret-dropbear-key;
634 boot.initrd.supportedFilesystems = mkOption {
636 inherit (options.boot.supportedFilesystems) example type description;
639 boot.initrd.verbose = mkOption {
643 Verbosity of the initrd. Please note that disabling verbosity removes
644 only the mandatory messages generated by the NixOS scripts. For a
645 completely silent boot, you might also want to set the two following
646 configuration options:
648 - `boot.consoleLogLevel = 0;`
649 - `boot.kernelParams = [ "quiet" "udev.log_level=3" ];`
653 boot.loader.supportsInitrdSecrets = mkOption
658 Whether the bootloader setup runs append-initrd-secrets.
659 If not, any needed secrets must be copied into the initrd
660 and thus added to the store.
664 fileSystems = mkOption {
665 type = with lib.types; attrsOf (submodule {
666 options.neededForBoot = mkOption {
670 If set, this file system will be mounted in the initial ramdisk.
671 Note that the file system will always be mounted in the initial
672 ramdisk if its mount point is one of the following:
673 ${concatStringsSep ", " (
674 forEach utils.pathsNeededForBoot (i: "{file}`${i}`")
683 config = mkIf config.boot.initrd.enable {
685 { assertion = !config.boot.initrd.systemd.enable -> any (fs: fs.mountPoint == "/") fileSystems;
686 message = "The ‘fileSystems’ option does not specify your root file system.";
688 { assertion = let inherit (config.boot) resumeDevice; in
689 resumeDevice == "" || builtins.substring 0 1 resumeDevice == "/";
690 message = "boot.resumeDevice has to be an absolute path."
691 + " Old \"x:y\" style is no longer supported.";
693 # TODO: remove when #85000 is fixed
694 { assertion = !config.boot.loader.supportsInitrdSecrets ->
696 builtins.isPath source ||
697 (builtins.isString source && hasPrefix builtins.storeDir source))
698 (attrValues config.boot.initrd.secrets);
700 boot.loader.initrd.secrets values must be unquoted paths when
701 using a bootloader that doesn't natively support initrd
704 boot.initrd.secrets = {
705 "/etc/secret" = /path/to/secret;
708 Note that this will result in all secrets being stored
709 world-readable in the Nix store!
714 system.build = mkMerge [
715 { inherit bootStage1 initialRamdiskSecretAppender extraUtils; }
717 # generated in nixos/modules/system/boot/systemd/initrd.nix
718 (mkIf (!config.boot.initrd.systemd.enable) { inherit initialRamdisk; })
721 system.requiredKernelConfig = with config.lib.kernelConfig; [
723 (isYes "BLK_DEV_INITRD")
726 boot.initrd.supportedFilesystems = map (fs: fs.fsType) fileSystems;
730 (mkRenamedOptionModule [ "boot" "initrd" "mdadmConf" ] [ "boot" "swraid" "mdadmConf" ])