From 6288562e9d057d0600661b256332cc6b52e488d6 Mon Sep 17 00:00:00 2001 From: jD91mZM2 Date: Sat, 9 Jan 2021 12:04:26 +0100 Subject: [PATCH] Use chroot over pivot_root MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We only used pivot_root in order to temporarily bind /nix to the host and lift in different files. Now, we instead bind everything beforehand and finally do a simple chroot. Co-authored-by: Jörg Thalheim --- .gitignore | 3 + container.sh | 29 ++++++ src/main.rs | 244 ++++++++++++++++++++++++--------------------------- 3 files changed, 147 insertions(+), 129 deletions(-) create mode 100755 container.sh diff --git a/.gitignore b/.gitignore index f2e972d..adc6c0c 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,6 @@ # These are backup files generated by rustfmt **/*.rs.bk + +# Example nix directory +/.nix/ diff --git a/container.sh b/container.sh new file mode 100755 index 0000000..77d4201 --- /dev/null +++ b/container.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash + +cd "$(dirname "${BASH_SOURCE[0]}")" + +if [[ -z $ENGINE ]]; then + if command -v podman &>/dev/null; then + ENGINE=podman + elif command -v docker &>/dev/null; then + ENGINE=docker + else + echo "No podman or docker in PATH" >&2 + exit 1 + fi +fi + +read -d '' script < = Err(e); - e2.unwrap_or_else(|_| panic!("failed to create {}", &mountpoint.display())); +pub struct RunChroot<'a> { + rootdir: &'a Path, +} + +impl<'a> RunChroot<'a> { + fn new(rootdir: &'a Path) -> Self { + Self { + rootdir } } - bind_mount(&entry.path(), &mountpoint) -} - -fn bind_mount_file(entry: &fs::DirEntry) { - let mountpoint = PathBuf::from("/").join(entry.file_name()); - fs::File::create(&mountpoint) - .unwrap_or_else(|_| panic!("failed to create {}", &mountpoint.display())); + fn bind_mount_directory(&self, entry: &fs::DirEntry) { + let mountpoint = self.rootdir.join(entry.file_name()); + if let Err(e) = fs::create_dir(&mountpoint) { + if e.kind() != io::ErrorKind::AlreadyExists { + let e2: io::Result<()> = Err(e); + e2.unwrap_or_else(|_| panic!("failed to create {}", &mountpoint.display())); + } + } - bind_mount(&entry.path(), &mountpoint) -} + bind_mount(&entry.path(), &mountpoint) + } -fn mirror_symlink(entry: &fs::DirEntry) { - let path = entry.path(); - let target = fs::read_link(&path) - .unwrap_or_else(|_| panic!("failed to resolve symlink {}", &path.display())); - let link_path = PathBuf::from("/").join(entry.file_name()); - symlink(&target, &link_path).unwrap_or_else(|_| { - panic!( - "failed to create symlink {} -> {}", - &link_path.display(), - &target.display() - ) - }); -} + fn bind_mount_file(&self, entry: &fs::DirEntry) { + let mountpoint = self.rootdir.join(entry.file_name()); + fs::File::create(&mountpoint) + .unwrap_or_else(|_| panic!("failed to create {}", &mountpoint.display())); -fn bind_mount_direntry(entry: io::Result) { - let entry = entry.expect("error while listing from /nix directory"); - // do not bind mount an existing nix installation - if entry.file_name() == PathBuf::from("nix") { - return; + bind_mount(&entry.path(), &mountpoint) } - let path = entry.path(); - let stat = entry - .metadata() - .unwrap_or_else(|_| panic!("cannot get stat of {}", path.display())); - if stat.is_dir() { - bind_mount_directory(&entry); - } else if stat.is_file() { - bind_mount_file(&entry); - } else if stat.file_type().is_symlink() { - mirror_symlink(&entry); + + fn mirror_symlink(&self, entry: &fs::DirEntry) { + let path = entry.path(); + let target = fs::read_link(&path) + .unwrap_or_else(|_| panic!("failed to resolve symlink {}", &path.display())); + let link_path = self.rootdir.join(entry.file_name()); + symlink(&target, &link_path).unwrap_or_else(|_| { + panic!( + "failed to create symlink {} -> {}", + &link_path.display(), + &target.display() + ) + }); } -} -fn run_chroot(nixdir: &Path, rootdir: &Path, cmd: &str, args: &[String]) { - let cwd = env::current_dir().expect("cannot get current working directory"); + fn bind_mount_direntry(&self, entry: io::Result) { + let entry = entry.expect("error while listing from /nix directory"); + // do not bind mount an existing nix installation + if entry.file_name() == PathBuf::from("nix") { + return; + } + let path = entry.path(); + let stat = entry + .metadata() + .unwrap_or_else(|_| panic!("cannot get stat of {}", path.display())); + if stat.is_dir() { + self.bind_mount_directory(&entry); + } else if stat.is_file() { + self.bind_mount_file(&entry); + } else if stat.file_type().is_symlink() { + self.mirror_symlink(&entry); + } + } - let uid = unistd::getuid(); - let gid = unistd::getgid(); + fn run_chroot(&self, nixdir: &Path, cmd: &str, args: &[String]) { + let cwd = env::current_dir().expect("cannot get current working directory"); - unshare(CloneFlags::CLONE_NEWNS | CloneFlags::CLONE_NEWUSER).expect("unshare failed"); + let uid = unistd::getuid(); + let gid = unistd::getgid(); - // prepare pivot_root call: - // rootdir must be a mount point + unshare(CloneFlags::CLONE_NEWNS | CloneFlags::CLONE_NEWUSER).expect("unshare failed"); - mount( - Some(rootdir), - rootdir, - Some("none"), - MsFlags::MS_BIND | MsFlags::MS_REC, - NONE, - ) - .expect("failed to re-bind mount / to our new chroot"); + // bind mount all / stuff into rootdir + let nix_root = PathBuf::from("/"); + let dir = fs::read_dir(&nix_root).expect("failed to list /nix directory"); + for entry in dir { + self.bind_mount_direntry(entry); + } - mount( - Some(rootdir), - rootdir, - Some("none"), - MsFlags::MS_PRIVATE | MsFlags::MS_REC, - NONE, - ).expect("failed to re-mount our chroot as private mount"); - - // create the mount point for the old root - // The old root cannot be unmounted/removed after pivot_root, the only way to - // keep / clean is to hide the directory with another mountpoint. Therefore - // we pivot the old root to /nix. This is somewhat confusing, though. - let nix_mountpoint = rootdir.join("nix"); - fs::create_dir(&nix_mountpoint) - .unwrap_or_else(|_| panic!("failed to create {}/nix", &nix_mountpoint.display())); - - unistd::pivot_root(rootdir, &nix_mountpoint).unwrap_or_else(|_| { - panic!( - "pivot_root({},{})", - rootdir.display(), - nix_mountpoint.display() + // mount the store + let nix_mount = self.rootdir.join("nix"); + fs::create_dir(&nix_mount) + .unwrap_or_else(|_| panic!("failed to create {}", &nix_mount.display())); + mount( + Some(nixdir), + &nix_mount, + Some("none"), + MsFlags::MS_BIND | MsFlags::MS_REC, + NONE, ) - }); + .unwrap_or_else(|_| panic!("failed to bind mount {} to /nix", nixdir.display())); + + // chroot + unistd::chroot(self.rootdir).unwrap_or_else(|_| { + panic!( + "chroot({})", + self.rootdir.display(), + ) + }); + + env::set_current_dir("/").expect("cannot change directory to /"); + + // fixes issue #1 where writing to /proc/self/gid_map fails + // see user_namespaces(7) for more documentation + if let Ok(mut file) = fs::File::create("/proc/self/setgroups") { + let _ = file.write_all(b"deny"); + } - env::set_current_dir("/").expect("cannot change directory to /"); + let mut uid_map = + fs::File::create("/proc/self/uid_map").expect("failed to open /proc/self/uid_map"); + uid_map + .write_all(format!("{} {} 1", uid, uid).as_bytes()) + .expect("failed to write new uid mapping to /proc/self/uid_map"); - // bind mount all / stuff into rootdir - // the orginal content of / now available under /nix - let nix_root = PathBuf::from("/nix"); - let dir = fs::read_dir(&nix_root).expect("failed to list /nix directory"); - for entry in dir { - bind_mount_direntry(entry); - } - // mount the store and hide the old root - // we fetch nixdir under the old root - let nix_store = nix_root.join(nixdir); - mount( - Some(&nix_store), - "/nix", - Some("none"), - MsFlags::MS_BIND | MsFlags::MS_REC, - NONE, - ) - .unwrap_or_else(|_| panic!("failed to bind mount {} to /nix", nix_store.display())); + let mut gid_map = + fs::File::create("/proc/self/gid_map").expect("failed to open /proc/self/gid_map"); + gid_map + .write_all(format!("{} {} 1", gid, gid).as_bytes()) + .expect("failed to write new gid mapping to /proc/self/gid_map"); - // fixes issue #1 where writing to /proc/self/gid_map fails - // see user_namespaces(7) for more documentation - if let Ok(mut file) = fs::File::create("/proc/self/setgroups") { - let _ = file.write_all(b"deny"); - } + // restore cwd + env::set_current_dir(&cwd) + .unwrap_or_else(|_| panic!("cannot restore working directory {}", cwd.display())); + + let err = process::Command::new(cmd) + .args(args) + .env("NIX_CONF_DIR", "/nix/etc/nix") + .exec(); - let mut uid_map = - fs::File::create("/proc/self/uid_map").expect("failed to open /proc/self/uid_map"); - uid_map - .write_all(format!("{} {} 1", uid, uid).as_bytes()) - .expect("failed to write new uid mapping to /proc/self/uid_map"); - - let mut gid_map = - fs::File::create("/proc/self/gid_map").expect("failed to open /proc/self/gid_map"); - gid_map - .write_all(format!("{} {} 1", gid, gid).as_bytes()) - .expect("failed to write new gid mapping to /proc/self/gid_map"); - - // restore cwd - env::set_current_dir(&cwd) - .unwrap_or_else(|_| panic!("cannot restore working directory {}", cwd.display())); - - let err = process::Command::new(cmd) - .args(args) - .env("NIX_CONF_DIR", "/nix/etc/nix") - .exec(); - - eprintln!("failed to execute {}: {}", &cmd, err); - process::exit(1); + eprintln!("failed to execute {}: {}", &cmd, err); + process::exit(1); + } } + fn wait_for_child(child_pid: unistd::Pid, tempdir: TempDir, rootdir: &Path) { loop { match waitpid(child_pid, Some(WaitPidFlag::WUNTRACED)) { @@ -243,7 +229,7 @@ fn main() { match fork() { Ok(ForkResult::Parent { child, .. }) => wait_for_child(child, tempdir, &rootdir), - Ok(ForkResult::Child) => run_chroot(&nixdir, &rootdir, &args[2], &args[3..]), + Ok(ForkResult::Child) => RunChroot::new(&rootdir).run_chroot(&nixdir, &args[2], &args[3..]), Err(e) => { eprintln!("fork failed: {}", e); }