nixos-init: skip the etc overlay for a non-NixOS init=

find-etc verified the init= was inside a NixOS toplevel and bailed otherwise,
failing initrd-find-etc.service. The etc-overlay mounts require it, so for a
non-NixOS init= (e.g. init=/bin/sh) the initrd dropped to emergency mode before
initrd-init could switch-root into the target.

Make find-etc skip silently for a non-NixOS init=, leaving the /etc-basedir and
/etc-metadata-image symlinks uncreated, and gate the etc-metadata mount, the
/sysroot/etc overlay and the rw-etc service on those symlinks with
ConditionPathExists so they skip instead of fail. initrd-init then switch-roots
into the non-NixOS init directly. The NixOS path is unchanged: the symlinks
exist, so the conditions hold.

Extend the systemd-initrd-non-nixos test with a second node enabling
system.nixos-init.enable, so both the bash initrd-nixos-activation path and the
nixos-init path are covered.

Assisted-by: Claude:claude-opus-4-8
This commit is contained in:
Ilan Joselevich
2026-06-16 17:32:17 +03:00
parent ef86f4c78e
commit f61ff68f63
4 changed files with 46 additions and 20 deletions
@@ -71,6 +71,10 @@
RequiresMountsFor = [
"/sysroot/nix/store"
];
# find-etc only creates this symlink for a NixOS init. For a
# non-NixOS init= (e.g. init=/bin/sh) it is absent, so skip the
# mount instead of failing the whole initrd.
ConditionPathExists = "/etc-metadata-image";
};
requires = [
config.boot.initrd.systemd.services.initrd-find-etc.name
@@ -123,6 +127,8 @@
"/run/nixos-etc-metadata"
];
DefaultDependencies = false;
# Skip for a non-NixOS init=, see the metadata mount above.
ConditionPathExists = "/etc-basedir";
};
}
];
@@ -140,6 +146,8 @@
# before the overlay is mounted.
"/run/nixos-etc-metadata"
];
# Skip for a non-NixOS init=, see the metadata mount above.
ConditionPathExists = "/etc-metadata-image";
};
serviceConfig = {
Type = "oneshot";
+23 -8
View File
@@ -10,11 +10,8 @@ let
echo "${marker}" > /dev/console
exec ${pkgs.coreutils}/bin/sleep infinity
'';
in
{
name = "systemd-initrd-non-nixos";
nodes.machine = {
common = {
boot.initrd.systemd.enable = true;
virtualisation = {
@@ -35,6 +32,20 @@ in
argument = "ID=test-non-nixos";
};
};
in
{
name = "systemd-initrd-non-nixos";
nodes = {
bashActivation = common;
nixosInit = {
imports = [ common ];
system.nixos-init.enable = true;
system.etc.overlay.enable = true;
services.userborn.enable = true;
};
};
testScript = ''
import os
@@ -43,11 +54,15 @@ in
# the default one, so this boots our non-NixOS init.
os.environ["QEMU_KERNEL_PARAMS"] = "init=${lib.getExe nonNixosInit}"
machine.start()
start_all()
# 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
# If a code path does not skip the non-NixOS init, switch-root is blocked and
# the machine drops to emergency mode: the marker never appears and the wait
# times out.
machine.wait_for_console_text("${marker}", timeout=300)
with subtest("bash initrd-nixos-activation skips a non-NixOS init"):
bashActivation.wait_for_console_text("${marker}", timeout=300)
with subtest("nixos-init switches to a non-NixOS init directly"):
nixosInit.wait_for_console_text("${marker}", timeout=300)
'';
}
+15 -2
View File
@@ -3,7 +3,7 @@ use std::{os::unix, path::Path};
use anyhow::{Context, Result};
use crate::config::Config;
use crate::{SYSROOT_PATH, find_toplevel_in_prefix, resolve_in_prefix};
use crate::{SYSROOT_PATH, find_init_in_prefix, resolve_in_prefix, verify_init_is_nixos};
/// Entrypoint for the `find-etc` binary.
///
@@ -12,7 +12,20 @@ use crate::{SYSROOT_PATH, find_toplevel_in_prefix, resolve_in_prefix};
/// This avoids needing a reference to the toplevel embedded in the initrd and thus reduces the
/// need to re-build it.
pub fn find_etc() -> Result<()> {
let toplevel = find_toplevel_in_prefix(SYSROOT_PATH)?;
let init_in_sysroot =
find_init_in_prefix(SYSROOT_PATH).context("Failed to find init in sysroot")?;
// A non-NixOS init= (e.g. init=/bin/sh) has no etc metadata image. Skip
// without creating the symlinks: the etc-overlay mounts are gated on them
// and so skip too, and initrd-init switches root to the init directly.
let Ok(toplevel) = verify_init_is_nixos(SYSROOT_PATH, &init_in_sysroot) else {
log::info!(
"{} is not a NixOS system - not setting up the etc overlay.",
init_in_sysroot.display()
);
return Ok(());
};
let config = Config::from_toplevel(&toplevel, SYSROOT_PATH)?;
let basedir = config
-10
View File
@@ -27,16 +27,6 @@ pub use crate::{
pub const SYSROOT_PATH: &str = "/sysroot";
/// Find the path to the toplevel closure of the system in a prefix.
///
/// Uses the `init=` parameter on the kernel command-line.
///
/// Returns the relative path of the init to the prefix, e.g. without the `/sysroot` prefix.
pub fn find_toplevel_in_prefix(prefix: &str) -> Result<PathBuf> {
let init_in_sysroot = find_init_in_prefix(prefix)?;
verify_init_is_nixos(prefix, init_in_sysroot)
}
/// Verify that an init path is inside a `NixOS` toplevel directory.
///
/// If the path is verified, returns the path to the toplevel.