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
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
'';
|
||||
}
|
||||
Reference in New Issue
Block a user