From 83c336fb18fa21df00a99620bb4f6a82b37f2568 Mon Sep 17 00:00:00 2001 From: nikstur Date: Sat, 31 Jan 2026 16:01:53 +0100 Subject: [PATCH] nixos-init: use libpathrs instead of chroot to resolve path in sysroot --- nixos/modules/system/boot/systemd/initrd.nix | 4 +- pkgs/by-name/ni/nixos-init/Cargo.lock | 82 +++++++++++++++++-- pkgs/by-name/ni/nixos-init/Cargo.toml | 1 + pkgs/by-name/ni/nixos-init/README.md | 2 +- pkgs/by-name/ni/nixos-init/package.nix | 2 +- .../ni/nixos-init/src/chroot_realpath.rs | 52 ------------ pkgs/by-name/ni/nixos-init/src/find_etc.rs | 8 +- pkgs/by-name/ni/nixos-init/src/lib.rs | 6 +- pkgs/by-name/ni/nixos-init/src/main.rs | 4 +- pkgs/by-name/ni/nixos-init/src/path.rs | 64 +++++++++++++++ 10 files changed, 152 insertions(+), 73 deletions(-) delete mode 100644 pkgs/by-name/ni/nixos-init/src/chroot_realpath.rs create mode 100644 pkgs/by-name/ni/nixos-init/src/path.rs diff --git a/nixos/modules/system/boot/systemd/initrd.nix b/nixos/modules/system/boot/systemd/initrd.nix index 6afb23bd421c..2c59d224176e 100644 --- a/nixos/modules/system/boot/systemd/initrd.nix +++ b/nixos/modules/system/boot/systemd/initrd.nix @@ -573,7 +573,7 @@ in "${cfg.package.util-linux}/bin/sulogin" # Resolving sysroot symlinks without code exec - "${config.system.nixos-init.package}/bin/chroot-realpath" + "${config.system.nixos-init.package}/bin/resolve-in-root" # Find the etc paths "${config.system.nixos-init.package}/bin/find-etc" ] @@ -664,7 +664,7 @@ in # Resolve symlinks in the init parameter. We need this for some boot loaders # (e.g. boot.loader.generationsDir). - closure="$(chroot-realpath /sysroot "$closure")" + closure="$(resolve-in-root /sysroot "$closure")" # Assume the directory containing the init script is the closure. closure="$(dirname "$closure")" diff --git a/pkgs/by-name/ni/nixos-init/Cargo.lock b/pkgs/by-name/ni/nixos-init/Cargo.lock index 4f44e49b6219..5aee8eedf701 100644 --- a/pkgs/by-name/ni/nixos-init/Cargo.lock +++ b/pkgs/by-name/ni/nixos-init/Cargo.lock @@ -22,7 +22,7 @@ checksum = "75726e2aa2b4c5a9d5c4cf3cb7f24658b6ec861616088f3ef3fb72edc0599286" dependencies = [ "serde", "serde_json", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -31,6 +31,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + [[package]] name = "env_filter" version = "0.1.3" @@ -84,6 +90,15 @@ version = "2.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.16" @@ -92,15 +107,15 @@ checksum = "7ee5b5339afb4c41626dde77b7a611bd4f2c202b897852b4bcf5d03eddc61010" [[package]] name = "libc" -version = "0.2.174" +version = "0.2.180" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" [[package]] name = "linux-raw-sys" -version = "0.9.4" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "log" @@ -123,6 +138,7 @@ dependencies = [ "env_logger", "indoc", "log", + "pathrs", "serde", "serde_json", "tempfile", @@ -134,6 +150,24 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "pathrs" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e1a93ab007fbfbd784b3015b60cae7fc564ed3dc44d3c9f9f4a5043040ad83" +dependencies = [ + "bitflags", + "itertools", + "libc", + "memchr", + "once_cell", + "rustix", + "rustversion", + "static_assertions", + "tempfile", + "thiserror 2.0.18", +] + [[package]] name = "proc-macro2" version = "1.0.103" @@ -160,9 +194,9 @@ checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "rustix" -version = "1.0.8" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" dependencies = [ "bitflags", "errno", @@ -171,6 +205,12 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + [[package]] name = "ryu" version = "1.0.21" @@ -220,6 +260,12 @@ dependencies = [ "serde_core", ] +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "syn" version = "2.0.111" @@ -250,7 +296,16 @@ version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", ] [[package]] @@ -264,6 +319,17 @@ dependencies = [ "syn", ] +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "unicode-ident" version = "1.0.22" diff --git a/pkgs/by-name/ni/nixos-init/Cargo.toml b/pkgs/by-name/ni/nixos-init/Cargo.toml index a6495ad1ffdf..46102be28304 100644 --- a/pkgs/by-name/ni/nixos-init/Cargo.toml +++ b/pkgs/by-name/ni/nixos-init/Cargo.toml @@ -7,6 +7,7 @@ edition = "2024" anyhow = "1.0.98" log = "0.4.27" env_logger = { version = "0.11.8", default-features = false } +pathrs = "0.2.2" serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.145" bootspec = "2.0.0" diff --git a/pkgs/by-name/ni/nixos-init/README.md b/pkgs/by-name/ni/nixos-init/README.md index 7bcf47e792ed..4d1677db0015 100644 --- a/pkgs/by-name/ni/nixos-init/README.md +++ b/pkgs/by-name/ni/nixos-init/README.md @@ -52,7 +52,7 @@ closure. Currently nixos-init comes in at ~500 KiB. - `find-etc`: Finds the `/etc` paths in `/sysroot` so that the initrd doesn't directly depend on the toplevel, reducing the need to rebuild the initrd on every generation. -- `chroot-realpath`: Figures out the canonical path inside a chroot. +- `resolve-in-root`: Figures out the canonical path inside a chroot. ## Future diff --git a/pkgs/by-name/ni/nixos-init/package.nix b/pkgs/by-name/ni/nixos-init/package.nix index aab7354c8856..0e4c26cb2dd3 100644 --- a/pkgs/by-name/ni/nixos-init/package.nix +++ b/pkgs/by-name/ni/nixos-init/package.nix @@ -47,7 +47,7 @@ rustPlatform.buildRustPackage (finalAttrs: { binaries = [ "initrd-init" "find-etc" - "chroot-realpath" + "resolve-in-root" ]; postInstall = '' diff --git a/pkgs/by-name/ni/nixos-init/src/chroot_realpath.rs b/pkgs/by-name/ni/nixos-init/src/chroot_realpath.rs deleted file mode 100644 index 3c4fd293ccb8..000000000000 --- a/pkgs/by-name/ni/nixos-init/src/chroot_realpath.rs +++ /dev/null @@ -1,52 +0,0 @@ -use std::{ - env, - io::{Write, stdout}, - os::unix::ffi::OsStrExt, - os::unix::fs, - path::{Path, PathBuf}, - process::Command, -}; - -use anyhow::{Context, Result, bail}; - -/// Canonicalize `path` in a chroot at the specified `root`. -pub fn canonicalize_in_chroot(root: &str, path: &Path) -> Result { - let output = Command::new("chroot-realpath") - .arg(root) - .arg(path.as_os_str()) - .output() - .context("Failed to run chroot-realpath. Most likely, the binary is not on PATH")?; - - if !output.status.success() { - bail!( - "chroot-realpath exited unsuccessfully: {}", - String::from_utf8_lossy(&output.stderr) - ); - } - - let output = - String::from_utf8(output.stdout).context("Failed to decode stdout of chroot-realpath")?; - - Ok(PathBuf::from(&output)) -} - -/// Entrypoint for the `chroot-realpath` binary. -pub fn chroot_realpath() -> Result<()> { - let args: Vec = env::args().collect(); - - if args.len() != 3 { - bail!("Usage: {} ", args[0]); - } - - fs::chroot(&args[1]).context("Failed to chroot")?; - std::env::set_current_dir("/").context("Failed to change directory")?; - - let path = std::fs::canonicalize(&args[2]) - .with_context(|| format!("Failed to canonicalize {}", args[2]))?; - - stdout() - .write_all(path.into_os_string().as_bytes()) - .context("Failed to write output")?; - - Ok(()) -} diff --git a/pkgs/by-name/ni/nixos-init/src/find_etc.rs b/pkgs/by-name/ni/nixos-init/src/find_etc.rs index 2af0dab6ffbb..8b994e1d7534 100644 --- a/pkgs/by-name/ni/nixos-init/src/find_etc.rs +++ b/pkgs/by-name/ni/nixos-init/src/find_etc.rs @@ -3,7 +3,7 @@ use std::{os::unix, path::Path}; use anyhow::{Context, Result}; use crate::config::Config; -use crate::{SYSROOT_PATH, canonicalize_in_chroot, find_toplevel_in_prefix}; +use crate::{SYSROOT_PATH, find_toplevel_in_prefix, resolve_in_prefix}; /// Entrypoint for the `find-etc` binary. /// @@ -18,14 +18,14 @@ pub fn find_etc() -> Result<()> { let basedir = config .etc_basedir .context("Failed to read etc_basedir from bootspec")?; - let etc_basedir = Path::new(SYSROOT_PATH) - .join(canonicalize_in_chroot(SYSROOT_PATH, Path::new(&basedir))?.strip_prefix("/")?); + let etc_basedir = + Path::new(SYSROOT_PATH).join(resolve_in_prefix(SYSROOT_PATH, &basedir)?.strip_prefix("/")?); let metadata_image = config .etc_metadata_image .context("Failed to read etc_metadata_image from bootspec")?; let etc_metadata_image = Path::new(SYSROOT_PATH) - .join(canonicalize_in_chroot(SYSROOT_PATH, Path::new(&metadata_image))?.strip_prefix("/")?); + .join(resolve_in_prefix(SYSROOT_PATH, &metadata_image)?.strip_prefix("/")?); unix::fs::symlink(etc_basedir, "/etc-basedir").context("Failed to link /etc-basedir")?; unix::fs::symlink(etc_metadata_image, "/etc-metadata-image") diff --git a/pkgs/by-name/ni/nixos-init/src/lib.rs b/pkgs/by-name/ni/nixos-init/src/lib.rs index aa123c5e30c2..2ee90c6f8228 100644 --- a/pkgs/by-name/ni/nixos-init/src/lib.rs +++ b/pkgs/by-name/ni/nixos-init/src/lib.rs @@ -1,10 +1,10 @@ mod activate; -mod chroot_realpath; mod config; mod find_etc; mod fs; mod init; mod initrd_init; +mod path; mod proc_mounts; mod switch_root; @@ -14,10 +14,10 @@ use anyhow::{Context, Result, bail}; pub use crate::{ activate::activate, - chroot_realpath::{canonicalize_in_chroot, chroot_realpath}, find_etc::find_etc, init::init, initrd_init::initrd_init, + path::{resolve_in_prefix, resolve_in_root}, switch_root::switch_root, }; @@ -77,7 +77,7 @@ pub fn verify_init_is_nixos(prefix: &str, path: impl AsRef) -> Result Result { let cmdline = std::fs::read_to_string("/proc/cmdline")?; let init = extract_init(&cmdline)?; - let canonicalized_init = canonicalize_in_chroot(prefix, &init)?; + let canonicalized_init = resolve_in_prefix(prefix, &init)?; log::info!("Found init: {}.", canonicalized_init.display()); Ok(canonicalized_init) } diff --git a/pkgs/by-name/ni/nixos-init/src/main.rs b/pkgs/by-name/ni/nixos-init/src/main.rs index d2ca7a5f3011..a051af590be3 100644 --- a/pkgs/by-name/ni/nixos-init/src/main.rs +++ b/pkgs/by-name/ni/nixos-init/src/main.rs @@ -2,7 +2,7 @@ use std::{env, io::Write, process::ExitCode}; use log::Level; -use nixos_init::{chroot_realpath, find_etc, initrd_init}; +use nixos_init::{find_etc, initrd_init, resolve_in_root}; fn main() -> ExitCode { let arg0 = env::args() @@ -13,7 +13,7 @@ fn main() -> ExitCode { setup_logger(); let entrypoint = match arg0.as_str() { "find-etc" => find_etc, - "chroot-realpath" => chroot_realpath, + "resolve-in-root" => resolve_in_root, "initrd-init" => initrd_init, _ => { log::error!("Command {arg0} unknown"); diff --git a/pkgs/by-name/ni/nixos-init/src/path.rs b/pkgs/by-name/ni/nixos-init/src/path.rs new file mode 100644 index 000000000000..7af9447aaf0d --- /dev/null +++ b/pkgs/by-name/ni/nixos-init/src/path.rs @@ -0,0 +1,64 @@ +use std::{ + env, + io::{Write, stdout}, + os::{ + fd::{AsFd, AsRawFd}, + unix::ffi::OsStrExt, + }, + path::{Path, PathBuf}, +}; + +use anyhow::{Context, Result, bail}; +use pathrs::{ + Root, + procfs::{ProcfsBase, ProcfsHandle}, +}; + +/// Resolve a path inside a prefix. +/// +/// This resolves the path by following all symlinks until the end. +/// +/// Uses `openat(2)` with the `RESOLVE_IN_ROOT` flag. +pub fn resolve_in_prefix(prefix: &str, path: impl AsRef) -> Result { + let root = Root::open(prefix).with_context(|| format!("Failed to open prefix {prefix}"))?; + let handle = root.resolve(&path).with_context(|| { + format!( + "Failed to resolve path {} in prefix {prefix}", + path.as_ref().display() + ) + })?; + + let fd = handle.as_fd().as_raw_fd(); + if fd.is_negative() { + bail!("File descriptor of resolved path is negative") + } + let proc = ProcfsHandle::new().context("Failed to open /proc")?; + let resolved_path = proc + .readlink(ProcfsBase::ProcSelf, format!("fd/{fd}")) + .context("Failed to read path from procfs fd")?; + + // Reading the path of the resolved FD will add the prefix to the path. Nonetheless this path + // has been correctly resolved and we can simply strip the prefix again. + Ok(Path::new("/").join( + resolved_path + .strip_prefix(prefix) + .context("Failed to strip prefix from path")?, + )) +} + +/// Entrypoint for the `resolve-in-root` binary. +pub fn resolve_in_root() -> Result<()> { + let args: Vec = env::args().collect(); + + if args.len() != 3 { + bail!("Usage: {} ", args[0]); + } + + let path = resolve_in_prefix(&args[1], &args[2])?; + + stdout() + .write_all(path.into_os_string().as_bytes()) + .context("Failed to write output")?; + + Ok(()) +}