Implement exact State Anchor mount and lease lifecycle

This commit is contained in:
ms2k 2026-07-15 16:54:03 +09:00
commit e6d6406c64
3 changed files with 1852 additions and 0 deletions

View file

@ -0,0 +1,19 @@
[package]
name = "lazaret-state-anchor"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
[dependencies]
lazaret-core = { version = "=0.0.0", path = "../lazaret-core" }
lazaret-linux = { version = "=0.0.0", path = "../lazaret-linux" }
rustix.workspace = true
[lints]
workspace = true
[[test]]
name = "state-anchor-lifecycle"
path = "tests/state-anchor-lifecycle.rs"
harness = false

View file

@ -0,0 +1,587 @@
#![doc = "Physical State Anchor layout and exact fuse-overlayfs mount lifecycle."]
use std::ffi::OsString;
use std::fmt;
use std::fs::{File, OpenOptions};
use std::io;
use std::os::fd::{AsFd, OwnedFd};
use std::os::unix::ffi::{OsStrExt, OsStringExt};
use std::path::{Path, PathBuf};
use std::thread;
use std::time::{Duration, Instant};
use lazaret_core::configuration::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::process::{
ProcessOwnershipContext, RetainedProcess, TargetCommand, child_broker,
};
use rustix::fs::{FileType, Mode, OFlags, ResolveFlags, mkdirat, openat2};
const MOUNT_TIMEOUT: Duration = Duration::from_secs(5);
const MOUNT_POLL_INTERVAL: Duration = Duration::from_millis(2);
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<io::Error> for StateAnchorError {
fn from(error: io::Error) -> Self {
Self::Io(error)
}
}
#[derive(Debug)]
pub struct StateAnchorLayout {
persistent_root: OwnedFd,
runtime_root: OwnedFd,
upper: PathBuf,
work: PathBuf,
merged: PathBuf,
runtime_files: PathBuf,
persistent_destinations: Vec<PathBuf>,
}
impl StateAnchorLayout {
/// 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> {
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"))?;
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"] {
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 runtime_files = runtime_base.join("state-anchor/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()),
}
}
rustix::fs::fsync(&runtime_anchor).map_err(io::Error::from)?;
rustix::fs::fsync(&persistent_anchor).map_err(io::Error::from)?;
let persistent_destinations = configuration
.fingerprint_record()
.persistent_dirs
.iter()
.map(|destination| PathBuf::from(destination.strip_prefix('/').unwrap_or(destination)))
.collect();
Ok(Self {
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,
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> {
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))?);
}
for name in PROTECTED_RUNTIME_FILES {
require_regular_file(&self.runtime_files.join(name))?;
}
Ok(())
}
/// 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()?;
for (name, bytes) in [
("resolv.conf", files.resolv_conf),
("hosts", files.hosts),
("hostname", files.hostname),
] {
atomic_replace(&self.runtime_files.join(name), bytes)?;
}
self.revalidate()
}
#[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)),
daemon: PersistedHostProcessOwnership::unassigned(),
mount: None,
}
}
}
#[derive(Debug)]
pub struct StateAnchorSandboxView {
lower: PathBuf,
lower_identity: (u64, u64),
merged: PathBuf,
mount: MountIdentity,
runtime_files: PathBuf,
persistent_destinations: Vec<PathBuf>,
}
impl StateAnchorSandboxView {
/// 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,
artifact: &PreparedArtifact,
mount: MountIdentity,
) -> Result<Self, StateAnchorError> {
let view = Self {
lower: path_from_host(&artifact.path),
lower_identity: (artifact.device.get(), artifact.inode.get()),
merged: layout.merged.clone(),
mount,
runtime_files: layout.runtime_files.clone(),
persistent_destinations: layout.persistent_destinations.clone(),
};
layout.revalidate()?;
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> {
require_directory_identity(&self.lower, self.lower_identity)?;
require_mount_identity(&self.merged, self.mount)
.map_err(|_| StateAnchorError::OwnershipMismatch)?;
for name in PROTECTED_RUNTIME_FILES {
require_regular_file(&self.runtime_files.join(name))?;
}
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)))
}
}
pub struct FuseOverlayfsMount {
process: RetainedProcess,
mount: MountIdentity,
merged: PathBuf,
}
impl FuseOverlayfsMount {
/// 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,
stage: Stage,
acknowledge: Acknowledge,
) -> Result<Self, StateAnchorError>
where
Stage: FnOnce(StagedProcessOwnership) -> io::Result<()>,
Acknowledge: FnOnce(StagedProcessOwnership) -> io::Result<ProcessOwnershipCommitAck>,
{
if 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 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)?;
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())?;
if observed.mount_id != baseline.mount_id {
return Ok(Self {
process,
mount: observed,
merged: layout.merged().to_owned(),
});
}
if Instant::now() >= deadline {
process.send_signal(rustix::process::Signal::KILL)?;
while !process.has_exited()? {
thread::yield_now();
}
if match current_mount_identity(layout.merged()) {
Ok(identity) => identity.mount_id != baseline.mount_id,
Err(_) => true,
} {
let _ = rootless_unmount(layout.merged());
}
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> {
require_mount_record(&self.merged, self.mount)?;
rootless_unmount(&self.merged)?;
if has_mount_record(&self.merged, self.mount)? {
return Err(StateAnchorError::OwnershipMismatch);
}
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> {
if has_mount_record(&self.merged, self.mount)? {
return Err(StateAnchorError::OwnershipMismatch);
}
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(
merged: &Path,
expected: MountIdentity,
) -> Result<(), StateAnchorError> {
require_mount_record(merged, expected)?;
rootless_unmount(merged)?;
if has_mount_record(merged, expected)? {
return Err(StateAnchorError::OwnershipMismatch);
}
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(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(())
}
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<OwnedFd, StateAnchorError> {
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,
)
.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 require_regular_file(path: &Path) -> Result<(), StateAnchorError> {
let metadata = path.symlink_metadata()?;
if metadata.file_type().is_file() {
Ok(())
} else {
Err(StateAnchorError::InvalidLayout(
"protected runtime path is not a regular file",
))
}
}
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 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<u8>, 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(path: &Path) -> Result<(), StateAnchorError> {
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(path: &Path, expected: MountIdentity) -> Result<(), StateAnchorError> {
if has_mount_record(path, 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();
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 {
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<u64> {
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<u8> {
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())
}

View file

@ -0,0 +1,242 @@
use std::collections::BTreeMap;
use std::ffi::OsString;
use std::fs;
use std::io;
use std::os::unix::ffi::OsStrExt;
use std::os::unix::fs::MetadataExt;
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use std::sync::atomic::{AtomicU64, Ordering};
use lazaret_core::configuration::{
ConfigurationLoadRequest, SandboxLayoutV1, load_trusted_configuration,
};
use lazaret_core::identity::{CoordinatorId, DomainId, GenerationId};
use lazaret_core::records::{
ArtifactState, HostProcessOwnershipSlot, PreparedArtifact, ProcessOwnershipCommitAck,
};
use lazaret_linux::process::{
ProcessOwnershipContext, current_process_identity, dispatch_private_process_role,
};
use lazaret_state_anchor::{
FuseOverlayfsMount, ProtectedRuntimeFiles, StateAnchorLayout, StateAnchorSandboxView,
unmount_after_coordinator_loss,
};
use rustix::process::Signal;
static SEQUENCE: AtomicU64 = AtomicU64::new(0);
fn main() -> ExitCode {
let arguments = std::env::args_os().skip(1).collect::<Vec<_>>();
if let Some(result) = dispatch_private_process_role(&arguments) {
return exit(result);
}
exit(run())
}
fn exit(result: io::Result<()>) -> ExitCode {
match result {
Ok(()) => ExitCode::SUCCESS,
Err(error) => {
eprintln!("State Anchor lifecycle fixture failed: {error}");
ExitCode::FAILURE
}
}
}
#[allow(clippy::too_many_lines)]
fn run() -> io::Result<()> {
match fs::metadata("/dev/fuse") {
Ok(_) => {}
Err(error) if error.kind() == io::ErrorKind::NotFound => {
eprintln!("skipped: /dev/fuse is unavailable");
return Ok(());
}
Err(error) => return Err(error),
}
let tree = TestTree::new()?;
let configuration = configuration(&tree)?;
let lower = tree.path.join("prepared-lower");
fs::create_dir_all(lower.join("usr/bin"))?;
fs::create_dir_all(lower.join("etc"))?;
fs::write(lower.join("usr/bin/base-tool"), b"base")?;
for name in ["resolv.conf", "hosts", "hostname"] {
fs::write(lower.join("etc").join(name), b"image")?;
}
let metadata = lower.metadata()?;
let artifact = PreparedArtifact {
prepared_artifact_id: lazaret_core::identity::PreparedArtifactId::new([8; 32]),
source_identity: lazaret_core::identity::SourceIdentity::new(b"fixture".to_vec()),
selected_manifest_digest: lazaret_core::identity::SelectedManifestDigest::new([9; 32]),
policy_id: lazaret_core::identity::PolicyId::new([10; 32]),
id_map_digest: lazaret_core::identity::IdMapDigest::new([11; 32]),
path: lazaret_core::identity::HostPath::new(lower.as_os_str().as_encoded_bytes().to_vec()),
device: lazaret_core::identity::DeviceId::new(metadata.dev()),
inode: lazaret_core::identity::InodeId::new(metadata.ino()),
state: ArtifactState::Available,
};
let layout = StateAnchorLayout::create(&configuration).map_err(io::Error::other)?;
let first = acquire(&layout, &artifact)?;
fs::write(
layout.merged().join("usr/bin/installed-tool"),
b"persistent",
)?;
fs::write(layout.merged().join("etc/developer.conf"), b"persistent")?;
if lower.join("usr/bin/installed-tool").exists()
|| lower.join("etc/developer.conf").exists()
|| !layout.upper().join("usr/bin/installed-tool").exists()
|| !layout.upper().join("etc/developer.conf").exists()
{
return Err(io::Error::other(
"writes did not remain upper-only over the read-only lower",
));
}
let view = StateAnchorSandboxView::from_mounted(&layout, &artifact, first.mount_identity())
.map_err(io::Error::other)?;
view.revalidate().map_err(io::Error::other)?;
layout
.replace_protected_runtime_files(&ProtectedRuntimeFiles {
resolv_conf: b"nameserver 192.0.2.53\n",
hosts: b"127.0.0.1 localhost\n",
hostname: b"lazaret\n",
})
.map_err(io::Error::other)?;
let runtime_sources = view.protected_runtime_mounts().collect::<Vec<_>>();
if runtime_sources.len() != 3
|| runtime_sources
.iter()
.any(|(source, _)| source.starts_with(&lower) || source.starts_with(layout.upper()))
{
return Err(io::Error::other(
"protected runtime files are not host-private runtime material",
));
}
if lower.join("etc/resolv.conf").read_link().is_ok()
|| fs::read(lower.join("etc/resolv.conf"))? != b"image"
|| layout.upper().join("etc/resolv.conf").exists()
|| fs::read(&runtime_sources[0].0)? != b"nameserver 192.0.2.53\n"
{
return Err(io::Error::other(
"runtime-file replacement escaped transient storage",
));
}
let wrong = lazaret_linux::filesystem::current_mount_identity(layout.upper())?;
if unmount_after_coordinator_loss(layout.merged(), wrong).is_ok() {
return Err(io::Error::other(
"ownership mismatch unmounted foreign identity",
));
}
first.unmount().map_err(io::Error::other)?;
let second = acquire(&layout, &artifact)?;
if fs::read(layout.merged().join("usr/bin/installed-tool"))? != b"persistent"
|| fs::read(layout.merged().join("etc/developer.conf"))? != b"persistent"
{
return Err(io::Error::other(
"upper state did not persist across remount",
));
}
let identity = second.mount_identity();
second.process().send_signal(Signal::KILL)?;
while !second.process().has_exited()? {
std::thread::yield_now();
}
unmount_after_coordinator_loss(layout.merged(), identity).map_err(io::Error::other)?;
second
.finish_after_external_unmount()
.map_err(io::Error::other)?;
drop(tree);
Ok(())
}
fn acquire(
layout: &StateAnchorLayout,
artifact: &PreparedArtifact,
) -> io::Result<FuseOverlayfsMount> {
let expected = std::cell::Cell::new(None);
FuseOverlayfsMount::acquire(
layout,
Path::new(std::ffi::OsStr::from_bytes(artifact.path.as_bytes())),
(artifact.device.get(), artifact.inode.get()),
current_process_identity()?,
ProcessOwnershipContext {
domain_id: DomainId::new([1; 32]),
generation_id: GenerationId::new(2),
coordinator_id: CoordinatorId::new([3; 32]),
slot: HostProcessOwnershipSlot::StateAnchorDaemon,
},
|staged| {
expected.set(Some(staged));
Ok(())
},
|staged| {
if expected.get() != Some(staged) {
return Err(io::Error::other("staged process identity changed"));
}
Ok(ProcessOwnershipCommitAck::v1(staged))
},
)
.map_err(io::Error::other)
}
struct TestTree {
path: PathBuf,
}
impl TestTree {
fn new() -> io::Result<Self> {
let path = std::env::temp_dir().join(format!(
"lazaret state-anchor,:{}-{}",
std::process::id(),
SEQUENCE.fetch_add(1, Ordering::Relaxed)
));
fs::create_dir(&path)?;
Ok(Self { path })
}
}
impl Drop for TestTree {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.path);
}
}
fn configuration(
tree: &TestTree,
) -> io::Result<lazaret_core::configuration::ResolvedDomainConfiguration> {
let configuration_directory = tree.path.join("configuration");
let workspace = tree.path.join("workspace");
let runtime = tree.path.join("runtime");
fs::create_dir(&configuration_directory)?;
fs::create_dir(&workspace)?;
fs::create_dir(&runtime)?;
let configuration_path = configuration_directory.join("lazaret.toml");
fs::write(
&configuration_path,
format!(
"schema = \"lazaret-trusted-host-config-v1\"\n[storage]\ndata_root = \"{}\"\n[profiles.default]\nimage = \"example.invalid/fixture:v1\"\npersistent_dirs = [\"/usr\", \"/etc\"]\n",
tree.path.join("data").display()
),
)?;
let layout = SandboxLayoutV1::from_production_layout([
"/workspace".to_owned(),
"/proc".to_owned(),
"/sys".to_owned(),
"/dev".to_owned(),
"/run/lazaret".to_owned(),
])
.map_err(io::Error::other)?;
load_trusted_configuration(ConfigurationLoadRequest {
explicit_configuration_path: Some(configuration_path),
selected_profile: "default".to_owned(),
workspace,
home: tree.path.join("home"),
xdg_config_home: None,
xdg_data_home: None,
xdg_runtime_dir: runtime,
environment: BTreeMap::<String, OsString>::new(),
sandbox_layout: layout,
})
.map_err(io::Error::other)
}