471 lines
17 KiB
Rust
471 lines
17 KiB
Rust
use super::storage::{
|
|
descriptor_child_path, descriptor_path, inherit_for_exec, open_directory,
|
|
open_exact_directory_path, open_mounted_directory, overlay_options, path_from_host,
|
|
reopen_inherited_directory, require_directory_descriptor_identity, rootless_unmount_path,
|
|
};
|
|
use super::{
|
|
AsFd, AsRawFd, FileType, HostProcessOwnershipSlot, Instant, MOUNT_POLL_INTERVAL, MOUNT_TIMEOUT,
|
|
Mode, MountIdentity, OFlags, OwnedFd, Path, PathBuf, PreparedArtifact,
|
|
ProcessOwnershipCommitAck, ProcessOwnershipContext, ResolveFlags, RetainedProcess,
|
|
StagedProcessOwnership, StateAnchorError, StateAnchorLayout, TargetCommand, child_broker, io,
|
|
mount_identity, openat2, thread,
|
|
};
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub struct VerifiedStateAnchorReadiness {
|
|
pub(super) staged: StagedProcessOwnership,
|
|
pub(super) 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<PathBuf>,
|
|
}
|
|
|
|
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<Self, StateAnchorError> {
|
|
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<Self, StateAnchorError> {
|
|
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<Self, StateAnchorError> {
|
|
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<Self, StateAnchorError> {
|
|
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<Stage, Acknowledge, Ready>(
|
|
self,
|
|
expected_parent: lazaret_core::identity::PidfsProcessIdentityV1,
|
|
ownership: ProcessOwnershipContext,
|
|
stage: Stage,
|
|
acknowledge: Acknowledge,
|
|
ready: Ready,
|
|
) -> Result<RuntimeStateAnchorMount, StateAnchorError>
|
|
where
|
|
Stage: FnOnce(StagedProcessOwnership) -> io::Result<()>,
|
|
Acknowledge: FnOnce(StagedProcessOwnership) -> io::Result<ProcessOwnershipCommitAck>,
|
|
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<MountIdentity, StateAnchorError> {
|
|
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<RuntimeStateAnchorAbsenceAuthority, StateAnchorError> {
|
|
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
|
|
}
|
|
}
|