2026-07-15 16:54:03 +09:00
|
|
|
#![doc = "Physical State Anchor layout and exact fuse-overlayfs mount lifecycle."]
|
|
|
|
|
|
|
|
|
|
use std::ffi::OsString;
|
|
|
|
|
use std::fmt;
|
2026-07-15 17:27:39 +09:00
|
|
|
use std::fs::File;
|
|
|
|
|
use std::io::{self, Write};
|
|
|
|
|
use std::os::fd::{AsFd, AsRawFd, OwnedFd};
|
2026-07-15 16:54:03 +09:00
|
|
|
use std::os::unix::ffi::{OsStrExt, OsStringExt};
|
2026-07-17 05:38:43 +09:00
|
|
|
use std::path::{Component, Path, PathBuf};
|
2026-07-15 17:27:39 +09:00
|
|
|
use std::sync::atomic::{AtomicU64, Ordering};
|
2026-07-15 16:54:03 +09:00
|
|
|
use std::thread;
|
|
|
|
|
use std::time::{Duration, Instant};
|
|
|
|
|
|
2026-07-15 17:27:39 +09:00
|
|
|
use lazaret_core::configuration::{ConfinedPathPlan, ResolvedDomainConfiguration};
|
2026-07-17 05:38:43 +09:00
|
|
|
use lazaret_core::identity::{
|
|
|
|
|
DeviceId, HostPath, InodeId, MountIdentity, NamespaceIdentity, PreparedArtifactId,
|
|
|
|
|
};
|
2026-07-15 16:54:03 +09:00
|
|
|
use lazaret_core::records::{
|
|
|
|
|
HostProcessOwnershipSlot, PersistedHostProcessOwnership, PreparedArtifact,
|
|
|
|
|
ProcessOwnershipCommitAck, StagedProcessOwnership, StateAnchorOwnership,
|
|
|
|
|
};
|
2026-07-17 05:38:43 +09:00
|
|
|
use lazaret_linux::filesystem::{mount_identity, namespace_identity};
|
|
|
|
|
use lazaret_linux::ipc::{
|
|
|
|
|
private_seqpacket_pair, receive_descriptors, receive_packet, send_descriptors, send_packet,
|
|
|
|
|
};
|
2026-07-15 16:54:03 +09:00
|
|
|
use lazaret_linux::process::{
|
2026-07-17 05:38:43 +09:00
|
|
|
ProcessOwnershipContext, RetainedProcess, TargetCommand, child_broker, require_pidfs_magic,
|
2026-07-15 16:54:03 +09:00
|
|
|
};
|
2026-07-15 17:27:39 +09:00
|
|
|
use rustix::fs::{
|
2026-07-17 07:54:46 +09:00
|
|
|
AtFlags, FileType, Mode, OFlags, RawDir, RenameFlags, ResolveFlags, mkdirat, openat2, renameat,
|
|
|
|
|
renameat_with, statat, unlinkat,
|
2026-07-15 17:27:39 +09:00
|
|
|
};
|
|
|
|
|
use rustix::io::dup;
|
2026-07-17 05:38:43 +09:00
|
|
|
use rustix::thread::{ThreadNameSpaceType, move_into_thread_name_spaces};
|
2026-07-15 16:54:03 +09:00
|
|
|
|
|
|
|
|
const MOUNT_TIMEOUT: Duration = Duration::from_secs(5);
|
|
|
|
|
const MOUNT_POLL_INTERVAL: Duration = Duration::from_millis(2);
|
2026-07-19 03:44:30 +09:00
|
|
|
pub const PRIVATE_STATE_ANCHOR_RECOVERY_ROLE: &str = "--lazaret-private-state-anchor-recovery";
|
2026-07-15 17:27:39 +09:00
|
|
|
static TEMPORARY_SEQUENCE: AtomicU64 = AtomicU64::new(0);
|
2026-07-15 16:54:03 +09:00
|
|
|
pub const PROTECTED_RUNTIME_FILES: [&str; 3] = ["resolv.conf", "hosts", "hostname"];
|
2026-07-17 07:54:46 +09:00
|
|
|
const RESET_QUARANTINE: &str = "reset-quarantine";
|
2026-07-15 16:54:03 +09:00
|
|
|
|
|
|
|
|
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<io::Error> for StateAnchorError {
|
|
|
|
|
fn from(error: io::Error) -> Self {
|
|
|
|
|
Self::Io(error)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug)]
|
2026-07-15 17:27:39 +09:00
|
|
|
pub struct StateAnchorLayout<'configuration> {
|
|
|
|
|
persistent_plan: &'configuration ConfinedPathPlan,
|
|
|
|
|
runtime_plan: &'configuration ConfinedPathPlan,
|
2026-07-15 16:54:03 +09:00
|
|
|
persistent_root: OwnedFd,
|
|
|
|
|
runtime_root: OwnedFd,
|
2026-07-15 17:27:39 +09:00
|
|
|
persistent_anchor: OwnedFd,
|
|
|
|
|
runtime_anchor: OwnedFd,
|
|
|
|
|
upper_descriptor: OwnedFd,
|
|
|
|
|
work_descriptor: OwnedFd,
|
|
|
|
|
merged_descriptor: OwnedFd,
|
|
|
|
|
runtime_files_descriptor: OwnedFd,
|
2026-07-15 16:54:03 +09:00
|
|
|
upper: PathBuf,
|
|
|
|
|
work: PathBuf,
|
|
|
|
|
merged: PathBuf,
|
|
|
|
|
runtime_files: PathBuf,
|
2026-07-15 17:27:39 +09:00
|
|
|
upper_label: PathBuf,
|
|
|
|
|
work_label: PathBuf,
|
|
|
|
|
merged_label: PathBuf,
|
2026-07-15 16:54:03 +09:00
|
|
|
persistent_destinations: Vec<PathBuf>,
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-15 17:27:39 +09:00
|
|
|
impl<'configuration> StateAnchorLayout<'configuration> {
|
2026-07-15 16:54:03 +09:00
|
|
|
/// Creates the fixed layout beneath retained, revalidated Domain authorities.
|
|
|
|
|
///
|
|
|
|
|
/// # Errors
|
|
|
|
|
/// Refuses changed roots, non-directory fixed paths, and unsafe persistent destinations.
|
2026-07-15 17:27:39 +09:00
|
|
|
pub fn create(
|
|
|
|
|
configuration: &'configuration ResolvedDomainConfiguration,
|
|
|
|
|
) -> Result<Self, StateAnchorError> {
|
2026-07-15 16:54:03 +09:00
|
|
|
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)?;
|
|
|
|
|
}
|
2026-07-15 17:27:39 +09:00
|
|
|
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"))?;
|
2026-07-15 16:54:03 +09:00
|
|
|
for name in PROTECTED_RUNTIME_FILES {
|
2026-07-15 17:27:39 +09:00
|
|
|
create_regular_file(&runtime_files_descriptor, name)?;
|
2026-07-15 16:54:03 +09:00
|
|
|
}
|
|
|
|
|
rustix::fs::fsync(&runtime_anchor).map_err(io::Error::from)?;
|
|
|
|
|
rustix::fs::fsync(&persistent_anchor).map_err(io::Error::from)?;
|
2026-07-15 17:27:39 +09:00
|
|
|
let persistent_base =
|
|
|
|
|
path_from_host(configuration.persistent_domain_root().canonical_label());
|
|
|
|
|
let runtime_base = path_from_host(configuration.runtime_domain_root().canonical_label());
|
2026-07-15 16:54:03 +09:00
|
|
|
let persistent_destinations = configuration
|
|
|
|
|
.fingerprint_record()
|
|
|
|
|
.persistent_dirs
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|destination| PathBuf::from(destination.strip_prefix('/').unwrap_or(destination)))
|
|
|
|
|
.collect();
|
|
|
|
|
Ok(Self {
|
2026-07-15 17:27:39 +09:00
|
|
|
persistent_plan: configuration.persistent_domain_root(),
|
|
|
|
|
runtime_plan: configuration.runtime_domain_root(),
|
2026-07-15 16:54:03 +09:00
|
|
|
persistent_root,
|
|
|
|
|
runtime_root,
|
2026-07-15 17:27:39 +09:00
|
|
|
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"),
|
2026-07-15 16:54:03 +09:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-17 05:38:43 +09:00
|
|
|
/// Returns the ordinary host path used as the merged mount label.
|
|
|
|
|
#[must_use]
|
|
|
|
|
pub fn merged_label(&self) -> &Path {
|
|
|
|
|
&self.merged_label
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-15 16:54:03 +09:00
|
|
|
#[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> {
|
2026-07-15 17:27:39 +09:00
|
|
|
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)?;
|
2026-07-15 16:54:03 +09:00
|
|
|
}
|
2026-07-15 17:27:39 +09:00
|
|
|
let runtime_files = open_directory(&self.runtime_anchor, Path::new("runtime-files"))?;
|
|
|
|
|
require_same_descriptor(&runtime_files, &self.runtime_files_descriptor)?;
|
2026-07-15 16:54:03 +09:00
|
|
|
for name in PROTECTED_RUNTIME_FILES {
|
2026-07-15 17:27:39 +09:00
|
|
|
drop(open_regular_file(&self.runtime_files_descriptor, name)?);
|
2026-07-15 16:54:03 +09:00
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-15 17:27:39 +09:00
|
|
|
fn current_merged_identity(&self) -> Result<MountIdentity, StateAnchorError> {
|
|
|
|
|
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()
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-15 16:54:03 +09:00
|
|
|
/// 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> {
|
2026-07-15 17:27:39 +09:00
|
|
|
self.revalidate_authorities()?;
|
2026-07-15 16:54:03 +09:00
|
|
|
for (name, bytes) in [
|
|
|
|
|
("resolv.conf", files.resolv_conf),
|
|
|
|
|
("hosts", files.hosts),
|
|
|
|
|
("hostname", files.hostname),
|
|
|
|
|
] {
|
2026-07-15 17:27:39 +09:00
|
|
|
atomic_replace_at(&self.runtime_files_descriptor, name, bytes)?;
|
2026-07-15 16:54:03 +09:00
|
|
|
}
|
2026-07-15 17:27:39 +09:00
|
|
|
self.revalidate_authorities()
|
2026-07-15 16:54:03 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[must_use]
|
|
|
|
|
pub fn ownership_intent(&self, artifact_id: PreparedArtifactId) -> StateAnchorOwnership {
|
|
|
|
|
StateAnchorOwnership {
|
|
|
|
|
lower_artifact_id: Some(artifact_id),
|
2026-07-15 17:27:39 +09:00
|
|
|
upper_path: Some(host_path(&self.upper_label)),
|
|
|
|
|
work_path: Some(host_path(&self.work_label)),
|
|
|
|
|
merged_path: Some(host_path(&self.merged_label)),
|
2026-07-15 16:54:03 +09:00
|
|
|
daemon: PersistedHostProcessOwnership::unassigned(),
|
2026-07-17 03:58:27 +09:00
|
|
|
user_namespace: None,
|
|
|
|
|
mount_namespace: None,
|
2026-07-15 16:54:03 +09:00
|
|
|
mount: None,
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-07-15 17:27:39 +09:00
|
|
|
|
|
|
|
|
#[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))
|
|
|
|
|
}
|
2026-07-17 07:54:46 +09:00
|
|
|
|
|
|
|
|
/// 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<StateAnchorInspection, StateAnchorError> {
|
|
|
|
|
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<PathBuf, StateAnchorError> {
|
|
|
|
|
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<u8>,
|
|
|
|
|
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<StateAnchorEntry>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Clone, Copy, Debug)]
|
|
|
|
|
pub enum StateAnchorResetScope<'a> {
|
|
|
|
|
All,
|
|
|
|
|
Selected(&'a Path),
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-17 08:05:17 +09:00
|
|
|
/// 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(())
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-17 07:54:46 +09:00
|
|
|
/// 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),
|
|
|
|
|
}
|
2026-07-15 16:54:03 +09:00
|
|
|
}
|
|
|
|
|
|
2026-07-15 17:27:39 +09:00
|
|
|
/// Pins the exact mounted and lower authorities until the view is dropped before unmount.
|
2026-07-15 16:54:03 +09:00
|
|
|
#[derive(Debug)]
|
2026-07-15 17:27:39 +09:00
|
|
|
pub struct StateAnchorSandboxView<'layout, 'configuration> {
|
|
|
|
|
layout: &'layout StateAnchorLayout<'configuration>,
|
|
|
|
|
lower_descriptor: OwnedFd,
|
|
|
|
|
merged_descriptor: OwnedFd,
|
2026-07-15 16:54:03 +09:00
|
|
|
lower: PathBuf,
|
|
|
|
|
lower_identity: (u64, u64),
|
|
|
|
|
merged: PathBuf,
|
|
|
|
|
mount: MountIdentity,
|
|
|
|
|
runtime_files: PathBuf,
|
|
|
|
|
persistent_destinations: Vec<PathBuf>,
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-15 17:27:39 +09:00
|
|
|
impl<'layout, 'configuration> StateAnchorSandboxView<'layout, 'configuration> {
|
2026-07-15 16:54:03 +09:00
|
|
|
/// 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(
|
2026-07-15 17:27:39 +09:00
|
|
|
layout: &'layout StateAnchorLayout<'configuration>,
|
2026-07-15 16:54:03 +09:00
|
|
|
artifact: &PreparedArtifact,
|
|
|
|
|
mount: MountIdentity,
|
|
|
|
|
) -> Result<Self, StateAnchorError> {
|
2026-07-15 17:27:39 +09:00
|
|
|
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"))?;
|
2026-07-15 16:54:03 +09:00
|
|
|
let view = Self {
|
2026-07-15 17:27:39 +09:00
|
|
|
layout,
|
|
|
|
|
lower: descriptor_path(&lower_descriptor),
|
2026-07-15 16:54:03 +09:00
|
|
|
lower_identity: (artifact.device.get(), artifact.inode.get()),
|
2026-07-15 17:27:39 +09:00
|
|
|
merged: descriptor_path(&merged_descriptor),
|
2026-07-15 16:54:03 +09:00
|
|
|
mount,
|
2026-07-15 17:27:39 +09:00
|
|
|
runtime_files: descriptor_path(&layout.runtime_files_descriptor),
|
2026-07-15 16:54:03 +09:00
|
|
|
persistent_destinations: layout.persistent_destinations.clone(),
|
2026-07-15 17:27:39 +09:00
|
|
|
lower_descriptor,
|
|
|
|
|
merged_descriptor,
|
2026-07-15 16:54:03 +09:00
|
|
|
};
|
|
|
|
|
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> {
|
2026-07-15 17:27:39 +09:00
|
|
|
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);
|
|
|
|
|
}
|
2026-07-15 16:54:03 +09:00
|
|
|
for name in PROTECTED_RUNTIME_FILES {
|
2026-07-15 17:27:39 +09:00
|
|
|
drop(open_regular_file(
|
|
|
|
|
&self.layout.runtime_files_descriptor,
|
|
|
|
|
name,
|
|
|
|
|
)?);
|
2026-07-15 16:54:03 +09:00
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[must_use]
|
|
|
|
|
pub fn lower(&self) -> &Path {
|
|
|
|
|
&self.lower
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn persistent_mounts(&self) -> impl Iterator<Item = (PathBuf, PathBuf)> + '_ {
|
|
|
|
|
self.persistent_destinations.iter().map(|destination| {
|
|
|
|
|
(
|
|
|
|
|
self.merged.join(destination),
|
|
|
|
|
Path::new("/").join(destination),
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn protected_runtime_mounts(&self) -> impl Iterator<Item = (PathBuf, PathBuf)> + '_ {
|
|
|
|
|
PROTECTED_RUNTIME_FILES
|
|
|
|
|
.into_iter()
|
|
|
|
|
.map(|name| (self.runtime_files.join(name), Path::new("/etc").join(name)))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-19 03:02:44 +09:00
|
|
|
include!("lib/authority.rs");
|
|
|
|
|
include!("lib/mount.rs");
|
|
|
|
|
include!("lib/storage.rs");
|
|
|
|
|
include!("lib/tests.rs");
|