use std::path::Path;
use a3s_box_core::error::{BoxError, Result};
pub fn overlay_mount(lower: &Path, upper: &Path, work: &Path, merged: &Path) -> Result<()> {
for path in [lower, upper, work] {
if path.to_string_lossy().contains(',') {
return Err(BoxError::BuildError(format!(
"overlay path contains a comma, which overlayfs options cannot express: {}",
path.display()
)));
}
}
let base_options = overlay_options(lower, upper, work, false);
#[cfg(target_os = "linux")]
{
use std::ffi::CString;
let metadata_options = overlay_options(lower, upper, work, true);
let options = if unsafe { libc::geteuid() } == 0 {
vec![(&metadata_options, true), (&base_options, false)]
} else {
vec![(&base_options, false)]
};
let source = CString::new("overlay").unwrap();
let target = CString::new(merged.to_string_lossy().as_ref())
.map_err(|e| BoxError::BuildError(format!("Invalid merged path for mount: {}", e)))?;
let fstype = CString::new("overlay").unwrap();
let mut failures = Vec::new();
for &(options, metadata_copy) in &options {
let data = CString::new(options.as_str()).map_err(|error| {
BoxError::BuildError(format!("Invalid overlay mount options: {error}"))
})?;
let ret = unsafe {
libc::mount(
source.as_ptr(),
target.as_ptr(),
fstype.as_ptr(),
0,
data.as_ptr() as *const libc::c_void,
)
};
if ret == 0 {
tracing::debug!(
lower = %lower.display(),
merged = %merged.display(),
metadata_copy,
"Overlay mounted via mount(2)"
);
return Ok(());
}
failures.push(format!(
"mount(2), metacopy={metadata_copy}: {}",
std::io::Error::last_os_error()
));
}
tracing::debug!(
errors = ?failures,
"mount(2) failed, trying mount command"
);
for &(options, metadata_copy) in &options {
match std::process::Command::new("mount")
.args(["-t", "overlay", "overlay", "-o", options])
.arg(merged)
.status()
{
Ok(status) if status.success() => {
tracing::debug!(
lower = %lower.display(),
merged = %merged.display(),
metadata_copy,
"Overlay mounted via mount command"
);
return Ok(());
}
Ok(status) => {
failures.push(format!("mount command, metacopy={metadata_copy}: {status}"))
}
Err(error) => {
failures.push(format!("mount command, metacopy={metadata_copy}: {error}"))
}
}
}
Err(BoxError::BuildError(format!(
"Failed to mount overlayfs at {}: {}",
merged.display(),
failures.join("; ")
)))
}
#[cfg(not(target_os = "linux"))]
{
let _ = (lower, upper, work, merged, base_options);
Err(BoxError::BuildError(
"Overlayfs is only supported on Linux".to_string(),
))
}
}
fn overlay_options(lower: &Path, upper: &Path, work: &Path, metadata_copy: bool) -> String {
let mut options = format!(
"lowerdir={},upperdir={},workdir={}",
lower.display(),
upper.display(),
work.display()
);
if metadata_copy {
options.push_str(",metacopy=on");
}
options
}
pub fn overlay_unmount(merged: &Path) -> Result<()> {
overlay_unmount_with_mode(merged, true)
}
pub(crate) fn overlay_unmount_for_reuse(merged: &Path) -> Result<()> {
overlay_unmount_with_mode(merged, false)
}
fn overlay_unmount_with_mode(merged: &Path, lazy: bool) -> Result<()> {
#[cfg(target_os = "linux")]
{
use std::ffi::CString;
let target = CString::new(merged.to_string_lossy().as_ref())
.map_err(|e| BoxError::BuildError(format!("Invalid path for umount: {}", e)))?;
let flags = if lazy { libc::MNT_DETACH } else { 0 };
let ret = unsafe { libc::umount2(target.as_ptr(), flags) };
if ret == 0 {
tracing::debug!(path = %merged.display(), lazy, "Overlay unmounted");
return Ok(());
}
let errno = std::io::Error::last_os_error();
let mut command = std::process::Command::new("umount");
if lazy {
command.arg("-l");
}
let status = command
.arg(merged)
.status()
.map_err(|e| BoxError::BuildError(format!("Failed to run umount command: {}", e)))?;
if status.success() {
tracing::debug!(path = %merged.display(), lazy, "Overlay unmounted via umount command");
return Ok(());
}
Err(BoxError::BuildError(format!(
"Failed to {}unmount overlayfs at {}: umount2 returned {}, umount command exited with {}",
if lazy { "lazily " } else { "synchronously " },
merged.display(),
errno,
status
)))
}
#[cfg(not(target_os = "linux"))]
{
let _ = (merged, lazy);
Ok(())
}
}
#[cfg(target_os = "linux")]
pub(crate) fn is_overlay_supported() -> bool {
static OVERLAY_SUPPORTED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
cached_overlay_support(&OVERLAY_SUPPORTED, probe_overlay_support)
}
#[cfg(target_os = "linux")]
fn cached_overlay_support(cache: &std::sync::OnceLock<bool>, probe: impl FnOnce() -> bool) -> bool {
*cache.get_or_init(probe)
}
#[cfg(target_os = "linux")]
fn probe_overlay_support() -> bool {
if let Ok(fs_list) = std::fs::read_to_string("/proc/filesystems") {
if !fs_list.contains("overlay") {
tracing::debug!("Overlay not listed in /proc/filesystems");
return false;
}
} else {
return false;
}
let tmp = match tempfile::TempDir::new() {
Ok(t) => t,
Err(_) => return false,
};
let lower = tmp.path().join("lower");
let upper = tmp.path().join("upper");
let work = tmp.path().join("work");
let merged = tmp.path().join("merged");
for dir in [&lower, &upper, &work, &merged] {
if std::fs::create_dir_all(dir).is_err() {
return false;
}
}
let ok = overlay_mount(&lower, &upper, &work, &merged).is_ok();
if ok {
let _ = overlay_unmount(&merged);
}
ok
}
#[cfg(not(target_os = "linux"))]
#[allow(dead_code)]
pub(crate) fn is_overlay_supported() -> bool {
false
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_overlay_supported_returns_bool() {
let _supported = is_overlay_supported();
}
#[cfg(target_os = "linux")]
#[test]
fn concurrent_overlay_support_queries_probe_once() {
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Barrier, OnceLock};
const THREADS: usize = 8;
let cache = Arc::new(OnceLock::new());
let calls = Arc::new(AtomicUsize::new(0));
let barrier = Arc::new(Barrier::new(THREADS));
std::thread::scope(|scope| {
let handles = (0..THREADS)
.map(|_| {
let cache = cache.clone();
let calls = calls.clone();
let barrier = barrier.clone();
scope.spawn(move || {
barrier.wait();
cached_overlay_support(&cache, || {
calls.fetch_add(1, Ordering::SeqCst);
std::thread::sleep(std::time::Duration::from_millis(20));
true
})
})
})
.collect::<Vec<_>>();
for handle in handles {
assert!(handle.join().unwrap());
}
});
assert_eq!(calls.load(Ordering::SeqCst), 1);
}
#[cfg(not(target_os = "linux"))]
#[test]
fn test_overlay_not_supported_on_non_linux() {
assert!(!is_overlay_supported());
}
#[cfg(not(target_os = "linux"))]
#[test]
fn test_overlay_mount_fails_on_non_linux() {
let tmp = tempfile::TempDir::new().unwrap();
let result = overlay_mount(
&tmp.path().join("l"),
&tmp.path().join("u"),
&tmp.path().join("w"),
&tmp.path().join("m"),
);
assert!(result.is_err());
}
#[test]
fn test_overlay_mount_rejects_comma_in_mount_option_paths() {
let tmp = tempfile::TempDir::new().unwrap();
let lower = tmp.path().join("lower,with-comma");
let upper = tmp.path().join("upper");
let work = tmp.path().join("work");
let merged = tmp.path().join("merged");
let err = overlay_mount(&lower, &upper, &work, &merged).unwrap_err();
assert!(err.to_string().contains("contains a comma"));
assert!(err.to_string().contains("lower,with-comma"));
}
#[test]
fn metadata_copy_option_is_explicit() {
let lower = Path::new("/cache/lower");
let upper = Path::new("/box/upper");
let work = Path::new("/box/work");
assert_eq!(
overlay_options(lower, upper, work, false),
"lowerdir=/cache/lower,upperdir=/box/upper,workdir=/box/work"
);
assert_eq!(
overlay_options(lower, upper, work, true),
"lowerdir=/cache/lower,upperdir=/box/upper,workdir=/box/work,metacopy=on"
);
}
#[cfg(not(target_os = "linux"))]
#[test]
fn test_overlay_unmount_noop_on_non_linux() {
let tmp = tempfile::TempDir::new().unwrap();
assert!(overlay_unmount(tmp.path()).is_ok());
}
#[cfg(target_os = "linux")]
#[test]
fn test_overlay_mount_and_unmount() {
if !is_overlay_supported() {
return;
}
let tmp = tempfile::TempDir::new().unwrap();
let lower = tmp.path().join("lower");
let upper = tmp.path().join("upper");
let work = tmp.path().join("work");
let merged = tmp.path().join("merged");
for dir in [&lower, &upper, &work, &merged] {
std::fs::create_dir_all(dir).unwrap();
}
std::fs::write(lower.join("hello.txt"), "from lower").unwrap();
overlay_mount(&lower, &upper, &work, &merged).unwrap();
assert_eq!(
std::fs::read_to_string(merged.join("hello.txt")).unwrap(),
"from lower"
);
std::fs::write(merged.join("new.txt"), "from upper").unwrap();
assert!(upper.join("new.txt").exists());
overlay_unmount(&merged).unwrap();
}
}