use std::collections::BTreeMap; use std::ffi::OsString; use std::fs; use std::io; use std::os::unix::ffi::OsStrExt; use std::os::unix::fs::MetadataExt; use std::path::{Path, PathBuf}; use std::process::ExitCode; use std::sync::atomic::{AtomicU64, Ordering}; use lazaret_core::configuration::{ ConfigurationLoadRequest, SandboxLayoutV1, load_trusted_configuration, }; use lazaret_core::identity::{CoordinatorId, DomainId, GenerationId}; use lazaret_core::records::{ ArtifactState, HostProcessOwnershipSlot, PreparedArtifact, ProcessOwnershipCommitAck, }; use lazaret_linux::process::{ ProcessOwnershipContext, current_process_identity, dispatch_private_process_role, }; use lazaret_state_anchor::{ FuseOverlayfsMount, ProtectedRuntimeFiles, RuntimeStateAnchorAuthority, StateAnchorAcquisition, StateAnchorLaunchAuthority, StateAnchorLayout, StateAnchorSandboxView, unmount_after_coordinator_loss, }; use rustix::process::Signal; static SEQUENCE: AtomicU64 = AtomicU64::new(0); fn main() -> ExitCode { let arguments = std::env::args_os().skip(1).collect::>(); if let Some(result) = dispatch_private_process_role(&arguments) { return exit(result); } exit(run()) } fn exit(result: io::Result<()>) -> ExitCode { match result { Ok(()) => ExitCode::SUCCESS, Err(error) => { eprintln!("State Anchor lifecycle fixture failed: {error}"); ExitCode::FAILURE } } } #[allow(clippy::too_many_lines)] fn run() -> io::Result<()> { match fs::metadata("/dev/fuse") { Ok(_) => {} Err(error) if error.kind() == io::ErrorKind::NotFound => { eprintln!("skipped: /dev/fuse is unavailable"); return Ok(()); } Err(error) => return Err(error), } let tree = TestTree::new()?; let configuration = configuration(&tree)?; let lower = tree.path.join("prepared-lower"); fs::create_dir_all(lower.join("usr/bin"))?; fs::create_dir_all(lower.join("etc"))?; fs::write(lower.join("usr/bin/base-tool"), b"base")?; for name in ["resolv.conf", "hosts", "hostname"] { fs::write(lower.join("etc").join(name), b"image")?; } let metadata = lower.metadata()?; let artifact = PreparedArtifact { prepared_artifact_id: lazaret_core::identity::PreparedArtifactId::new([8; 32]), source_identity: lazaret_core::identity::SourceIdentity::new(b"fixture".to_vec()), selected_manifest_digest: lazaret_core::identity::SelectedManifestDigest::new([9; 32]), policy_id: lazaret_core::identity::PolicyId::new([10; 32]), id_map_digest: lazaret_core::identity::IdMapDigest::new([11; 32]), path: lazaret_core::identity::HostPath::new(lower.as_os_str().as_encoded_bytes().to_vec()), device: lazaret_core::identity::DeviceId::new(metadata.dev()), inode: lazaret_core::identity::InodeId::new(metadata.ino()), state: ArtifactState::Available, }; let layout = StateAnchorLayout::create(&configuration).map_err(io::Error::other)?; let launch = StateAnchorLaunchAuthority::capture(&layout, &artifact).map_err(io::Error::other)?; let runtime_authority = RuntimeStateAnchorAuthority::from_inherited( launch.lower_fd(), launch.persistent_anchor_fd(), launch.runtime_anchor_fd(), launch.expected_lower(), ) .map_err(io::Error::other)?; let expected = std::cell::Cell::new(None); let runtime_mount = runtime_authority .acquire( current_process_identity()?, ProcessOwnershipContext { domain_id: DomainId::new([1; 32]), generation_id: GenerationId::new(1), coordinator_id: CoordinatorId::new([3; 32]), slot: HostProcessOwnershipSlot::StateAnchorDaemon, }, |staged| { expected.set(Some(staged)); Ok(()) }, |staged| Ok(ProcessOwnershipCommitAck::v1(staged)), |readiness| { if expected.get() != Some(readiness.staged()) || lazaret_linux::filesystem::current_mount_identity(layout.merged())? != readiness.mount() { return Err(io::Error::other( "inherited State Anchor readiness changed authority", )); } Ok(()) }, ) .map_err(io::Error::other)?; runtime_mount.unmount().map_err(io::Error::other)?; let first = acquire(&layout, &artifact)?; fs::write( layout.merged().join("usr/bin/installed-tool"), b"persistent", )?; fs::write(layout.merged().join("etc/developer.conf"), b"persistent")?; if lower.join("usr/bin/installed-tool").exists() || lower.join("etc/developer.conf").exists() || !layout.upper().join("usr/bin/installed-tool").exists() || !layout.upper().join("etc/developer.conf").exists() { return Err(io::Error::other( "writes did not remain upper-only over the read-only lower", )); } let view = StateAnchorSandboxView::from_mounted(&layout, &artifact, first.mount_identity()) .map_err(io::Error::other)?; view.revalidate().map_err(io::Error::other)?; layout .replace_protected_runtime_files(&ProtectedRuntimeFiles { resolv_conf: b"nameserver 192.0.2.53\n", hosts: b"127.0.0.1 localhost\n", hostname: b"lazaret\n", }) .map_err(io::Error::other)?; let runtime_sources = view.protected_runtime_mounts().collect::>(); if runtime_sources.len() != 3 || runtime_sources .iter() .any(|(source, _)| source.starts_with(&lower) || source.starts_with(layout.upper())) { return Err(io::Error::other( "protected runtime files are not host-private runtime material", )); } if lower.join("etc/resolv.conf").read_link().is_ok() || fs::read(lower.join("etc/resolv.conf"))? != b"image" || layout.upper().join("etc/resolv.conf").exists() || fs::read(&runtime_sources[0].0)? != b"nameserver 192.0.2.53\n" { return Err(io::Error::other( "runtime-file replacement escaped transient storage", )); } drop(view); let wrong = lazaret_linux::filesystem::current_mount_identity(layout.upper())?; if unmount_after_coordinator_loss(&layout, wrong).is_ok() { return Err(io::Error::other( "ownership mismatch unmounted foreign identity", )); } first.unmount().map_err(io::Error::other)?; let second = acquire(&layout, &artifact)?; if fs::read(layout.merged().join("usr/bin/installed-tool"))? != b"persistent" || fs::read(layout.merged().join("etc/developer.conf"))? != b"persistent" { return Err(io::Error::other( "upper state did not persist across remount", )); } let identity = second.mount_identity(); second.process().send_signal(Signal::KILL)?; while !second.process().has_exited()? { std::thread::yield_now(); } unmount_after_coordinator_loss(&layout, identity).map_err(io::Error::other)?; second .finish_after_external_unmount() .map_err(io::Error::other)?; drop(tree); Ok(()) } fn acquire<'layout, 'configuration>( layout: &'layout StateAnchorLayout<'configuration>, artifact: &PreparedArtifact, ) -> io::Result> { let expected = std::cell::Cell::new(None); FuseOverlayfsMount::acquire( layout, StateAnchorAcquisition { lower: Path::new(std::ffi::OsStr::from_bytes(artifact.path.as_bytes())), expected_lower: (artifact.device.get(), artifact.inode.get()), expected_parent: current_process_identity()?, ownership: ProcessOwnershipContext { domain_id: DomainId::new([1; 32]), generation_id: GenerationId::new(2), coordinator_id: CoordinatorId::new([3; 32]), slot: HostProcessOwnershipSlot::StateAnchorDaemon, }, }, |staged| { expected.set(Some(staged)); Ok(()) }, |staged| { if expected.get() != Some(staged) { return Err(io::Error::other("staged process identity changed")); } Ok(ProcessOwnershipCommitAck::v1(staged)) }, |readiness| { if expected.get() != Some(readiness.staged()) { return Err(io::Error::other("ready process identity changed")); } if lazaret_linux::filesystem::current_mount_identity(layout.merged())? != readiness.mount() { return Err(io::Error::other( "readiness preceded exact mount verification", )); } Ok(()) }, ) .map_err(io::Error::other) } struct TestTree { path: PathBuf, } impl TestTree { fn new() -> io::Result { let path = std::env::temp_dir().join(format!( "lazaret state-anchor,:{}-{}", std::process::id(), SEQUENCE.fetch_add(1, Ordering::Relaxed) )); fs::create_dir(&path)?; Ok(Self { path }) } } impl Drop for TestTree { fn drop(&mut self) { let _ = fs::remove_dir_all(&self.path); } } fn configuration( tree: &TestTree, ) -> io::Result { let configuration_directory = tree.path.join("configuration"); let workspace = tree.path.join("workspace"); let runtime = tree.path.join("runtime"); fs::create_dir(&configuration_directory)?; fs::create_dir(&workspace)?; fs::create_dir(&runtime)?; 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\", \"/etc\"]\n[profiles.default.readiness]\ndenied = [{{ address = \"10.20.30.40\", port = 443 }}]\npermitted = [{{ address = \"192.0.2.10\", port = 443 }}]\ndns = [{{ resolver = \"198.51.100.53\", query = \"example.invalid\" }}]\n", tree.path.join("data").display() ), )?; let layout = SandboxLayoutV1::from_production_layout([ "/workspace".to_owned(), "/proc".to_owned(), "/sys".to_owned(), "/dev".to_owned(), "/run/lazaret".to_owned(), ]) .map_err(io::Error::other)?; 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, }) .map_err(io::Error::other) }