use std::ffi::OsString; use std::fmt; use std::os::unix::ffi::OsStringExt; use std::path::{Path, PathBuf}; use lazaret_core::configuration::ResolvedDomainConfiguration; use lazaret_core::records::{ DurableDecision, ParticipantState, SessionPhase, SessionTerminalState, }; pub use lazaret_state_anchor::StateAnchorEntryKind; use lazaret_state_anchor::{ StateAnchorEntry, StateAnchorInspection, StateAnchorLayout, StateAnchorResetScope, require_no_orphan_reset_quarantine, require_state_anchor_absent, }; use crate::domain_store::{HostCoordinatorDomainStore, StateResetIntentV1, StateResetScopeV1}; #[derive(Debug)] pub struct StateMaintenanceError(String); impl fmt::Display for StateMaintenanceError { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter.write_str(&self.0) } } impl std::error::Error for StateMaintenanceError {} fn failure(error: impl fmt::Display) -> StateMaintenanceError { StateMaintenanceError(error.to_string()) } #[derive(Clone, Debug, Eq, PartialEq)] pub struct StateAnchorReport { pub container_path: String, pub changes: Vec, } #[derive(Clone, Debug, Eq, PartialEq)] pub struct DomainStateReport { pub profile: String, pub workspace: Vec, pub domain_id: [u8; 32], pub anchors: Vec, } /// Returns an empty report only after proving that no physical State Anchor exists. /// /// # Errors /// Refuses changed authority, links, inaccessible paths, or any persistent or runtime anchor. pub fn inspect_absent_domain_state( configuration: &ResolvedDomainConfiguration, ) -> Result { require_state_anchor_absent(configuration).map_err(failure)?; Ok(report( configuration, &StateAnchorInspection { entries: Vec::new(), }, )) } /// Recovers a durable reset before any maintenance observer or runtime mount can expose state. /// /// # Errors /// Refuses malformed intent, changed authority, unsupported scope, orphan quarantine, or cleanup /// failure. pub fn recover_state_reset( configuration: &ResolvedDomainConfiguration, store: &HostCoordinatorDomainStore, ) -> Result<(), StateMaintenanceError> { let Some(intent) = store.state_reset_intent().map_err(failure)? else { return require_no_orphan_reset_quarantine(configuration).map_err(failure); }; require_intent(configuration, &intent)?; let layout = StateAnchorLayout::create(configuration).map_err(failure)?; complete_intent(&layout, &intent)?; store.finish_state_reset(&intent).map_err(failure) } /// Returns content-free State Anchor metadata after mandatory recovery and structural checks. /// /// # Errors /// Refuses active or malformed Domain state, changed authority, mounts, links in fixed paths, and /// unsupported upper-layer entries. pub fn inspect_domain_state( configuration: &ResolvedDomainConfiguration, store: &HostCoordinatorDomainStore, ) -> Result { recover_state_reset(configuration, store)?; require_inactive(store)?; let layout = StateAnchorLayout::create(configuration).map_err(failure)?; let inspection = layout.inspect_state().map_err(failure)?; Ok(report(configuration, &inspection)) } /// Commits and completes one all-or-selected State Anchor reset generation. /// /// # Errors /// Refuses active state, an arbitrary target, malformed durable state, changed authority, mounts, /// links in fixed paths, quarantine collisions, and unsupported upper-layer entries. pub fn reset_domain_state( configuration: &ResolvedDomainConfiguration, store: &HostCoordinatorDomainStore, scope: StateResetScopeV1, ) -> Result { recover_state_reset(configuration, store)?; require_inactive(store)?; validate_scope(configuration, &scope)?; let intent = StateResetIntentV1::new(configuration.domain_id(), scope); store.begin_state_reset(&intent).map_err(failure)?; let layout = StateAnchorLayout::create(configuration).map_err(failure)?; complete_intent(&layout, &intent)?; store.finish_state_reset(&intent).map_err(failure)?; inspect_domain_state(configuration, store) } fn complete_intent( layout: &StateAnchorLayout<'_>, intent: &StateResetIntentV1, ) -> Result<(), StateMaintenanceError> { match intent.scope() { StateResetScopeV1::All => layout .complete_state_reset(StateAnchorResetScope::All) .map_err(failure), StateResetScopeV1::Selected(path) => { let path = PathBuf::from(OsString::from_vec(path.clone())); layout .complete_state_reset(StateAnchorResetScope::Selected(&path)) .map_err(failure) } } } fn require_intent( configuration: &ResolvedDomainConfiguration, intent: &StateResetIntentV1, ) -> Result<(), StateMaintenanceError> { if intent.domain_id() != configuration.domain_id() { return Err(failure( "reset intent Domain differs from resolved authority", )); } validate_scope(configuration, intent.scope()) } fn validate_scope( configuration: &ResolvedDomainConfiguration, scope: &StateResetScopeV1, ) -> Result<(), StateMaintenanceError> { let StateResetScopeV1::Selected(bytes) = scope else { return Ok(()); }; let selected = PathBuf::from(OsString::from_vec(bytes.clone())); if configuration .fingerprint_record() .persistent_dirs .iter() .any(|configured| Path::new(configured) == selected) { Ok(()) } else { Err(failure( "reset target is not an exact configured persistent container path", )) } } fn require_inactive(store: &HostCoordinatorDomainStore) -> Result<(), StateMaintenanceError> { let snapshot = store.snapshot().map_err(failure)?; let inactive = snapshot .generations .iter() .all(|generation| generation.durable_decision == DurableDecision::Clean) && snapshot .participants .iter() .all(|participant| participant.state() == ParticipantState::Clean) && snapshot.sessions.iter().all(|session| { matches!( session.session_phase, SessionPhase::Closed | SessionPhase::Failed ) }) && snapshot .session_terminals .iter() .all(|terminal| terminal.state() == SessionTerminalState::Clean) && snapshot .mailboxes .iter() .all(|mailbox| mailbox.state == ParticipantState::Clean); if inactive { Ok(()) } else { Err(failure("Domain is active or has unfinished runtime state")) } } fn report( configuration: &ResolvedDomainConfiguration, inspection: &StateAnchorInspection, ) -> DomainStateReport { let anchors = configuration .fingerprint_record() .persistent_dirs .iter() .map(|container_path| { let relative = container_path.trim_start_matches('/').as_bytes(); let path = Path::new(container_path.trim_start_matches('/')); let mut whiteout = path .parent() .unwrap_or(Path::new("")) .as_os_str() .as_encoded_bytes() .to_vec(); if !whiteout.is_empty() { whiteout.push(b'/'); } whiteout.extend_from_slice(b".wh."); whiteout.extend_from_slice( path.file_name() .expect("validated persistent path has a name") .as_encoded_bytes(), ); let changes = inspection .entries .iter() .filter(|entry| { entry.path == relative || entry.path == whiteout || entry .path .strip_prefix(relative) .is_some_and(|suffix| suffix.first() == Some(&b'/')) }) .cloned() .collect(); StateAnchorReport { container_path: container_path.clone(), changes, } }) .collect(); DomainStateReport { profile: configuration.selected_profile().to_owned(), workspace: configuration .workspace() .canonical_label() .as_bytes() .to_vec(), domain_id: *configuration.domain_id().as_bytes(), anchors, } } #[cfg(test)] mod tests { use std::collections::BTreeMap; use std::fs::{self, File, OpenOptions}; use std::os::unix::ffi::OsStringExt; use std::os::unix::fs::symlink; use std::path::{Path, PathBuf}; use lazaret_core::configuration::{ ConfigurationLoadRequest, ResolvedDomainConfiguration, load_trusted_configuration, }; use lazaret_core::identity::{ ConfigurationFingerprint, CoordinatorId, GenerationId, NetworkSidecarId, SandboxId, }; use lazaret_core::records::{Domain, Generation}; use lazaret_linux::filesystem::try_exclusive_flock; use rustix::fs::{FileType, Mode, XattrFlags, fsetxattr, makedev, mknodat}; use super::*; #[derive(Clone, Copy)] enum EntryCase { Directory, File, Symlink, WhiteoutFile, WhiteoutDevice, } #[test] fn every_reset_boundary_recovers_to_new_empty() { for entry_case in [ EntryCase::Directory, EntryCase::File, EntryCase::Symlink, EntryCase::WhiteoutFile, EntryCase::WhiteoutDevice, ] { for boundary in 0..9 { let root = tempfile::tempdir().unwrap(); let configuration = test_configuration(root.path()); let store = open_store(&configuration); let layout = StateAnchorLayout::create(&configuration).unwrap(); let anchor = domain_root(&configuration).join("state-anchor"); let upper = anchor.join("upper"); fs::create_dir(upper.join("unrelated")).unwrap(); fs::write(upper.join("unrelated/value"), b"unchanged").unwrap(); create_entry(&upper, entry_case); let intent = StateResetIntentV1::new( configuration.domain_id(), StateResetScopeV1::Selected(b"/usr".to_vec()), ); store.begin_state_reset(&intent).unwrap(); stage_boundary(&anchor, &upper, entry_case, boundary); if boundary == 8 { store.finish_state_reset(&intent).unwrap(); } drop(layout); recover_state_reset(&configuration, &store).unwrap(); assert!(!upper.join("usr").exists()); assert!(!upper.join("usr").is_symlink()); assert!(!upper.join(".wh.usr").exists()); assert_eq!( fs::read(upper.join("unrelated/value")).unwrap(), b"unchanged" ); assert!(store.state_reset_intent().unwrap().is_none()); assert!(!anchor.join("reset-quarantine").exists()); } } } #[test] fn corrupt_mismatched_and_orphan_recovery_state_refuses() { let root = tempfile::tempdir().unwrap(); let configuration = test_configuration(root.path()); let store = open_store(&configuration); let foreign = StateResetIntentV1::new( configuration.domain_id(), StateResetScopeV1::Selected(b"/etc".to_vec()), ); store.begin_state_reset(&foreign).unwrap(); assert!(recover_state_reset(&configuration, &store).is_err()); let root = tempfile::tempdir().unwrap(); let configuration = test_configuration(root.path()); let store = open_store(&configuration); StateAnchorLayout::create(&configuration).unwrap(); fs::create_dir(domain_root(&configuration).join("state-anchor/reset-quarantine")).unwrap(); assert!(recover_state_reset(&configuration, &store).is_err()); } #[test] fn active_domain_refuses_inspection_and_reset() { let root = tempfile::tempdir().unwrap(); let configuration = test_configuration(root.path()); let store = open_store(&configuration); store .apply(&[crate::domain_store::DomainStoreMutation::PutGeneration( Generation { domain_id: configuration.domain_id(), generation_id: GenerationId::new(1), configuration_fingerprint: ConfigurationFingerprint::new([2; 32]), coordinator_id: CoordinatorId::new([3; 32]), network_sidecar_id: NetworkSidecarId::new([4; 32]), sandbox_id: SandboxId::new([5; 32]), durable_decision: DurableDecision::None, }, )]) .unwrap(); assert!(inspect_domain_state(&configuration, &store).is_err()); assert!(reset_domain_state(&configuration, &store, StateResetScopeV1::All).is_err()); assert!(store.state_reset_intent().unwrap().is_none()); } fn create_entry(upper: &Path, entry_case: EntryCase) { match entry_case { EntryCase::Directory => { fs::create_dir(upper.join("usr")).unwrap(); fs::write(upper.join("usr/value"), b"state").unwrap(); fs::write(upper.join("usr/.wh..wh..opq"), b"").unwrap(); let directory = File::open(upper.join("usr")).unwrap(); fsetxattr( directory, "user.fuseoverlayfs.opaque", b"y", XattrFlags::empty(), ) .unwrap(); } EntryCase::File => fs::write(upper.join("usr"), b"state").unwrap(), EntryCase::Symlink => symlink("/foreign", upper.join("usr")).unwrap(), EntryCase::WhiteoutFile => fs::write(upper.join(".wh.usr"), b"").unwrap(), EntryCase::WhiteoutDevice => mknodat( rustix::fs::CWD, upper.join("usr"), FileType::CharacterDevice, Mode::empty(), makedev(0, 0), ) .unwrap(), } } fn stage_boundary(anchor: &Path, upper: &Path, entry_case: EntryCase, boundary: usize) { if boundary == 0 { return; } let quarantine = anchor.join("reset-quarantine"); fs::create_dir(&quarantine).unwrap(); if boundary == 1 { return; } let (source, destination) = match entry_case { EntryCase::WhiteoutFile => (upper.join(".wh.usr"), quarantine.join("whiteout")), _ => (upper.join("usr"), quarantine.join("entry")), }; fs::rename(source, &destination).unwrap(); if boundary <= 3 { return; } remove_entry(&destination); if boundary <= 5 { return; } fs::remove_dir(&quarantine).unwrap(); } fn remove_entry(path: &Path) { let metadata = fs::symlink_metadata(path).unwrap(); if metadata.file_type().is_dir() { fs::remove_dir_all(path).unwrap(); } else { fs::remove_file(path).unwrap(); } } fn open_store(configuration: &ResolvedDomainConfiguration) -> HostCoordinatorDomainStore { let root = domain_root(configuration); let lock = OpenOptions::new() .read(true) .write(true) .create(true) .truncate(false) .open(root.join("domain.lock")) .unwrap(); HostCoordinatorDomainStore::open( try_exclusive_flock(lock).unwrap(), &root.join("domain.redb"), &Domain { domain_id: configuration.domain_id(), profile_name: configuration.selected_profile().to_owned(), canonical_workspace: configuration.workspace().canonical_label().clone(), }, ) .unwrap() } fn domain_root(configuration: &ResolvedDomainConfiguration) -> PathBuf { PathBuf::from(std::ffi::OsString::from_vec( configuration .persistent_domain_root() .canonical_label() .as_bytes() .to_vec(), )) } fn test_configuration(root: &Path) -> ResolvedDomainConfiguration { let configuration_directory = root.join("configuration"); let workspace = root.join("workspace"); let runtime = root.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/state:v1\"\npersistent_dirs = [\"/usr\"]\n[profiles.default.readiness]\ndenied = [{{ address = \"10.20.30.40\", port = 8443 }}]\npermitted = [{{ address = \"192.0.2.10\", port = 443 }}]\ndns = [{{ resolver = \"198.51.100.53\", query = \"example.invalid\" }}]\n", root.join("data").display() ), ) .unwrap(); load_trusted_configuration(ConfigurationLoadRequest { explicit_configuration_path: Some(configuration_path), selected_profile: "default".into(), workspace, home: root.join("home"), xdg_config_home: None, xdg_data_home: None, xdg_runtime_dir: runtime, environment: BTreeMap::new(), sandbox_layout: lazaret_sandbox_contract::default_sandbox_layout().unwrap(), }) .unwrap() } }