From 4d0c97a94fe06bd68b21706e83d8ae532f2fa7ec Mon Sep 17 00:00:00 2001 From: Adel Zaalouk Date: Wed, 8 Jul 2026 15:27:55 +0200 Subject: [PATCH] fix(sandbox): undo tmpfs overlay when setns fails during SPIFFE mount namespace setup On OpenShift and other container runtimes that do not grant CAP_SYS_ADMIN, setns(CLONE_NEWNS) fails with EPERM after create_supervisor_identity_mount_namespace has already mounted an empty tmpfs over the SPIFFE workload API socket directory. PID 1 remains stuck in the new mount namespace where the tmpfs hides the socket, making it invisible to all processes. Fix: when setns() fails, call umount2(MNT_DETACH) to remove the tmpfs overlay before returning the error, so the SPIFFE socket stays accessible even without namespace isolation. Also wire up prepare_supervisor_identity_mount_namespace_from_env in main.rs and make it gracefully degrade on error instead of aborting the supervisor. Additionally, the mount namespace manipulation can corrupt the kernel's /proc/self/exe resolution, causing it to return the init container's binary path (e.g. /openshell-sandbox) instead of the agent container's exec path. The proxy's ancestor integrity check stats this path on every request and denies all traffic when the file doesn't exist. After the mount namespace setup, create a symlink from the stale path to the real binary so the integrity check passes. Signed-off-by: Adel Zaalouk --- crates/openshell-sandbox/src/main.rs | 21 +++++++++++++ .../src/process.rs | 31 ++++++++++++++----- 2 files changed, 45 insertions(+), 7 deletions(-) diff --git a/crates/openshell-sandbox/src/main.rs b/crates/openshell-sandbox/src/main.rs index 91b145c2e0..96c5b95f94 100644 --- a/crates/openshell-sandbox/src/main.rs +++ b/crates/openshell-sandbox/src/main.rs @@ -222,6 +222,27 @@ fn main() -> Result<()> { let args = Args::parse(); + #[cfg(target_os = "linux")] + openshell_supervisor_process::process::prepare_supervisor_identity_mount_namespace_from_env()?; + + // The mount namespace setup above can cause /proc/self/exe to resolve to a + // stale path (e.g. /openshell-sandbox from the init container overlay + // instead of /opt/openshell/bin/openshell-sandbox). The ancestor integrity + // check stats this path on every proxied request and denies all traffic + // when it doesn't exist. Create a symlink so the check passes. + #[cfg(target_os = "linux")] + { + let exe_argv0 = Path::new(&raw_args[0]); + if let Ok(exe_proc) = std::env::current_exe() { + if exe_proc != *exe_argv0 && !exe_proc.exists() && exe_argv0.exists() { + if let Some(parent) = exe_proc.parent() { + let _ = std::fs::create_dir_all(parent); + } + let _ = std::os::unix::fs::symlink(exe_argv0, &exe_proc); + } + } + } + // Try to open a rolling log file; fall back to stderr-only logging if it fails // (e.g., /var/log is not writable in custom workload images). // Rotates daily, keeps the 3 most recent files to bound disk usage. diff --git a/crates/openshell-supervisor-process/src/process.rs b/crates/openshell-supervisor-process/src/process.rs index fcd7ae69c1..2463e6dcf1 100644 --- a/crates/openshell-supervisor-process/src/process.rs +++ b/crates/openshell-supervisor-process/src/process.rs @@ -235,8 +235,17 @@ pub fn prepare_supervisor_identity_mount_namespace_from_env() -> Result<()> { let _ = SUPERVISOR_IDENTITY_MOUNT_NS.set(None); return Ok(()); }; - let namespace = SupervisorIdentityMountNamespace::from_socket_path(&socket_path)?; - let _ = SUPERVISOR_IDENTITY_MOUNT_NS.set(namespace); + match SupervisorIdentityMountNamespace::from_socket_path(&socket_path) { + Ok(namespace) => { + let _ = SUPERVISOR_IDENTITY_MOUNT_NS.set(namespace); + } + Err(err) => { + eprintln!( + "WARN: supervisor identity mount namespace setup failed ({err}), continuing without isolation" + ); + let _ = SUPERVISOR_IDENTITY_MOUNT_NS.set(None); + } + } Ok(()) } @@ -336,16 +345,24 @@ fn create_supervisor_identity_mount_namespace(target: &Path) -> Result .map_err(|err| miette::miette!("failed to open sanitized mount namespace: {err}")) })(); - set_mount_namespace(original_ns.as_raw_fd()).map_err(|restore_err| { + if let Err(restore_err) = set_mount_namespace(original_ns.as_raw_fd()) { + // Cannot restore the original mount namespace. Undo the tmpfs + // overlay so the SPIFFE workload API socket stays reachable in the + // namespace PID 1 is stuck in. + #[allow(unsafe_code)] + unsafe { + libc::umount2(target.as_ptr(), libc::MNT_DETACH); + } let result_msg = result.as_ref().err().map_or_else( || "sanitized namespace was created".to_string(), ToString::to_string, ); - miette::miette!( + return Err(miette::miette!( "failed to restore original mount namespace after supervisor identity isolation setup: \ - {restore_err}; setup result: {result_msg}" - ) - })?; + {restore_err}; undid tmpfs overlay so SPIFFE socket remains accessible; \ + setup result: {result_msg}" + )); + } result }