deb: use overlayfs instead of copy
This commit is contained in:
@@ -34,6 +34,12 @@ pub trait ContextDriver {
|
||||
fn read_file(&self, path: &Path) -> io::Result<String>;
|
||||
fn write_file(&self, path: &Path, content: &str) -> io::Result<()>;
|
||||
fn exists(&self, path: &Path) -> io::Result<bool>;
|
||||
|
||||
/// Clean up any resources held by the driver (e.g. unmount overlay filesystems).
|
||||
/// Called before the chroot directory is removed.
|
||||
fn cleanup(&self) -> io::Result<()> {
|
||||
Ok(()) // default no-op
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents an execution environment (Local or via SSH).
|
||||
@@ -186,6 +192,12 @@ impl Context {
|
||||
self.driver().as_ref().unwrap().exists(path)
|
||||
}
|
||||
|
||||
/// Clean up any resources held by the driver (e.g. unmount overlay filesystems).
|
||||
/// Called before the chroot directory is removed.
|
||||
pub fn cleanup(&self) -> io::Result<()> {
|
||||
self.driver().as_ref().unwrap().cleanup()
|
||||
}
|
||||
|
||||
/// Create and obtain a specific driver for the context
|
||||
pub fn driver(
|
||||
&self,
|
||||
@@ -207,6 +219,7 @@ impl Context {
|
||||
ContextConfig::Unshare { path, .. } => Box::new(UnshareDriver {
|
||||
path: path.clone(),
|
||||
parent: self.parent.clone(),
|
||||
overlay_mounts: std::sync::Mutex::new(Vec::new()),
|
||||
}),
|
||||
};
|
||||
*driver_lock = Some(driver);
|
||||
|
||||
+135
-7
@@ -4,11 +4,13 @@ use std::fs;
|
||||
use std::io;
|
||||
use std::os::unix::fs::symlink;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
pub struct UnshareDriver {
|
||||
pub path: String,
|
||||
pub parent: Option<Arc<super::api::Context>>,
|
||||
/// Host-side paths where overlay filesystems are mounted, tracked for cleanup.
|
||||
pub overlay_mounts: Mutex<Vec<PathBuf>>,
|
||||
}
|
||||
|
||||
/// Recursively copy a directory and all its contents.
|
||||
@@ -141,15 +143,16 @@ fn copy_file_with_times(src: &Path, dest: &Path) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check whether the overlay filesystem is available on this system.
|
||||
fn is_overlayfs_available() -> bool {
|
||||
let content = std::fs::read_to_string("/proc/filesystems").unwrap_or_default();
|
||||
content.contains("overlay\n") || content.contains("overlay\t")
|
||||
}
|
||||
|
||||
impl ContextDriver for UnshareDriver {
|
||||
fn ensure_available(&self, src: &Path, dest_root: &str) -> io::Result<PathBuf> {
|
||||
// Construct the destination path inside the chroot
|
||||
let dest_dir = Path::new(&self.path).join(dest_root.trim_start_matches('/'));
|
||||
debug!(
|
||||
"unshare/ensure_available: copy '{}' to '{}'",
|
||||
src.display(),
|
||||
dest_dir.display()
|
||||
);
|
||||
|
||||
// Ensure the destination directory exists
|
||||
std::fs::create_dir_all(&dest_dir)?;
|
||||
@@ -162,7 +165,34 @@ impl ContextDriver for UnshareDriver {
|
||||
// Construct the full destination path
|
||||
let dest_path = dest_dir.join(filename);
|
||||
|
||||
// Copy the file or directory into the chroot
|
||||
// Try overlayfs for directories first — avoids a potentially expensive full copy
|
||||
if src.is_dir() && is_overlayfs_available() {
|
||||
match self.try_overlay_mount(src, &dest_path) {
|
||||
Ok(()) => {
|
||||
debug!(
|
||||
"Mounted overlay for {} at {}",
|
||||
src.display(),
|
||||
dest_path.display()
|
||||
);
|
||||
return Ok(Path::new(dest_root).join(filename));
|
||||
}
|
||||
Err(e) => {
|
||||
debug!(
|
||||
"Overlay mount failed for '{}', falling back to copy: {}",
|
||||
src.display(),
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: copy the file or directory into the chroot
|
||||
debug!(
|
||||
"unshare/ensure_available: copy '{}' to '{}'",
|
||||
src.display(),
|
||||
dest_dir.display()
|
||||
);
|
||||
|
||||
if src.is_dir() {
|
||||
copy_dir_recursive(src, &dest_path)?;
|
||||
debug!(
|
||||
@@ -179,6 +209,25 @@ impl ContextDriver for UnshareDriver {
|
||||
Ok(Path::new(dest_root).join(filename))
|
||||
}
|
||||
|
||||
fn cleanup(&self) -> io::Result<()> {
|
||||
let mounts = self.overlay_mounts.lock().unwrap();
|
||||
for mount_path in mounts.iter() {
|
||||
debug!("Unmounting overlay at {}", mount_path.display());
|
||||
let is_root = crate::utils::root::is_root().unwrap_or(false);
|
||||
let mut cmd = self
|
||||
.parent()
|
||||
.command(if is_root { "umount" } else { "sudo" });
|
||||
if !is_root {
|
||||
cmd.arg("umount");
|
||||
}
|
||||
let status = cmd.arg(mount_path.to_string_lossy().to_string()).status()?;
|
||||
if !status.success() {
|
||||
log::warn!("Failed to unmount overlay at {}", mount_path.display());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn retrieve_path(&self, src: &Path, dest: &Path) -> io::Result<()> {
|
||||
let host_src = Path::new(&self.path).join(src.to_string_lossy().trim_start_matches('/'));
|
||||
self.parent().retrieve_path(&host_src, dest)
|
||||
@@ -287,6 +336,85 @@ impl UnshareDriver {
|
||||
.expect("UnshareDriver requires a parent context")
|
||||
}
|
||||
|
||||
/// Try to mount `src` as an overlay at `dest_path` inside the chroot.
|
||||
///
|
||||
/// On success, the overlay is tracked in `overlay_mounts` for later cleanup.
|
||||
/// On failure, the caller should fall back to the copy-based approach.
|
||||
fn try_overlay_mount(&self, src: &Path, dest_path: &Path) -> io::Result<()> {
|
||||
// Create unique upper/work dirs inside the chroot
|
||||
let overlay_base = Path::new(&self.path).join("pkh-overlay");
|
||||
let id = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos();
|
||||
let upper_dir = overlay_base.join(format!("upper-{}", id));
|
||||
let work_dir = overlay_base.join(format!("work-{}", id));
|
||||
|
||||
fs::create_dir_all(&upper_dir)?;
|
||||
fs::create_dir_all(&work_dir)?;
|
||||
fs::create_dir_all(dest_path)?;
|
||||
|
||||
// Canonicalize the source path so overlayfs can find it reliably
|
||||
let src_canonical = src.canonicalize().map_err(|e| {
|
||||
io::Error::new(
|
||||
e.kind(),
|
||||
format!("Failed to canonicalize '{}': {}", src.display(), e),
|
||||
)
|
||||
})?;
|
||||
|
||||
// Mount overlay from host (requires CAP_SYS_ADMIN, same as bind_mount_proc).
|
||||
// Use .output() instead of .status() so that mount errors are captured
|
||||
// rather than printed to stderr — the caller will fall back to a copy.
|
||||
let is_root = crate::utils::root::is_root().unwrap_or(false);
|
||||
let mut cmd = self
|
||||
.parent()
|
||||
.command(if is_root { "mount" } else { "sudo" });
|
||||
if !is_root {
|
||||
cmd.arg("mount");
|
||||
}
|
||||
let output = cmd
|
||||
.arg("-t")
|
||||
.arg("overlay")
|
||||
.arg("overlay")
|
||||
.arg("-o")
|
||||
.arg(format!(
|
||||
"lowerdir={},upperdir={},workdir={}",
|
||||
src_canonical.display(),
|
||||
upper_dir.display(),
|
||||
work_dir.display()
|
||||
))
|
||||
.arg(dest_path.to_string_lossy().to_string())
|
||||
.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
// Clean up dirs we created
|
||||
let _ = fs::remove_dir_all(&upper_dir);
|
||||
let _ = fs::remove_dir_all(&work_dir);
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
return Err(io::Error::other(format!(
|
||||
"Overlay mount of '{}' at '{}' failed: {}",
|
||||
src.display(),
|
||||
dest_path.display(),
|
||||
stderr.trim()
|
||||
)));
|
||||
}
|
||||
|
||||
debug!(
|
||||
"Overlay-mounted {} at {} (upper: {}, work: {})",
|
||||
src.display(),
|
||||
dest_path.display(),
|
||||
upper_dir.display(),
|
||||
work_dir.display()
|
||||
);
|
||||
|
||||
// Track for cleanup
|
||||
self.overlay_mounts
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push(dest_path.to_path_buf());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn command(
|
||||
&self,
|
||||
program: &str,
|
||||
|
||||
@@ -337,6 +337,16 @@ impl EphemeralContextGuard {
|
||||
impl Drop for EphemeralContextGuard {
|
||||
fn drop(&mut self) {
|
||||
log::debug!("Cleaning up ephemeral context ({:?})...", self.chroot_path);
|
||||
|
||||
// Clean up any overlay mounts before resetting the context.
|
||||
// This must happen while the ephemeral context is still current so its
|
||||
// driver is accessible. The actual unmount commands run via the parent
|
||||
// (base) context, so they work regardless.
|
||||
let ephemeral_ctx = context::current();
|
||||
if let Err(e) = ephemeral_ctx.cleanup() {
|
||||
log::warn!("Failed to clean up overlay mounts: {}", e);
|
||||
}
|
||||
|
||||
// Reset to normal context
|
||||
if let Err(e) = context::manager().set_current(&self.previous_context) {
|
||||
log::error!("Failed to restore context {}: {}", self.previous_context, e);
|
||||
|
||||
Reference in New Issue
Block a user