diff --git a/src/core/diagnostics.rs b/src/core/diagnostics.rs index 8cde4bb..e3f7ffd 100644 --- a/src/core/diagnostics.rs +++ b/src/core/diagnostics.rs @@ -447,6 +447,32 @@ pub fn check_seccomp() -> DiagnosticResult { } } +/// Check if project root is on overlayfs (e.g., inside Docker) +/// +/// Nested overlay mounts are unsupported by the Linux kernel, so tach +/// must fall back to fork-only isolation when running inside a container +/// that uses the overlay2 storage driver. +pub fn check_overlay_filesystem(project_root: &std::path::Path) -> DiagnosticResult { + if crate::isolation::is_overlayfs(project_root) { + DiagnosticResult::warn( + "Overlay FS", + "Project root is on overlayfs (Docker detected)", + ) + .with_details( + "Nested overlay mounts are unsupported by the Linux kernel.\n\ + Tach will automatically fall back to fork-only isolation.\n\ + This is safe but disables filesystem write protection.", + ) + .with_remediation(Remediation { + command: Some("Run natively or use --no-isolation to suppress this check".to_string()), + docs_url: None, + explanation: "Overlay isolation requires ext4/btrfs/xfs host filesystem".to_string(), + }) + } else { + DiagnosticResult::pass("Overlay FS", "Native filesystem — full isolation available") + } +} + /// Check jemalloc allocator status pub fn check_jemalloc() -> DiagnosticResult { match crate::allocator::verify_jemalloc_active() { @@ -839,6 +865,7 @@ pub fn run_diagnostics() -> DiagnosticReport { check_userfaultfd(), check_landlock(), check_seccomp(), + check_overlay_filesystem(&std::env::current_dir().unwrap_or_default()), check_jemalloc(), check_ptrace_capability(), check_python(), @@ -892,6 +919,9 @@ pub fn run_and_print_diagnose() -> bool { let seccomp_result = check_seccomp(); print_diagnose_line(" Seccomp", &seccomp_result); + let overlay_result = check_overlay_filesystem(&std::env::current_dir().unwrap_or_default()); + print_diagnose_line(" Overlay FS", &overlay_result); + let jemalloc_result = check_jemalloc(); print_diagnose_line(" Jemalloc", &jemalloc_result); eprintln!(); diff --git a/src/isolation/mod.rs b/src/isolation/mod.rs index ffe089c..9a8cf8a 100644 --- a/src/isolation/mod.rs +++ b/src/isolation/mod.rs @@ -12,7 +12,7 @@ pub mod sandbox; pub mod snapshot; // Re-export main functions from namespace for backward compatibility -pub use namespace::setup_filesystem; +pub use namespace::{is_overlayfs, setup_filesystem}; // Re-export calibration for Zygote warm-up pub use calibration::TlsCalibration; diff --git a/src/isolation/namespace.rs b/src/isolation/namespace.rs index 9d6814b..1593f29 100644 --- a/src/isolation/namespace.rs +++ b/src/isolation/namespace.rs @@ -13,6 +13,27 @@ use std::fs; use std::path::{Path, PathBuf}; use std::process::Command; +/// overlayfs filesystem magic number from the Linux kernel +const OVERLAYFS_SUPER_MAGIC: i64 = 0x794c7630; + +/// Detect if the given path resides on an overlayfs filesystem. +/// +/// Used to prevent nested overlay mounts which the Linux kernel does not +/// support — this is the root cause of isolation failures in Docker +/// containers where the storage driver is overlay2. +pub fn is_overlayfs(path: &Path) -> bool { + let mut buf: libc::statfs = unsafe { std::mem::zeroed() }; + let c_path = match std::ffi::CString::new(path.to_string_lossy().as_bytes()) { + Ok(p) => p, + Err(_) => return false, + }; + let rc = unsafe { libc::statfs(c_path.as_ptr(), &mut buf) }; + if rc != 0 { + return false; + } + buf.f_type == OVERLAYFS_SUPER_MAGIC +} + /// Set up complete isolation for a worker (Iron Dome) /// /// CRITICAL SEQUENCE: @@ -30,6 +51,14 @@ pub fn setup_filesystem(worker_id: u32, project_root: &Path) -> Result<()> { return Ok(()); } + let overlay_disabled = is_overlayfs(project_root); + if overlay_disabled { + eprintln!( + "[tach:isolation] Project root is on overlayfs (Docker detected). \ + Overlay mounts disabled — using fork-only isolation." + ); + } + // 1. Create new mount AND network namespaces unshare(CloneFlags::CLONE_NEWNS | CloneFlags::CLONE_NEWNET) .context("unshare(CLONE_NEWNS | CLONE_NEWNET) failed - requires CAP_SYS_ADMIN")?; @@ -86,38 +115,40 @@ pub fn setup_filesystem(worker_id: u32, project_root: &Path) -> Result<()> { fs::create_dir_all(&proj_upper)?; fs::create_dir_all(&proj_work)?; - // 8. Overlay /tmp (writable zone #1) - let tmp_overlay_opts = format!( - "lowerdir=/tmp,upperdir={},workdir={}", - tmp_upper.display(), - tmp_work.display() - ); - - mount::( - Some("overlay"), - "/tmp", - Some("overlay"), - MsFlags::empty(), - Some(&tmp_overlay_opts), - ) - .context("Failed to mount overlay on /tmp")?; + if !overlay_disabled { + // 8. Overlay /tmp (writable zone #1) + let tmp_overlay_opts = format!( + "lowerdir=/tmp,upperdir={},workdir={}", + tmp_upper.display(), + tmp_work.display() + ); - // 9. Overlay project root (writable zone #2) - let proj_overlay_opts = format!( - "lowerdir={},upperdir={},workdir={}", - project_root.display(), - proj_upper.display(), - proj_work.display() - ); + mount::( + Some("overlay"), + "/tmp", + Some("overlay"), + MsFlags::empty(), + Some(&tmp_overlay_opts), + ) + .context("Failed to mount overlay on /tmp")?; + + // 9. Overlay project root (writable zone #2) + let proj_overlay_opts = format!( + "lowerdir={},upperdir={},workdir={}", + project_root.display(), + proj_upper.display(), + proj_work.display() + ); - mount::( - Some("overlay"), - project_root, - Some("overlay"), - MsFlags::empty(), - Some(&proj_overlay_opts), - ) - .context("Failed to mount overlay on project root")?; + mount::( + Some("overlay"), + project_root, + Some("overlay"), + MsFlags::empty(), + Some(&proj_overlay_opts), + ) + .context("Failed to mount overlay on project root")?; + } Ok(()) }