Files
ecr/src/mount.rs
T
2026-06-09 19:09:57 +02:00

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"
);
}
}