587 lines
20 KiB
Rust
587 lines
20 KiB
Rust
|
|
#![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())
|
||
|
|
}
|