From feaf19ea26038d7e6cc7c163d82fd2cfea0ffd31 Mon Sep 17 00:00:00 2001 From: Ilan Joselevich Date: Sun, 14 Jun 2026 14:47:52 +0300 Subject: [PATCH] nixos/systemd-initrd: skip activation when init= is not a NixOS system initrd-nixos-activation ran the closure's prepare-root unconditionally. For a non-NixOS init= there is no prepare-root, so the service failed, and since initrd-switch-root.service requires it, switch-root never ran and the machine dropped to the emergency shell. This broke init=/bin/sh recovery, and microVMs that serve /nix/store over virtiofs and boot an arbitrary binary as init=. initrd-find-nixos-closure already detects this and writes a non-empty NEW_INIT to /etc/switch-root.conf (empty for a NixOS init). Read it via the same EnvironmentFile= initrd-switch-root uses and skip activation when it's set, so a non-NixOS init= switch-roots into its target directly. The NixOS path is unchanged. Also add a test booting a non-NixOS init=. It uses a store path rather than /bin/sh: a real root already has /bin/sh and an os-release, but a fresh test root has neither, so the test uses a tmpfs root and writes os-release first. Assisted-by: Claude:claude-opus-4-8 --- nixos/modules/system/boot/systemd/initrd.nix | 9 ++++ nixos/tests/all-tests.nix | 1 + nixos/tests/systemd-initrd-non-nixos.nix | 53 ++++++++++++++++++++ 3 files changed, 63 insertions(+) create mode 100644 nixos/tests/systemd-initrd-non-nixos.nix diff --git a/nixos/modules/system/boot/systemd/initrd.nix b/nixos/modules/system/boot/systemd/initrd.nix index f242cb8c6a5b..b53365ff26cf 100644 --- a/nixos/modules/system/boot/systemd/initrd.nix +++ b/nixos/modules/system/boot/systemd/initrd.nix @@ -763,6 +763,7 @@ in ]; }; serviceConfig.Type = "oneshot"; + serviceConfig.EnvironmentFile = "-/etc/switch-root.conf"; description = "NixOS Activation"; script = # bash @@ -770,6 +771,14 @@ in set -uo pipefail export PATH="/bin:${cfg.package.util-linux}/bin" + # A non-NixOS closure (e.g. init=/bin/sh) has no prepare-root; + # initrd-find-nixos-closure records this as a non-empty NEW_INIT. + # Skip activation and let initrd-switch-root hand over to it directly. + if [ -n "''${NEW_INIT:-}" ]; then + echo "$NEW_INIT is not a NixOS system - not activating" + exit 0 + fi + closure="$(realpath /nixos-closure)" # Initialize the system diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 93ec2703c3e5..ef5f74aa8da1 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -1628,6 +1628,7 @@ in "i686-linux" ] ./initrd-network-openvpn { systemdStage1 = true; }; systemd-initrd-networkd-ssh = runTest ./systemd-initrd-networkd-ssh.nix; + systemd-initrd-non-nixos = runTest ./systemd-initrd-non-nixos.nix; systemd-initrd-shutdown = runTest { imports = [ ./systemd-shutdown.nix ]; _module.args.systemdStage1 = true; diff --git a/nixos/tests/systemd-initrd-non-nixos.nix b/nixos/tests/systemd-initrd-non-nixos.nix new file mode 100644 index 000000000000..225a98afbca2 --- /dev/null +++ b/nixos/tests/systemd-initrd-non-nixos.nix @@ -0,0 +1,53 @@ +{ lib, pkgs, ... }: +let + marker = "REACHED NON-NIXOS INIT AS PID 1"; + + # A non-NixOS init (no prepare-root). We use a store path, not literal + # /bin/sh: a fresh disk has no /bin/sh yet (it is created by the activation a + # non-NixOS init skips), while the store is always mounted; init=/bin/sh works + # the same on a real system. Writes a marker, then stays alive so PID 1 lives. + nonNixosInit = pkgs.writeShellScriptBin "non-nixos" '' + echo "${marker}" > /dev/console + exec ${pkgs.coreutils}/bin/sleep infinity + ''; +in +{ + name = "systemd-initrd-non-nixos"; + + nodes.machine = { + boot.initrd.systemd.enable = true; + + virtualisation = { + # tmpfs root, like real non-NixOS closure init= microvm consumers. + diskImage = null; + + graphics = false; + }; + + # Speed up wait_for_console. + boot.consoleLogLevel = lib.mkForce 3; + boot.initrd.systemd.managerEnvironment.SYSTEMD_LOG_LEVEL = "warning"; + + # switch-root needs an os-release on the target root. A real system has one + # on disk; our fresh tmpfs does not, so create it. + boot.initrd.systemd.tmpfiles.settings."10-os-release"."/sysroot/etc/os-release".f = { + mode = "0644"; + argument = "ID=test-non-nixos"; + }; + }; + + testScript = '' + import os + + # The last init= on the cmdline wins; QEMU_KERNEL_PARAMS is appended after + # the default one, so this boots our non-NixOS init. + os.environ["QEMU_KERNEL_PARAMS"] = "init=${lib.getExe nonNixosInit}" + + machine.start() + + # If activation does not skip the non-NixOS init, switch-root is blocked and + # the machine drops to emergency mode: the marker never appears and this + # times out. + machine.wait_for_console_text("${marker}", timeout=300) + ''; +}