use std::collections::BTreeMap; use std::ffi::OsString; use std::fs::{self, File}; use std::os::unix::ffi::OsStringExt; use std::os::unix::fs::{MetadataExt, PermissionsExt}; use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicU64, Ordering}; use lazaret_core::configuration::{ ConfigurationLoadRequest, SandboxLayoutV1, load_trusted_configuration, }; use lazaret_core::identity::{ DeviceId, HostPath, IdMapDigest, InodeId, PolicyId, PreparedArtifactId, SelectedManifestDigest, SourceIdentity, }; use lazaret_core::records::{ArtifactState, PreparedArtifact}; use lazaret_state_anchor::{ ProtectedRuntimeFiles, RuntimeStateAnchorAuthority, RuntimeStateAnchorInherited, StateAnchorLaunchAuthority, StateAnchorLayout, StateAnchorResetScope, }; use rustix::fs::{XattrFlags, fsetxattr}; static SEQUENCE: AtomicU64 = AtomicU64::new(0); #[test] fn maintenance_accepts_exact_empty_fuse_overlayfs_work_shape() { let tree = TestTree::new(); let configuration = configuration(&tree); let layout = StateAnchorLayout::create(&configuration).unwrap(); let nested = domain_path(&configuration).join("state-anchor/work/work"); fs::create_dir(&nested).unwrap(); fs::set_permissions(&nested, fs::Permissions::from_mode(0o700)).unwrap(); layout.inspect_state().unwrap(); } #[test] fn inactive_normalization_removes_exact_opaque_fuse_overlayfs_residue() { let tree = TestTree::new(); let configuration = configuration(&tree); let layout = StateAnchorLayout::create(&configuration).unwrap(); let nested = domain_path(&configuration).join("state-anchor/work/work"); fs::create_dir(&nested).unwrap(); fs::set_permissions(&nested, fs::Permissions::from_mode(0o700)).unwrap(); create_opaque_work_residue(&nested, "4"); create_opaque_work_residue(&nested, "12"); assert!(layout.inspect_state().is_err()); layout.normalize_inactive_work_directory().unwrap(); layout.inspect_state().unwrap(); let work = domain_path(&configuration).join("state-anchor/work"); assert!(work.read_dir().unwrap().next().is_none()); } #[test] fn inactive_normalization_unlinks_work_symlinks_without_following_them() { let tree = TestTree::new(); let configuration = configuration(&tree); let layout = StateAnchorLayout::create(&configuration).unwrap(); let work = domain_path(&configuration).join("state-anchor/work"); let foreign = tree.path.join("foreign-work"); fs::create_dir(&foreign).unwrap(); fs::write(foreign.join("value"), b"foreign").unwrap(); std::os::unix::fs::symlink(&foreign, work.join("residue")).unwrap(); layout.normalize_inactive_work_directory().unwrap(); assert!(!work.join("residue").exists()); assert_eq!(fs::read(foreign.join("value")).unwrap(), b"foreign"); } #[test] fn maintenance_refuses_mutated_fuse_overlayfs_work_shapes() { let tree = TestTree::new(); let configuration = configuration(&tree); let layout = StateAnchorLayout::create(&configuration).unwrap(); let work = domain_path(&configuration).join("state-anchor/work"); let nested = work.join("work"); fs::create_dir(&nested).unwrap(); fs::set_permissions(&nested, fs::Permissions::from_mode(0o755)).unwrap(); assert!(layout.inspect_state().is_err()); fs::set_permissions(&nested, fs::Permissions::from_mode(0o700)).unwrap(); fs::write(nested.join("unexpected"), b"").unwrap(); assert!(layout.inspect_state().is_err()); fs::remove_file(nested.join("unexpected")).unwrap(); fs::write(work.join("unexpected"), b"").unwrap(); assert!(layout.inspect_state().is_err()); fs::remove_file(work.join("unexpected")).unwrap(); fs::remove_dir(&nested).unwrap(); let foreign = tree.path.join("foreign-work"); fs::create_dir(&foreign).unwrap(); std::os::unix::fs::symlink(foreign, nested).unwrap(); assert!(layout.inspect_state().is_err()); } fn create_opaque_work_residue(nested: &Path, name: &str) { let residue = nested.join(name); fs::create_dir(&residue).unwrap(); fs::set_permissions(&residue, fs::Permissions::from_mode(0o755)).unwrap(); fs::write(residue.join(".wh..wh..opq"), b"").unwrap(); fs::set_permissions( residue.join(".wh..wh..opq"), fs::Permissions::from_mode(0o700), ) .unwrap(); let residue = File::open(residue).unwrap(); fsetxattr( &residue, "user.fuseoverlayfs.opaque", b"y", XattrFlags::empty(), ) .unwrap(); } #[test] fn selected_reset_removes_exact_state_and_whiteout_without_touching_siblings() { let tree = TestTree::new(); let configuration = configuration(&tree); let layout = StateAnchorLayout::create(&configuration).unwrap(); let upper = domain_path(&configuration).join("state-anchor/upper"); fs::create_dir_all(upper.join("usr/bin")).unwrap(); fs::write(upper.join("usr/bin/tool"), b"state").unwrap(); fs::write(upper.join(".wh.usr"), b"").unwrap(); fs::create_dir(upper.join("var")).unwrap(); fs::write(upper.join("var/unrelated"), b"unchanged").unwrap(); let inspection = layout.inspect_state().unwrap(); assert!( inspection .entries .iter() .any(|entry| entry.path == b"usr/bin/tool") ); layout .complete_state_reset(StateAnchorResetScope::Selected(Path::new("/usr"))) .unwrap(); assert!(!upper.join("usr").exists()); assert!(!upper.join(".wh.usr").exists()); assert_eq!(fs::read(upper.join("var/unrelated")).unwrap(), b"unchanged"); layout.require_no_reset_quarantine().unwrap(); } #[test] fn all_reset_unlinks_symlink_state_without_following_it() { let tree = TestTree::new(); let configuration = configuration(&tree); let layout = StateAnchorLayout::create(&configuration).unwrap(); let upper = domain_path(&configuration).join("state-anchor/upper"); let foreign = tree.path.join("foreign"); fs::create_dir(&foreign).unwrap(); fs::write(foreign.join("value"), b"foreign").unwrap(); std::os::unix::fs::symlink(&foreign, upper.join("link")).unwrap(); layout .complete_state_reset(StateAnchorResetScope::All) .unwrap(); assert!(upper.read_dir().unwrap().next().is_none()); assert_eq!(fs::read(foreign.join("value")).unwrap(), b"foreign"); } #[test] fn arbitrary_target_and_orphan_quarantine_are_refused() { let tree = TestTree::new(); let configuration = configuration(&tree); let layout = StateAnchorLayout::create(&configuration).unwrap(); assert!( layout .complete_state_reset(StateAnchorResetScope::Selected(Path::new("/etc"))) .is_err() ); fs::create_dir(domain_path(&configuration).join("state-anchor/reset-quarantine")).unwrap(); assert!(layout.require_no_reset_quarantine().is_err()); assert!(layout.inspect_state().is_err()); } #[test] fn replaced_runtime_root_is_refused_before_runtime_file_write() { let tree = TestTree::new(); let configuration = configuration(&tree); let layout = StateAnchorLayout::create(&configuration).unwrap(); let runtime_root = PathBuf::from(OsString::from_vec( configuration .runtime_domain_root() .canonical_label() .as_bytes() .to_vec(), )); let moved_root = runtime_root.with_extension("retained"); fs::rename(&runtime_root, &moved_root).unwrap(); let replacement_files = runtime_root.join("state-anchor/runtime-files"); fs::create_dir_all(&replacement_files).unwrap(); fs::write(replacement_files.join("hostname"), b"replacement\n").unwrap(); assert!(layout.revalidate().is_err()); assert!( layout .replace_protected_runtime_files(&ProtectedRuntimeFiles { resolv_conf: b"nameserver 192.0.2.1\n", hosts: b"127.0.0.1 localhost\n", hostname: b"changed\n", }) .is_err() ); assert_eq!( fs::read(replacement_files.join("hostname")).unwrap(), b"replacement\n" ); assert!(!replacement_files.join("resolv.conf").exists()); assert!(!replacement_files.join("hosts").exists()); } #[test] fn inherited_runtime_authority_accepts_only_the_exact_prepared_lower() { let tree = TestTree::new(); let configuration = configuration(&tree); let layout = StateAnchorLayout::create(&configuration).unwrap(); let lower = tree.path.join("prepared-lower"); fs::create_dir(&lower).unwrap(); let metadata = lower.metadata().unwrap(); let artifact = PreparedArtifact { prepared_artifact_id: PreparedArtifactId::new([8; 32]), source_identity: SourceIdentity::new(b"fixture".to_vec()), selected_manifest_digest: SelectedManifestDigest::new([9; 32]), policy_id: PolicyId::new([10; 32]), id_map_digest: IdMapDigest::new([11; 32]), path: HostPath::new(lower.as_os_str().as_encoded_bytes().to_vec()), device: DeviceId::new(metadata.dev()), inode: InodeId::new(metadata.ino()), state: ArtifactState::Available, }; let launch = StateAnchorLaunchAuthority::capture(&layout, &artifact).unwrap(); let expected = launch.expected_lower(); RuntimeStateAnchorAuthority::from_inherited(inherited(&launch, expected)).unwrap(); assert!( RuntimeStateAnchorAuthority::from_inherited(inherited( &launch, (expected.0, expected.1 + 1), )) .is_err() ); } #[test] fn retained_prepared_lower_authority_refuses_a_replacement_descriptor() { let tree = TestTree::new(); let configuration = configuration(&tree); let layout = StateAnchorLayout::create(&configuration).unwrap(); let lower = tree.path.join("prepared-lower"); let replacement = tree.path.join("replacement-lower"); fs::create_dir(&lower).unwrap(); fs::create_dir(&replacement).unwrap(); let metadata = lower.metadata().unwrap(); let artifact = PreparedArtifact { prepared_artifact_id: PreparedArtifactId::new([8; 32]), source_identity: SourceIdentity::new(b"fixture".to_vec()), selected_manifest_digest: SelectedManifestDigest::new([9; 32]), policy_id: PolicyId::new([10; 32]), id_map_digest: IdMapDigest::new([11; 32]), path: HostPath::new(b"artifacts/fixture".to_vec()), device: DeviceId::new(metadata.dev()), inode: InodeId::new(metadata.ino()), state: ArtifactState::Available, }; let exact = File::open(&lower).unwrap(); StateAnchorLaunchAuthority::capture_retained(&layout, &artifact, &lower, &exact).unwrap(); let replacement = File::open(&replacement).unwrap(); assert!( StateAnchorLaunchAuthority::capture_retained(&layout, &artifact, &lower, &replacement) .is_err() ); } #[test] fn namespace_local_rebound_refuses_a_replaced_state_anchor() { let tree = TestTree::new(); let configuration = configuration(&tree); let layout = StateAnchorLayout::create(&configuration).unwrap(); let lower = tree.path.join("prepared-lower"); fs::create_dir(&lower).unwrap(); let artifact = prepared_artifact(&lower); let launch = StateAnchorLaunchAuthority::capture(&layout, &artifact).unwrap(); let anchor = launch.runtime_anchor_label().to_owned(); fs::rename(&anchor, anchor.with_extension("retained")).unwrap(); fs::create_dir(&anchor).unwrap(); assert!( RuntimeStateAnchorAuthority::from_inherited(inherited(&launch, launch.expected_lower(),)) .is_err() ); } #[test] fn namespace_local_rebound_refuses_a_replaced_prepared_lower() { let tree = TestTree::new(); let configuration = configuration(&tree); let layout = StateAnchorLayout::create(&configuration).unwrap(); let lower = tree.path.join("prepared-lower"); fs::create_dir(&lower).unwrap(); let artifact = prepared_artifact(&lower); let launch = StateAnchorLaunchAuthority::capture(&layout, &artifact).unwrap(); fs::rename(&lower, lower.with_extension("retained")).unwrap(); fs::create_dir(&lower).unwrap(); assert!( RuntimeStateAnchorAuthority::from_inherited(inherited(&launch, launch.expected_lower(),)) .is_err() ); } #[test] fn namespace_local_rebound_refuses_a_replaced_persistent_anchor() { let tree = TestTree::new(); let configuration = configuration(&tree); let layout = StateAnchorLayout::create(&configuration).unwrap(); let lower = tree.path.join("prepared-lower"); fs::create_dir(&lower).unwrap(); let artifact = prepared_artifact(&lower); let launch = StateAnchorLaunchAuthority::capture(&layout, &artifact).unwrap(); let anchor = launch.persistent_anchor_label().to_owned(); fs::rename(&anchor, anchor.with_extension("retained")).unwrap(); fs::create_dir(&anchor).unwrap(); assert!( RuntimeStateAnchorAuthority::from_inherited(inherited(&launch, launch.expected_lower(),)) .is_err() ); } #[test] fn namespace_local_rebound_refuses_a_state_anchor_symlink() { let tree = TestTree::new(); let configuration = configuration(&tree); let layout = StateAnchorLayout::create(&configuration).unwrap(); let lower = tree.path.join("prepared-lower"); fs::create_dir(&lower).unwrap(); let artifact = prepared_artifact(&lower); let launch = StateAnchorLaunchAuthority::capture(&layout, &artifact).unwrap(); let anchor = launch.runtime_anchor_label().to_owned(); let retained = anchor.with_extension("retained"); fs::rename(&anchor, &retained).unwrap(); std::os::unix::fs::symlink(&retained, &anchor).unwrap(); assert!( RuntimeStateAnchorAuthority::from_inherited(inherited(&launch, launch.expected_lower(),)) .is_err() ); } fn inherited( launch: &StateAnchorLaunchAuthority, expected_lower: (u64, u64), ) -> RuntimeStateAnchorInherited<'_> { RuntimeStateAnchorInherited { lower_fd: launch.lower_fd(), lower_label: launch.lower_label(), persistent_root_fd: launch.persistent_root_fd(), persistent_anchor_fd: launch.persistent_anchor_fd(), persistent_anchor_label: launch.persistent_anchor_label(), runtime_root_fd: launch.runtime_root_fd(), runtime_anchor_fd: launch.runtime_anchor_fd(), runtime_anchor_label: launch.runtime_anchor_label(), expected_lower, } } fn prepared_artifact(lower: &Path) -> PreparedArtifact { let metadata = lower.metadata().unwrap(); PreparedArtifact { prepared_artifact_id: PreparedArtifactId::new([8; 32]), source_identity: SourceIdentity::new(b"fixture".to_vec()), selected_manifest_digest: SelectedManifestDigest::new([9; 32]), policy_id: PolicyId::new([10; 32]), id_map_digest: IdMapDigest::new([11; 32]), path: HostPath::new(lower.as_os_str().as_encoded_bytes().to_vec()), device: DeviceId::new(metadata.dev()), inode: InodeId::new(metadata.ino()), state: ArtifactState::Available, } } fn domain_path( configuration: &lazaret_core::configuration::ResolvedDomainConfiguration, ) -> PathBuf { PathBuf::from(OsString::from_vec( configuration .persistent_domain_root() .canonical_label() .as_bytes() .to_vec(), )) } struct TestTree { path: PathBuf, } impl TestTree { fn new() -> Self { let parent = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("../../target/test-artifacts/state-anchor-authority"); fs::create_dir_all(&parent).unwrap(); let parent = parent.canonicalize().unwrap(); let path = parent.join(format!( "lazaret-state-anchor-authority-{}-{}", std::process::id(), SEQUENCE.fetch_add(1, Ordering::Relaxed) )); fs::create_dir(&path).unwrap(); Self { path } } } impl Drop for TestTree { fn drop(&mut self) { fs::remove_dir_all(&self.path).unwrap(); } } fn configuration(tree: &TestTree) -> lazaret_core::configuration::ResolvedDomainConfiguration { let configuration_directory = tree.path.join("configuration"); let workspace = tree.path.join("workspace"); let runtime = tree.path.join("runtime"); fs::create_dir(&configuration_directory).unwrap(); fs::create_dir(&workspace).unwrap(); fs::create_dir(&runtime).unwrap(); let configuration_path = configuration_directory.join("lazaret.toml"); fs::write( &configuration_path, format!( "schema = \"lazaret-trusted-host-config-v1\"\n[storage]\ndata_root = \"{}\"\n[network]\ndefault_allow = [\"192.0.2.10/32\"]\n[profiles.default]\nimage = \"example.invalid/fixture:v1\"\npersistent_dirs = [\"/usr\"] ", tree.path.join("data").display() ), ) .unwrap(); let layout = SandboxLayoutV1::from_protected_destinations([ "/workspace".to_owned(), "/proc".to_owned(), "/sys".to_owned(), "/dev".to_owned(), "/run/lazaret".to_owned(), ]) .unwrap(); load_trusted_configuration(ConfigurationLoadRequest { explicit_configuration_path: Some(configuration_path), selected_profile: "default".to_owned(), workspace, home: tree.path.join("home"), xdg_config_home: None, xdg_data_home: None, xdg_runtime_dir: runtime, environment: BTreeMap::::new(), sandbox_layout: layout, }) .unwrap() }