deb: use overlayfs instead of copy
CI / build (push) Failing after 14m44s
CI / snap (push) Has been skipped

This commit is contained in:
2026-08-12 15:44:13 +02:00
parent e25645b3bb
commit 317dbcd062
3 changed files with 158 additions and 7 deletions
+13
View File
@@ -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
View File
@@ -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,
+10
View File
@@ -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);