diff --git a/src/compute.rs b/src/compute.rs index 27ff041d..4f2989fb 100644 --- a/src/compute.rs +++ b/src/compute.rs @@ -153,7 +153,8 @@ fn prepare_snapshot_dir(path: &Path) -> Result<()> { { use std::os::unix::fs::MetadataExt; - let metadata = std::fs::symlink_metadata(path)?; + // Follow symlinks: a user-owned link to a user-owned directory is fine. + let metadata = std::fs::metadata(path)?; if !metadata.file_type().is_dir() || metadata.uid() != unsafe { libc::geteuid() } { return Err(anyhow!( "Source snapshot directory {} is not owned by the current user.", @@ -1081,6 +1082,18 @@ fn not_ready(detail: impl Into) -> Preflight { pub(crate) mod tests { use super::*; + #[cfg(unix)] + #[test] + fn snapshot_dir_accepts_a_user_owned_symlinked_directory() { + let root = std::env::temp_dir().join(format!("orx-snapshot-link-{}", uuid::Uuid::new_v4())); + let target = root.join("target"); + let link = root.join("source-snapshots"); + std::fs::create_dir_all(&target).unwrap(); + std::os::unix::fs::symlink(&target, &link).unwrap(); + prepare_snapshot_dir(&link).unwrap(); + std::fs::remove_dir_all(&root).unwrap(); + } + #[test] fn run_reports_name_the_native_invoker_or_an_explicit_exception() { let identity = |harness: &str| crate::store::InvocationIdentity { diff --git a/src/store.rs b/src/store.rs index 60eb65e2..d08a4dc8 100644 --- a/src/store.rs +++ b/src/store.rs @@ -1931,7 +1931,13 @@ impl Store { .chain(["source-snapshots"]) { let path = self.data_dir.join(name); - match std::fs::symlink_metadata(&path) { + // Launch accepts user-owned links for these roots (compute::prepare_snapshot_dir). + let metadata = if matches!(name, "source-snapshots" | "submission-handles") { + std::fs::metadata(&path) + } else { + std::fs::symlink_metadata(&path) + }; + match metadata { Ok(metadata) if !metadata.file_type().is_dir() => { return Err(anyhow!( "Refusing artifact cleanup through {}", @@ -5058,6 +5064,23 @@ mod tests { std::fs::remove_dir_all(dir).unwrap(); } + #[cfg(unix)] + #[test] + fn artifact_cleanup_follows_symlinked_snapshot_roots_only() { + let dir = std::env::temp_dir().join(format!("orx-linked-roots-{}", uuid::Uuid::new_v4())); + let store = Store::open_at(dir.clone()).unwrap(); + for name in ["source-snapshots", "submission-handles"] { + std::fs::create_dir_all(dir.join(format!("outside-{name}"))).unwrap(); + std::os::unix::fs::symlink(dir.join(format!("outside-{name}")), dir.join(name)) + .unwrap(); + } + store.validate_run_artifact_roots().unwrap(); + std::fs::create_dir_all(dir.join("outside-local-runs")).unwrap(); + std::os::unix::fs::symlink(dir.join("outside-local-runs"), dir.join("local-runs")).unwrap(); + assert!(store.validate_run_artifact_roots().is_err()); + std::fs::remove_dir_all(&dir).unwrap(); + } + #[test] fn refused_project_deletion_never_reaches_the_native_purge() { let dir = std::env::temp_dir().join(format!("orx-delete-order-{}", uuid::Uuid::new_v4()));