From ad1433f9e3a6c14c8f276c5d167528f4c7563f46 Mon Sep 17 00:00:00 2001 From: Rahul Butani Date: Wed, 9 Feb 2022 18:55:00 -0600 Subject: [PATCH 1/5] mount-paths: first pass at extra profile-relative and absolute paths for the chroot --- src/main.rs | 326 ++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 290 insertions(+), 36 deletions(-) diff --git a/src/main.rs b/src/main.rs index 66a62e1..0361c83 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,18 +1,24 @@ -use nix::mount::{mount, MsFlags}; -use nix::sched::{unshare, CloneFlags}; -use nix::sys::signal::{kill, Signal}; -use nix::sys::wait::{waitpid, WaitPidFlag, WaitStatus}; -use nix::unistd::{self, fork, ForkResult}; -use std::env; -use std::ffi::OsStr; -use std::fs; -use std::io; -use std::io::prelude::*; -use std::os::unix::fs::symlink; -use std::os::unix::process::CommandExt; -use std::path::{Path, PathBuf}; -use std::process; -use std::string::String; +use std::{ + borrow::ToOwned, + env, + ffi::{OsString, OsStr}, + fs::{self, DirEntry}, + io::{self, Write}, + os::unix::{ + fs::symlink, + process::CommandExt, + }, + path::{Path, PathBuf}, + process, +}; + +use nix::{ + mount::{mount, MsFlags}, + sched::{unshare, CloneFlags}, + sys::signal::{kill, Signal}, + sys::wait::{waitpid, WaitPidFlag, WaitStatus}, + unistd::{self, fork, ForkResult}, +}; mod mkdtemp; @@ -35,17 +41,154 @@ fn bind_mount(source: &Path, dest: &Path) { } } +/// When constructing the chroot, the mounts we make either mirror the +/// directory structure in `/` or are explicit user provided mounts that do +/// not necessarily have a source location that mirrors the mount location +/// (i.e. `/home/foo/my/special/groups` -> `/etc/group`). +/// +/// We represent the former as [`DirEntry`]s and the latter as regular +/// [`Path`]s. +/// +/// We do this instead of just passing around [`PathBuf`]s because `DirEntry`s +/// (which we get when we iterate over `/` recursively as part of mirroring it) +/// have additional guarantees like `file_name` being infallible. +#[derive(Debug, Clone, Copy)] +pub enum DirEntryOrExplicitMount<'a> { + /// Assumed to share it's directory with the `rootdir` of the [`RunChroot`] + /// its used with (this is not enforced, however). + DirEntry(&'a DirEntry), + /// Assumed to have a different directory than the `rootdir` of the + /// [`RunChroot`] it's used with. + /// + /// For example to mount `/home/foo/bar` to `/bin/bar` you would pass an + /// `ExplicitMount("/home/foo/bar")` to a `RunChroot` with `rootdir` + /// `"/bin"`. + /// + /// This path *can* be `/`. + ExplicitMount { src: &'a Path, dst_file_name: Option<&'a OsStr> }, +} + +impl<'a> From<&'a DirEntry> for DirEntryOrExplicitMount<'a> { + fn from(de: &'a DirEntry) -> DirEntryOrExplicitMount { + DirEntryOrExplicitMount::DirEntry(de) + } +} + +impl<'a> DirEntryOrExplicitMount<'a> { + fn explicit_mount_with_dest_file_name(mount: &'a Path, dst_file: &'a (impl AsRef + 'a)) -> Self { + DirEntryOrExplicitMount::ExplicitMount { + src: mount, + dst_file_name: dst_file.as_ref().file_name(), + } + } +} + +impl DirEntryOrExplicitMount<'_> { + fn file_name(&self) -> Option { + use DirEntryOrExplicitMount::*; + + match self { + DirEntry(d) => Some(d.file_name()), + ExplicitMount { dst_file_name, .. } => dst_file_name.map(|p| p.to_owned()), + } + } + + fn path(&self) -> PathBuf { + use DirEntryOrExplicitMount::*; + + match self { + DirEntry(d) => d.path(), + ExplicitMount { src, .. } => (*src).to_owned(), + } + } + + fn metadata(&self) -> io::Result { + use DirEntryOrExplicitMount::*; + + match self { + DirEntry(d) => d.metadata(), + ExplicitMount { src, .. } => src.symlink_metadata(), + } + } +} + pub struct RunChroot<'a> { rootdir: &'a Path, + nixdir: &'a Path, } impl<'a> RunChroot<'a> { - fn new(rootdir: &'a Path) -> Self { - Self { rootdir } + fn new(rootdir: &'a Path, nixdir: &'a Path) -> Self { + Self { rootdir, nixdir } + } + + fn with_rootdir(&self, rootdir: &'a Path) -> Self { + Self { rootdir, nixdir: self.nixdir } + } + + /// Recursively resolves a symlink, replacing references to `/nix` with + /// `self.nixdir` as it goes. + /// + /// `stop_at_first_non_nix_path` stops when it sees a path (symlink or not) + /// that isn't in `/nix`. This exists for [`mirror_symlink`] which + /// intentionally does not resolve symlinks all the way down when mirroring + /// them into the chroot. + fn resolve_nix_path(&self, p: PathBuf, stop_at_first_non_nix_path: bool) -> io::Result { + if p.is_symlink() { + let mut target = fs::read_link(&p)?; + if !target.is_absolute() { + // need to resolve relative symlinks: + target = p.parent().unwrap().join(target); + } + + // replace `/nix` with the actual profile path: + let p = if let Ok(rest) = target.strip_prefix("/nix") { + self.nixdir.join(rest) + } else { + if stop_at_first_non_nix_path { + return Ok(target) + } + + target + }; + + self.resolve_nix_path(p, stop_at_first_non_nix_path) + } else if p.exists() { + Ok(p) + } else { + // peel off components of the path, seeing if at some point we + // hit a symlink containing `/nix` which would explain why we + // couldn't stat the file + let mut i = 0; + let mut path = p.clone(); + + // NOTE: this is the bad N^2 way of doing this; we should actually + // resolve the path from the root onwards + while path.pop() { + i += 1; + + if path.is_symlink() && path.read_link().map(|p| p.starts_with("/nix")).unwrap_or(false) { + // if we did find a parent that's a symlink, resolve it: + let actual_parent = self.resolve_nix_path(path, stop_at_first_non_nix_path)?; + + // append the components we stripped off to the resolved parent: + let parts = p.iter().collect::>(); + let stripped = &parts[parts.len()-i..]; + let path = actual_parent.join(stripped.iter().collect::()); + + // and try again: + return self.resolve_nix_path(path, stop_at_first_non_nix_path); + } + } + + Err(io::ErrorKind::NotFound)? + } } - fn bind_mount_directory(&self, entry: &fs::DirEntry) { - let mountpoint = self.rootdir.join(entry.file_name()); + // We assume `entry` exists and is actually a directory (not a file or symlink), + fn bind_mount_directory<'p>(&self, entry: impl Into>) { + let entry = entry.into(); + let mountpoint = self.rootdir.join(entry.file_name().unwrap_or_default()); // if the destination doesn't exist we can proceed as normal if !mountpoint.exists() { @@ -55,6 +198,8 @@ impl<'a> RunChroot<'a> { } } + eprintln!("BIND DIRECTORY {} -> {}", entry.path().display(), mountpoint.display()); + bind_mount(&entry.path(), &mountpoint) } else { // otherwise, if the dest is also a dir, we can recurse into it @@ -64,7 +209,7 @@ impl<'a> RunChroot<'a> { panic!("failed to list dir {}: {}", entry.path().display(), err) }); - let child = RunChroot::new(&mountpoint); + let child = self.with_rootdir(&mountpoint); for entry in dir { let entry = entry.expect("error while listing subdir"); child.bind_mount_direntry(&entry); @@ -73,8 +218,11 @@ impl<'a> RunChroot<'a> { } } - fn bind_mount_file(&self, entry: &fs::DirEntry) { - let mountpoint = self.rootdir.join(entry.file_name()); + // We assume `entry` exists and is actually a file (not a directory or symlink). + fn bind_mount_file<'p>(&self, entry: impl Into>) { + let entry = entry.into(); + let mountpoint = self.rootdir.join(entry.file_name().unwrap_or_default()); + eprintln!("BIND FILE {} -> {}", entry.path().display(), mountpoint.display()); if mountpoint.exists() { return; } @@ -84,24 +232,51 @@ impl<'a> RunChroot<'a> { bind_mount(&entry.path(), &mountpoint) } - fn mirror_symlink(&self, entry: &fs::DirEntry) { - let link_path = self.rootdir.join(entry.file_name()); + // We assume `entry` exists and either points to a path that exists *or* + // points to a `/nix` path (which we'll attempt to resolve against `self.nixdir`). + fn mirror_symlink<'p>(&self, entry: impl Into>) { + let entry = entry.into(); + let link_path = self.rootdir.join(entry.file_name().unwrap_or_default()); if link_path.exists() { return; } let path = entry.path(); - let target = fs::read_link(&path) + + // stops resolving the symlink at the first non-nix path + let target = self.resolve_nix_path(path.clone(), true) .unwrap_or_else(|err| panic!("failed to resolve symlink {}: {}", &path.display(), err)); - symlink(&target, &link_path).unwrap_or_else(|_| { + + eprintln!("MIRROR SYMLINK {} -> {}", link_path.display(), target.display()); + + symlink(&target, &link_path).unwrap_or_else(|err| { panic!( - "failed to create symlink {} -> {}", + "failed to create symlink {} -> {} ({err:?})", &link_path.display(), &target.display() ) }); } - fn bind_mount_direntry(&self, entry: &fs::DirEntry) { + fn bind_mount_direntry<'p>(&self, entry: impl Into>) { + use DirEntryOrExplicitMount::*; + let mut entry = entry.into(); + + // resolve any `/nix`s now so we can actually stat the file + // + // as with `mirror_symlink`, stop once we hit a non-nix path + let adj_path; + let dst_file_name; + if entry.path().starts_with("/nix") { + adj_path = self.resolve_nix_path(entry.path(), true).unwrap(); + entry = match entry { + DirEntry(d) => { + dst_file_name = d.file_name(); + ExplicitMount { src: &*adj_path, dst_file_name: Some(&dst_file_name) } + }, + ExplicitMount { dst_file_name, .. } => ExplicitMount { src: &*adj_path, dst_file_name } + }; + } + let path = entry.path(); let stat = entry .metadata() @@ -116,7 +291,7 @@ impl<'a> RunChroot<'a> { } } - fn run_chroot(&self, nixdir: &Path, cmd: &str, args: &[String]) { + fn run_chroot(&self, cmd: &str, args: &[String]) { let cwd = env::current_dir().expect("cannot get current working directory"); let uid = unistd::getuid(); @@ -126,7 +301,7 @@ impl<'a> RunChroot<'a> { // create /run/opengl-driver/lib in chroot, to behave like NixOS // (needed for nix pkgs with OpenGL or CUDA support to work) - let ogldir = nixdir.join("var/nix/opengl-driver/lib"); + let ogldir = self.nixdir.join("var/nix/opengl-driver/lib"); if ogldir.is_dir() { let ogl_mount = self.rootdir.join("run/opengl-driver/lib"); fs::create_dir_all(&ogl_mount) @@ -134,13 +309,90 @@ impl<'a> RunChroot<'a> { bind_mount(&ogldir, &ogl_mount); } + // TODO: test mounting in something to `/`; should work + // TODO: test `cargo` or something else where the symlink's name is actually important (both as an explicit bind mount and an incidental one to make sure the logic is right) + + // can be "absolute" (wrt to the profile dir) or relative + let profile_links = vec![ + (Path::new("/sbin/zic"), Path::new("/usr/bin/zic")), + (Path::new("/bin/sh"), Path::new("/bin/sh")), + (Path::new("/bin/bash"), Path::new("/bin/bash")), + (Path::new("/bin/python3"), Path::new("/bin/python3")), + (Path::new("/bin/env"), Path::new("/usr/bin/env")), + ]; + // must be absolute + let regular_links = vec![ + (PathBuf::from("/some/dir/home/.nix-profile/bin/cargo"), Path::new("/bin/cargo")), + (PathBuf::from("/some/dir/config/group"), Path::new("/etc/group")), + ]; + + // mount in explicit mounts (profile relative and absolute): + let user = unistd::User::from_uid(uid).unwrap().unwrap(); + let profile_dir = self.nixdir.join("var/nix/profiles/per-user").join(&user.name).join("profile"); + + + let profile_dir = self.resolve_nix_path(profile_dir, false); + + let explicit_mounts = profile_links + .into_iter() + .filter(|(s, d)| if profile_dir.is_ok() { + true + } else { + eprintln!("Warning: couldn't find a profile for user `{}`; skipping profile mount `{}` -> `{}`", &user.name, s.display(), d.display()); + false + }) + .map(|(mut prof_p, chroot_p)| { + // to allow for both "absolute" and relative paths in the profile relative mounts + if prof_p.is_absolute() { + prof_p = prof_p.strip_prefix("/").unwrap() + } + + (prof_p, chroot_p) + }) + .map(|(prof_p, chroot_p)| (profile_dir.as_ref().unwrap().join(prof_p), chroot_p)) + .chain( + regular_links + .into_iter() + .inspect(|(src, _)| { + if !src.is_absolute() { + panic!("Explicit mount sources (excluding profile mounts) must be absolute paths! `{}` is not absolute.", src.display()) + } + }) + ) + .inspect(|(_, dest)| { + if !dest.is_absolute() { + panic!("All explicit mount destinations must be absolute paths! `{}` is not absolute.", dest.display()) + } + }); + + for (src, dest) in explicit_mounts { + if let Ok(src) = self.resolve_nix_path(src.clone(), true) { + eprintln!("{} -> {}", src.display(), dest.display()); + + let adjusted_dest = dest + .strip_prefix("/") // we have guarantees that `dest` is absolute + .unwrap() + .parent() + .map(ToOwned::to_owned) + .unwrap_or_default(); + let parent = self.rootdir.join(adjusted_dest); + + fs::create_dir_all(&parent).unwrap(); + + let parent = self.with_rootdir(&parent); + parent.bind_mount_direntry(DirEntryOrExplicitMount::explicit_mount_with_dest_file_name(&*src, &dest)); + } else { + eprintln!("warning: explicit mount source `{}` doesn't seem to exist!", src.display()); + } + } + // bind the rest of / stuff into rootdir let nix_root = PathBuf::from("/"); - let dir = fs::read_dir(&nix_root).expect("failed to list /nix directory"); + let dir = fs::read_dir(&nix_root).expect("failed to list / directory"); for entry in dir { - let entry = entry.expect("error while listing from /nix directory"); + let entry = entry.expect("error while listing from / directory"); // do not bind mount an existing nix installation - if entry.file_name() == OsStr::new("nix") { + if entry.file_name() == "nix" { continue; } self.bind_mount_direntry(&entry); @@ -151,13 +403,13 @@ impl<'a> RunChroot<'a> { fs::create_dir(&nix_mount) .unwrap_or_else(|err| panic!("failed to create {}: {}", &nix_mount.display(), err)); mount( - Some(nixdir), + Some(self.nixdir), &nix_mount, Some("none"), MsFlags::MS_BIND | MsFlags::MS_REC, NONE, ) - .unwrap_or_else(|err| panic!("failed to bind mount {} to /nix: {}", nixdir.display(), err)); + .unwrap_or_else(|err| panic!("failed to bind mount {} to /nix: {}", self.nixdir.display(), err)); // chroot unistd::chroot(self.rootdir) @@ -171,6 +423,8 @@ impl<'a> RunChroot<'a> { let _ = file.write_all(b"deny"); } + // println!("cap: {}", std::fs::read_to_string(format!("/proc/self/status")).unwrap()); + let mut uid_map = fs::File::create("/proc/self/uid_map").expect("failed to open /proc/self/uid_map"); uid_map @@ -246,7 +500,7 @@ fn main() { match unsafe { fork() } { Ok(ForkResult::Parent { child, .. }) => wait_for_child(&rootdir, child), - Ok(ForkResult::Child) => RunChroot::new(&rootdir).run_chroot(&nixdir, &args[2], &args[3..]), + Ok(ForkResult::Child) => RunChroot::new(&rootdir, &nixdir).run_chroot(&args[2], &args[3..]), Err(e) => { eprintln!("fork failed: {}", e); } From c9dbf0e0274381f1c6ea47135c24994a9cbfa873 Mon Sep 17 00:00:00 2001 From: Rahul Butani Date: Thu, 10 Feb 2022 00:17:00 -0600 Subject: [PATCH 2/5] mount-paths: add exclude paths --- src/main.rs | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/src/main.rs b/src/main.rs index 0361c83..befa2bb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,7 +13,7 @@ use std::{ }; use nix::{ - mount::{mount, MsFlags}, + mount::{mount, umount, MsFlags}, sched::{unshare, CloneFlags}, sys::signal::{kill, Signal}, sys::wait::{waitpid, WaitPidFlag, WaitStatus}, @@ -246,7 +246,7 @@ impl<'a> RunChroot<'a> { let target = self.resolve_nix_path(path.clone(), true) .unwrap_or_else(|err| panic!("failed to resolve symlink {}: {}", &path.display(), err)); - eprintln!("MIRROR SYMLINK {} -> {}", link_path.display(), target.display()); + eprintln!("MIRROR SYMLINK {} -> {}", target.display(), link_path.display()); symlink(&target, &link_path).unwrap_or_else(|err| { panic!( @@ -284,10 +284,12 @@ impl<'a> RunChroot<'a> { if stat.is_dir() { self.bind_mount_directory(entry); - } else if stat.is_file() { + } else if stat.is_file() || path == Path::new("/dev/null") { self.bind_mount_file(entry); } else if stat.file_type().is_symlink() { self.mirror_symlink(entry); + } else { + panic!("don't know what to do with: {}", path.display()) } } @@ -312,6 +314,10 @@ impl<'a> RunChroot<'a> { // TODO: test mounting in something to `/`; should work // TODO: test `cargo` or something else where the symlink's name is actually important (both as an explicit bind mount and an incidental one to make sure the logic is right) + let mount_exclude_list = vec![ + (Path::new("/var/run/nscd")), + ]; + // can be "absolute" (wrt to the profile dir) or relative let profile_links = vec![ (Path::new("/sbin/zic"), Path::new("/usr/bin/zic")), @@ -324,8 +330,8 @@ impl<'a> RunChroot<'a> { let regular_links = vec![ (PathBuf::from("/some/dir/home/.nix-profile/bin/cargo"), Path::new("/bin/cargo")), (PathBuf::from("/some/dir/config/group"), Path::new("/etc/group")), + (PathBuf::from("/some/dir/config/passwd"), Path::new("/etc/passwd")), ]; - // mount in explicit mounts (profile relative and absolute): let user = unistd::User::from_uid(uid).unwrap().unwrap(); let profile_dir = self.nixdir.join("var/nix/profiles/per-user").join(&user.name).join("profile"); @@ -350,6 +356,12 @@ impl<'a> RunChroot<'a> { (prof_p, chroot_p) }) .map(|(prof_p, chroot_p)| (profile_dir.as_ref().unwrap().join(prof_p), chroot_p)) + .chain( + // TODO: this should actually probably happen first. + mount_exclude_list + .iter() + .map(|&ex| (PathBuf::from("/dev/null"), ex)) + ) .chain( regular_links .into_iter() @@ -398,6 +410,12 @@ impl<'a> RunChroot<'a> { self.bind_mount_direntry(&entry); } + for p in mount_exclude_list { + let mount = self.rootdir.join(p.strip_prefix("/").unwrap()); + eprintln!("UNBIND {}", mount.display()); + umount(&mount).unwrap(); + } + // mount the store let nix_mount = self.rootdir.join("nix"); fs::create_dir(&nix_mount) From 06ffbf1e016c66f50b8c5a61713e1e056602ce29 Mon Sep 17 00:00:00 2001 From: Rahul Butani Date: Thu, 10 Feb 2022 01:22:00 -0600 Subject: [PATCH 3/5] misc: use `log` and `env_logger` --- Cargo.toml | 5 ++++- src/main.rs | 16 +++++++++++----- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7122ada..6a61ba3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,5 +10,8 @@ documentation = "https://github.com/nix-community/nix-user-chroot" repository = "https://github.com/nix-community/nix-user-chroot" [dependencies] -nix = "0.23.1" +env_logger = "0.9" libc = "0.2.117" +log = "0.4" +nix = "0.23.1" + diff --git a/src/main.rs b/src/main.rs index befa2bb..527f808 100644 --- a/src/main.rs +++ b/src/main.rs @@ -32,7 +32,7 @@ fn bind_mount(source: &Path, dest: &Path) { MsFlags::MS_BIND | MsFlags::MS_REC | MsFlags::MS_PRIVATE, NONE, ) { - eprintln!( + log::error!( "failed to bind mount {} to {}: {}", source.display(), dest.display(), @@ -198,7 +198,7 @@ impl<'a> RunChroot<'a> { } } - eprintln!("BIND DIRECTORY {} -> {}", entry.path().display(), mountpoint.display()); + log::info!("BIND DIRECTORY {} -> {}", entry.path().display(), mountpoint.display()); bind_mount(&entry.path(), &mountpoint) } else { @@ -246,7 +246,7 @@ impl<'a> RunChroot<'a> { let target = self.resolve_nix_path(path.clone(), true) .unwrap_or_else(|err| panic!("failed to resolve symlink {}: {}", &path.display(), err)); - eprintln!("MIRROR SYMLINK {} -> {}", target.display(), link_path.display()); + log::info!("MIRROR SYMLINK {} -> {}", target.display(), link_path.display()); symlink(&target, &link_path).unwrap_or_else(|err| { panic!( @@ -379,7 +379,7 @@ impl<'a> RunChroot<'a> { for (src, dest) in explicit_mounts { if let Ok(src) = self.resolve_nix_path(src.clone(), true) { - eprintln!("{} -> {}", src.display(), dest.display()); + log::info!("EXPLICIT {} -> {}", src.display(), dest.display()); let adjusted_dest = dest .strip_prefix("/") // we have guarantees that `dest` is absolute @@ -412,7 +412,7 @@ impl<'a> RunChroot<'a> { for p in mount_exclude_list { let mount = self.rootdir.join(p.strip_prefix("/").unwrap()); - eprintln!("UNBIND {}", mount.display()); + log::info!("UNBIND {}", mount.display()); umount(&mount).unwrap(); } @@ -504,6 +504,12 @@ fn wait_for_child(rootdir: &Path, child_pid: unistd::Pid) -> ! { } fn main() { + let mut builder = env_logger::Builder::new(); + builder + .filter_level(log::LevelFilter::Warn) + .parse_default_env() + .init(); + let args: Vec = env::args().collect(); if args.len() < 3 { eprintln!("Usage: {} \n", args[0]); From fd4c70b636da600a50b6de872673678271cfd0b4 Mon Sep 17 00:00:00 2001 From: Rahul Butani Date: Thu, 10 Feb 2022 03:40:00 -0600 Subject: [PATCH 4/5] mount-paths: read from a config file --- Cargo.lock | 198 ++++++++++++++++++++++++++++++++++-- Cargo.toml | 4 +- src/main.rs | 283 ++++++++++++++++++++++++++++++++-------------------- 3 files changed, 370 insertions(+), 115 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3906026..629b9cb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,11 +2,31 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "aho-corasick" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +dependencies = [ + "memchr", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + [[package]] name = "autocfg" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "bitflags" @@ -16,9 +36,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "cc" -version = "1.0.67" +version = "1.0.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c69b077ad434294d3ce9f1f6143a2a4b89a8a2d54ef813d85003a4fd1137fd" +checksum = "22a9137b95ea06864e018375b72adfb7db6e6f68cfc8df5a04d00288050485ee" [[package]] name = "cfg-if" @@ -26,17 +46,60 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "env_logger" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + [[package]] name = "libc" version = "0.2.117" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e74d72e0f9b65b5b4ca49a346af3976df0f9c61d550727f349ecd559f251a26c" +[[package]] +name = "log" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "memchr" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" + [[package]] name = "memoffset" -version = "0.6.4" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59accc507f1338036a0477ef61afdae33cde60840f4dfe481319ce3ad116ddf9" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" dependencies = [ "autocfg", ] @@ -58,6 +121,129 @@ dependencies = [ name = "nix-user-chroot" version = "1.2.2" dependencies = [ + "env_logger", "libc", + "log", "nix", + "serde", + "serde_derive", + "toml", +] + +[[package]] +name = "proc-macro2" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" + +[[package]] +name = "serde" +version = "1.0.136" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789" + +[[package]] +name = "serde_derive" +version = "1.0.136" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syn" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", ] + +[[package]] +name = "termcolor" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "toml" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" +dependencies = [ + "serde", +] + +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml index 6a61ba3..84b8208 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,4 +14,6 @@ env_logger = "0.9" libc = "0.2.117" log = "0.4" nix = "0.23.1" - +serde = "1.0" +serde_derive = "1.0" +toml = "0.5" diff --git a/src/main.rs b/src/main.rs index 527f808..30374f9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,13 +1,11 @@ use std::{ borrow::ToOwned, + collections::{HashMap, HashSet}, env, - ffi::{OsString, OsStr}, + ffi::{OsStr, OsString}, fs::{self, DirEntry}, io::{self, Write}, - os::unix::{ - fs::symlink, - process::CommandExt, - }, + os::unix::{fs::symlink, process::CommandExt}, path::{Path, PathBuf}, process, }; @@ -19,6 +17,7 @@ use nix::{ sys::wait::{waitpid, WaitPidFlag, WaitStatus}, unistd::{self, fork, ForkResult}, }; +use serde_derive::Deserialize; mod mkdtemp; @@ -65,7 +64,10 @@ pub enum DirEntryOrExplicitMount<'a> { /// `"/bin"`. /// /// This path *can* be `/`. - ExplicitMount { src: &'a Path, dst_file_name: Option<&'a OsStr> }, + ExplicitMount { + src: &'a Path, + dst_file_name: Option<&'a OsStr>, + }, } impl<'a> From<&'a DirEntry> for DirEntryOrExplicitMount<'a> { @@ -75,7 +77,10 @@ impl<'a> From<&'a DirEntry> for DirEntryOrExplicitMount<'a> { } impl<'a> DirEntryOrExplicitMount<'a> { - fn explicit_mount_with_dest_file_name(mount: &'a Path, dst_file: &'a (impl AsRef + 'a)) -> Self { + fn explicit_mount_with_dest_file_name( + mount: &'a Path, + dst_file: &'a (impl AsRef + 'a), + ) -> Self { DirEntryOrExplicitMount::ExplicitMount { src: mount, dst_file_name: dst_file.as_ref().file_name(), @@ -112,6 +117,21 @@ impl DirEntryOrExplicitMount<'_> { } } +#[derive(Debug, Clone, PartialEq, Deserialize)] +pub struct PathConfig<'a> { + excludes: ExcludePaths<'a>, + #[serde(borrow)] + profile: HashMap<&'a Path, &'a Path>, + #[serde(borrow)] + absolute: HashMap<&'a Path, &'a Path>, +} + +#[derive(Debug, Clone, PartialEq, Deserialize)] +pub struct ExcludePaths<'a> { + #[serde(borrow)] + paths: HashSet<&'a Path>, +} + pub struct RunChroot<'a> { rootdir: &'a Path, nixdir: &'a Path, @@ -123,7 +143,10 @@ impl<'a> RunChroot<'a> { } fn with_rootdir(&self, rootdir: &'a Path) -> Self { - Self { rootdir, nixdir: self.nixdir } + Self { + rootdir, + nixdir: self.nixdir, + } } /// Recursively resolves a symlink, replacing references to `/nix` with @@ -133,7 +156,11 @@ impl<'a> RunChroot<'a> { /// that isn't in `/nix`. This exists for [`mirror_symlink`] which /// intentionally does not resolve symlinks all the way down when mirroring /// them into the chroot. - fn resolve_nix_path(&self, p: PathBuf, stop_at_first_non_nix_path: bool) -> io::Result { + fn resolve_nix_path( + &self, + p: PathBuf, + stop_at_first_non_nix_path: bool, + ) -> io::Result { if p.is_symlink() { let mut target = fs::read_link(&p)?; if !target.is_absolute() { @@ -146,7 +173,7 @@ impl<'a> RunChroot<'a> { self.nixdir.join(rest) } else { if stop_at_first_non_nix_path { - return Ok(target) + return Ok(target); } target @@ -167,13 +194,18 @@ impl<'a> RunChroot<'a> { while path.pop() { i += 1; - if path.is_symlink() && path.read_link().map(|p| p.starts_with("/nix")).unwrap_or(false) { + if path.is_symlink() + && path + .read_link() + .map(|p| p.starts_with("/nix")) + .unwrap_or(false) + { // if we did find a parent that's a symlink, resolve it: let actual_parent = self.resolve_nix_path(path, stop_at_first_non_nix_path)?; // append the components we stripped off to the resolved parent: let parts = p.iter().collect::>(); - let stripped = &parts[parts.len()-i..]; + let stripped = &parts[parts.len() - i..]; let path = actual_parent.join(stripped.iter().collect::()); // and try again: @@ -181,7 +213,7 @@ impl<'a> RunChroot<'a> { } } - Err(io::ErrorKind::NotFound)? + Err(io::ErrorKind::NotFound.into()) } } @@ -198,7 +230,11 @@ impl<'a> RunChroot<'a> { } } - log::info!("BIND DIRECTORY {} -> {}", entry.path().display(), mountpoint.display()); + log::info!( + "BIND DIRECTORY {} -> {}", + entry.path().display(), + mountpoint.display() + ); bind_mount(&entry.path(), &mountpoint) } else { @@ -212,7 +248,7 @@ impl<'a> RunChroot<'a> { let child = self.with_rootdir(&mountpoint); for entry in dir { let entry = entry.expect("error while listing subdir"); - child.bind_mount_direntry(&entry); + child.bind_mount_entry(&entry); } } } @@ -222,7 +258,11 @@ impl<'a> RunChroot<'a> { fn bind_mount_file<'p>(&self, entry: impl Into>) { let entry = entry.into(); let mountpoint = self.rootdir.join(entry.file_name().unwrap_or_default()); - eprintln!("BIND FILE {} -> {}", entry.path().display(), mountpoint.display()); + log::info!( + "BIND FILE {} -> {}", + entry.path().display(), + mountpoint.display() + ); if mountpoint.exists() { return; } @@ -243,10 +283,15 @@ impl<'a> RunChroot<'a> { let path = entry.path(); // stops resolving the symlink at the first non-nix path - let target = self.resolve_nix_path(path.clone(), true) + let target = self + .resolve_nix_path(path.clone(), true) .unwrap_or_else(|err| panic!("failed to resolve symlink {}: {}", &path.display(), err)); - log::info!("MIRROR SYMLINK {} -> {}", target.display(), link_path.display()); + log::info!( + "MIRROR SYMLINK {} -> {}", + target.display(), + link_path.display() + ); symlink(&target, &link_path).unwrap_or_else(|err| { panic!( @@ -257,7 +302,7 @@ impl<'a> RunChroot<'a> { }); } - fn bind_mount_direntry<'p>(&self, entry: impl Into>) { + fn bind_mount_entry<'p>(&self, entry: impl Into>) { use DirEntryOrExplicitMount::*; let mut entry = entry.into(); @@ -271,9 +316,15 @@ impl<'a> RunChroot<'a> { entry = match entry { DirEntry(d) => { dst_file_name = d.file_name(); - ExplicitMount { src: &*adj_path, dst_file_name: Some(&dst_file_name) } + ExplicitMount { + src: &*adj_path, + dst_file_name: Some(&dst_file_name), + } + } + ExplicitMount { dst_file_name, .. } => ExplicitMount { + src: &*adj_path, + dst_file_name, }, - ExplicitMount { dst_file_name, .. } => ExplicitMount { src: &*adj_path, dst_file_name } }; } @@ -293,7 +344,7 @@ impl<'a> RunChroot<'a> { } } - fn run_chroot(&self, cmd: &str, args: &[String]) { + fn run_chroot(&self, cmd: &str, args: &[String], path_config: Option>) { let cwd = env::current_dir().expect("cannot get current working directory"); let uid = unistd::getuid(); @@ -314,87 +365,83 @@ impl<'a> RunChroot<'a> { // TODO: test mounting in something to `/`; should work // TODO: test `cargo` or something else where the symlink's name is actually important (both as an explicit bind mount and an incidental one to make sure the logic is right) - let mount_exclude_list = vec![ - (Path::new("/var/run/nscd")), - ]; - - // can be "absolute" (wrt to the profile dir) or relative - let profile_links = vec![ - (Path::new("/sbin/zic"), Path::new("/usr/bin/zic")), - (Path::new("/bin/sh"), Path::new("/bin/sh")), - (Path::new("/bin/bash"), Path::new("/bin/bash")), - (Path::new("/bin/python3"), Path::new("/bin/python3")), - (Path::new("/bin/env"), Path::new("/usr/bin/env")), - ]; - // must be absolute - let regular_links = vec![ - (PathBuf::from("/some/dir/home/.nix-profile/bin/cargo"), Path::new("/bin/cargo")), - (PathBuf::from("/some/dir/config/group"), Path::new("/etc/group")), - (PathBuf::from("/some/dir/config/passwd"), Path::new("/etc/passwd")), - ]; - // mount in explicit mounts (profile relative and absolute): - let user = unistd::User::from_uid(uid).unwrap().unwrap(); - let profile_dir = self.nixdir.join("var/nix/profiles/per-user").join(&user.name).join("profile"); - - - let profile_dir = self.resolve_nix_path(profile_dir, false); - - let explicit_mounts = profile_links - .into_iter() - .filter(|(s, d)| if profile_dir.is_ok() { - true - } else { - eprintln!("Warning: couldn't find a profile for user `{}`; skipping profile mount `{}` -> `{}`", &user.name, s.display(), d.display()); - false - }) - .map(|(mut prof_p, chroot_p)| { - // to allow for both "absolute" and relative paths in the profile relative mounts - if prof_p.is_absolute() { - prof_p = prof_p.strip_prefix("/").unwrap() - } - - (prof_p, chroot_p) - }) - .map(|(prof_p, chroot_p)| (profile_dir.as_ref().unwrap().join(prof_p), chroot_p)) - .chain( - // TODO: this should actually probably happen first. - mount_exclude_list + // mount in explicit mounts (profile relative, absolute, and placeholders to "reserve" the excludes): + if let Some(ref c) = path_config { + let user = unistd::User::from_uid(uid).unwrap().unwrap(); + let profile_dir = self + .nixdir + .join("var/nix/profiles/per-user") + .join(&user.name) + .join("profile"); + let profile_dir = self.resolve_nix_path(profile_dir, false); + + let explicit_mounts = c.profile .iter() - .map(|&ex| (PathBuf::from("/dev/null"), ex)) - ) - .chain( - regular_links - .into_iter() - .inspect(|(src, _)| { - if !src.is_absolute() { - panic!("Explicit mount sources (excluding profile mounts) must be absolute paths! `{}` is not absolute.", src.display()) - } - }) - ) - .inspect(|(_, dest)| { - if !dest.is_absolute() { - panic!("All explicit mount destinations must be absolute paths! `{}` is not absolute.", dest.display()) - } - }); - - for (src, dest) in explicit_mounts { - if let Ok(src) = self.resolve_nix_path(src.clone(), true) { - log::info!("EXPLICIT {} -> {}", src.display(), dest.display()); - - let adjusted_dest = dest - .strip_prefix("/") // we have guarantees that `dest` is absolute - .unwrap() - .parent() - .map(ToOwned::to_owned) - .unwrap_or_default(); - let parent = self.rootdir.join(adjusted_dest); - - fs::create_dir_all(&parent).unwrap(); + .map(|(s, d)| (*s, *d)) + .filter(|(s, d)| if profile_dir.is_ok() { + true + } else { + eprintln!("Warning: couldn't find a profile for user `{}`; skipping profile mount `{}` -> `{}`", &user.name, s.display(), d.display()); + false + }) + .map(|(mut prof_p, chroot_p)| { + // to allow for both "absolute" and relative paths in the profile relative mounts + if prof_p.is_absolute() { + prof_p = prof_p.strip_prefix("/").unwrap() + } + + (prof_p, chroot_p) + }) + .map(|(prof_p, chroot_p)| (profile_dir.as_ref().unwrap().join(prof_p), chroot_p)) + .chain( + // TODO: this should actually probably happen first. + c.excludes.paths + .iter() + .map(|&ex| (PathBuf::from("/dev/null"), ex)) + ) + .chain( + c.absolute + .iter() + .map(|(s, d)| (*s, *d)) + .inspect(|(src, _)| { + if !src.is_absolute() { + panic!("Explicit mount sources (excluding profile mounts) must be absolute paths! `{}` is not absolute.", src.display()) + } + }) + .map(|(src, dest)| { + (src.to_owned(), dest) + }) + ) + .inspect(|(_, dest)| { + if !dest.is_absolute() { + panic!("All explicit mount destinations must be absolute paths! `{}` is not absolute.", dest.display()) + } + }); - let parent = self.with_rootdir(&parent); - parent.bind_mount_direntry(DirEntryOrExplicitMount::explicit_mount_with_dest_file_name(&*src, &dest)); - } else { - eprintln!("warning: explicit mount source `{}` doesn't seem to exist!", src.display()); + for (src, dest) in explicit_mounts { + if let Ok(src) = self.resolve_nix_path(src.clone(), true) { + log::info!("EXPLICIT {} -> {}", src.display(), dest.display()); + + let adjusted_dest = dest + .strip_prefix("/") // we have guarantees that `dest` is absolute + .unwrap() + .parent() + .map(ToOwned::to_owned) + .unwrap_or_default(); + let parent = self.rootdir.join(adjusted_dest); + + fs::create_dir_all(&parent).unwrap(); + + let parent = self.with_rootdir(&parent); + parent.bind_mount_entry( + DirEntryOrExplicitMount::explicit_mount_with_dest_file_name(&*src, &dest), + ); + } else { + eprintln!( + "warning: explicit mount source `{}` doesn't seem to exist!", + src.display() + ); + } } } @@ -407,13 +454,16 @@ impl<'a> RunChroot<'a> { if entry.file_name() == "nix" { continue; } - self.bind_mount_direntry(&entry); + self.bind_mount_entry(&entry); } - for p in mount_exclude_list { - let mount = self.rootdir.join(p.strip_prefix("/").unwrap()); - log::info!("UNBIND {}", mount.display()); - umount(&mount).unwrap(); + // remove the placeholders we used for the excludes + if let Some(c) = path_config { + for &p in c.excludes.paths.iter() { + let mount = self.rootdir.join(p.strip_prefix("/").unwrap()); + log::info!("UNBIND {}", mount.display()); + umount(&mount).unwrap(); + } } // mount the store @@ -427,7 +477,13 @@ impl<'a> RunChroot<'a> { MsFlags::MS_BIND | MsFlags::MS_REC, NONE, ) - .unwrap_or_else(|err| panic!("failed to bind mount {} to /nix: {}", self.nixdir.display(), err)); + .unwrap_or_else(|err| { + panic!( + "failed to bind mount {} to /nix: {}", + self.nixdir.display(), + err + ) + }); // chroot unistd::chroot(self.rootdir) @@ -522,9 +578,20 @@ fn main() { let nixdir = fs::canonicalize(&args[1]) .unwrap_or_else(|err| panic!("failed to resolve nix directory {}: {}", &args[1], err)); + let path_config_file_path = nixdir.join("etc/nix-user-chroot/path-config.toml"); + let config_file; + let config_file = if path_config_file_path.exists() { + config_file = fs::read_to_string(path_config_file_path).unwrap(); + Some(toml::from_str(&*config_file).unwrap()) + } else { + None + }; + match unsafe { fork() } { Ok(ForkResult::Parent { child, .. }) => wait_for_child(&rootdir, child), - Ok(ForkResult::Child) => RunChroot::new(&rootdir, &nixdir).run_chroot(&args[2], &args[3..]), + Ok(ForkResult::Child) => { + RunChroot::new(&rootdir, &nixdir).run_chroot(&args[2], &args[3..], config_file) + } Err(e) => { eprintln!("fork failed: {}", e); } From d996450cf503d8792cd08e909c8ce04672603092 Mon Sep 17 00:00:00 2001 From: Rahul Butani Date: Mon, 14 Feb 2022 16:40:35 -0600 Subject: [PATCH 5/5] mount-paths: warn (instead of erroring) on source dirs that we do not have permissions for --- src/main.rs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/main.rs b/src/main.rs index 30374f9..c917a78 100644 --- a/src/main.rs +++ b/src/main.rs @@ -241,9 +241,17 @@ impl<'a> RunChroot<'a> { // otherwise, if the dest is also a dir, we can recurse into it // and mount subdirectory siblings of existing paths if mountpoint.is_dir() { - let dir = fs::read_dir(entry.path()).unwrap_or_else(|err| { - panic!("failed to list dir {}: {}", entry.path().display(), err) - }); + let dir = match fs::read_dir(entry.path()) { + Ok(dir) => dir, + Err(err) if err.kind() == io::ErrorKind::PermissionDenied => { + log::warn!( + "don't have persmission to access directory {}, skipping...", + entry.path().display() + ); + return; + } + Err(err) => panic!("failed to list dir {}: {}", entry.path().display(), err), + }; let child = self.with_rootdir(&mountpoint); for entry in dir {