nixos-init: use libpathrs instead of chroot to resolve path in sysroot

This commit is contained in:
nikstur
2026-01-31 16:01:53 +01:00
parent 54212ba31c
commit 83c336fb18
10 changed files with 152 additions and 73 deletions

View File

@@ -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")"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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

View File

@@ -47,7 +47,7 @@ rustPlatform.buildRustPackage (finalAttrs: {
binaries = [
"initrd-init"
"find-etc"
"chroot-realpath"
"resolve-in-root"
];
postInstall = ''

View File

@@ -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<PathBuf> {
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<String> = env::args().collect();
if args.len() != 3 {
bail!("Usage: {} <chroot> <path>", 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(())
}

View File

@@ -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")

View File

@@ -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<Path>) -> Result<Path
pub fn find_init_in_prefix(prefix: &str) -> Result<PathBuf> {
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)
}

View File

@@ -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");

View File

@@ -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<Path>) -> Result<PathBuf> {
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<String> = env::args().collect();
if args.len() != 3 {
bail!("Usage: {} <root> <path>", 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(())
}