#![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::{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::{HostPath, MountIdentity, PreparedArtifactId}; use lazaret_core::records::{ HostProcessOwnershipSlot, PersistedHostProcessOwnership, PreparedArtifact, ProcessOwnershipCommitAck, StagedProcessOwnership, StateAnchorOwnership, }; use lazaret_linux::filesystem::mount_identity; use lazaret_linux::process::{ ProcessOwnershipContext, RetainedProcess, TargetCommand, child_broker, }; use rustix::fs::{ AtFlags, FileType, Mode, OFlags, ResolveFlags, mkdirat, openat2, renameat, unlinkat, }; use rustix::io::dup; const MOUNT_TIMEOUT: Duration = Duration::from_secs(5); const MOUNT_POLL_INTERVAL: Duration = Duration::from_millis(2); static TEMPORARY_SEQUENCE: AtomicU64 = AtomicU64::new(0); pub const PROTECTED_RUNTIME_FILES: [&str; 3] = ["resolv.conf", "hosts", "hostname"]; 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 } #[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(), 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)) } } /// 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, } impl VerifiedStateAnchorReadiness { #[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(()) } /// 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() } 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_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 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); } } #[allow(clippy::disallowed_methods, clippy::disallowed_types)] fn rootless_unmount(layout: &StateAnchorLayout<'_>) -> Result<(), StateAnchorError> { let inherited_anchor = inherit_for_exec(&layout.runtime_anchor)?; let path = descriptor_child_path(&inherited_anchor, "merged"); let status = std::process::Command::new("fusermount3") .arg("-u") .arg(&path) .status()?; if status.success() { Ok(()) } else { Err(StateAnchorError::Io(io::Error::other(format!( "fusermount3 exited with {status}" )))) } } 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()) }