feat: basic functionality
This commit is contained in:
+278
@@ -0,0 +1,278 @@
|
||||
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"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user