279 lines
7.7 KiB
Rust
279 lines
7.7 KiB
Rust
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<String> {
|
|
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<Vec<TempDir>> {
|
|
// Keep all overlay temp dirs alive
|
|
let mut overlay_temps: Vec<TempDir> = 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<TempDir> {
|
|
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"
|
|
);
|
|
}
|
|
}
|