diff --git a/Cargo.lock b/Cargo.lock index 89f1cf9..0da1f17 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -273,13 +273,14 @@ dependencies = [ [[package]] name = "dive" -version = "0.1.0" +version = "0.1.1" dependencies = [ "anyhow", "clap", "dirs", "env_logger", "exitcode", + "fd-lock", "image_builder", "indicatif", "liblzma", @@ -350,6 +351,17 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" +[[package]] +name = "fd-lock" +version = "4.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e5768da2206272c81ef0b5e951a41862938a6070da63bcea197899942d3b947" +dependencies = [ + "cfg-if", + "rustix", + "windows-sys 0.52.0", +] + [[package]] name = "filetime" version = "0.2.25" @@ -618,7 +630,7 @@ dependencies = [ [[package]] name = "image_builder" -version = "0.1.0" +version = "0.1.1" dependencies = [ "anyhow", "clap", diff --git a/Cargo.toml b/Cargo.toml index 7944ef6..2975816 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,6 +39,7 @@ serde_json = "1.0" tar = { workspace = true } tempfile = { workspace = true } which = "6.0.3" +fd-lock = "4.0.2" [[bin]] name = "dive" diff --git a/src/main.rs b/src/main.rs index 134d318..92c7fa2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,28 +1,30 @@ use std::{ ffi::OsString, - fmt::Debug, - fs::{create_dir_all, read_link}, + fs::read_link, io, os::unix::process::CommandExt, path::{Path, PathBuf}, - process::{exit, Command}, + process::{self, exit, Command}, }; -use anyhow::{Context, Result}; +use anyhow::{bail, Context, Result}; use clap::Parser; -use namespaces::enter_namespaces_as_root; -use overlay::OverlayMount; -use pid_lookup::pid_lookup; use procfs::process::Process; use rustix::{ - path::Arg, - process::geteuid, - thread::{unshare, UnshareFlags}, + process::{geteuid, waitpid, WaitOptions}, + runtime::{fork, Fork}, }; mod namespaces; mod overlay; +mod pid_file; mod pid_lookup; +mod shared_mount; + +use namespaces::*; +use overlay::*; +use pid_lookup::*; +use shared_mount::*; #[cfg(feature = "embedded_image")] mod embedded_image; @@ -72,18 +74,6 @@ fn get_overlay_dir() -> PathBuf { dirs::state_dir().unwrap().join(APP_NAME).join(OVL_DIR) } -fn make_overlay_dirs() -> Result<(PathBuf, PathBuf, PathBuf)> { - let overlay_dir = get_overlay_dir(); - let upper_dir = overlay_dir.join("upper"); - let work_dir = overlay_dir.join("work"); - - create_dir_all(&upper_dir) - .and(create_dir_all(&work_dir)) - .context("could not create state directory")?; - - Ok((overlay_dir, upper_dir, work_dir)) -} - fn init_logging() { env_logger::Builder::new() .filter_level(log::LevelFilter::Info) @@ -111,74 +101,41 @@ fn reexec_with_sudo( .exec()) } -fn main() -> Result<()> { - let args = Args::parse(); - init_logging(); - - let lead_pid = if let Ok(pid) = args.container_id.parse::() { - pid - } else if let Some(pid) = pid_lookup(&args.container_id) { - pid - } else { - log::error!("could not find container PID"); - exit(exitcode::NOINPUT); - }; - log::debug!("container PID: {}", lead_pid); - - let img_dir = get_img_dir(&args); - if !img_dir.exists() || !img_dir.join("store").exists() { - #[cfg(feature = "embedded_image")] - embedded_image::install_base_image(&img_dir) - .context("could not unpack base image")?; - - #[cfg(not(feature = "embedded_image"))] - BaseImageBuilder::new(&img_dir) - .build_base() - .context("could not build base image")?; - } - - let (overlay_dir, upper_dir, work_dir) = make_overlay_dirs()?; - - if !geteuid().is_root() { - log::debug!("re-executing with sudo..."); - reexec_with_sudo(lead_pid, &img_dir, &overlay_dir)? - } - - let overlay = match OverlayMount::new(img_dir, upper_dir, work_dir) { - Err(err) => { - log::error!("could not create base image mount: {:?}", err); - exit(exitcode::OSERR); - } - Ok(mnt) => { - log::debug!("detached mount created"); - mnt - } - }; - - let proc_env = match Process::new(lead_pid).and_then(|p| p.environ()) { +fn prepare_shell_environment( + shared_mount: &SharedMount, + lead_pid: i32, +) -> Result<()> { + let detached_mount = match shared_mount.make_detached_mount() { Err(err) => { - log::error!("could not fetch the process environment: {err}"); - exit(exitcode::OSERR); + bail!("could not make detached mount: {err}"); } - Ok(env) => env, + Ok(m) => m, }; + if let Err(err) = enter_namespaces_as_root(lead_pid) { + bail!("cannot enter container namespaces: {err}"); + } + if let Err(err) = detached_mount.mount_in_new_namespace("/nix") { + bail!("cannot mount /nix: {err}"); + } + Ok(()) +} - enter_namespaces_as_root(lead_pid)?; - - log::debug!("mounting overlay..."); - unshare(UnshareFlags::NEWNS) - .context("could not create new mount namespace")?; - overlay - .mount("/nix") - .context("could not mount base image")?; - +fn exec_shell() -> Result<()> { + // // TODO: path HOME w/ user as defined by /etc/passwd - + // // TODO: find shell in this order: // - zsh // - bash // - sh at last + let proc_env = match Process::new(1).and_then(|p| p.environ()) { + Err(err) => { + bail!("could not fetch the process environment: {err}"); + } + Ok(env) => env, + }; + let mut cmd = Command::new("zsh"); cmd.env_clear(); cmd.envs(&proc_env); @@ -216,7 +173,67 @@ fn main() -> Result<()> { ("INFOPATH", format!("{nix_base}/share/info")), ]); - cmd.status()?; + let err = cmd.exec(); + bail!("cannot exec: {}", err) +} +fn wait_for_child(child_pid: rustix::thread::Pid) -> Result<()> { + // TODO: propagate return code properly + log::debug!("parent pid = {}", process::id()); + let _ = waitpid(Some(child_pid), WaitOptions::empty()) + .context("waitpid failed")?; Ok(()) } + +fn main() -> Result<()> { + let args = Args::parse(); + init_logging(); + + let lead_pid = if let Some(pid) = pid_lookup(&args.container_id) { + pid + } else { + log::error!("could not find container PID"); + exit(exitcode::NOINPUT); + }; + log::debug!("container PID: {}", lead_pid); + + let img_dir = get_img_dir(&args); + if !img_dir.exists() || !img_dir.join("store").exists() { + #[cfg(feature = "embedded_image")] + embedded_image::install_base_image(&img_dir) + .context("could not unpack base image")?; + + #[cfg(not(feature = "embedded_image"))] + BaseImageBuilder::new(&img_dir) + .build_base() + .context("could not build base image")?; + } + + let overlay_dir = get_overlay_dir(); + let overlay_builder = OverlayBuilder::new(&overlay_dir, &img_dir)?; + + if !geteuid().is_root() { + log::debug!("re-executing with sudo..."); + reexec_with_sudo(lead_pid, &img_dir, &overlay_dir)? + } + + let shared_mount = SharedMount::new(&overlay_dir, overlay_builder) + .context("could not init shared mount")?; + + match unsafe { fork()? } { + Fork::Child(_) => { + if let Err(err) = prepare_shell_environment(&shared_mount, lead_pid) + { + log::error!("{err}"); + exit(1); + } + // in normal cases, there is no return from exec_shell() + if let Err(err) = exec_shell() { + log::error!("cannot execute shell: {err}"); + exit(1); + } + exit(0); + } + Fork::Parent(child_pid) => wait_for_child(child_pid), + } +} diff --git a/src/namespaces.rs b/src/namespaces.rs index 0bfa4c3..9384fc4 100644 --- a/src/namespaces.rs +++ b/src/namespaces.rs @@ -3,7 +3,7 @@ use std::{ process::exit, }; -use anyhow::Result; +use anyhow::{bail, Result}; use procfs::process::{Namespace, Process}; use rustix::{ process::{pidfd_open, Pid, PidfdFlags}, @@ -49,21 +49,18 @@ pub fn enter_namespaces_as_root(lead_pid: i32) -> Result<()> { let lead = Process::new(lead_pid)?; let lead_ns = match namespace_set(&lead) { Err(err) => { - log::error!("cannot inspect lead process namespaces: {err}"); - exit(exitcode::OSERR); + bail!("cannot inspect lead process namespaces: {err}"); } Ok(ns) => ns, }; - log::debug!("lead_ns = {:?}", lead_ns); let me = Process::myself()?; let me_id = me.pid; - log::debug!("my pid = {}", me_id); + log::debug!("own pid is {}", me_id); let my_ns = match namespace_set(&me) { Err(err) => { - log::error!("cannot inspect own namespaces: {err}"); - exit(exitcode::OSERR); + bail!("cannot inspect own namespaces: {err}"); } Ok(ns) => ns, }; @@ -74,7 +71,6 @@ pub fn enter_namespaces_as_root(lead_pid: i32) -> Result<()> { ns_set = ns_set.union(namespace_type_by_name(&ns.0)); } - log::debug!("ns_set: {:?}", ns_set); if ns_set.is_empty() { log::debug!("no point in entering anything: we're already there!"); return Ok(()); diff --git a/src/overlay.rs b/src/overlay.rs index 1652022..6122320 100644 --- a/src/overlay.rs +++ b/src/overlay.rs @@ -2,7 +2,7 @@ use std::fs::create_dir_all; use std::path::Path; use std::{os::fd::OwnedFd, path::PathBuf}; -use anyhow::Result; +use anyhow::{Context, Result}; use rustix::fd::AsFd; use rustix::fs; use rustix::mount::{ @@ -20,7 +20,7 @@ impl OverlayMount { ) -> Result { let lower_dir = lower_dir.into().canonicalize()?; let fsfd = fsopen("overlay", FsOpenFlags::FSOPEN_CLOEXEC)?; - fsconfig_set_string(fsfd.as_fd(), "source", "nsdb")?; + fsconfig_set_string(fsfd.as_fd(), "source", "user-data")?; fsconfig_set_string(fsfd.as_fd(), "lowerdir", lower_dir)?; fsconfig_set_string(fsfd.as_fd(), "upperdir", upper_dir.as_ref())?; fsconfig_set_string(fsfd.as_fd(), "workdir", work_dir.as_ref())?; @@ -50,3 +50,41 @@ impl OverlayMount { Ok(()) } } + +pub struct OverlayBuilder { + pub lower_dir: PathBuf, + pub merged_dir: PathBuf, + pub upper_dir: PathBuf, + pub work_dir: PathBuf, + pub pids_dir: PathBuf, +} + +impl OverlayBuilder { + pub fn new(base_dir: P, lower_dir: Q) -> Result + where + P: AsRef, + Q: AsRef, + { + let base_dir = base_dir.as_ref(); + let lower_dir = lower_dir.as_ref().to_owned(); + + let merged_dir = base_dir.join("merged"); + let upper_dir = base_dir.join("upper"); + let work_dir = base_dir.join("work"); + let pids_dir = base_dir.join("pids"); + + create_dir_all(&merged_dir) + .and(create_dir_all(&upper_dir)) + .and(create_dir_all(&work_dir)) + .and(create_dir_all(&pids_dir)) + .context("could not create overlay directories")?; + + Ok(OverlayBuilder { + lower_dir, + merged_dir, + upper_dir, + work_dir, + pids_dir, + }) + } +} diff --git a/src/pid_file.rs b/src/pid_file.rs new file mode 100644 index 0000000..74e2415 --- /dev/null +++ b/src/pid_file.rs @@ -0,0 +1,29 @@ +use std::{ + fs, + path::{Path, PathBuf}, + process, +}; + +use anyhow::Result; + +pub struct PidFile { + path: PathBuf, +} + +impl PidFile { + pub fn new(dir: &Path) -> Result { + let pid = process::id().to_string(); + let path = dir.join(&pid); + std::fs::write(&path, pid)?; + Ok(PidFile { path }) + } +} + +impl Drop for PidFile { + fn drop(&mut self) { + log::debug!("removing {}", self.path.display()); + if let Err(err) = fs::remove_file(&self.path) { + log::error!("while removing: {}", err); + } + } +} diff --git a/src/pid_lookup.rs b/src/pid_lookup.rs index df73bdf..035a9a8 100644 --- a/src/pid_lookup.rs +++ b/src/pid_lookup.rs @@ -103,9 +103,12 @@ const _RUNTIMES: &[ContainerRuntime] = &[ }, ]; -pub(crate) fn pid_lookup(name: &str) -> Option { +pub(crate) fn pid_lookup(value: &str) -> Option { + if let Ok(pid) = value.parse::() { + return Some(pid); + }; return _RUNTIMES .iter() .filter(|r| (r.available)()) - .find_map(|r| (r.get_pid)(name)); + .find_map(|r| (r.get_pid)(value)); } diff --git a/src/shared_mount.rs b/src/shared_mount.rs new file mode 100644 index 0000000..3656c0e --- /dev/null +++ b/src/shared_mount.rs @@ -0,0 +1,140 @@ +use std::{ + fs::{remove_file, File, OpenOptions}, + os::fd::{AsFd, OwnedFd}, + path::{Path, PathBuf}, +}; + +use anyhow::{Context, Result}; +use fd_lock::RwLock; +use rustix::{ + fs::MountPropagationFlags, + mount::{ + mount_change, move_mount, open_tree, MoveMountFlags, OpenTreeFlags, + }, + thread::{ + move_into_thread_name_spaces, unshare, ThreadNameSpaceType, + UnshareFlags, + }, +}; + +use crate::{pid_file::PidFile, OverlayBuilder, OverlayMount}; + +pub struct SharedMount { + _pid_file: PidFile, + merged_dir: PathBuf, +} + +impl SharedMount { + pub fn new( + base_dir: &Path, + overlay_builder: OverlayBuilder, + ) -> Result { + let mut flock = RwLock::new( + OpenOptions::new() + .append(true) + .create(true) + .open(base_dir.join("lock"))?, + ); + let _guard = flock.write()?; + + let mut ns_found = false; + let pids_dir = &overlay_builder.pids_dir; + let merged_dir = overlay_builder.merged_dir.clone(); + + for entry in pids_dir.read_dir().unwrap() { + let entry = entry?; + let pid = entry.file_name(); + log::debug!("checking {:?}", pid); + match File::open(format!("/proc/{}/ns/mnt", pid.to_string_lossy())) + { + Err(err) => { + log::debug!("pid file open: {}", err); + let _ = remove_file(entry.path()).is_ok(); + continue; + } + Ok(f) => { + if move_into_thread_name_spaces( + f.as_fd(), + ThreadNameSpaceType::MOUNT, + ) + .is_ok() + { + log::debug!("entered mount namespace"); + ns_found = true; + break; + } + } + } + } + + if !ns_found { + log::debug!("enter new mount namespace"); + unshare(UnshareFlags::NEWNS) + .context("could not create new mount namespace")?; + + log::debug!("make mounts private"); + mount_change( + "/", + MountPropagationFlags::PRIVATE | MountPropagationFlags::REC, + )?; + + log::debug!("mount base in state dir"); + OverlayMount::new( + overlay_builder.lower_dir, + overlay_builder.upper_dir, + overlay_builder.work_dir, + ) + .and_then(|ovl| ovl.mount(&overlay_builder.merged_dir)) + .context("could not mount base image")?; + } + + let pid_file = + PidFile::new(pids_dir).context("failed to create pid file")?; + + let mnt = SharedMount { + _pid_file: pid_file, + merged_dir, + }; + + Ok(mnt) + } + + pub fn make_detached_mount(&self) -> Result { + let tree_fd = open_tree( + rustix::fs::CWD, + &self.merged_dir, + OpenTreeFlags::OPEN_TREE_CLONE | OpenTreeFlags::OPEN_TREE_CLOEXEC, + )?; + + Ok(DetachedMount(tree_fd)) + } +} + +pub struct DetachedMount(OwnedFd); + +impl DetachedMount { + pub fn mount

(self, target: P) -> Result<()> + where + P: AsRef, + { + move_mount( + self.0.as_fd(), + "", + rustix::fs::CWD, + target.as_ref(), + MoveMountFlags::MOVE_MOUNT_F_EMPTY_PATH, + )?; + + Ok(()) + } + + pub fn mount_in_new_namespace

(self, target: P) -> Result<()> + where + P: AsRef, + { + unshare(UnshareFlags::NEWNS) + .context("could not create new mount namespace")?; + + self.mount(target) + } +}