#![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}; const MOUNT_TIMEOUT: Duration = Duration::from_secs(5); const MOUNT_POLL_INTERVAL: Duration = Duration::from_millis(2); const PRIVATE_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()?; if directory_has_entries(&self.work_descriptor)? { return Err(StateAnchorError::InvalidLayout( "State Anchor work directory is not empty", )); } Ok(()) } } #[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))) } } #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct VerifiedStateAnchorReadiness { staged: StagedProcessOwnership, mount: MountIdentity, } #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct StateAnchorAcquisition<'path> { pub lower: &'path Path, pub expected_lower: (u64, u64), pub expected_parent: lazaret_core::identity::PidfsProcessIdentityV1, pub ownership: ProcessOwnershipContext, } pub struct StateAnchorLaunchAuthority { lower: OwnedFd, lower_label: PathBuf, persistent_root: OwnedFd, persistent_anchor: OwnedFd, persistent_anchor_label: PathBuf, runtime_root: OwnedFd, runtime_anchor: OwnedFd, runtime_anchor_label: PathBuf, expected_lower: (u64, u64), persistent_destinations: Vec, } impl StateAnchorLaunchAuthority { /// Captures exact directory authorities for inheritance by the Runtime Supervisor. /// /// # Errors /// Refuses a changed layout or prepared lower. pub fn capture( layout: &StateAnchorLayout<'_>, artifact: &PreparedArtifact, ) -> Result { layout.revalidate()?; let expected_lower = (artifact.device.get(), artifact.inode.get()); let lower = open_exact_directory_path(&path_from_host(&artifact.path), expected_lower)?; Self::capture_open( layout, expected_lower, path_from_host(&artifact.path), lower, ) } /// Captures an already-confined prepared-lower authority for inheritance. /// /// # Errors /// Refuses a changed layout or prepared-lower descriptor. pub fn capture_retained( layout: &StateAnchorLayout<'_>, artifact: &PreparedArtifact, lower_label: &Path, lower: impl AsFd, ) -> Result { layout.revalidate()?; let expected_lower = (artifact.device.get(), artifact.inode.get()); require_directory_descriptor_identity(&lower, expected_lower)?; Self::capture_open(layout, expected_lower, lower_label.to_owned(), lower) } fn capture_open( layout: &StateAnchorLayout<'_>, expected_lower: (u64, u64), lower_label: PathBuf, lower: impl AsFd, ) -> Result { Ok(Self { lower: inherit_for_exec(lower)?, lower_label, persistent_root: inherit_for_exec(&layout.persistent_root)?, persistent_anchor: inherit_for_exec(&layout.persistent_anchor)?, persistent_anchor_label: layout .upper_label .parent() .ok_or(StateAnchorError::InvalidLayout( "upper label has no State Anchor parent", ))? .to_owned(), runtime_root: inherit_for_exec(&layout.runtime_root)?, runtime_anchor: inherit_for_exec(&layout.runtime_anchor)?, runtime_anchor_label: layout .merged_label .parent() .ok_or(StateAnchorError::InvalidLayout( "merged label has no State Anchor parent", ))? .to_owned(), expected_lower, persistent_destinations: layout.persistent_destinations.clone(), }) } #[must_use] pub fn lower_fd(&self) -> i32 { self.lower.as_raw_fd() } #[must_use] pub fn persistent_anchor_fd(&self) -> i32 { self.persistent_anchor.as_raw_fd() } #[must_use] pub fn persistent_root_fd(&self) -> i32 { self.persistent_root.as_raw_fd() } #[must_use] pub fn runtime_anchor_fd(&self) -> i32 { self.runtime_anchor.as_raw_fd() } #[must_use] pub fn runtime_root_fd(&self) -> i32 { self.runtime_root.as_raw_fd() } #[must_use] pub fn runtime_anchor_label(&self) -> &Path { &self.runtime_anchor_label } #[must_use] pub fn lower_label(&self) -> &Path { &self.lower_label } #[must_use] pub fn persistent_anchor_label(&self) -> &Path { &self.persistent_anchor_label } #[must_use] pub const fn expected_lower(&self) -> (u64, u64) { self.expected_lower } #[must_use] pub fn persistent_destinations(&self) -> &[PathBuf] { &self.persistent_destinations } } pub struct RuntimeStateAnchorAuthority { lower: OwnedFd, persistent_anchor: OwnedFd, runtime_anchor: OwnedFd, merged_label: PathBuf, merged_descriptor: OwnedFd, } #[derive(Clone, Copy)] pub struct RuntimeStateAnchorInherited<'a> { pub lower_fd: i32, pub lower_label: &'a Path, pub persistent_root_fd: i32, pub persistent_anchor_fd: i32, pub persistent_anchor_label: &'a Path, pub runtime_root_fd: i32, pub runtime_anchor_fd: i32, pub runtime_anchor_label: &'a Path, pub expected_lower: (u64, u64), } impl RuntimeStateAnchorAuthority { /// Reopens inherited authorities without falling back to ambient configured paths. /// /// # Errors /// Refuses missing, replaced, or incorrectly typed descriptors and fixed children. pub fn from_inherited( inherited: RuntimeStateAnchorInherited<'_>, ) -> Result { let retained_lower = reopen_inherited_directory(inherited.lower_fd)?; require_directory_descriptor_identity(&retained_lower, inherited.expected_lower)?; if !inherited.lower_label.is_absolute() { return Err(StateAnchorError::InvalidLayout( "prepared lower label is not absolute", )); } let lower = open_exact_directory_path(inherited.lower_label, inherited.expected_lower)?; let persistent_root = reopen_inherited_directory(inherited.persistent_root_fd)?; let retained_persistent_anchor = reopen_inherited_directory(inherited.persistent_anchor_fd)?; if !inherited.persistent_anchor_label.is_absolute() { return Err(StateAnchorError::InvalidLayout( "persistent State Anchor label is not absolute", )); } let retained_persistent = rustix::fs::fstat(&retained_persistent_anchor).map_err(io::Error::from)?; let confined_persistent_anchor = open_directory(&persistent_root, Path::new("state-anchor"))?; require_directory_descriptor_identity( &confined_persistent_anchor, (retained_persistent.st_dev, retained_persistent.st_ino), )?; let persistent_anchor = open_exact_directory_path( inherited.persistent_anchor_label, (retained_persistent.st_dev, retained_persistent.st_ino), )?; let runtime_root = reopen_inherited_directory(inherited.runtime_root_fd)?; let retained_runtime_anchor = reopen_inherited_directory(inherited.runtime_anchor_fd)?; if !inherited.runtime_anchor_label.is_absolute() { return Err(StateAnchorError::InvalidLayout( "runtime State Anchor label is not absolute", )); } let confined_runtime_anchor = open_directory(&runtime_root, Path::new("state-anchor"))?; let retained = rustix::fs::fstat(&retained_runtime_anchor).map_err(io::Error::from)?; require_directory_descriptor_identity( &confined_runtime_anchor, (retained.st_dev, retained.st_ino), )?; let runtime_anchor = open_exact_directory_path( inherited.runtime_anchor_label, (retained.st_dev, retained.st_ino), )?; for name in ["upper", "work"] { drop(open_directory(&persistent_anchor, Path::new(name))?); } let merged_descriptor = open_directory(&runtime_anchor, Path::new("merged"))?; let merged_label = inherited.runtime_anchor_label.join("merged"); Ok(Self { lower, persistent_anchor, runtime_anchor, merged_label, merged_descriptor, }) } /// Mounts the exact inherited prepared lower and anchors under RS child ownership. /// /// # Errors /// Refuses acquisition, acknowledgment, readiness, or authority failures. pub fn acquire( self, expected_parent: lazaret_core::identity::PidfsProcessIdentityV1, ownership: ProcessOwnershipContext, stage: Stage, acknowledge: Acknowledge, ready: Ready, ) -> Result where Stage: FnOnce(StagedProcessOwnership) -> io::Result<()>, Acknowledge: FnOnce(StagedProcessOwnership) -> io::Result, Ready: FnOnce(VerifiedStateAnchorReadiness) -> io::Result<()>, { if ownership.slot != HostProcessOwnershipSlot::StateAnchorDaemon { return Err(StateAnchorError::InvalidLayout( "process slot is not State Anchor daemon", )); } let baseline = mount_identity(&self.merged_descriptor)?; let lower_launch = inherit_for_exec(&self.lower)?; let persistent_launch = inherit_for_exec(&self.persistent_anchor)?; let target = TargetCommand::new("fuse-overlayfs") .argument("-f") .argument("-o") .argument(overlay_options( &descriptor_path(&lower_launch), &descriptor_child_path(&persistent_launch, "upper"), &descriptor_child_path(&persistent_launch, "work"), )) .argument(&self.merged_label); let process = child_broker().acquire(expected_parent, ownership, &target, stage, acknowledge)?; let staged = StagedProcessOwnership::v1( ownership.domain_id, ownership.generation_id, ownership.coordinator_id, ownership.slot, process.identity(), ); let deadline = Instant::now() + MOUNT_TIMEOUT; loop { if process.has_exited()? { child_broker().reap(process)?; return Err(StateAnchorError::DaemonExited); } let mounted = open_mounted_directory(&self.runtime_anchor, Path::new("merged"))?; let observed = mount_identity(&mounted)?; if observed.mount_id != baseline.mount_id { let readiness = VerifiedStateAnchorReadiness { staged, mount: observed, }; if let Err(error) = ready(readiness) { process.send_signal(rustix::process::Signal::KILL)?; while !process.has_exited()? { thread::yield_now(); } let _ = rootless_unmount_path(&self.merged_label); child_broker().reap(process)?; return Err(error.into()); } return Ok(RuntimeStateAnchorMount { authority: self, process, mount: observed, merged: mounted, }); } if Instant::now() >= deadline { process.send_signal(rustix::process::Signal::KILL)?; while !process.has_exited()? { thread::yield_now(); } let merged = open_mounted_directory(&self.runtime_anchor, Path::new("merged"))?; let observed = mount_identity(&merged)?; if observed.mount_id != baseline.mount_id { let _ = rootless_unmount_path(&self.merged_label); } child_broker().reap(process)?; return Err(StateAnchorError::MountTimeout); } thread::sleep(MOUNT_POLL_INTERVAL); } } } pub struct RuntimeStateAnchorMount { authority: RuntimeStateAnchorAuthority, process: RetainedProcess, mount: MountIdentity, merged: OwnedFd, } pub struct RuntimeStateAnchorAbsenceAuthority { runtime_anchor: OwnedFd, mount: MountIdentity, } impl RuntimeStateAnchorAbsenceAuthority { /// Reopens the exact runtime anchor to produce a fresh post-unmount observation. /// /// # Errors /// Refuses an unreadable anchor or the continued presence of the retired mount. pub fn observe_absence( &self, expected_mount: MountIdentity, ) -> Result { if expected_mount != self.mount { return Err(StateAnchorError::OwnershipMismatch); } let observed = mount_identity(&open_directory(&self.runtime_anchor, Path::new("merged"))?)?; if observed == self.mount { Err(StateAnchorError::OwnershipMismatch) } else { Ok(observed) } } } impl RuntimeStateAnchorMount { #[must_use] pub const fn mount_identity(&self) -> MountIdentity { self.mount } /// Validates configured persistent subpaths beneath the exact mounted view. /// /// # Errors /// Refuses missing paths, symlinks, nested mounts, or mount replacement. pub fn require_persistent_destinations( &self, destinations: &[PathBuf], ) -> Result<(), StateAnchorError> { if mount_identity(&self.merged)? != self.mount { return Err(StateAnchorError::OwnershipMismatch); } for destination in destinations { if destination.as_os_str().is_empty() || destination .components() .any(|component| !matches!(component, std::path::Component::Normal(_))) { return Err(StateAnchorError::InvalidLayout( "persistent destination is not normalized and relative", )); } let descriptor = openat2( &self.merged, destination, OFlags::PATH | OFlags::DIRECTORY | OFlags::CLOEXEC | OFlags::NOFOLLOW, Mode::empty(), ResolveFlags::BENEATH | ResolveFlags::NO_MAGICLINKS | ResolveFlags::NO_SYMLINKS | ResolveFlags::NO_XDEV, ) .map_err(io::Error::from)?; if !FileType::from_raw_mode( rustix::fs::fstat(&descriptor) .map_err(io::Error::from)? .st_mode, ) .is_dir() { return Err(StateAnchorError::InvalidLayout( "persistent destination is not a directory", )); } } Ok(()) } /// Unmounts and reaps only the retained exact RS-owned State Anchor. /// /// # Errors /// Refuses mount replacement, unmount failure, or exact child cleanup failure. pub fn unmount(self) -> Result { let Self { authority, process, mount, merged, } = self; let observed = mount_identity(&merged)?; if observed != mount { return Err(StateAnchorError::OwnershipMismatch); } drop(merged); rootless_unmount_path(&authority.merged_label)?; let after = mount_identity(&open_directory( &authority.runtime_anchor, Path::new("merged"), )?)?; if after == mount { return Err(StateAnchorError::OwnershipMismatch); } child_broker().reap(process)?; Ok(RuntimeStateAnchorAbsenceAuthority { runtime_anchor: authority.runtime_anchor, mount, }) } } impl VerifiedStateAnchorReadiness { #[must_use] pub const fn from_parts(staged: StagedProcessOwnership, mount: MountIdentity) -> Self { Self { staged, mount } } #[must_use] pub const fn staged(self) -> StagedProcessOwnership { self.staged } #[must_use] pub const fn mount(self) -> MountIdentity { self.mount } } pub struct FuseOverlayfsMount<'layout, 'configuration> { process: RetainedProcess, mount: MountIdentity, layout: &'layout StateAnchorLayout<'configuration>, } impl<'layout, 'configuration> FuseOverlayfsMount<'layout, 'configuration> { /// Starts fuse-overlayfs through causal child acquisition and records staged/committed identity /// through caller-owned durable callbacks. /// /// # Errors /// Refuses changed layout or lower identity, callback failure, daemon loss, and mount timeout. pub fn acquire( layout: &'layout StateAnchorLayout<'configuration>, acquisition: StateAnchorAcquisition<'_>, stage: Stage, acknowledge: Acknowledge, ready: Ready, ) -> Result where Stage: FnOnce(StagedProcessOwnership) -> io::Result<()>, Acknowledge: FnOnce(StagedProcessOwnership) -> io::Result, Ready: FnOnce(VerifiedStateAnchorReadiness) -> io::Result<()>, { if acquisition.ownership.slot != HostProcessOwnershipSlot::StateAnchorDaemon { return Err(StateAnchorError::InvalidLayout( "process slot is not State Anchor daemon", )); } layout.revalidate()?; let lower_descriptor = open_exact_directory_path(acquisition.lower, acquisition.expected_lower)?; let lower_launch = inherit_for_exec(&lower_descriptor)?; let persistent_launch = inherit_for_exec(&layout.persistent_anchor)?; let runtime_launch = inherit_for_exec(&layout.runtime_anchor)?; let lower_path = descriptor_path(&lower_launch); let upper_path = descriptor_child_path(&persistent_launch, "upper"); let work_path = descriptor_child_path(&persistent_launch, "work"); let merged_path = descriptor_child_path(&runtime_launch, "merged"); let baseline = layout.current_merged_identity()?; if baseline != mount_identity(&layout.merged_descriptor)? { return Err(StateAnchorError::OwnershipMismatch); } let options = overlay_options(&lower_path, &upper_path, &work_path); let target = TargetCommand::new("fuse-overlayfs") .argument("-f") .argument("-o") .argument(options) .argument(merged_path.as_os_str()); let process = child_broker().acquire( acquisition.expected_parent, acquisition.ownership, &target, stage, acknowledge, )?; let staged = StagedProcessOwnership::v1( acquisition.ownership.domain_id, acquisition.ownership.generation_id, acquisition.ownership.coordinator_id, acquisition.ownership.slot, process.identity(), ); let deadline = Instant::now() + MOUNT_TIMEOUT; loop { if process.has_exited()? { child_broker().reap(process)?; return Err(StateAnchorError::DaemonExited); } let observed = layout.current_merged_identity()?; if observed.mount_id != baseline.mount_id { let readiness = VerifiedStateAnchorReadiness { staged, mount: observed, }; if let Err(error) = ready(readiness) { process.send_signal(rustix::process::Signal::KILL)?; while !process.has_exited()? { thread::yield_now(); } rootless_unmount(layout)?; child_broker().reap(process)?; return Err(error.into()); } return Ok(Self { process, mount: observed, layout, }); } if Instant::now() >= deadline { process.send_signal(rustix::process::Signal::KILL)?; while !process.has_exited()? { thread::yield_now(); } if match layout.current_merged_identity() { Ok(identity) => identity.mount_id != baseline.mount_id, Err(_) => true, } { let _ = rootless_unmount(layout); } child_broker().reap(process)?; return Err(StateAnchorError::MountTimeout); } thread::sleep(MOUNT_POLL_INTERVAL); } } #[must_use] pub const fn mount_identity(&self) -> MountIdentity { self.mount } #[must_use] pub fn process_ownership(&self) -> PersistedHostProcessOwnership { PersistedHostProcessOwnership::committed(self.process.identity()) } #[must_use] pub const fn process(&self) -> &RetainedProcess { &self.process } /// Verifies and unmounts only the exact retained State Anchor, then reaps its daemon. /// /// # Errors /// A missing, replaced, renamed, or remounted target is left untouched. pub fn unmount(self) -> Result<(), StateAnchorError> { self.layout.revalidate_authorities()?; require_mount_record(self.layout, self.mount)?; rootless_unmount(self.layout)?; if has_mount_record(self.layout, self.mount)? { return Err(StateAnchorError::OwnershipMismatch); } self.layout.require_unmounted()?; child_broker().reap(self.process)?; Ok(()) } /// Reaps the retained daemon after an independently verified external unmount. /// /// # Errors /// Refuses while the exact recorded mount remains or when exact reaping fails. pub fn finish_after_external_unmount(self) -> Result<(), StateAnchorError> { self.layout.revalidate_authorities()?; if has_mount_record(self.layout, self.mount)? { return Err(StateAnchorError::OwnershipMismatch); } self.layout.require_unmounted()?; child_broker().reap(self.process)?; Ok(()) } } /// Verifies and unmounts an exact recorded mount without opening either durable store. /// /// # Errors /// A missing or mismatched mount is left untouched. pub fn unmount_after_coordinator_loss( layout: &StateAnchorLayout<'_>, expected: MountIdentity, ) -> Result<(), StateAnchorError> { layout.revalidate_authorities()?; require_mount_record(layout, expected)?; rootless_unmount(layout)?; if has_mount_record(layout, expected)? { return Err(StateAnchorError::OwnershipMismatch); } layout.require_unmounted()?; Ok(()) } /// Enters an exact live holder's private namespaces and unmounts only its recorded State Anchor. /// /// # Errors /// Refuses descriptor transfer, pidfs, namespace, path, mount, helper, or acknowledgment drift. pub fn recover_private_state_anchor_mount( layout: &StateAnchorLayout<'_>, holder: &RetainedProcess, user_namespace: NamespaceIdentity, mount_namespace: NamespaceIdentity, expected_mount: MountIdentity, ) -> Result { let merged = &layout.merged_label; let (coordinator, helper) = private_seqpacket_pair()?; send_descriptors( &coordinator, &[holder.as_fd(), layout.runtime_anchor.as_fd()], )?; let authority = holder.identity().authority(); let target = TargetCommand::new("/proc/self/exe") .argument(PRIVATE_RECOVERY_ROLE) .argument(authority.pidfs_st_dev.get().to_string()) .argument(authority.pidfs_st_ino.get().to_string()) .argument(user_namespace.device.get().to_string()) .argument(user_namespace.inode.get().to_string()) .argument(mount_namespace.device.get().to_string()) .argument(mount_namespace.inode.get().to_string()) .argument(expected_mount.mount_id.get().to_string()) .argument(expected_mount.device.get().to_string()) .argument(expected_mount.inode.get().to_string()) .argument(merged.as_os_str()); child_broker().run_transient_with_private_stdout(&target, helper)?; let observed: MountIdentity = receive_packet(coordinator)?; if observed == expected_mount { Err(StateAnchorError::OwnershipMismatch) } else { Ok(observed) } } /// Dispatches the bounded private-namespace State Anchor recovery helper. #[must_use] pub fn dispatch_private_state_anchor_recovery(arguments: &[OsString]) -> Option> { if arguments .first() .is_none_or(|arg| arg != PRIVATE_RECOVERY_ROLE) { return None; } Some(run_private_state_anchor_recovery(arguments)) } fn run_private_state_anchor_recovery(arguments: &[OsString]) -> io::Result<()> { if arguments.len() != 11 { return Err(io::Error::other( "private State Anchor recovery arguments are malformed", )); } let values = arguments[1..10] .iter() .map(|value| { value .to_str() .ok_or_else(|| io::Error::other("private recovery integer is not UTF-8"))? .parse::() .map_err(|_| io::Error::other("private recovery integer is invalid")) }) .collect::>>()?; let expected_pidfs = (values[0], values[1]); let expected_user = NamespaceIdentity { device: DeviceId::new(values[2]), inode: InodeId::new(values[3]), }; let expected_mount_namespace = NamespaceIdentity { device: DeviceId::new(values[4]), inode: InodeId::new(values[5]), }; let expected_mount = MountIdentity { mount_id: lazaret_core::identity::MountId::new(values[6]), device: DeviceId::new(values[7]), inode: InodeId::new(values[8]), }; let merged = PathBuf::from(&arguments[10]); let stdout = io::stdout(); let mut descriptors = receive_descriptors(&stdout, 2)?; let runtime_anchor = descriptors .pop() .ok_or_else(|| io::Error::other("runtime anchor descriptor is absent"))?; let pidfd = descriptors .pop() .ok_or_else(|| io::Error::other("pidfd descriptor is absent"))?; let mut operations = KernelPrivateRecovery { pidfd, runtime_anchor, expected_pidfs, merged, reopened_parent: None, }; let observed = execute_private_recovery( &mut operations, expected_user, expected_mount_namespace, expected_mount, )?; send_packet(stdout, &observed) } trait PrivateRecoveryOperations { fn validate_pidfs(&mut self) -> io::Result<()>; fn enter_user_namespace(&mut self) -> io::Result<()>; fn observe_user_namespace(&mut self) -> io::Result; fn enter_mount_namespace(&mut self) -> io::Result<()>; fn observe_mount_namespace(&mut self) -> io::Result; fn verify_mount(&mut self) -> io::Result; fn unmount(&mut self) -> io::Result<()>; fn observe_after_unmount(&mut self) -> io::Result; } fn execute_private_recovery( operations: &mut impl PrivateRecoveryOperations, expected_user: NamespaceIdentity, expected_mount_namespace: NamespaceIdentity, expected_mount: MountIdentity, ) -> io::Result { operations .validate_pidfs() .map_err(|error| io::Error::other(format!("validate pidfs: {error}")))?; operations .enter_user_namespace() .map_err(|error| io::Error::other(format!("enter user namespace: {error}")))?; if operations .observe_user_namespace() .map_err(|error| io::Error::other(format!("observe user namespace: {error}")))? != expected_user { return Err(io::Error::other("private recovery user namespace mismatch")); } operations .enter_mount_namespace() .map_err(|error| io::Error::other(format!("enter mount namespace: {error}")))?; if operations .observe_mount_namespace() .map_err(|error| io::Error::other(format!("observe mount namespace: {error}")))? != expected_mount_namespace { return Err(io::Error::other( "private recovery mount namespace mismatch", )); } if operations .verify_mount() .map_err(|error| io::Error::other(format!("verify mount: {error}")))? != expected_mount { return Err(io::Error::other("private recovery mount identity mismatch")); } operations .unmount() .map_err(|error| io::Error::other(format!("unmount: {error}")))?; let observed = operations .observe_after_unmount() .map_err(|error| io::Error::other(format!("observe after unmount: {error}")))?; if observed == expected_mount { Err(io::Error::other("private recovery mount remains present")) } else { Ok(observed) } } struct KernelPrivateRecovery { pidfd: OwnedFd, runtime_anchor: OwnedFd, expected_pidfs: (u64, u64), merged: PathBuf, reopened_parent: Option, } impl PrivateRecoveryOperations for KernelPrivateRecovery { fn validate_pidfs(&mut self) -> io::Result<()> { require_pidfs_magic(rustix::fs::fstatfs(&self.pidfd)?.f_type as i64)?; let stat = rustix::fs::fstat(&self.pidfd)?; if (stat.st_dev, stat.st_ino) == self.expected_pidfs { Ok(()) } else { Err(io::Error::other( "private recovery pidfs authority mismatch", )) } } fn enter_user_namespace(&mut self) -> io::Result<()> { move_into_thread_name_spaces(self.pidfd.as_fd(), ThreadNameSpaceType::USER)?; Ok(()) } fn observe_user_namespace(&mut self) -> io::Result { namespace_identity(Path::new("/proc/self/ns/user")) } fn enter_mount_namespace(&mut self) -> io::Result<()> { move_into_thread_name_spaces(self.pidfd.as_fd(), ThreadNameSpaceType::MOUNT)?; Ok(()) } fn observe_mount_namespace(&mut self) -> io::Result { namespace_identity(Path::new("/proc/self/ns/mnt")) } fn verify_mount(&mut self) -> io::Result { let parent_path = self .merged .parent() .ok_or_else(|| io::Error::other("private recovery merged path has no parent"))?; let reopened_parent = openat2( rustix::fs::CWD, parent_path, OFlags::RDONLY | OFlags::DIRECTORY | OFlags::CLOEXEC | OFlags::NOFOLLOW, Mode::empty(), ResolveFlags::NO_MAGICLINKS | ResolveFlags::NO_SYMLINKS, )?; let retained = rustix::fs::fstat(&self.runtime_anchor)?; let reopened = rustix::fs::fstat(&reopened_parent)?; if (retained.st_dev, retained.st_ino) != (reopened.st_dev, reopened.st_ino) { return Err(io::Error::other("private recovery runtime anchor mismatch")); } let mounted = open_final_mount_directory(&reopened_parent, self.merged_name()?)?; let identity = mount_identity(&mounted)?; self.reopened_parent = Some(reopened_parent); Ok(identity) } fn unmount(&mut self) -> io::Result<()> { rustix::mount::unmount(&self.merged, rustix::mount::UnmountFlags::empty())?; Ok(()) } fn observe_after_unmount(&mut self) -> io::Result { let parent = self .reopened_parent .as_ref() .ok_or_else(|| io::Error::other("private recovery parent was not verified"))?; mount_identity(&open_final_mount_directory(parent, self.merged_name()?)?) } } impl KernelPrivateRecovery { fn merged_name(&self) -> io::Result<&Path> { self.merged .file_name() .map(Path::new) .ok_or_else(|| io::Error::other("private recovery merged path has no name")) } } /// Independently verifies that no mount is attached at the exact merged path. /// /// # Errors /// Refuses unreadable mount information or an inaccessible merged directory. pub fn require_mount_absent(layout: &StateAnchorLayout<'_>) -> Result<(), StateAnchorError> { layout.require_unmounted() } /// Freshly observes absence of one exact retired mount in the caller's current namespace. /// /// # Errors /// Refuses unreadable layout evidence or continued presence of the exact mount. pub fn observe_mount_absence( layout: &StateAnchorLayout<'_>, expected: MountIdentity, ) -> Result { let observed = layout.current_merged_identity()?; if observed == expected { Err(StateAnchorError::OwnershipMismatch) } else { Ok(observed) } } fn inspect_directory( directory: impl AsFd, prefix: &Path, entries: &mut Vec, ) -> Result<(), StateAnchorError> { for name in directory_names(&directory)? { let descriptor = openat2( directory.as_fd(), Path::new(&name), OFlags::PATH | OFlags::CLOEXEC | OFlags::NOFOLLOW, Mode::empty(), confined_resolve_flags(), ) .map_err(io::Error::from)?; let metadata = rustix::fs::fstat(&descriptor).map_err(io::Error::from)?; let kind = FileType::from_raw_mode(metadata.st_mode); let relative = prefix.join(&name); let entry_kind = if kind.is_dir() { StateAnchorEntryKind::Directory } else if kind.is_file() { StateAnchorEntryKind::RegularFile } else if kind.is_symlink() { StateAnchorEntryKind::Symlink } else if kind.is_char_device() && rustix::fs::major(metadata.st_rdev) == 0 && rustix::fs::minor(metadata.st_rdev) == 0 { StateAnchorEntryKind::Whiteout } else { return Err(StateAnchorError::InvalidLayout( "upper layer contains an unsupported entry type", )); }; entries.push(StateAnchorEntry { path: relative.as_os_str().as_bytes().to_vec(), kind: entry_kind, mode: metadata.st_mode, uid: metadata.st_uid, gid: metadata.st_gid, size: u64::try_from(metadata.st_size).map_err(|_| { StateAnchorError::InvalidLayout("upper-layer entry has a negative size") })?, }); if kind.is_dir() { let child = open_directory(&directory, Path::new(&name))?; inspect_directory(child, &relative, entries)?; } } Ok(()) } fn open_or_create_quarantine(parent: impl AsFd) -> Result { match mkdirat(parent.as_fd(), RESET_QUARANTINE, Mode::from_raw_mode(0o700)) { Ok(()) => rustix::fs::fsync(parent.as_fd()).map_err(io::Error::from)?, Err(rustix::io::Errno::EXIST) => {} Err(error) => return Err(io::Error::from(error).into()), } open_directory(parent, Path::new(RESET_QUARANTINE)) } fn move_all_entries(upper: impl AsFd, quarantine: impl AsFd) -> Result<(), StateAnchorError> { for name in directory_names(&upper)? { move_if_present(&upper, Path::new(&name), &quarantine, Path::new(&name))?; } Ok(()) } fn move_selected_entries( upper: impl AsFd, quarantine: impl AsFd, relative: &Path, ) -> Result<(), StateAnchorError> { let leaf = relative.file_name().ok_or(StateAnchorError::InvalidLayout( "reset target has no final component", ))?; let parent = open_optional_parent(&upper, relative.parent().unwrap_or(Path::new("")))?; let Some(parent) = parent else { return Ok(()); }; move_if_present(&parent, Path::new(leaf), &quarantine, Path::new("entry"))?; let mut whiteout = OsString::from(".wh."); whiteout.push(leaf); move_if_present( &parent, Path::new(&whiteout), &quarantine, Path::new("whiteout"), )?; Ok(()) } fn open_optional_parent(root: impl AsFd, path: &Path) -> Result, StateAnchorError> { if path.as_os_str().is_empty() { return rustix::io::dup(root.as_fd()) .map(Some) .map_err(io::Error::from) .map_err(StateAnchorError::from); } match openat2( root.as_fd(), path, OFlags::RDONLY | OFlags::DIRECTORY | OFlags::CLOEXEC | OFlags::NOFOLLOW, Mode::empty(), confined_resolve_flags(), ) { Ok(directory) => Ok(Some(directory)), Err(rustix::io::Errno::NOENT) => Ok(None), Err(error) => Err(io::Error::from(error).into()), } } fn move_if_present( source_parent: impl AsFd, source: &Path, destination_parent: impl AsFd, destination: &Path, ) -> Result<(), StateAnchorError> { let source_exists = entry_exists(source_parent.as_fd(), source)?; let destination_exists = entry_exists(destination_parent.as_fd(), destination)?; match (source_exists, destination_exists) { (false, false | true) => Ok(()), (true, true) => Err(StateAnchorError::InvalidLayout( "reset source and quarantine entry both exist", )), (true, false) => { renameat_with( source_parent.as_fd(), source, destination_parent.as_fd(), destination, RenameFlags::NOREPLACE, ) .map_err(io::Error::from)?; rustix::fs::fsync(source_parent.as_fd()).map_err(io::Error::from)?; rustix::fs::fsync(destination_parent.as_fd()).map_err(io::Error::from)?; Ok(()) } } } fn entry_exists(parent: impl AsFd, path: &Path) -> Result { match statat(parent.as_fd(), path, AtFlags::SYMLINK_NOFOLLOW) { Ok(_) => Ok(true), Err(rustix::io::Errno::NOENT) => Ok(false), Err(error) => Err(io::Error::from(error).into()), } } fn remove_directory_contents(directory: impl AsFd) -> Result<(), StateAnchorError> { for name in directory_names(&directory)? { let path = Path::new(&name); let descriptor = openat2( directory.as_fd(), path, OFlags::PATH | OFlags::CLOEXEC | OFlags::NOFOLLOW, Mode::empty(), confined_resolve_flags(), ) .map_err(io::Error::from)?; let kind = FileType::from_raw_mode( rustix::fs::fstat(&descriptor) .map_err(io::Error::from)? .st_mode, ); if kind.is_dir() { let child = open_directory(&directory, path)?; remove_directory_contents(&child)?; unlinkat(directory.as_fd(), path, AtFlags::REMOVEDIR).map_err(io::Error::from)?; } else { unlinkat(directory.as_fd(), path, AtFlags::empty()).map_err(io::Error::from)?; } rustix::fs::fsync(directory.as_fd()).map_err(io::Error::from)?; } Ok(()) } fn directory_names(directory: impl AsFd) -> Result, StateAnchorError> { let descriptor = rustix::io::dup(directory.as_fd()).map_err(io::Error::from)?; let mut buffer = [std::mem::MaybeUninit::uninit(); 8192]; let mut raw = RawDir::new(descriptor, &mut buffer); let mut names = Vec::new(); while let Some(entry) = raw.next() { let entry = entry.map_err(io::Error::from)?; let bytes = entry.file_name().to_bytes(); if bytes != b"." && bytes != b".." { names.push(OsString::from_vec(bytes.to_vec())); } } Ok(names) } fn directory_has_entries(directory: impl AsFd) -> Result { Ok(!directory_names(directory)?.is_empty()) } fn validate_relative_path(path: &Path) -> Result<(), StateAnchorError> { if path.as_os_str().is_empty() || path .components() .any(|component| !matches!(component, Component::Normal(_))) { Err(StateAnchorError::InvalidLayout( "reset target is not a normalized relative path", )) } else { Ok(()) } } fn create_directory(parent: impl AsFd, name: &str) -> Result<(), StateAnchorError> { match mkdirat(parent.as_fd(), name, Mode::from_raw_mode(0o700)) { Ok(()) | Err(rustix::io::Errno::EXIST) => {} Err(error) => return Err(io::Error::from(error).into()), } drop(open_directory(parent, Path::new(name))?); Ok(()) } fn open_directory(parent: impl AsFd, path: &Path) -> Result { let descriptor = openat2( parent.as_fd(), path, OFlags::RDONLY | OFlags::DIRECTORY | OFlags::CLOEXEC | OFlags::NOFOLLOW, Mode::empty(), ResolveFlags::BENEATH | ResolveFlags::NO_SYMLINKS | ResolveFlags::NO_MAGICLINKS | ResolveFlags::NO_XDEV, ) .map_err(io::Error::from)?; let metadata = rustix::fs::fstat(&descriptor).map_err(io::Error::from)?; if !FileType::from_raw_mode(metadata.st_mode).is_dir() { return Err(StateAnchorError::InvalidLayout( "fixed path is not a directory", )); } Ok(descriptor) } fn open_final_mount_directory(parent: impl AsFd, path: &Path) -> io::Result { let mut components = path.components(); if !matches!(components.next(), Some(Component::Normal(_))) || components.next().is_some() { return Err(io::Error::other( "final mount path is not one normal component", )); } open_mounted_directory(parent, path).map_err(io::Error::other) } fn open_mounted_directory(parent: impl AsFd, path: &Path) -> Result { let descriptor = openat2( parent.as_fd(), path, OFlags::PATH | OFlags::DIRECTORY | OFlags::CLOEXEC | OFlags::NOFOLLOW, Mode::empty(), ResolveFlags::BENEATH | ResolveFlags::NO_SYMLINKS | ResolveFlags::NO_MAGICLINKS, ) .map_err(io::Error::from)?; Ok(descriptor) } fn open_exact_directory_path( path: &Path, expected: (u64, u64), ) -> Result { let descriptor = openat2( rustix::fs::CWD, path, OFlags::PATH | OFlags::DIRECTORY | OFlags::CLOEXEC | OFlags::NOFOLLOW, Mode::empty(), ResolveFlags::NO_SYMLINKS | ResolveFlags::NO_MAGICLINKS, ) .map_err(io::Error::from)?; require_directory_descriptor_identity(&descriptor, expected)?; Ok(descriptor) } fn require_directory_descriptor_identity( descriptor: impl AsFd, expected: (u64, u64), ) -> Result<(), StateAnchorError> { let metadata = rustix::fs::fstat(descriptor).map_err(io::Error::from)?; if FileType::from_raw_mode(metadata.st_mode).is_dir() && (metadata.st_dev, metadata.st_ino) == expected { Ok(()) } else { Err(StateAnchorError::OwnershipMismatch) } } fn require_same_descriptor( observed: impl AsFd, expected: impl AsFd, ) -> Result<(), StateAnchorError> { let observed = mount_identity(observed)?; let expected = mount_identity(expected)?; if observed == expected { Ok(()) } else { Err(StateAnchorError::OwnershipMismatch) } } fn create_regular_file(parent: impl AsFd, name: &str) -> Result<(), StateAnchorError> { match openat2( parent.as_fd(), name, OFlags::WRONLY | OFlags::CREATE | OFlags::EXCL | OFlags::CLOEXEC | OFlags::NOFOLLOW, Mode::from_raw_mode(0o600), confined_resolve_flags(), ) { Ok(descriptor) => File::from(descriptor).sync_all()?, Err(rustix::io::Errno::EXIST) => drop(open_regular_file(parent, name)?), Err(error) => return Err(io::Error::from(error).into()), } Ok(()) } fn open_regular_file(parent: impl AsFd, name: &str) -> Result { let descriptor = openat2( parent.as_fd(), name, OFlags::PATH | OFlags::CLOEXEC | OFlags::NOFOLLOW, Mode::empty(), confined_resolve_flags(), ) .map_err(io::Error::from)?; let metadata = rustix::fs::fstat(&descriptor).map_err(io::Error::from)?; if FileType::from_raw_mode(metadata.st_mode).is_file() { Ok(descriptor) } else { Err(StateAnchorError::InvalidLayout( "protected runtime path is not a regular file", )) } } fn atomic_replace_at(parent: impl AsFd, name: &str, bytes: &[u8]) -> Result<(), StateAnchorError> { let temporary_name = loop { let sequence = TEMPORARY_SEQUENCE.fetch_add(1, Ordering::Relaxed); let candidate = OsString::from(format!(".{name}.lazaret-{}-{sequence}", std::process::id())); match openat2( parent.as_fd(), Path::new(&candidate), OFlags::WRONLY | OFlags::CREATE | OFlags::EXCL | OFlags::CLOEXEC | OFlags::NOFOLLOW, Mode::from_raw_mode(0o600), confined_resolve_flags(), ) { Ok(descriptor) => { let mut file = File::from(descriptor); if let Err(error) = file.write_all(bytes).and_then(|()| file.sync_all()) { let _ = unlinkat(parent.as_fd(), Path::new(&candidate), AtFlags::empty()); return Err(error.into()); } break candidate; } Err(rustix::io::Errno::EXIST) => {} Err(error) => return Err(io::Error::from(error).into()), } }; let result = renameat( parent.as_fd(), Path::new(&temporary_name), parent.as_fd(), name, ) .and_then(|()| rustix::fs::fsync(parent.as_fd())); if result.is_err() { let _ = unlinkat(parent.as_fd(), Path::new(&temporary_name), AtFlags::empty()); } result .map_err(io::Error::from) .map_err(StateAnchorError::from) } const fn confined_resolve_flags() -> ResolveFlags { ResolveFlags::BENEATH .union(ResolveFlags::NO_SYMLINKS) .union(ResolveFlags::NO_MAGICLINKS) .union(ResolveFlags::NO_XDEV) } fn descriptor_path(descriptor: impl AsFd) -> PathBuf { PathBuf::from(format!("/proc/self/fd/{}", descriptor.as_fd().as_raw_fd())) } fn descriptor_child_path(descriptor: impl AsFd, child: &str) -> PathBuf { descriptor_path(descriptor).join(child) } fn inherit_for_exec(descriptor: impl AsFd) -> Result { dup(descriptor).map_err(io::Error::from).map_err(Into::into) } fn reopen_inherited_directory(descriptor: i32) -> Result { if descriptor < 0 { return Err(StateAnchorError::InvalidLayout( "inherited directory descriptor is invalid", )); } let file = File::open(format!("/proc/self/fd/{descriptor}"))?; let descriptor: OwnedFd = file.into(); let metadata = rustix::fs::fstat(&descriptor).map_err(io::Error::from)?; if FileType::from_raw_mode(metadata.st_mode).is_dir() { Ok(descriptor) } else { Err(StateAnchorError::InvalidLayout( "inherited authority is not a directory", )) } } fn overlay_options(lower: &Path, upper: &Path, work: &Path) -> OsString { let mut bytes = b"lowerdir=".to_vec(); append_fuse_option_path(&mut bytes, lower); bytes.extend_from_slice(b",upperdir="); append_fuse_option_path(&mut bytes, upper); bytes.extend_from_slice(b",workdir="); append_fuse_option_path(&mut bytes, work); OsString::from_vec(bytes) } fn append_fuse_option_path(target: &mut Vec, path: &Path) { for byte in path.as_os_str().as_bytes() { if *byte == b':' { target.push(b'\\'); target.push(b'\\'); } else if *byte == b',' { target.push(b'\\'); } target.push(*byte); } } fn rootless_unmount(layout: &StateAnchorLayout<'_>) -> Result<(), StateAnchorError> { rootless_unmount_anchor(&layout.runtime_anchor) } fn rootless_unmount_anchor(runtime_anchor: impl AsFd) -> Result<(), StateAnchorError> { let inherited_anchor = inherit_for_exec(runtime_anchor)?; let path = descriptor_child_path(&inherited_anchor, "merged"); rootless_unmount_path(&path) } fn rootless_unmount_path(path: &Path) -> Result<(), StateAnchorError> { child_broker() .run_transient( &TargetCommand::new("fusermount3") .argument("-u") .argument(path), ) .map(drop) .map_err(StateAnchorError::from) } fn require_mount_record( layout: &StateAnchorLayout<'_>, expected: MountIdentity, ) -> Result<(), StateAnchorError> { if has_mount_record(layout, expected)? { Ok(()) } else { Err(StateAnchorError::OwnershipMismatch) } } fn has_mount_record( layout: &StateAnchorLayout<'_>, expected: MountIdentity, ) -> Result { let expected_path = std::fs::read_link(descriptor_path(&layout.runtime_anchor))? .join("merged") .into_os_string() .into_vec(); for line in std::fs::read("/proc/self/mountinfo")?.split(|byte| *byte == b'\n') { let fields = line.split(|byte| *byte == b' ').take(5).collect::>(); if fields.len() != 5 { continue; } let Some(mount_id) = parse_decimal(fields[0]) else { continue; }; let Some((major, minor)) = parse_device(fields[2]) else { continue; }; if decode_mount_field(fields[4]) == expected_path && mount_id == expected.mount_id.get() && rustix::fs::makedev(major, minor) == expected.device.get() { return Ok(true); } } Ok(false) } fn parse_decimal(bytes: &[u8]) -> Option { std::str::from_utf8(bytes).ok()?.parse().ok() } fn parse_device(bytes: &[u8]) -> Option<(u32, u32)> { let separator = bytes.iter().position(|byte| *byte == b':')?; let (major, minor) = (&bytes[..separator], &bytes[separator + 1..]); Some(( u32::try_from(parse_decimal(major)?).ok()?, u32::try_from(parse_decimal(minor)?).ok()?, )) } fn decode_mount_field(bytes: &[u8]) -> Vec { let mut decoded = Vec::with_capacity(bytes.len()); let mut index = 0; while index < bytes.len() { if bytes[index] == b'\\' && index + 3 < bytes.len() { let digits = &bytes[index + 1..index + 4]; if digits.iter().all(|digit| matches!(digit, b'0'..=b'7')) { decoded.push((digits[0] - b'0') * 64 + (digits[1] - b'0') * 8 + digits[2] - b'0'); index += 4; continue; } } decoded.push(bytes[index]); index += 1; } decoded } fn path_from_host(path: &HostPath) -> PathBuf { PathBuf::from(OsString::from_vec(path.as_bytes().to_vec())) } fn host_path(path: &Path) -> HostPath { HostPath::new(path.as_os_str().as_bytes().to_vec()) } #[cfg(test)] mod recovery_tests { use super::*; use std::os::unix::fs::symlink; struct FailpointRecovery { fail_at: Option, boundary: usize, events: Vec<&'static str>, user: NamespaceIdentity, mount_namespace: NamespaceIdentity, mount: MountIdentity, observed: MountIdentity, } impl FailpointRecovery { fn boundary(&mut self, name: &'static str) -> io::Result<()> { self.boundary += 1; self.events.push(name); if self.fail_at == Some(self.boundary) { Err(io::Error::other("injected recovery boundary failure")) } else { Ok(()) } } } impl PrivateRecoveryOperations for FailpointRecovery { fn validate_pidfs(&mut self) -> io::Result<()> { self.boundary("pidfs") } fn enter_user_namespace(&mut self) -> io::Result<()> { self.boundary("enter-user") } fn observe_user_namespace(&mut self) -> io::Result { self.boundary("observe-user")?; Ok(self.user) } fn enter_mount_namespace(&mut self) -> io::Result<()> { self.boundary("enter-mount") } fn observe_mount_namespace(&mut self) -> io::Result { self.boundary("observe-mount")?; Ok(self.mount_namespace) } fn verify_mount(&mut self) -> io::Result { self.boundary("verify-mount")?; Ok(self.mount) } fn unmount(&mut self) -> io::Result<()> { self.boundary("unmount") } fn observe_after_unmount(&mut self) -> io::Result { self.boundary("observe-absence")?; Ok(self.observed) } } fn fixture(fail_at: Option) -> FailpointRecovery { FailpointRecovery { fail_at, boundary: 0, events: Vec::new(), user: NamespaceIdentity { device: DeviceId::new(1), inode: InodeId::new(2), }, mount_namespace: NamespaceIdentity { device: DeviceId::new(3), inode: InodeId::new(4), }, mount: MountIdentity { mount_id: lazaret_core::identity::MountId::new(5), device: DeviceId::new(6), inode: InodeId::new(7), }, observed: MountIdentity { mount_id: lazaret_core::identity::MountId::new(8), device: DeviceId::new(6), inode: InodeId::new(7), }, } } #[test] fn private_recovery_failpoints_refuse_before_every_later_boundary() { for boundary in 1..=8 { let mut operations = fixture(Some(boundary)); let expected_user = operations.user; let expected_mount_namespace = operations.mount_namespace; let expected_mount = operations.mount; let result = execute_private_recovery( &mut operations, expected_user, expected_mount_namespace, expected_mount, ); assert!(result.is_err()); assert_eq!(operations.events.len(), boundary); } let mut operations = fixture(None); let expected_user = operations.user; let expected_mount_namespace = operations.mount_namespace; let expected_mount = operations.mount; assert_eq!( execute_private_recovery( &mut operations, expected_user, expected_mount_namespace, expected_mount, ) .unwrap(), operations.observed ); assert_eq!( operations.events, [ "pidfs", "enter-user", "observe-user", "enter-mount", "observe-mount", "verify-mount", "unmount", "observe-absence", ] ); } #[test] fn final_mount_open_refuses_intermediate_and_link_traversal() { let root = std::env::temp_dir().join(format!( "lazaret-final-mount-open-{}-{}", std::process::id(), TEMPORARY_SEQUENCE.fetch_add(1, Ordering::Relaxed) )); std::fs::create_dir_all(root.join("nested/final")).unwrap(); symlink("nested", root.join("link")).unwrap(); let parent = File::open(&root).unwrap(); let descriptor = File::open(root.join("nested")).unwrap(); symlink( format!("/proc/self/fd/{}", descriptor.as_raw_fd()), root.join("magic"), ) .unwrap(); assert!(open_final_mount_directory(&parent, Path::new("nested/final")).is_err()); assert!(open_final_mount_directory(&parent, Path::new("link")).is_err()); assert!(open_final_mount_directory(&parent, Path::new("magic")).is_err()); assert!(open_final_mount_directory(&parent, Path::new("nested")).is_ok()); std::fs::remove_dir_all(root).unwrap(); } #[test] fn private_recovery_refuses_wrong_or_absent_final_mount_before_action() { for observed_mount_id in [4, 8] { let mut operations = fixture(None); operations.mount.mount_id = lazaret_core::identity::MountId::new(observed_mount_id); let expected_user = operations.user; let expected_mount_namespace = operations.mount_namespace; let expected_mount = MountIdentity { mount_id: lazaret_core::identity::MountId::new(5), device: DeviceId::new(6), inode: InodeId::new(7), }; assert!( execute_private_recovery( &mut operations, expected_user, expected_mount_namespace, expected_mount, ) .is_err() ); assert_eq!( operations.events, [ "pidfs", "enter-user", "observe-user", "enter-mount", "observe-mount", "verify-mount", ] ); } } }