use anyhow::{anyhow, Context, Result}; use nix::mount::{mount, MsFlags}; use std::path::Path; use tempfile::TempDir; /// Escape a path for use as an overlayfs mount option value. /// /// The overlayfs kernel driver uses `,` as its option delimiter and `\` as /// the escape character (Linux ≥ 5.1, commit 6b2d09a). A bare comma in a /// path would silently split the option string at the wrong boundary and /// produce a cryptic kernel error; a bare backslash would be mis-interpreted /// as starting an escape sequence. fn escape_overlay_path(path: &Path) -> Result { let s = path.to_str().ok_or_else(|| { anyhow!( "Overlay path '{}' contains non-UTF-8 characters", path.display() ) })?; // Backslashes must be escaped before commas to avoid double-escaping. Ok(s.replace('\\', "\\\\").replace(',', "\\,")) } use crate::cli::Args; /// Setup all required mounts inside the chroot /// Returns a TempDir that must be kept alive for the duration of the chroot pub fn setup_mounts( rootfs: &Path, bind_paths: &[std::path::PathBuf], bind_rw_paths: &[std::path::PathBuf], args: &Args, ) -> Result> { // Keep all overlay temp dirs alive let mut overlay_temps: Vec = Vec::new(); // Make all mounts private to avoid propagation to host if let Err(e) = mount( None::<&str>, "/", None::<&str>, MsFlags::MS_PRIVATE | MsFlags::MS_REC, None::<&str>, ) { eprintln!("Warning: Failed to make mounts private: {}", e); } // Mount /proc mount_proc(rootfs)?; // Setup /dev by bind mounting from host mount_dev(rootfs)?; // Mount /dev/pts mount_devpts(rootfs)?; // Try to mount /sys (may fail in some environments) if let Err(e) = mount_sys(rootfs) { eprintln!("Warning: Could not mount /sys: {}", e); } // Setup overlay mounts for bind paths (read-only via overlay) if !args.no_bind { for bind_path in bind_paths { // Skip if this path is also in bind_rw (bind_rw takes precedence) if !bind_rw_paths.contains(bind_path) { let temp = setup_overlay(rootfs, bind_path)?; overlay_temps.push(temp); } } } // Setup read-write bind mounts (these override regular bind for same paths) for bind_rw_path in bind_rw_paths { setup_bind_rw(rootfs, bind_rw_path)?; } Ok(overlay_temps) } fn mount_proc(rootfs: &Path) -> Result<()> { let proc_path = rootfs.join("proc"); std::fs::create_dir_all(&proc_path)?; let flags = MsFlags::MS_NOSUID | MsFlags::MS_NOEXEC | MsFlags::MS_NODEV; mount(Some("proc"), &proc_path, Some("proc"), flags, None::<&str>) .with_context(|| format!("Failed to mount proc at {}", proc_path.display()))?; Ok(()) } fn mount_sys(rootfs: &Path) -> Result<()> { let sys_path = rootfs.join("sys"); std::fs::create_dir_all(&sys_path)?; // Bind mount /sys from host as read-only mount( Some("/sys"), &sys_path, None::<&str>, MsFlags::MS_BIND | MsFlags::MS_REC, None::<&str>, ) .with_context(|| format!("Failed to bind mount sys at {}", sys_path.display()))?; // Remount as read-only with full security flags. // MS_BIND | MS_REMOUNT does NOT inherit the original mount's flags; every // desired flag must be listed explicitly. /proc uses the same set. mount( Some(&sys_path), &sys_path, None::<&str>, MsFlags::MS_BIND | MsFlags::MS_REMOUNT | MsFlags::MS_RDONLY | MsFlags::MS_NOSUID | MsFlags::MS_NODEV | MsFlags::MS_NOEXEC, None::<&str>, ) .with_context(|| { format!( "Failed to remount sys as read-only at {}", sys_path.display() ) })?; Ok(()) } fn mount_dev(rootfs: &Path) -> Result<()> { let dev_path = rootfs.join("dev"); std::fs::create_dir_all(&dev_path)?; // Bind mount /dev from host mount( Some("/dev"), &dev_path, None::<&str>, MsFlags::MS_BIND | MsFlags::MS_REC, None::<&str>, ) .with_context(|| format!("Failed to bind mount dev at {}", dev_path.display()))?; Ok(()) } fn mount_devpts(rootfs: &Path) -> Result<()> { let devpts_path = rootfs.join("dev/pts"); std::fs::create_dir_all(&devpts_path)?; let flags = MsFlags::MS_NOSUID | MsFlags::MS_NOEXEC; mount( Some("devpts"), &devpts_path, Some("devpts"), flags, None::<&str>, ) .with_context(|| format!("Failed to mount devpts at {}", devpts_path.display()))?; Ok(()) } /// Setup overlay mount for workspace directory /// Returns a TempDir that must be kept alive for the overlay to work fn setup_overlay(rootfs: &Path, source: &Path) -> Result { let basename = source .file_name() .ok_or_else(|| anyhow!("Invalid bind path"))? .to_string_lossy(); let mount_point = rootfs.join("root").join(basename.as_ref()); std::fs::create_dir_all(&mount_point)?; // Create temp directories for overlay let temp_dir = tempfile::tempdir()?; let upper_dir = temp_dir.path().join("upper"); let work_dir = temp_dir.path().join("work"); std::fs::create_dir_all(&upper_dir)?; std::fs::create_dir_all(&work_dir)?; // Create overlay mount options let lowerdir = source.canonicalize()?; let upperdir = upper_dir.canonicalize()?; let workdir = work_dir.canonicalize()?; let options = format!( "lowerdir={},upperdir={},workdir={}", escape_overlay_path(&lowerdir)?, escape_overlay_path(&upperdir)?, escape_overlay_path(&workdir)?, ); mount( Some("overlay"), &mount_point, Some("overlay"), MsFlags::empty(), Some(options.as_str()), ) .with_context(|| format!("Failed to mount overlay at {}", mount_point.display()))?; // Return temp_dir so caller can keep it alive Ok(temp_dir) } /// Setup read-write bind mount fn setup_bind_rw(rootfs: &Path, source: &Path) -> Result<()> { let basename = source .file_name() .ok_or_else(|| anyhow!("Invalid bind-rw path"))? .to_string_lossy(); let mount_point = rootfs.join("mnt").join(basename.as_ref()); std::fs::create_dir_all(&mount_point)?; let source = source.canonicalize()?; mount( Some(&source), &mount_point, None::<&str>, MsFlags::MS_BIND | MsFlags::MS_REC, None::<&str>, ) .with_context(|| { format!( "Failed to bind mount {} at {}", source.display(), mount_point.display() ) })?; Ok(()) } #[cfg(test)] mod tests { use super::escape_overlay_path; use std::path::Path; #[test] fn plain_path_unchanged() { assert_eq!( escape_overlay_path(Path::new("/home/user/project")).unwrap(), "/home/user/project" ); } #[test] fn comma_in_path_escaped() { assert_eq!( escape_overlay_path(Path::new("/home/user/my,project")).unwrap(), "/home/user/my\\,project" ); } #[test] fn backslash_escaped_before_comma() { // Backslash must be doubled first so a path like "a\,b" becomes // "a\\\,b" and not "a\,b" (which would look like an escaped comma). assert_eq!( escape_overlay_path(Path::new("/a\\,b")).unwrap(), "/a\\\\\\,b" ); } #[test] fn multiple_commas_all_escaped() { assert_eq!( escape_overlay_path(Path::new("/a,b,c")).unwrap(), "/a\\,b\\,c" ); } }