Make State Anchor acquisition resumable and descriptor-confined

This commit is contained in:
ms2k 2026-07-15 17:27:39 +09:00
commit 48c10e9a7a

View file

@ -2,28 +2,33 @@
use std::ffi::OsString;
use std::fmt;
use std::fs::{File, OpenOptions};
use std::io;
use std::os::fd::{AsFd, OwnedFd};
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::ResolvedDomainConfiguration;
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::{atomic_replace, current_mount_identity, require_mount_identity};
use lazaret_linux::filesystem::mount_identity;
use lazaret_linux::process::{
ProcessOwnershipContext, RetainedProcess, TargetCommand, child_broker,
};
use rustix::fs::{FileType, Mode, OFlags, ResolveFlags, mkdirat, openat2};
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> {
@ -74,22 +79,35 @@ impl From<io::Error> for StateAnchorError {
}
#[derive(Debug)]
pub struct StateAnchorLayout {
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<PathBuf>,
}
impl StateAnchorLayout {
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: &ResolvedDomainConfiguration) -> Result<Self, StateAnchorError> {
pub fn create(
configuration: &'configuration ResolvedDomainConfiguration,
) -> Result<Self, StateAnchorError> {
let persistent_root = configuration
.persistent_domain_root()
.open_authority()
@ -98,9 +116,6 @@ impl StateAnchorLayout {
.runtime_domain_root()
.open_authority()
.map_err(|_| StateAnchorError::InvalidLayout("runtime Domain authority changed"))?;
let persistent_base =
path_from_host(configuration.persistent_domain_root().canonical_label());
let runtime_base = path_from_host(configuration.runtime_domain_root().canonical_label());
create_directory(&persistent_root, "state-anchor")?;
let persistent_anchor = open_directory(&persistent_root, Path::new("state-anchor"))?;
for name in ["upper", "work"] {
@ -111,19 +126,18 @@ impl StateAnchorLayout {
for name in ["merged", "runtime-files"] {
create_directory(&runtime_anchor, name)?;
}
let runtime_files = runtime_base.join("state-anchor/runtime-files");
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 {
let path = runtime_files.join(name);
match OpenOptions::new().create_new(true).write(true).open(&path) {
Ok(file) => file.sync_all()?,
Err(error) if error.kind() == io::ErrorKind::AlreadyExists => {
require_regular_file(&path)?;
}
Err(error) => return Err(error.into()),
}
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
@ -131,12 +145,23 @@ impl StateAnchorLayout {
.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: persistent_base.join("state-anchor/upper"),
work: persistent_base.join("state-anchor/work"),
merged: runtime_base.join("state-anchor/merged"),
runtime_files,
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,
})
}
@ -171,18 +196,59 @@ impl StateAnchorLayout {
/// # Errors
/// Refuses replacement, symlink, mount crossing, or a non-directory fixed path.
pub fn revalidate(&self) -> Result<(), StateAnchorError> {
for path in ["state-anchor/upper", "state-anchor/work"] {
drop(open_directory(&self.persistent_root, Path::new(path))?);
}
for path in ["state-anchor/merged", "state-anchor/runtime-files"] {
drop(open_directory(&self.runtime_root, Path::new(path))?);
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 {
require_regular_file(&self.runtime_files.join(name))?;
drop(open_regular_file(&self.runtime_files_descriptor, name)?);
}
Ok(())
}
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()
}
/// Atomically replaces sandbox runtime files in host-private transient storage.
///
/// # Errors
@ -191,32 +257,43 @@ impl StateAnchorLayout {
&self,
files: &ProtectedRuntimeFiles<'_>,
) -> Result<(), StateAnchorError> {
self.revalidate()?;
self.revalidate_authorities()?;
for (name, bytes) in [
("resolv.conf", files.resolv_conf),
("hosts", files.hosts),
("hostname", files.hostname),
] {
atomic_replace(&self.runtime_files.join(name), bytes)?;
atomic_replace_at(&self.runtime_files_descriptor, name, bytes)?;
}
self.revalidate()
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)),
work_path: Some(host_path(&self.work)),
merged_path: Some(host_path(&self.merged)),
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 {
pub struct StateAnchorSandboxView<'layout, 'configuration> {
layout: &'layout StateAnchorLayout<'configuration>,
lower_descriptor: OwnedFd,
merged_descriptor: OwnedFd,
lower: PathBuf,
lower_identity: (u64, u64),
merged: PathBuf,
@ -225,25 +302,34 @@ pub struct StateAnchorSandboxView {
persistent_destinations: Vec<PathBuf>,
}
impl StateAnchorSandboxView {
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: &StateAnchorLayout,
layout: &'layout StateAnchorLayout<'configuration>,
artifact: &PreparedArtifact,
mount: MountIdentity,
) -> Result<Self, StateAnchorError> {
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 {
lower: path_from_host(&artifact.path),
layout,
lower: descriptor_path(&lower_descriptor),
lower_identity: (artifact.device.get(), artifact.inode.get()),
merged: layout.merged.clone(),
merged: descriptor_path(&merged_descriptor),
mount,
runtime_files: layout.runtime_files.clone(),
runtime_files: descriptor_path(&layout.runtime_files_descriptor),
persistent_destinations: layout.persistent_destinations.clone(),
lower_descriptor,
merged_descriptor,
};
layout.revalidate()?;
view.revalidate()?;
Ok(view)
}
@ -253,11 +339,16 @@ impl StateAnchorSandboxView {
/// # Errors
/// Refuses replacement or unreadable physical evidence.
pub fn revalidate(&self) -> Result<(), StateAnchorError> {
require_directory_identity(&self.lower, self.lower_identity)?;
require_mount_identity(&self.merged, self.mount)
.map_err(|_| StateAnchorError::OwnershipMismatch)?;
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 {
require_regular_file(&self.runtime_files.join(name))?;
drop(open_regular_file(
&self.layout.runtime_files_descriptor,
name,
)?);
}
Ok(())
}
@ -283,59 +374,120 @@ impl StateAnchorSandboxView {
}
}
pub struct FuseOverlayfsMount {
process: RetainedProcess,
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct VerifiedStateAnchorReadiness {
staged: StagedProcessOwnership,
mount: MountIdentity,
merged: PathBuf,
}
impl FuseOverlayfsMount {
#[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<Stage, Acknowledge>(
layout: &StateAnchorLayout,
lower: &Path,
expected_lower: (u64, u64),
expected_parent: lazaret_core::identity::PidfsProcessIdentityV1,
ownership: ProcessOwnershipContext,
pub fn acquire<Stage, Acknowledge, Ready>(
layout: &'layout StateAnchorLayout<'configuration>,
acquisition: StateAnchorAcquisition<'_>,
stage: Stage,
acknowledge: Acknowledge,
ready: Ready,
) -> Result<Self, StateAnchorError>
where
Stage: FnOnce(StagedProcessOwnership) -> io::Result<()>,
Acknowledge: FnOnce(StagedProcessOwnership) -> io::Result<ProcessOwnershipCommitAck>,
Ready: FnOnce(VerifiedStateAnchorReadiness) -> io::Result<()>,
{
if ownership.slot != HostProcessOwnershipSlot::StateAnchorDaemon {
if acquisition.ownership.slot != HostProcessOwnershipSlot::StateAnchorDaemon {
return Err(StateAnchorError::InvalidLayout(
"process slot is not State Anchor daemon",
));
}
layout.revalidate()?;
require_directory_identity(lower, expected_lower)?;
let baseline = current_mount_identity(layout.merged())?;
let options = overlay_options(lower, layout.upper(), layout.work());
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(layout.merged().as_os_str());
let process =
child_broker().acquire(expected_parent, ownership, &target, stage, acknowledge)?;
.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 = current_mount_identity(layout.merged())?;
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,
merged: layout.merged().to_owned(),
layout,
});
}
if Instant::now() >= deadline {
@ -343,11 +495,11 @@ impl FuseOverlayfsMount {
while !process.has_exited()? {
thread::yield_now();
}
if match current_mount_identity(layout.merged()) {
if match layout.current_merged_identity() {
Ok(identity) => identity.mount_id != baseline.mount_id,
Err(_) => true,
} {
let _ = rootless_unmount(layout.merged());
let _ = rootless_unmount(layout);
}
child_broker().reap(process)?;
return Err(StateAnchorError::MountTimeout);
@ -376,11 +528,13 @@ impl FuseOverlayfsMount {
/// # Errors
/// A missing, replaced, renamed, or remounted target is left untouched.
pub fn unmount(self) -> Result<(), StateAnchorError> {
require_mount_record(&self.merged, self.mount)?;
rootless_unmount(&self.merged)?;
if has_mount_record(&self.merged, self.mount)? {
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(())
}
@ -390,9 +544,11 @@ impl FuseOverlayfsMount {
/// # Errors
/// Refuses while the exact recorded mount remains or when exact reaping fails.
pub fn finish_after_external_unmount(self) -> Result<(), StateAnchorError> {
if has_mount_record(&self.merged, self.mount)? {
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(())
}
@ -403,14 +559,16 @@ impl FuseOverlayfsMount {
/// # Errors
/// A missing or mismatched mount is left untouched.
pub fn unmount_after_coordinator_loss(
merged: &Path,
layout: &StateAnchorLayout<'_>,
expected: MountIdentity,
) -> Result<(), StateAnchorError> {
require_mount_record(merged, expected)?;
rootless_unmount(merged)?;
if has_mount_record(merged, expected)? {
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(())
}
@ -418,16 +576,8 @@ pub fn unmount_after_coordinator_loss(
///
/// # Errors
/// Refuses unreadable mount information or an inaccessible merged directory.
pub fn require_mount_absent(merged: &Path) -> Result<(), StateAnchorError> {
current_mount_identity(merged)?;
let expected_path = merged.as_os_str().as_bytes();
for line in std::fs::read("/proc/self/mountinfo")?.split(|byte| *byte == b'\n') {
let fields = line.split(|byte| *byte == b' ').take(5).collect::<Vec<_>>();
if fields.len() == 5 && decode_mount_field(fields[4]) == expected_path {
return Err(StateAnchorError::OwnershipMismatch);
}
}
Ok(())
pub fn require_mount_absent(layout: &StateAnchorLayout<'_>) -> Result<(), StateAnchorError> {
layout.require_unmounted()
}
fn create_directory(parent: impl AsFd, name: &str) -> Result<(), StateAnchorError> {
@ -445,7 +595,10 @@ fn open_directory(parent: impl AsFd, path: &Path) -> Result<OwnedFd, StateAnchor
path,
OFlags::RDONLY | OFlags::DIRECTORY | OFlags::CLOEXEC | OFlags::NOFOLLOW,
Mode::empty(),
ResolveFlags::BENEATH | ResolveFlags::NO_SYMLINKS | ResolveFlags::NO_MAGICLINKS,
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)?;
@ -457,10 +610,88 @@ fn open_directory(parent: impl AsFd, path: &Path) -> Result<OwnedFd, StateAnchor
Ok(descriptor)
}
fn require_regular_file(path: &Path) -> Result<(), StateAnchorError> {
let metadata = path.symlink_metadata()?;
if metadata.file_type().is_file() {
fn open_mounted_directory(parent: impl AsFd, path: &Path) -> Result<OwnedFd, StateAnchorError> {
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<OwnedFd, StateAnchorError> {
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<OwnedFd, StateAnchorError> {
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",
@ -468,15 +699,62 @@ fn require_regular_file(path: &Path) -> Result<(), StateAnchorError> {
}
}
fn require_directory_identity(path: &Path, expected: (u64, u64)) -> Result<(), StateAnchorError> {
use std::os::unix::fs::MetadataExt;
let file = File::open(path)?;
let metadata = file.metadata()?;
if metadata.is_dir() && (metadata.dev(), metadata.ino()) == expected {
Ok(())
} else {
Err(StateAnchorError::OwnershipMismatch)
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<OwnedFd, StateAnchorError> {
dup(descriptor).map_err(io::Error::from).map_err(Into::into)
}
fn overlay_options(lower: &Path, upper: &Path, work: &Path) -> OsString {
@ -502,10 +780,12 @@ fn append_fuse_option_path(target: &mut Vec<u8>, path: &Path) {
}
#[allow(clippy::disallowed_methods, clippy::disallowed_types)]
fn rootless_unmount(path: &Path) -> Result<(), StateAnchorError> {
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)
.arg(&path)
.status()?;
if status.success() {
Ok(())
@ -516,16 +796,25 @@ fn rootless_unmount(path: &Path) -> Result<(), StateAnchorError> {
}
}
fn require_mount_record(path: &Path, expected: MountIdentity) -> Result<(), StateAnchorError> {
if has_mount_record(path, expected)? {
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(path: &Path, expected: MountIdentity) -> Result<bool, StateAnchorError> {
let expected_path = path.as_os_str().as_bytes();
fn has_mount_record(
layout: &StateAnchorLayout<'_>,
expected: MountIdentity,
) -> Result<bool, StateAnchorError> {
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::<Vec<_>>();
if fields.len() != 5 {