#![doc = "Physical State Anchor layout and exact fuse-overlayfs mount lifecycle."] use std::ffi::OsString; use std::fmt; use std::fs::File; use std::io::{self, Write}; use std::os::fd::{AsFd, AsRawFd, OwnedFd}; use std::os::unix::ffi::{OsStrExt, OsStringExt}; use std::path::{Component, Path, PathBuf}; use std::sync::atomic::{AtomicU64, Ordering}; use std::thread; use std::time::{Duration, Instant}; use lazaret_core::configuration::{ConfinedPathPlan, ResolvedDomainConfiguration}; use lazaret_core::identity::{ DeviceId, HostPath, InodeId, MountIdentity, NamespaceIdentity, PreparedArtifactId, }; use lazaret_core::records::{ HostProcessOwnershipSlot, PersistedHostProcessOwnership, PreparedArtifact, ProcessOwnershipCommitAck, StagedProcessOwnership, StateAnchorOwnership, }; use lazaret_linux::filesystem::{mount_identity, namespace_identity}; use lazaret_linux::ipc::{ private_seqpacket_pair, receive_descriptors, receive_packet, send_descriptors, send_packet, }; use lazaret_linux::process::{ ProcessOwnershipContext, RetainedProcess, TargetCommand, child_broker, require_pidfs_magic, }; use rustix::fs::{ AtFlags, FileType, Mode, OFlags, RawDir, RenameFlags, ResolveFlags, mkdirat, openat2, renameat, renameat_with, statat, unlinkat, }; use rustix::io::dup; use rustix::thread::{ThreadNameSpaceType, move_into_thread_name_spaces}; use storage::{ atomic_replace_at, create_directory, create_regular_file, descriptor_child_path, descriptor_path, host_path, inspect_directory, move_all_entries, move_selected_entries, open_directory, open_exact_directory_path, open_mounted_directory, open_or_create_quarantine, open_regular_file, path_from_host, remove_directory_contents, require_directory_descriptor_identity, require_maintenance_work_shape, require_same_descriptor, validate_relative_path, }; const MOUNT_TIMEOUT: Duration = Duration::from_secs(5); const MOUNT_POLL_INTERVAL: Duration = Duration::from_millis(2); pub const PRIVATE_STATE_ANCHOR_RECOVERY_ROLE: &str = "--lazaret-private-state-anchor-recovery"; static TEMPORARY_SEQUENCE: AtomicU64 = AtomicU64::new(0); pub const PROTECTED_RUNTIME_FILES: [&str; 3] = ["resolv.conf", "hosts", "hostname"]; const RESET_QUARANTINE: &str = "reset-quarantine"; pub struct ProtectedRuntimeFiles<'a> { pub resolv_conf: &'a [u8], pub hosts: &'a [u8], pub hostname: &'a [u8], } #[derive(Debug)] pub enum StateAnchorError { Io(io::Error), InvalidLayout(&'static str), MountTimeout, DaemonExited, OwnershipMismatch, } impl fmt::Display for StateAnchorError { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Io(error) => write!(formatter, "State Anchor I/O failed: {error}"), Self::InvalidLayout(detail) => { write!(formatter, "invalid State Anchor layout: {detail}") } Self::MountTimeout => formatter.write_str("fuse-overlayfs mount timed out"), Self::DaemonExited => formatter.write_str("fuse-overlayfs exited before mounting"), Self::OwnershipMismatch => formatter.write_str("State Anchor mount identity mismatch"), } } } impl std::error::Error for StateAnchorError { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { match self { Self::Io(error) => Some(error), Self::InvalidLayout(_) | Self::MountTimeout | Self::DaemonExited | Self::OwnershipMismatch => None, } } } impl From for StateAnchorError { fn from(error: io::Error) -> Self { Self::Io(error) } } #[derive(Debug)] pub struct StateAnchorLayout<'configuration> { persistent_plan: &'configuration ConfinedPathPlan, runtime_plan: &'configuration ConfinedPathPlan, persistent_root: OwnedFd, runtime_root: OwnedFd, persistent_anchor: OwnedFd, runtime_anchor: OwnedFd, upper_descriptor: OwnedFd, work_descriptor: OwnedFd, merged_descriptor: OwnedFd, runtime_files_descriptor: OwnedFd, upper: PathBuf, work: PathBuf, merged: PathBuf, runtime_files: PathBuf, upper_label: PathBuf, work_label: PathBuf, merged_label: PathBuf, persistent_destinations: Vec, } impl<'configuration> StateAnchorLayout<'configuration> { /// Creates the fixed layout beneath retained, revalidated Domain authorities. /// /// # Errors /// Refuses changed roots, non-directory fixed paths, and unsafe persistent destinations. pub fn create( configuration: &'configuration ResolvedDomainConfiguration, ) -> Result { let persistent_root = configuration .persistent_domain_root() .open_authority() .map_err(|_| StateAnchorError::InvalidLayout("persistent Domain authority changed"))?; let runtime_root = configuration .runtime_domain_root() .open_authority() .map_err(|_| StateAnchorError::InvalidLayout("runtime Domain authority changed"))?; create_directory(&persistent_root, "state-anchor")?; let persistent_anchor = open_directory(&persistent_root, Path::new("state-anchor"))?; for name in ["upper", "work"] { create_directory(&persistent_anchor, name)?; } create_directory(&runtime_root, "state-anchor")?; let runtime_anchor = open_directory(&runtime_root, Path::new("state-anchor"))?; for name in ["merged", "runtime-files"] { create_directory(&runtime_anchor, name)?; } let upper_descriptor = open_directory(&persistent_anchor, Path::new("upper"))?; let work_descriptor = open_directory(&persistent_anchor, Path::new("work"))?; let merged_descriptor = open_directory(&runtime_anchor, Path::new("merged"))?; let runtime_files_descriptor = open_directory(&runtime_anchor, Path::new("runtime-files"))?; for name in PROTECTED_RUNTIME_FILES { create_regular_file(&runtime_files_descriptor, name)?; } rustix::fs::fsync(&runtime_anchor).map_err(io::Error::from)?; rustix::fs::fsync(&persistent_anchor).map_err(io::Error::from)?; let persistent_base = path_from_host(configuration.persistent_domain_root().canonical_label()); let runtime_base = path_from_host(configuration.runtime_domain_root().canonical_label()); let persistent_destinations = configuration .fingerprint_record() .persistent_dirs .iter() .map(|destination| PathBuf::from(destination.strip_prefix('/').unwrap_or(destination))) .collect(); Ok(Self { persistent_plan: configuration.persistent_domain_root(), runtime_plan: configuration.runtime_domain_root(), persistent_root, runtime_root, upper: descriptor_child_path(&persistent_anchor, "upper"), work: descriptor_child_path(&persistent_anchor, "work"), merged: descriptor_child_path(&runtime_anchor, "merged"), runtime_files: descriptor_child_path(&runtime_anchor, "runtime-files"), persistent_anchor, runtime_anchor, upper_descriptor, work_descriptor, merged_descriptor, runtime_files_descriptor, upper_label: persistent_base.join("state-anchor/upper"), work_label: persistent_base.join("state-anchor/work"), merged_label: runtime_base.join("state-anchor/merged"), persistent_destinations, }) } #[must_use] pub fn upper(&self) -> &Path { &self.upper } #[must_use] pub fn work(&self) -> &Path { &self.work } #[must_use] pub fn merged(&self) -> &Path { &self.merged } /// Returns the ordinary host path used as the merged mount label. #[must_use] pub fn merged_label(&self) -> &Path { &self.merged_label } #[must_use] pub fn runtime_files(&self) -> &Path { &self.runtime_files } #[must_use] pub fn persistent_destinations(&self) -> &[PathBuf] { &self.persistent_destinations } /// Revalidates every fixed directory through the retained Domain authorities. /// /// # Errors /// Refuses replacement, symlink, mount crossing, or a non-directory fixed path. pub fn revalidate(&self) -> Result<(), StateAnchorError> { self.revalidate_authorities()?; let merged = open_directory(&self.runtime_anchor, Path::new("merged"))?; require_same_descriptor(&merged, &self.merged_descriptor) } fn revalidate_authorities(&self) -> Result<(), StateAnchorError> { let persistent_root = self .persistent_plan .open_authority() .map_err(|_| StateAnchorError::InvalidLayout("persistent Domain authority changed"))?; let runtime_root = self .runtime_plan .open_authority() .map_err(|_| StateAnchorError::InvalidLayout("runtime Domain authority changed"))?; require_same_descriptor(&persistent_root, &self.persistent_root)?; require_same_descriptor(&runtime_root, &self.runtime_root)?; let persistent_anchor = open_directory(&self.persistent_root, Path::new("state-anchor"))?; let runtime_anchor = open_directory(&self.runtime_root, Path::new("state-anchor"))?; require_same_descriptor(&persistent_anchor, &self.persistent_anchor)?; require_same_descriptor(&runtime_anchor, &self.runtime_anchor)?; for (name, expected) in [ ("upper", &self.upper_descriptor), ("work", &self.work_descriptor), ] { let observed = open_directory(&self.persistent_anchor, Path::new(name))?; require_same_descriptor(&observed, expected)?; } let runtime_files = open_directory(&self.runtime_anchor, Path::new("runtime-files"))?; require_same_descriptor(&runtime_files, &self.runtime_files_descriptor)?; for name in PROTECTED_RUNTIME_FILES { drop(open_regular_file(&self.runtime_files_descriptor, name)?); } Ok(()) } fn current_merged_identity(&self) -> Result { self.revalidate_authorities()?; let merged = open_mounted_directory(&self.runtime_anchor, Path::new("merged"))?; mount_identity(&merged).map_err(StateAnchorError::from) } fn require_mounted(&self, expected: MountIdentity) -> Result<(), StateAnchorError> { if self.current_merged_identity()? == expected { Ok(()) } else { Err(StateAnchorError::OwnershipMismatch) } } fn require_unmounted(&self) -> Result<(), StateAnchorError> { self.revalidate() } /// Atomically replaces sandbox runtime files in host-private transient storage. /// /// # Errors /// Refuses changed layout and incomplete durable replacement. pub fn replace_protected_runtime_files( &self, files: &ProtectedRuntimeFiles<'_>, ) -> Result<(), StateAnchorError> { self.revalidate_authorities()?; for (name, bytes) in [ ("resolv.conf", files.resolv_conf), ("hosts", files.hosts), ("hostname", files.hostname), ] { atomic_replace_at(&self.runtime_files_descriptor, name, bytes)?; } self.revalidate_authorities() } #[must_use] pub fn ownership_intent(&self, artifact_id: PreparedArtifactId) -> StateAnchorOwnership { StateAnchorOwnership { lower_artifact_id: Some(artifact_id), upper_path: Some(host_path(&self.upper_label)), work_path: Some(host_path(&self.work_label)), merged_path: Some(host_path(&self.merged_label)), daemon: PersistedHostProcessOwnership::unassigned(), user_namespace: None, mount_namespace: None, mount: None, } } #[must_use] pub fn matches_recorded_paths(&self, ownership: &StateAnchorOwnership) -> bool { ownership.upper_path.as_ref() == Some(&host_path(&self.upper_label)) && ownership.work_path.as_ref() == Some(&host_path(&self.work_label)) && ownership.merged_path.as_ref() == Some(&host_path(&self.merged_label)) } /// Verifies inactive structural authority and returns content-free upper-layer metadata. /// /// # Errors /// Refuses changed roots, mounts, fixed-path links, orphan quarantine, and unsupported entries. pub fn inspect_state(&self) -> Result { self.require_maintenance_ready()?; let mut entries = Vec::new(); inspect_directory(&self.upper_descriptor, Path::new(""), &mut entries)?; entries.sort_by(|left, right| left.path.cmp(&right.path)); Ok(StateAnchorInspection { entries }) } /// Completes the physical half of one durable reset intent. /// /// # Errors /// Refuses changed authority, mounts, unsupported scope, collisions, links in authority paths, /// or incomplete durable cleanup. pub fn complete_state_reset( &self, scope: StateAnchorResetScope<'_>, ) -> Result<(), StateAnchorError> { self.require_maintenance_ready_allowing_quarantine()?; let selected = match scope { StateAnchorResetScope::All => None, StateAnchorResetScope::Selected(destination) => { Some(self.selected_relative(destination)?) } }; let quarantine = open_or_create_quarantine(&self.persistent_anchor)?; if let Some(relative) = selected { move_selected_entries(&self.upper_descriptor, &quarantine, &relative)?; } else { move_all_entries(&self.upper_descriptor, &quarantine)?; } remove_directory_contents(&quarantine)?; rustix::fs::fsync(&quarantine).map_err(io::Error::from)?; unlinkat( self.persistent_anchor.as_fd(), RESET_QUARANTINE, AtFlags::REMOVEDIR, ) .map_err(io::Error::from)?; rustix::fs::fsync(&self.persistent_anchor).map_err(io::Error::from)?; Ok(()) } /// Refuses a physical quarantine that lacks a matching durable reset intent. /// /// # Errors /// Returns an error for changed layout or an orphan quarantine. pub fn require_no_reset_quarantine(&self) -> Result<(), StateAnchorError> { self.revalidate_authorities()?; match open_directory(&self.persistent_anchor, Path::new(RESET_QUARANTINE)) { Ok(_) => Err(StateAnchorError::InvalidLayout( "State Anchor reset quarantine has no durable intent", )), Err(StateAnchorError::Io(error)) if error.kind() == io::ErrorKind::NotFound => Ok(()), Err(error) => Err(error), } } fn selected_relative(&self, destination: &Path) -> Result { if !self .persistent_destinations .iter() .any(|configured| configured == destination.strip_prefix("/").unwrap_or(destination)) { return Err(StateAnchorError::InvalidLayout( "reset target is not a configured persistent path", )); } let relative = destination.strip_prefix("/").unwrap_or(destination); validate_relative_path(relative)?; Ok(relative.to_path_buf()) } fn require_maintenance_ready(&self) -> Result<(), StateAnchorError> { self.require_maintenance_ready_allowing_quarantine()?; self.require_no_reset_quarantine() } fn require_maintenance_ready_allowing_quarantine(&self) -> Result<(), StateAnchorError> { self.require_unmounted()?; require_maintenance_work_shape(&self.work_descriptor) } } #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum StateAnchorEntryKind { Directory, RegularFile, Symlink, Whiteout, } #[derive(Clone, Debug, Eq, PartialEq)] pub struct StateAnchorEntry { pub path: Vec, pub kind: StateAnchorEntryKind, pub mode: u32, pub uid: u32, pub gid: u32, pub size: u64, } #[derive(Clone, Debug, Eq, PartialEq)] pub struct StateAnchorInspection { pub entries: Vec, } #[derive(Clone, Copy, Debug)] pub enum StateAnchorResetScope<'a> { All, Selected(&'a Path), } /// Proves that no physical State Anchor exists under either retained Domain authority. /// /// # Errors /// Refuses changed authority, links, inaccessible paths, or any persistent or runtime anchor. pub fn require_state_anchor_absent( configuration: &ResolvedDomainConfiguration, ) -> Result<(), StateAnchorError> { for plan in [ configuration.persistent_domain_root(), configuration.runtime_domain_root(), ] { let root = plan .open_authority() .map_err(|_| StateAnchorError::InvalidLayout("Domain authority changed"))?; match open_directory(&root, Path::new("state-anchor")) { Ok(_) => { return Err(StateAnchorError::InvalidLayout( "State Anchor exists without Domain Store authority", )); } Err(StateAnchorError::Io(error)) if error.kind() == io::ErrorKind::NotFound => {} Err(error) => return Err(error), } } Ok(()) } /// Refuses an orphan reset quarantine without opening transient mount paths. /// /// # Errors /// Refuses changed persistent authority, fixed-path links, and orphan quarantine. pub fn require_no_orphan_reset_quarantine( configuration: &ResolvedDomainConfiguration, ) -> Result<(), StateAnchorError> { let root = configuration .persistent_domain_root() .open_authority() .map_err(|_| StateAnchorError::InvalidLayout("persistent Domain authority changed"))?; let anchor = match open_directory(&root, Path::new("state-anchor")) { Ok(anchor) => anchor, Err(StateAnchorError::Io(error)) if error.kind() == io::ErrorKind::NotFound => { return Ok(()); } Err(error) => return Err(error), }; match open_directory(&anchor, Path::new(RESET_QUARANTINE)) { Ok(_) => Err(StateAnchorError::InvalidLayout( "State Anchor reset quarantine has no durable intent", )), Err(StateAnchorError::Io(error)) if error.kind() == io::ErrorKind::NotFound => Ok(()), Err(error) => Err(error), } } /// Pins the exact mounted and lower authorities until the view is dropped before unmount. #[derive(Debug)] pub struct StateAnchorSandboxView<'layout, 'configuration> { layout: &'layout StateAnchorLayout<'configuration>, lower_descriptor: OwnedFd, merged_descriptor: OwnedFd, lower: PathBuf, lower_identity: (u64, u64), merged: PathBuf, mount: MountIdentity, runtime_files: PathBuf, persistent_destinations: Vec, } impl<'layout, 'configuration> StateAnchorSandboxView<'layout, 'configuration> { /// Binds a serviceable State Anchor view to exact prepared-lower and mount evidence. /// /// # Errors /// Refuses changed layout, prepared lower, or mount identity. pub fn from_mounted( layout: &'layout StateAnchorLayout<'configuration>, artifact: &PreparedArtifact, mount: MountIdentity, ) -> Result { let lower_descriptor = open_exact_directory_path( &path_from_host(&artifact.path), (artifact.device.get(), artifact.inode.get()), )?; layout.require_mounted(mount)?; let merged_descriptor = open_mounted_directory(&layout.runtime_anchor, Path::new("merged"))?; let view = Self { layout, lower: descriptor_path(&lower_descriptor), lower_identity: (artifact.device.get(), artifact.inode.get()), merged: descriptor_path(&merged_descriptor), mount, runtime_files: descriptor_path(&layout.runtime_files_descriptor), persistent_destinations: layout.persistent_destinations.clone(), lower_descriptor, merged_descriptor, }; view.revalidate()?; Ok(view) } /// Revalidates the immutable lower, exact overlay mount, and host-private runtime files. /// /// # Errors /// Refuses replacement or unreadable physical evidence. pub fn revalidate(&self) -> Result<(), StateAnchorError> { self.layout.require_mounted(self.mount)?; require_directory_descriptor_identity(&self.lower_descriptor, self.lower_identity)?; if mount_identity(&self.merged_descriptor)? != self.mount { return Err(StateAnchorError::OwnershipMismatch); } for name in PROTECTED_RUNTIME_FILES { drop(open_regular_file( &self.layout.runtime_files_descriptor, name, )?); } Ok(()) } #[must_use] pub fn lower(&self) -> &Path { &self.lower } pub fn persistent_mounts(&self) -> impl Iterator + '_ { self.persistent_destinations.iter().map(|destination| { ( self.merged.join(destination), Path::new("/").join(destination), ) }) } pub fn protected_runtime_mounts(&self) -> impl Iterator + '_ { PROTECTED_RUNTIME_FILES .into_iter() .map(|name| (self.runtime_files.join(name), Path::new("/etc").join(name))) } } #[path = "lib/authority.rs"] mod authority; #[path = "lib/mount.rs"] mod mount; #[path = "lib/storage.rs"] mod storage; pub use authority::{ RuntimeStateAnchorAbsenceAuthority, RuntimeStateAnchorAuthority, RuntimeStateAnchorInherited, RuntimeStateAnchorMount, StateAnchorAcquisition, StateAnchorLaunchAuthority, VerifiedStateAnchorReadiness, }; pub use mount::{ FuseOverlayfsMount, dispatch_private_state_anchor_recovery, recover_private_state_anchor_mount, unmount_after_coordinator_loss, }; pub use storage::{observe_mount_absence, require_mount_absent}; #[cfg(test)] #[path = "lib/tests.rs"] mod tests;