Require fresh State Anchor absence before lease release
This commit is contained in:
parent
4dd2b150d2
commit
3b76b59bdf
9 changed files with 543 additions and 194 deletions
|
|
@ -26,7 +26,10 @@ use crate::lifecycle::{
|
||||||
SessionLifecycleAdapter,
|
SessionLifecycleAdapter,
|
||||||
};
|
};
|
||||||
use crate::network::NetworkCoordinator;
|
use crate::network::NetworkCoordinator;
|
||||||
use crate::runtime::{AwaitingAttachedRunsc, CommittedSandboxRuntime, SandboxCoordinator};
|
use crate::runtime::{
|
||||||
|
AwaitingAttachedRunsc, CommittedSandboxRuntime, SandboxCoordinator,
|
||||||
|
StateAnchorAbsenceObservation,
|
||||||
|
};
|
||||||
use crate::state_anchor::{StateAnchorCoordinator, StateAnchorReleaseAuthority};
|
use crate::state_anchor::{StateAnchorCoordinator, StateAnchorReleaseAuthority};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
|
@ -80,6 +83,7 @@ pub struct ProductionLifecycleAdapter<'a> {
|
||||||
runtime: Option<CommittedSandboxRuntime>,
|
runtime: Option<CommittedSandboxRuntime>,
|
||||||
state_anchor_mount: Option<MountIdentity>,
|
state_anchor_mount: Option<MountIdentity>,
|
||||||
state_anchor_release: Option<StateAnchorReleaseAuthority>,
|
state_anchor_release: Option<StateAnchorReleaseAuthority>,
|
||||||
|
state_anchor_absence: Option<StateAnchorAbsenceObservation>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type ProductionHostCoordinatorLifecycle<'store, 'resources> = HostCoordinatorLifecycle<
|
pub type ProductionHostCoordinatorLifecycle<'store, 'resources> = HostCoordinatorLifecycle<
|
||||||
|
|
@ -175,6 +179,7 @@ impl<'a> ProductionLifecycleAdapter<'a> {
|
||||||
runtime: None,
|
runtime: None,
|
||||||
state_anchor_mount: None,
|
state_anchor_mount: None,
|
||||||
state_anchor_release: None,
|
state_anchor_release: None,
|
||||||
|
state_anchor_absence: None,
|
||||||
},
|
},
|
||||||
ProductionSessionRegistry { registry },
|
ProductionSessionRegistry { registry },
|
||||||
)
|
)
|
||||||
|
|
@ -409,18 +414,26 @@ impl<'a> ProductionLifecycleAdapter<'a> {
|
||||||
)
|
)
|
||||||
.map_err(failure)?,
|
.map_err(failure)?,
|
||||||
);
|
);
|
||||||
self.runtime
|
let runtime = self
|
||||||
|
.runtime
|
||||||
.as_mut()
|
.as_mut()
|
||||||
.ok_or_else(|| ProductionLifecycleError("committed runtime is absent".into()))?
|
.ok_or_else(|| ProductionLifecycleError("committed runtime is absent".into()))?;
|
||||||
.unmount_state_anchor(
|
let release = self
|
||||||
self.state_anchor_release
|
.state_anchor_release
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| ProductionLifecycleError("State Anchor release is absent".into()))?;
|
||||||
ProductionLifecycleError("State Anchor release is absent".into())
|
runtime
|
||||||
})?
|
.unmount_state_anchor(release.mount())
|
||||||
.mount(),
|
|
||||||
)
|
|
||||||
.map_err(failure)?;
|
.map_err(failure)?;
|
||||||
|
self.state_anchor_absence = Some(
|
||||||
|
runtime
|
||||||
|
.observe_state_anchor_absence(
|
||||||
|
release.user_namespace(),
|
||||||
|
release.mount_namespace(),
|
||||||
|
release.mount(),
|
||||||
|
)
|
||||||
|
.map_err(failure)?,
|
||||||
|
);
|
||||||
self.state_anchor_mount = None;
|
self.state_anchor_mount = None;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -428,11 +441,44 @@ impl<'a> ProductionLifecycleAdapter<'a> {
|
||||||
fn release_prepared_lower(&mut self) -> Result<(), ProductionLifecycleError> {
|
fn release_prepared_lower(&mut self) -> Result<(), ProductionLifecycleError> {
|
||||||
let release = self
|
let release = self
|
||||||
.state_anchor_release
|
.state_anchor_release
|
||||||
.take()
|
.as_ref()
|
||||||
.ok_or_else(|| ProductionLifecycleError("State Anchor release is absent".into()))?;
|
.ok_or_else(|| ProductionLifecycleError("State Anchor release is absent".into()))?;
|
||||||
self.state_anchor()
|
let first_observation = self.state_anchor_absence.ok_or_else(|| {
|
||||||
.complete_release_after_unmount(&release, self.plan.state_anchor_layout)
|
ProductionLifecycleError("State Anchor absence evidence is absent".into())
|
||||||
.map_err(failure)
|
})?;
|
||||||
|
let runtime = self
|
||||||
|
.runtime
|
||||||
|
.as_mut()
|
||||||
|
.ok_or_else(|| ProductionLifecycleError("committed runtime is absent".into()))?;
|
||||||
|
let coordinator = StateAnchorCoordinator::new(&self.plan.store, self.plan.catalog);
|
||||||
|
coordinator
|
||||||
|
.complete_release_after_unmount(
|
||||||
|
release,
|
||||||
|
self.plan.state_anchor_layout,
|
||||||
|
first_observation,
|
||||||
|
|| {
|
||||||
|
runtime
|
||||||
|
.observe_state_anchor_absence(
|
||||||
|
release.user_namespace(),
|
||||||
|
release.mount_namespace(),
|
||||||
|
release.mount(),
|
||||||
|
)
|
||||||
|
.map_err(|error| {
|
||||||
|
crate::state_anchor::StateAnchorCoordinatorError::InvalidState(
|
||||||
|
match error {
|
||||||
|
crate::runtime::SandboxCoordinatorError::InvalidState(
|
||||||
|
detail,
|
||||||
|
) => detail,
|
||||||
|
_ => "Runtime Supervisor absence observation failed",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.map_err(failure)?;
|
||||||
|
self.state_anchor_release = None;
|
||||||
|
self.state_anchor_absence = None;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn graceful_exit(&mut self) -> Result<(), ProductionLifecycleError> {
|
fn graceful_exit(&mut self) -> Result<(), ProductionLifecycleError> {
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,14 @@ use crate::network::{NetworkCoordinator, NetworkCoordinatorError};
|
||||||
|
|
||||||
const SUPERVISOR_EXIT_TIMEOUT: Duration = Duration::from_secs(5);
|
const SUPERVISOR_EXIT_TIMEOUT: Duration = Duration::from_secs(5);
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub struct StateAnchorAbsenceObservation {
|
||||||
|
pub user_namespace: NamespaceIdentity,
|
||||||
|
pub mount_namespace: NamespaceIdentity,
|
||||||
|
pub mount: MountIdentity,
|
||||||
|
pub observed: MountIdentity,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum SandboxCoordinatorError {
|
pub enum SandboxCoordinatorError {
|
||||||
Store(DomainStoreError),
|
Store(DomainStoreError),
|
||||||
|
|
@ -967,6 +975,45 @@ impl CommittedSandboxRuntime {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Requests a fresh namespace-local observation of exact State Anchor absence.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Refuses an RS refusal, echoed mount, mismatched authority, or channel failure.
|
||||||
|
pub fn observe_state_anchor_absence(
|
||||||
|
&mut self,
|
||||||
|
user_namespace: NamespaceIdentity,
|
||||||
|
mount_namespace: NamespaceIdentity,
|
||||||
|
mount: MountIdentity,
|
||||||
|
) -> Result<StateAnchorAbsenceObservation, SandboxCoordinatorError> {
|
||||||
|
self.control
|
||||||
|
.observe_state_anchor_absence(user_namespace, mount_namespace, mount)?;
|
||||||
|
match self.control.receive()? {
|
||||||
|
SupervisorEvent::StateAnchorAbsenceObserved {
|
||||||
|
user_namespace: observed_user_namespace,
|
||||||
|
mount_namespace: observed_mount_namespace,
|
||||||
|
mount: observed_mount,
|
||||||
|
observed,
|
||||||
|
} if observed_user_namespace == user_namespace
|
||||||
|
&& observed_mount_namespace == mount_namespace
|
||||||
|
&& observed_mount == mount
|
||||||
|
&& observed != mount =>
|
||||||
|
{
|
||||||
|
Ok(StateAnchorAbsenceObservation {
|
||||||
|
user_namespace: observed_user_namespace,
|
||||||
|
mount_namespace: observed_mount_namespace,
|
||||||
|
mount: observed_mount,
|
||||||
|
observed,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
SupervisorEvent::Refused(detail) => {
|
||||||
|
Err(SandboxCoordinatorError::Io(io::Error::other(detail)))
|
||||||
|
}
|
||||||
|
_ => Err(SandboxCoordinatorError::InvalidState(
|
||||||
|
"Runtime Supervisor did not provide exact State Anchor absence evidence",
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Gracefully exits and reaps the resource-empty exact Runtime Supervisor.
|
/// Gracefully exits and reaps the resource-empty exact Runtime Supervisor.
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ use lazaret_state_anchor::{StateAnchorError, StateAnchorLayout, VerifiedStateAnc
|
||||||
use lazaret_storage_catalog::{Catalog, CatalogError};
|
use lazaret_storage_catalog::{Catalog, CatalogError};
|
||||||
|
|
||||||
use crate::domain_store::{DomainStoreError, DomainStoreMutation, HostCoordinatorDomainStore};
|
use crate::domain_store::{DomainStoreError, DomainStoreMutation, HostCoordinatorDomainStore};
|
||||||
|
use crate::runtime::StateAnchorAbsenceObservation;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum StateAnchorCoordinatorError {
|
pub enum StateAnchorCoordinatorError {
|
||||||
|
|
@ -367,11 +368,17 @@ impl<'a> StateAnchorCoordinator<'a> {
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// Refuses a stale authority, any remaining mount, or release-evidence mismatch.
|
/// Refuses a stale authority, any remaining mount, or release-evidence mismatch.
|
||||||
pub fn complete_release_after_unmount(
|
pub fn complete_release_after_unmount<Observe>(
|
||||||
&self,
|
&self,
|
||||||
authority: &StateAnchorReleaseAuthority,
|
authority: &StateAnchorReleaseAuthority,
|
||||||
layout: &StateAnchorLayout<'_>,
|
layout: &StateAnchorLayout<'_>,
|
||||||
) -> Result<(), StateAnchorCoordinatorError> {
|
first_observation: StateAnchorAbsenceObservation,
|
||||||
|
mut observe_again: Observe,
|
||||||
|
) -> Result<(), StateAnchorCoordinatorError>
|
||||||
|
where
|
||||||
|
Observe: FnMut() -> Result<StateAnchorAbsenceObservation, StateAnchorCoordinatorError>,
|
||||||
|
{
|
||||||
|
require_absence_observation(authority, first_observation)?;
|
||||||
let expected_layout = layout.ownership_intent(authority.lease.prepared_artifact_id);
|
let expected_layout = layout.ownership_intent(authority.lease.prepared_artifact_id);
|
||||||
let expected_merged = expected_layout.merged_path.as_ref().ok_or(
|
let expected_merged = expected_layout.merged_path.as_ref().ok_or(
|
||||||
StateAnchorCoordinatorError::InvalidState("State Anchor merged path is absent"),
|
StateAnchorCoordinatorError::InvalidState("State Anchor merged path is absent"),
|
||||||
|
|
@ -407,7 +414,7 @@ impl<'a> StateAnchorCoordinator<'a> {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.release_catalog_lease_if_present(authority)?;
|
self.release_catalog_lease_if_present(authority, &mut observe_again)?;
|
||||||
self.domain_store
|
self.domain_store
|
||||||
.apply(&[DomainStoreMutation::CleanParticipantAfterAbsence(
|
.apply(&[DomainStoreMutation::CleanParticipantAfterAbsence(
|
||||||
clean_participant(authority.generation_id, ParticipantRole::PreparedLower)?,
|
clean_participant(authority.generation_id, ParticipantRole::PreparedLower)?,
|
||||||
|
|
@ -528,10 +535,14 @@ impl<'a> StateAnchorCoordinator<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn release_catalog_lease_if_present(
|
fn release_catalog_lease_if_present<Observe>(
|
||||||
&self,
|
&self,
|
||||||
authority: &StateAnchorReleaseAuthority,
|
authority: &StateAnchorReleaseAuthority,
|
||||||
) -> Result<(), StateAnchorCoordinatorError> {
|
observe_again: &mut Observe,
|
||||||
|
) -> Result<(), StateAnchorCoordinatorError>
|
||||||
|
where
|
||||||
|
Observe: FnMut() -> Result<StateAnchorAbsenceObservation, StateAnchorCoordinatorError>,
|
||||||
|
{
|
||||||
let snapshot = self.catalog.snapshot()?;
|
let snapshot = self.catalog.snapshot()?;
|
||||||
let generation_lease = snapshot.artifact_leases.iter().find(|lease| {
|
let generation_lease = snapshot.artifact_leases.iter().find(|lease| {
|
||||||
lease.domain_id == authority.lease.domain_id
|
lease.domain_id == authority.lease.domain_id
|
||||||
|
|
@ -570,12 +581,33 @@ impl<'a> StateAnchorCoordinator<'a> {
|
||||||
"Domain release evidence is not committed",
|
"Domain release evidence is not committed",
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
let observation =
|
||||||
|
observe_again().map_err(|error| CatalogError::Io(io::Error::other(error)))?;
|
||||||
|
require_absence_observation(authority, observation)
|
||||||
|
.map_err(|error| CatalogError::Io(io::Error::other(error)))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
})?;
|
})?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn require_absence_observation(
|
||||||
|
authority: &StateAnchorReleaseAuthority,
|
||||||
|
observation: StateAnchorAbsenceObservation,
|
||||||
|
) -> Result<(), StateAnchorCoordinatorError> {
|
||||||
|
if observation.user_namespace == authority.user_namespace
|
||||||
|
&& observation.mount_namespace == authority.mount_namespace
|
||||||
|
&& observation.mount == authority.mount
|
||||||
|
&& observation.observed != authority.mount
|
||||||
|
{
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(StateAnchorCoordinatorError::InvalidState(
|
||||||
|
"State Anchor absence evidence mismatches release authority",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn release_authority(
|
fn release_authority(
|
||||||
domain: &lazaret_core::records::Domain,
|
domain: &lazaret_core::records::Domain,
|
||||||
anchor: &Participant,
|
anchor: &Participant,
|
||||||
|
|
|
||||||
|
|
@ -182,6 +182,44 @@ fn durable_phases_resume_release_without_collapsing_lease_order() {
|
||||||
ParticipantState::ReleaseIntent,
|
ParticipantState::ReleaseIntent,
|
||||||
);
|
);
|
||||||
assert_eq!(catalog.snapshot().unwrap().artifact_leases, vec![lease]);
|
assert_eq!(catalog.snapshot().unwrap().artifact_leases, vec![lease]);
|
||||||
|
let valid = absence_observation(&authority);
|
||||||
|
let invalid_observations = [
|
||||||
|
StateAnchorAbsenceObservation {
|
||||||
|
observed: authority.mount(),
|
||||||
|
..valid
|
||||||
|
},
|
||||||
|
StateAnchorAbsenceObservation {
|
||||||
|
user_namespace: NamespaceIdentity {
|
||||||
|
device: DeviceId::new(193),
|
||||||
|
inode: InodeId::new(194),
|
||||||
|
},
|
||||||
|
..valid
|
||||||
|
},
|
||||||
|
StateAnchorAbsenceObservation {
|
||||||
|
mount_namespace: NamespaceIdentity {
|
||||||
|
device: DeviceId::new(195),
|
||||||
|
inode: InodeId::new(196),
|
||||||
|
},
|
||||||
|
..valid
|
||||||
|
},
|
||||||
|
StateAnchorAbsenceObservation {
|
||||||
|
mount: valid.observed,
|
||||||
|
..valid
|
||||||
|
},
|
||||||
|
];
|
||||||
|
for invalid in invalid_observations {
|
||||||
|
assert!(
|
||||||
|
coordinator
|
||||||
|
.complete_release_after_unmount(&authority, &layout, invalid, || Ok(valid))
|
||||||
|
.is_err()
|
||||||
|
);
|
||||||
|
assert_states(
|
||||||
|
&domain_store,
|
||||||
|
ParticipantState::LeaseHeld,
|
||||||
|
ParticipantState::ReleaseIntent,
|
||||||
|
);
|
||||||
|
assert_eq!(catalog.snapshot().unwrap().artifact_leases, vec![lease]);
|
||||||
|
}
|
||||||
let snapshot = domain_store.snapshot().unwrap();
|
let snapshot = domain_store.snapshot().unwrap();
|
||||||
let prepared = role(
|
let prepared = role(
|
||||||
&snapshot.participants,
|
&snapshot.participants,
|
||||||
|
|
@ -197,9 +235,42 @@ fn durable_phases_resume_release_without_collapsing_lease_order() {
|
||||||
DomainStoreMutation::PutParticipant(prepared_release_intent(prepared).unwrap()),
|
DomainStoreMutation::PutParticipant(prepared_release_intent(prepared).unwrap()),
|
||||||
])
|
])
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
for invalid in invalid_observations {
|
||||||
|
assert!(
|
||||||
coordinator
|
coordinator
|
||||||
.complete_release_after_unmount(&authority, &layout)
|
.complete_release_after_unmount(&authority, &layout, valid, || Ok(invalid))
|
||||||
|
.is_err()
|
||||||
|
);
|
||||||
|
assert_eq!(catalog.snapshot().unwrap().artifact_leases, vec![lease]);
|
||||||
|
}
|
||||||
|
assert!(
|
||||||
|
coordinator
|
||||||
|
.complete_release_after_unmount(&authority, &layout, valid, || {
|
||||||
|
Err(StateAnchorCoordinatorError::InvalidState(
|
||||||
|
"State Anchor absence evidence is unreadable",
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.is_err()
|
||||||
|
);
|
||||||
|
assert_eq!(catalog.snapshot().unwrap().artifact_leases, vec![lease]);
|
||||||
|
assert!(
|
||||||
|
coordinator
|
||||||
|
.complete_release_after_unmount(&authority, &layout, valid, || {
|
||||||
|
Err(StateAnchorCoordinatorError::InvalidState(
|
||||||
|
"second State Anchor observation was skipped",
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.is_err()
|
||||||
|
);
|
||||||
|
assert_eq!(catalog.snapshot().unwrap().artifact_leases, vec![lease]);
|
||||||
|
let mut observations = 0;
|
||||||
|
coordinator
|
||||||
|
.complete_release_after_unmount(&authority, &layout, valid, || {
|
||||||
|
observations += 1;
|
||||||
|
Ok(valid)
|
||||||
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
assert_eq!(observations, 1);
|
||||||
assert_states(
|
assert_states(
|
||||||
&domain_store,
|
&domain_store,
|
||||||
ParticipantState::Clean,
|
ParticipantState::Clean,
|
||||||
|
|
@ -208,6 +279,19 @@ fn durable_phases_resume_release_without_collapsing_lease_order() {
|
||||||
assert!(catalog.snapshot().unwrap().artifact_leases.is_empty());
|
assert!(catalog.snapshot().unwrap().artifact_leases.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn absence_observation(authority: &StateAnchorReleaseAuthority) -> StateAnchorAbsenceObservation {
|
||||||
|
StateAnchorAbsenceObservation {
|
||||||
|
user_namespace: authority.user_namespace(),
|
||||||
|
mount_namespace: authority.mount_namespace(),
|
||||||
|
mount: authority.mount(),
|
||||||
|
observed: MountIdentity {
|
||||||
|
mount_id: MountId::new(authority.mount().mount_id.get() + 1),
|
||||||
|
device: authority.mount().device,
|
||||||
|
inode: authority.mount().inode,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn acquisition_resumes_after_prepared_intent_reopen() {
|
fn acquisition_resumes_after_prepared_intent_reopen() {
|
||||||
let fixture = AcquisitionFixture::new(17);
|
let fixture = AcquisitionFixture::new(17);
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ use std::time::{Duration, Instant};
|
||||||
use lazaret_core::codec::{decode, encode};
|
use lazaret_core::codec::{decode, encode};
|
||||||
use lazaret_core::identity::{
|
use lazaret_core::identity::{
|
||||||
ConfigurationFingerprint, CoordinatorId, DerivedPolicyDigest, DomainId, GenerationId,
|
ConfigurationFingerprint, CoordinatorId, DerivedPolicyDigest, DomainId, GenerationId,
|
||||||
NetworkSidecarId,
|
MountIdentity, NamespaceIdentity, NetworkSidecarId,
|
||||||
};
|
};
|
||||||
use lazaret_core::network::CanonicalDerivedNetworkPolicy;
|
use lazaret_core::network::CanonicalDerivedNetworkPolicy;
|
||||||
use lazaret_core::records::{
|
use lazaret_core::records::{
|
||||||
|
|
@ -23,7 +23,8 @@ use lazaret_linux::process::{
|
||||||
child_broker, current_process_identity,
|
child_broker, current_process_identity,
|
||||||
};
|
};
|
||||||
use lazaret_state_anchor::{
|
use lazaret_state_anchor::{
|
||||||
RuntimeStateAnchorAuthority, RuntimeStateAnchorMount, StateAnchorLaunchAuthority,
|
RuntimeStateAnchorAbsenceAuthority, RuntimeStateAnchorAuthority, RuntimeStateAnchorInherited,
|
||||||
|
RuntimeStateAnchorMount, StateAnchorLaunchAuthority,
|
||||||
};
|
};
|
||||||
use rustix::event::{PollFd, PollFlags, Timespec, poll};
|
use rustix::event::{PollFd, PollFlags, Timespec, poll};
|
||||||
use rustix::fs::{MemfdFlags, memfd_create};
|
use rustix::fs::{MemfdFlags, memfd_create};
|
||||||
|
|
@ -86,6 +87,12 @@ pub enum SupervisorEvent {
|
||||||
StateAnchorUnmounted {
|
StateAnchorUnmounted {
|
||||||
mount: lazaret_core::identity::MountIdentity,
|
mount: lazaret_core::identity::MountIdentity,
|
||||||
},
|
},
|
||||||
|
StateAnchorAbsenceObserved {
|
||||||
|
user_namespace: NamespaceIdentity,
|
||||||
|
mount_namespace: NamespaceIdentity,
|
||||||
|
mount: MountIdentity,
|
||||||
|
observed: MountIdentity,
|
||||||
|
},
|
||||||
Exited,
|
Exited,
|
||||||
AttachedRunscExited,
|
AttachedRunscExited,
|
||||||
Stopped,
|
Stopped,
|
||||||
|
|
@ -106,6 +113,11 @@ enum CoordinatorMessage {
|
||||||
StopSandbox,
|
StopSandbox,
|
||||||
StopSidecar,
|
StopSidecar,
|
||||||
UnmountStateAnchor,
|
UnmountStateAnchor,
|
||||||
|
ObserveStateAnchorAbsence {
|
||||||
|
user_namespace: NamespaceIdentity,
|
||||||
|
mount_namespace: NamespaceIdentity,
|
||||||
|
mount: MountIdentity,
|
||||||
|
},
|
||||||
Exit,
|
Exit,
|
||||||
Stop,
|
Stop,
|
||||||
}
|
}
|
||||||
|
|
@ -225,6 +237,24 @@ impl CoordinatorChannel {
|
||||||
send_message(&self.descriptor, &CoordinatorMessage::UnmountStateAnchor)
|
send_message(&self.descriptor, &CoordinatorMessage::UnmountStateAnchor)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// # Errors
|
||||||
|
/// Returns an error when exact namespace-local absence observation cannot be requested.
|
||||||
|
pub fn observe_state_anchor_absence(
|
||||||
|
&self,
|
||||||
|
user_namespace: NamespaceIdentity,
|
||||||
|
mount_namespace: NamespaceIdentity,
|
||||||
|
mount: MountIdentity,
|
||||||
|
) -> io::Result<()> {
|
||||||
|
send_message(
|
||||||
|
&self.descriptor,
|
||||||
|
&CoordinatorMessage::ObserveStateAnchorAbsence {
|
||||||
|
user_namespace,
|
||||||
|
mount_namespace,
|
||||||
|
mount,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// Returns an error when graceful Runtime Supervisor exit cannot be requested.
|
/// Returns an error when graceful Runtime Supervisor exit cannot be requested.
|
||||||
pub fn exit(&self) -> io::Result<()> {
|
pub fn exit(&self) -> io::Result<()> {
|
||||||
|
|
@ -657,8 +687,19 @@ fn run_supervisor(arguments: &RuntimeArguments) -> io::Result<()> {
|
||||||
let control = std::io::stdout();
|
let control = std::io::stdout();
|
||||||
let parent = current_process_identity()?;
|
let parent = current_process_identity()?;
|
||||||
let mut state_anchor = prepare_state_anchor(arguments, parent, namespaces, &control)?;
|
let mut state_anchor = prepare_state_anchor(arguments, parent, namespaces, &control)?;
|
||||||
let result = run_supervisor_with_state(arguments, parent, &control, &mut state_anchor);
|
let mut state_anchor_absence = None;
|
||||||
let cleanup = state_anchor.take().map(RuntimeStateAnchorMount::unmount);
|
let result = run_supervisor_with_state(
|
||||||
|
arguments,
|
||||||
|
parent,
|
||||||
|
&control,
|
||||||
|
namespaces,
|
||||||
|
&mut state_anchor,
|
||||||
|
&mut state_anchor_absence,
|
||||||
|
);
|
||||||
|
let cleanup = state_anchor
|
||||||
|
.take()
|
||||||
|
.map(RuntimeStateAnchorMount::unmount)
|
||||||
|
.map(|result| result.map(drop));
|
||||||
match (result, cleanup) {
|
match (result, cleanup) {
|
||||||
(Err(error), Some(Err(cleanup))) => Err(io::Error::new(
|
(Err(error), Some(Err(cleanup))) => Err(io::Error::new(
|
||||||
error.kind(),
|
error.kind(),
|
||||||
|
|
@ -688,17 +729,17 @@ fn prepare_state_anchor(
|
||||||
"Runtime Supervisor received no State Anchor mount command",
|
"Runtime Supervisor received no State Anchor mount command",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
let authority = RuntimeStateAnchorAuthority::from_inherited(
|
let authority = RuntimeStateAnchorAuthority::from_inherited(RuntimeStateAnchorInherited {
|
||||||
state.lower_fd,
|
lower_fd: state.lower_fd,
|
||||||
&state.lower_label,
|
lower_label: &state.lower_label,
|
||||||
state.persistent_root_fd,
|
persistent_root_fd: state.persistent_root_fd,
|
||||||
state.persistent_anchor_fd,
|
persistent_anchor_fd: state.persistent_anchor_fd,
|
||||||
&state.persistent_anchor_label,
|
persistent_anchor_label: &state.persistent_anchor_label,
|
||||||
state.runtime_root_fd,
|
runtime_root_fd: state.runtime_root_fd,
|
||||||
state.runtime_anchor_fd,
|
runtime_anchor_fd: state.runtime_anchor_fd,
|
||||||
&state.runtime_anchor_label,
|
runtime_anchor_label: &state.runtime_anchor_label,
|
||||||
state.expected_lower,
|
expected_lower: state.expected_lower,
|
||||||
)
|
})
|
||||||
.map_err(io::Error::other)?;
|
.map_err(io::Error::other)?;
|
||||||
let ownership = ProcessOwnershipContext {
|
let ownership = ProcessOwnershipContext {
|
||||||
slot: HostProcessOwnershipSlot::StateAnchorDaemon,
|
slot: HostProcessOwnershipSlot::StateAnchorDaemon,
|
||||||
|
|
@ -742,7 +783,9 @@ fn run_supervisor_with_state(
|
||||||
arguments: &RuntimeArguments,
|
arguments: &RuntimeArguments,
|
||||||
parent: lazaret_core::identity::PidfsProcessIdentityV1,
|
parent: lazaret_core::identity::PidfsProcessIdentityV1,
|
||||||
control: &std::io::Stdout,
|
control: &std::io::Stdout,
|
||||||
|
namespaces: (NamespaceIdentity, NamespaceIdentity),
|
||||||
state_anchor: &mut Option<RuntimeStateAnchorMount>,
|
state_anchor: &mut Option<RuntimeStateAnchorMount>,
|
||||||
|
state_anchor_absence: &mut Option<RuntimeStateAnchorAbsenceAuthority>,
|
||||||
) -> io::Result<()> {
|
) -> io::Result<()> {
|
||||||
if arguments.state_anchor.is_some() {
|
if arguments.state_anchor.is_some() {
|
||||||
let message: CoordinatorMessage = receive_message(control)?;
|
let message: CoordinatorMessage = receive_message(control)?;
|
||||||
|
|
@ -827,7 +870,15 @@ fn run_supervisor_with_state(
|
||||||
io::Error::other("sandbox commit acknowledgment was not received"),
|
io::Error::other("sandbox commit acknowledgment was not received"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
supervise(control, process, network, state_anchor, &delete)
|
supervise(
|
||||||
|
control,
|
||||||
|
process,
|
||||||
|
network,
|
||||||
|
namespaces,
|
||||||
|
state_anchor,
|
||||||
|
state_anchor_absence,
|
||||||
|
&delete,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prepare_runtime_network(
|
fn prepare_runtime_network(
|
||||||
|
|
@ -983,10 +1034,17 @@ fn supervise(
|
||||||
control: &std::io::Stdout,
|
control: &std::io::Stdout,
|
||||||
process: RetainedProcess,
|
process: RetainedProcess,
|
||||||
mut network: Option<PreparedNetwork>,
|
mut network: Option<PreparedNetwork>,
|
||||||
|
namespaces: (NamespaceIdentity, NamespaceIdentity),
|
||||||
state_anchor: &mut Option<RuntimeStateAnchorMount>,
|
state_anchor: &mut Option<RuntimeStateAnchorMount>,
|
||||||
|
state_anchor_absence: &mut Option<RuntimeStateAnchorAbsenceAuthority>,
|
||||||
delete: &TargetCommand,
|
delete: &TargetCommand,
|
||||||
) -> io::Result<()> {
|
) -> io::Result<()> {
|
||||||
let mut process = Some(process);
|
let mut process = Some(process);
|
||||||
|
let mut state_anchor = StateAnchorSupervision {
|
||||||
|
namespaces,
|
||||||
|
mount: state_anchor,
|
||||||
|
absence: state_anchor_absence,
|
||||||
|
};
|
||||||
loop {
|
loop {
|
||||||
let exited = match process
|
let exited = match process
|
||||||
.as_ref()
|
.as_ref()
|
||||||
|
|
@ -1059,7 +1117,7 @@ fn supervise(
|
||||||
&message,
|
&message,
|
||||||
&mut process,
|
&mut process,
|
||||||
&mut network,
|
&mut network,
|
||||||
state_anchor,
|
&mut state_anchor,
|
||||||
delete,
|
delete,
|
||||||
) {
|
) {
|
||||||
Ok(true) => return Ok(()),
|
Ok(true) => return Ok(()),
|
||||||
|
|
@ -1070,12 +1128,18 @@ fn supervise(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct StateAnchorSupervision<'a> {
|
||||||
|
namespaces: (NamespaceIdentity, NamespaceIdentity),
|
||||||
|
mount: &'a mut Option<RuntimeStateAnchorMount>,
|
||||||
|
absence: &'a mut Option<RuntimeStateAnchorAbsenceAuthority>,
|
||||||
|
}
|
||||||
|
|
||||||
fn handle_supervisor_message(
|
fn handle_supervisor_message(
|
||||||
control: &std::io::Stdout,
|
control: &std::io::Stdout,
|
||||||
message: &CoordinatorMessage,
|
message: &CoordinatorMessage,
|
||||||
process: &mut Option<RetainedProcess>,
|
process: &mut Option<RetainedProcess>,
|
||||||
network: &mut Option<PreparedNetwork>,
|
network: &mut Option<PreparedNetwork>,
|
||||||
state_anchor: &mut Option<RuntimeStateAnchorMount>,
|
state_anchor: &mut StateAnchorSupervision<'_>,
|
||||||
delete: &TargetCommand,
|
delete: &TargetCommand,
|
||||||
) -> io::Result<bool> {
|
) -> io::Result<bool> {
|
||||||
match message {
|
match message {
|
||||||
|
|
@ -1102,25 +1166,17 @@ fn handle_supervisor_message(
|
||||||
stop_network(network.take())?;
|
stop_network(network.take())?;
|
||||||
send_message(control, &SupervisorEvent::SidecarStopped)?;
|
send_message(control, &SupervisorEvent::SidecarStopped)?;
|
||||||
}
|
}
|
||||||
CoordinatorMessage::UnmountStateAnchor => {
|
CoordinatorMessage::UnmountStateAnchor
|
||||||
if process.is_some() || network.is_some() {
|
| CoordinatorMessage::ObserveStateAnchorAbsence { .. } => {
|
||||||
return Err(io::Error::other(
|
handle_state_anchor_message(
|
||||||
"State Anchor unmount preceded runtime resource stop",
|
control,
|
||||||
));
|
message,
|
||||||
}
|
process.is_some() || network.is_some(),
|
||||||
let mount = state_anchor
|
state_anchor,
|
||||||
.as_ref()
|
)?;
|
||||||
.ok_or_else(|| io::Error::other("State Anchor is absent"))?
|
|
||||||
.mount_identity();
|
|
||||||
state_anchor
|
|
||||||
.take()
|
|
||||||
.ok_or_else(|| io::Error::other("State Anchor is absent"))?
|
|
||||||
.unmount()
|
|
||||||
.map_err(io::Error::other)?;
|
|
||||||
send_message(control, &SupervisorEvent::StateAnchorUnmounted { mount })?;
|
|
||||||
}
|
}
|
||||||
CoordinatorMessage::Exit => {
|
CoordinatorMessage::Exit => {
|
||||||
if process.is_some() || network.is_some() || state_anchor.is_some() {
|
if process.is_some() || network.is_some() || state_anchor.mount.is_some() {
|
||||||
return Err(io::Error::other(
|
return Err(io::Error::other(
|
||||||
"Runtime Supervisor exit preceded resource cleanup",
|
"Runtime Supervisor exit preceded resource cleanup",
|
||||||
));
|
));
|
||||||
|
|
@ -1150,6 +1206,70 @@ fn handle_supervisor_message(
|
||||||
Ok(false)
|
Ok(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn handle_state_anchor_message(
|
||||||
|
control: &std::io::Stdout,
|
||||||
|
message: &CoordinatorMessage,
|
||||||
|
runtime_resources_present: bool,
|
||||||
|
state: &mut StateAnchorSupervision<'_>,
|
||||||
|
) -> io::Result<()> {
|
||||||
|
match message {
|
||||||
|
CoordinatorMessage::UnmountStateAnchor => {
|
||||||
|
if runtime_resources_present {
|
||||||
|
return Err(io::Error::other(
|
||||||
|
"State Anchor unmount preceded runtime resource stop",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let mount = state
|
||||||
|
.mount
|
||||||
|
.as_ref()
|
||||||
|
.ok_or_else(|| io::Error::other("State Anchor is absent"))?
|
||||||
|
.mount_identity();
|
||||||
|
let absence = state
|
||||||
|
.mount
|
||||||
|
.take()
|
||||||
|
.ok_or_else(|| io::Error::other("State Anchor is absent"))?
|
||||||
|
.unmount()
|
||||||
|
.map_err(io::Error::other)?;
|
||||||
|
*state.absence = Some(absence);
|
||||||
|
send_message(control, &SupervisorEvent::StateAnchorUnmounted { mount })
|
||||||
|
}
|
||||||
|
CoordinatorMessage::ObserveStateAnchorAbsence {
|
||||||
|
user_namespace,
|
||||||
|
mount_namespace,
|
||||||
|
mount,
|
||||||
|
} => {
|
||||||
|
if (*user_namespace, *mount_namespace) != state.namespaces {
|
||||||
|
return send_message(
|
||||||
|
control,
|
||||||
|
&SupervisorEvent::Refused(
|
||||||
|
"State Anchor absence request namespace authority mismatched".into(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
match state.absence.as_ref().map_or_else(
|
||||||
|
|| {
|
||||||
|
Err(lazaret_state_anchor::StateAnchorError::InvalidLayout(
|
||||||
|
"State Anchor absence authority is absent",
|
||||||
|
))
|
||||||
|
},
|
||||||
|
|authority| authority.observe_absence(*mount),
|
||||||
|
) {
|
||||||
|
Ok(observed) => send_message(
|
||||||
|
control,
|
||||||
|
&SupervisorEvent::StateAnchorAbsenceObserved {
|
||||||
|
user_namespace: state.namespaces.0,
|
||||||
|
mount_namespace: state.namespaces.1,
|
||||||
|
mount: *mount,
|
||||||
|
observed,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Err(error) => send_message(control, &SupervisorEvent::Refused(error.to_string())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => Err(io::Error::other("message is not a State Anchor command")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn fail_active_runtime(
|
fn fail_active_runtime(
|
||||||
process: Option<RetainedProcess>,
|
process: Option<RetainedProcess>,
|
||||||
network: Option<PreparedNetwork>,
|
network: Option<PreparedNetwork>,
|
||||||
|
|
@ -1347,6 +1467,30 @@ mod tests {
|
||||||
receive_message::<CoordinatorMessage>(&supervisor).unwrap(),
|
receive_message::<CoordinatorMessage>(&supervisor).unwrap(),
|
||||||
CoordinatorMessage::UnmountStateAnchor
|
CoordinatorMessage::UnmountStateAnchor
|
||||||
);
|
);
|
||||||
|
let user_namespace = NamespaceIdentity {
|
||||||
|
device: lazaret_core::identity::DeviceId::new(11),
|
||||||
|
inode: lazaret_core::identity::InodeId::new(12),
|
||||||
|
};
|
||||||
|
let mount_namespace = NamespaceIdentity {
|
||||||
|
device: lazaret_core::identity::DeviceId::new(13),
|
||||||
|
inode: lazaret_core::identity::InodeId::new(14),
|
||||||
|
};
|
||||||
|
let mount = MountIdentity {
|
||||||
|
mount_id: lazaret_core::identity::MountId::new(15),
|
||||||
|
device: lazaret_core::identity::DeviceId::new(16),
|
||||||
|
inode: lazaret_core::identity::InodeId::new(17),
|
||||||
|
};
|
||||||
|
channel
|
||||||
|
.observe_state_anchor_absence(user_namespace, mount_namespace, mount)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
receive_message::<CoordinatorMessage>(&supervisor).unwrap(),
|
||||||
|
CoordinatorMessage::ObserveStateAnchorAbsence {
|
||||||
|
user_namespace,
|
||||||
|
mount_namespace,
|
||||||
|
mount,
|
||||||
|
}
|
||||||
|
);
|
||||||
channel.exit().unwrap();
|
channel.exit().unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
receive_message::<CoordinatorMessage>(&supervisor).unwrap(),
|
receive_message::<CoordinatorMessage>(&supervisor).unwrap(),
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ use lazaret_core::identity::{ContainerId, DeviceId, InodeId, NamespaceIdentity};
|
||||||
use lazaret_runtime_supervisor::sandbox::{
|
use lazaret_runtime_supervisor::sandbox::{
|
||||||
CREDENTIALS_DESTINATION, ESCALATION_CLIENT_DESTINATION, HostTerminal,
|
CREDENTIALS_DESTINATION, ESCALATION_CLIENT_DESTINATION, HostTerminal,
|
||||||
MAILBOX_REQUESTS_DESTINATION, MAILBOX_RESULTS_DESTINATION, NETWORK_NAMESPACE_PATH, RootProcess,
|
MAILBOX_REQUESTS_DESTINATION, MAILBOX_RESULTS_DESTINATION, NETWORK_NAMESPACE_PATH, RootProcess,
|
||||||
SANDBOX_INIT_DESTINATION, SandboxBuildErrorKind, SandboxBuildRequest,
|
SANDBOX_INIT_DESTINATION, SandboxBuildErrorKind, SandboxBuildRequest, SandboxBundle,
|
||||||
TERMINFO_ROOT_DESTINATION, WORKSPACE_DESTINATION, build_sandbox_bundle,
|
TERMINFO_ROOT_DESTINATION, WORKSPACE_DESTINATION, build_sandbox_bundle,
|
||||||
production_sandbox_layout, stage_host_terminal,
|
production_sandbox_layout, stage_host_terminal,
|
||||||
};
|
};
|
||||||
|
|
@ -312,7 +312,20 @@ fn deterministic_snapshot_has_only_private_namespaces_and_fixed_runtime_shape()
|
||||||
}])
|
}])
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(first.command().program(), OsStr::new("runsc"));
|
assert_runtime_command(&first, &fixture);
|
||||||
|
assert_eq!(
|
||||||
|
specification["root"]["path"],
|
||||||
|
fixture
|
||||||
|
.tree
|
||||||
|
.path()
|
||||||
|
.join("prepared-root")
|
||||||
|
.display()
|
||||||
|
.to_string()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn assert_runtime_command(bundle: &SandboxBundle, fixture: &Fixture) {
|
||||||
|
assert_eq!(bundle.command().program(), OsStr::new("runsc"));
|
||||||
let runtime_domain = String::from_utf8(
|
let runtime_domain = String::from_utf8(
|
||||||
fixture
|
fixture
|
||||||
.configuration
|
.configuration
|
||||||
|
|
@ -323,7 +336,7 @@ fn deterministic_snapshot_has_only_private_namespaces_and_fixed_runtime_shape()
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
first
|
bundle
|
||||||
.command()
|
.command()
|
||||||
.arguments()
|
.arguments()
|
||||||
.iter()
|
.iter()
|
||||||
|
|
@ -355,16 +368,7 @@ fn deterministic_snapshot_has_only_private_namespaces_and_fixed_runtime_shape()
|
||||||
"11".repeat(32),
|
"11".repeat(32),
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert!(!bundle.command().arguments().iter().any(|argument| {
|
||||||
specification["root"]["path"],
|
|
||||||
fixture
|
|
||||||
.tree
|
|
||||||
.path()
|
|
||||||
.join("prepared-root")
|
|
||||||
.display()
|
|
||||||
.to_string()
|
|
||||||
);
|
|
||||||
assert!(!first.command().arguments().iter().any(|argument| {
|
|
||||||
matches!(
|
matches!(
|
||||||
argument.to_str(),
|
argument.to_str(),
|
||||||
Some("-detach" | "-console-socket" | "-c")
|
Some("-detach" | "-console-socket" | "-c")
|
||||||
|
|
|
||||||
|
|
@ -530,33 +530,39 @@ pub struct RuntimeStateAnchorAuthority {
|
||||||
merged_descriptor: OwnedFd,
|
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 {
|
impl RuntimeStateAnchorAuthority {
|
||||||
/// Reopens inherited authorities without falling back to ambient configured paths.
|
/// Reopens inherited authorities without falling back to ambient configured paths.
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// Refuses missing, replaced, or incorrectly typed descriptors and fixed children.
|
/// Refuses missing, replaced, or incorrectly typed descriptors and fixed children.
|
||||||
pub fn from_inherited(
|
pub fn from_inherited(
|
||||||
lower_fd: i32,
|
inherited: RuntimeStateAnchorInherited<'_>,
|
||||||
lower_label: &Path,
|
|
||||||
persistent_root_fd: i32,
|
|
||||||
persistent_anchor_fd: i32,
|
|
||||||
persistent_anchor_label: &Path,
|
|
||||||
runtime_root_fd: i32,
|
|
||||||
runtime_anchor_fd: i32,
|
|
||||||
runtime_anchor_label: &Path,
|
|
||||||
expected_lower: (u64, u64),
|
|
||||||
) -> Result<Self, StateAnchorError> {
|
) -> Result<Self, StateAnchorError> {
|
||||||
let retained_lower = reopen_inherited_directory(lower_fd)?;
|
let retained_lower = reopen_inherited_directory(inherited.lower_fd)?;
|
||||||
require_directory_descriptor_identity(&retained_lower, expected_lower)?;
|
require_directory_descriptor_identity(&retained_lower, inherited.expected_lower)?;
|
||||||
if !lower_label.is_absolute() {
|
if !inherited.lower_label.is_absolute() {
|
||||||
return Err(StateAnchorError::InvalidLayout(
|
return Err(StateAnchorError::InvalidLayout(
|
||||||
"prepared lower label is not absolute",
|
"prepared lower label is not absolute",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
let lower = open_exact_directory_path(lower_label, expected_lower)?;
|
let lower = open_exact_directory_path(inherited.lower_label, inherited.expected_lower)?;
|
||||||
let persistent_root = reopen_inherited_directory(persistent_root_fd)?;
|
let persistent_root = reopen_inherited_directory(inherited.persistent_root_fd)?;
|
||||||
let retained_persistent_anchor = reopen_inherited_directory(persistent_anchor_fd)?;
|
let retained_persistent_anchor =
|
||||||
if !persistent_anchor_label.is_absolute() {
|
reopen_inherited_directory(inherited.persistent_anchor_fd)?;
|
||||||
|
if !inherited.persistent_anchor_label.is_absolute() {
|
||||||
return Err(StateAnchorError::InvalidLayout(
|
return Err(StateAnchorError::InvalidLayout(
|
||||||
"persistent State Anchor label is not absolute",
|
"persistent State Anchor label is not absolute",
|
||||||
));
|
));
|
||||||
|
|
@ -570,12 +576,12 @@ impl RuntimeStateAnchorAuthority {
|
||||||
(retained_persistent.st_dev, retained_persistent.st_ino),
|
(retained_persistent.st_dev, retained_persistent.st_ino),
|
||||||
)?;
|
)?;
|
||||||
let persistent_anchor = open_exact_directory_path(
|
let persistent_anchor = open_exact_directory_path(
|
||||||
persistent_anchor_label,
|
inherited.persistent_anchor_label,
|
||||||
(retained_persistent.st_dev, retained_persistent.st_ino),
|
(retained_persistent.st_dev, retained_persistent.st_ino),
|
||||||
)?;
|
)?;
|
||||||
let runtime_root = reopen_inherited_directory(runtime_root_fd)?;
|
let runtime_root = reopen_inherited_directory(inherited.runtime_root_fd)?;
|
||||||
let retained_runtime_anchor = reopen_inherited_directory(runtime_anchor_fd)?;
|
let retained_runtime_anchor = reopen_inherited_directory(inherited.runtime_anchor_fd)?;
|
||||||
if !runtime_anchor_label.is_absolute() {
|
if !inherited.runtime_anchor_label.is_absolute() {
|
||||||
return Err(StateAnchorError::InvalidLayout(
|
return Err(StateAnchorError::InvalidLayout(
|
||||||
"runtime State Anchor label is not absolute",
|
"runtime State Anchor label is not absolute",
|
||||||
));
|
));
|
||||||
|
|
@ -586,13 +592,15 @@ impl RuntimeStateAnchorAuthority {
|
||||||
&confined_runtime_anchor,
|
&confined_runtime_anchor,
|
||||||
(retained.st_dev, retained.st_ino),
|
(retained.st_dev, retained.st_ino),
|
||||||
)?;
|
)?;
|
||||||
let runtime_anchor =
|
let runtime_anchor = open_exact_directory_path(
|
||||||
open_exact_directory_path(runtime_anchor_label, (retained.st_dev, retained.st_ino))?;
|
inherited.runtime_anchor_label,
|
||||||
|
(retained.st_dev, retained.st_ino),
|
||||||
|
)?;
|
||||||
for name in ["upper", "work"] {
|
for name in ["upper", "work"] {
|
||||||
drop(open_directory(&persistent_anchor, Path::new(name))?);
|
drop(open_directory(&persistent_anchor, Path::new(name))?);
|
||||||
}
|
}
|
||||||
let merged_descriptor = open_directory(&runtime_anchor, Path::new("merged"))?;
|
let merged_descriptor = open_directory(&runtime_anchor, Path::new("merged"))?;
|
||||||
let merged_label = runtime_anchor_label.join("merged");
|
let merged_label = inherited.runtime_anchor_label.join("merged");
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
lower,
|
lower,
|
||||||
persistent_anchor,
|
persistent_anchor,
|
||||||
|
|
@ -699,6 +707,32 @@ pub struct RuntimeStateAnchorMount {
|
||||||
merged: OwnedFd,
|
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 {
|
impl RuntimeStateAnchorMount {
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub const fn mount_identity(&self) -> MountIdentity {
|
pub const fn mount_identity(&self) -> MountIdentity {
|
||||||
|
|
@ -756,7 +790,7 @@ impl RuntimeStateAnchorMount {
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// Refuses mount replacement, unmount failure, or exact child cleanup failure.
|
/// Refuses mount replacement, unmount failure, or exact child cleanup failure.
|
||||||
pub fn unmount(self) -> Result<(), StateAnchorError> {
|
pub fn unmount(self) -> Result<RuntimeStateAnchorAbsenceAuthority, StateAnchorError> {
|
||||||
let Self {
|
let Self {
|
||||||
authority,
|
authority,
|
||||||
process,
|
process,
|
||||||
|
|
@ -777,7 +811,10 @@ impl RuntimeStateAnchorMount {
|
||||||
return Err(StateAnchorError::OwnershipMismatch);
|
return Err(StateAnchorError::OwnershipMismatch);
|
||||||
}
|
}
|
||||||
child_broker().reap(process)?;
|
child_broker().reap(process)?;
|
||||||
Ok(())
|
Ok(RuntimeStateAnchorAbsenceAuthority {
|
||||||
|
runtime_anchor: authority.runtime_anchor,
|
||||||
|
mount,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1195,12 +1232,10 @@ fn append_fuse_option_path(target: &mut Vec<u8>, path: &Path) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::disallowed_methods, clippy::disallowed_types)]
|
|
||||||
fn rootless_unmount(layout: &StateAnchorLayout<'_>) -> Result<(), StateAnchorError> {
|
fn rootless_unmount(layout: &StateAnchorLayout<'_>) -> Result<(), StateAnchorError> {
|
||||||
rootless_unmount_anchor(&layout.runtime_anchor)
|
rootless_unmount_anchor(&layout.runtime_anchor)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::disallowed_methods, clippy::disallowed_types)]
|
|
||||||
fn rootless_unmount_anchor(runtime_anchor: impl AsFd) -> Result<(), StateAnchorError> {
|
fn rootless_unmount_anchor(runtime_anchor: impl AsFd) -> Result<(), StateAnchorError> {
|
||||||
let inherited_anchor = inherit_for_exec(runtime_anchor)?;
|
let inherited_anchor = inherit_for_exec(runtime_anchor)?;
|
||||||
let path = descriptor_child_path(&inherited_anchor, "merged");
|
let path = descriptor_child_path(&inherited_anchor, "merged");
|
||||||
|
|
@ -1208,17 +1243,14 @@ fn rootless_unmount_anchor(runtime_anchor: impl AsFd) -> Result<(), StateAnchorE
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rootless_unmount_path(path: &Path) -> Result<(), StateAnchorError> {
|
fn rootless_unmount_path(path: &Path) -> Result<(), StateAnchorError> {
|
||||||
let status = std::process::Command::new("fusermount3")
|
child_broker()
|
||||||
.arg("-u")
|
.run_transient(
|
||||||
.arg(path)
|
&TargetCommand::new("fusermount3")
|
||||||
.status()?;
|
.argument("-u")
|
||||||
if status.success() {
|
.argument(path),
|
||||||
Ok(())
|
)
|
||||||
} else {
|
.map(drop)
|
||||||
Err(StateAnchorError::Io(io::Error::other(format!(
|
.map_err(StateAnchorError::from)
|
||||||
"fusermount3 exited with {status}"
|
|
||||||
))))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn require_mount_record(
|
fn require_mount_record(
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,8 @@ use lazaret_core::identity::{
|
||||||
};
|
};
|
||||||
use lazaret_core::records::{ArtifactState, PreparedArtifact};
|
use lazaret_core::records::{ArtifactState, PreparedArtifact};
|
||||||
use lazaret_state_anchor::{
|
use lazaret_state_anchor::{
|
||||||
ProtectedRuntimeFiles, RuntimeStateAnchorAuthority, StateAnchorLaunchAuthority,
|
ProtectedRuntimeFiles, RuntimeStateAnchorAuthority, RuntimeStateAnchorInherited,
|
||||||
StateAnchorLayout,
|
StateAnchorLaunchAuthority, StateAnchorLayout,
|
||||||
};
|
};
|
||||||
|
|
||||||
static SEQUENCE: AtomicU64 = AtomicU64::new(0);
|
static SEQUENCE: AtomicU64 = AtomicU64::new(0);
|
||||||
|
|
@ -78,30 +78,12 @@ fn inherited_runtime_authority_accepts_only_the_exact_prepared_lower() {
|
||||||
};
|
};
|
||||||
let launch = StateAnchorLaunchAuthority::capture(&layout, &artifact).unwrap();
|
let launch = StateAnchorLaunchAuthority::capture(&layout, &artifact).unwrap();
|
||||||
let expected = launch.expected_lower();
|
let expected = launch.expected_lower();
|
||||||
RuntimeStateAnchorAuthority::from_inherited(
|
RuntimeStateAnchorAuthority::from_inherited(inherited(&launch, expected)).unwrap();
|
||||||
launch.lower_fd(),
|
|
||||||
launch.lower_label(),
|
|
||||||
launch.persistent_root_fd(),
|
|
||||||
launch.persistent_anchor_fd(),
|
|
||||||
launch.persistent_anchor_label(),
|
|
||||||
launch.runtime_root_fd(),
|
|
||||||
launch.runtime_anchor_fd(),
|
|
||||||
launch.runtime_anchor_label(),
|
|
||||||
expected,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
assert!(
|
assert!(
|
||||||
RuntimeStateAnchorAuthority::from_inherited(
|
RuntimeStateAnchorAuthority::from_inherited(inherited(
|
||||||
launch.lower_fd(),
|
&launch,
|
||||||
launch.lower_label(),
|
|
||||||
launch.persistent_root_fd(),
|
|
||||||
launch.persistent_anchor_fd(),
|
|
||||||
launch.persistent_anchor_label(),
|
|
||||||
launch.runtime_root_fd(),
|
|
||||||
launch.runtime_anchor_fd(),
|
|
||||||
launch.runtime_anchor_label(),
|
|
||||||
(expected.0, expected.1 + 1),
|
(expected.0, expected.1 + 1),
|
||||||
)
|
))
|
||||||
.is_err()
|
.is_err()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -150,17 +132,7 @@ fn namespace_local_rebound_refuses_a_replaced_state_anchor() {
|
||||||
fs::create_dir(&anchor).unwrap();
|
fs::create_dir(&anchor).unwrap();
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
RuntimeStateAnchorAuthority::from_inherited(
|
RuntimeStateAnchorAuthority::from_inherited(inherited(&launch, launch.expected_lower(),))
|
||||||
launch.lower_fd(),
|
|
||||||
launch.lower_label(),
|
|
||||||
launch.persistent_root_fd(),
|
|
||||||
launch.persistent_anchor_fd(),
|
|
||||||
launch.persistent_anchor_label(),
|
|
||||||
launch.runtime_root_fd(),
|
|
||||||
launch.runtime_anchor_fd(),
|
|
||||||
launch.runtime_anchor_label(),
|
|
||||||
launch.expected_lower(),
|
|
||||||
)
|
|
||||||
.is_err()
|
.is_err()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -178,17 +150,7 @@ fn namespace_local_rebound_refuses_a_replaced_prepared_lower() {
|
||||||
fs::create_dir(&lower).unwrap();
|
fs::create_dir(&lower).unwrap();
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
RuntimeStateAnchorAuthority::from_inherited(
|
RuntimeStateAnchorAuthority::from_inherited(inherited(&launch, launch.expected_lower(),))
|
||||||
launch.lower_fd(),
|
|
||||||
launch.lower_label(),
|
|
||||||
launch.persistent_root_fd(),
|
|
||||||
launch.persistent_anchor_fd(),
|
|
||||||
launch.persistent_anchor_label(),
|
|
||||||
launch.runtime_root_fd(),
|
|
||||||
launch.runtime_anchor_fd(),
|
|
||||||
launch.runtime_anchor_label(),
|
|
||||||
launch.expected_lower(),
|
|
||||||
)
|
|
||||||
.is_err()
|
.is_err()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -207,17 +169,7 @@ fn namespace_local_rebound_refuses_a_replaced_persistent_anchor() {
|
||||||
fs::create_dir(&anchor).unwrap();
|
fs::create_dir(&anchor).unwrap();
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
RuntimeStateAnchorAuthority::from_inherited(
|
RuntimeStateAnchorAuthority::from_inherited(inherited(&launch, launch.expected_lower(),))
|
||||||
launch.lower_fd(),
|
|
||||||
launch.lower_label(),
|
|
||||||
launch.persistent_root_fd(),
|
|
||||||
launch.persistent_anchor_fd(),
|
|
||||||
launch.persistent_anchor_label(),
|
|
||||||
launch.runtime_root_fd(),
|
|
||||||
launch.runtime_anchor_fd(),
|
|
||||||
launch.runtime_anchor_label(),
|
|
||||||
launch.expected_lower(),
|
|
||||||
)
|
|
||||||
.is_err()
|
.is_err()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -237,21 +189,28 @@ fn namespace_local_rebound_refuses_a_state_anchor_symlink() {
|
||||||
std::os::unix::fs::symlink(&retained, &anchor).unwrap();
|
std::os::unix::fs::symlink(&retained, &anchor).unwrap();
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
RuntimeStateAnchorAuthority::from_inherited(
|
RuntimeStateAnchorAuthority::from_inherited(inherited(&launch, launch.expected_lower(),))
|
||||||
launch.lower_fd(),
|
|
||||||
launch.lower_label(),
|
|
||||||
launch.persistent_root_fd(),
|
|
||||||
launch.persistent_anchor_fd(),
|
|
||||||
launch.persistent_anchor_label(),
|
|
||||||
launch.runtime_root_fd(),
|
|
||||||
launch.runtime_anchor_fd(),
|
|
||||||
launch.runtime_anchor_label(),
|
|
||||||
launch.expected_lower(),
|
|
||||||
)
|
|
||||||
.is_err()
|
.is_err()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn inherited(
|
||||||
|
launch: &StateAnchorLaunchAuthority,
|
||||||
|
expected_lower: (u64, u64),
|
||||||
|
) -> RuntimeStateAnchorInherited<'_> {
|
||||||
|
RuntimeStateAnchorInherited {
|
||||||
|
lower_fd: launch.lower_fd(),
|
||||||
|
lower_label: launch.lower_label(),
|
||||||
|
persistent_root_fd: launch.persistent_root_fd(),
|
||||||
|
persistent_anchor_fd: launch.persistent_anchor_fd(),
|
||||||
|
persistent_anchor_label: launch.persistent_anchor_label(),
|
||||||
|
runtime_root_fd: launch.runtime_root_fd(),
|
||||||
|
runtime_anchor_fd: launch.runtime_anchor_fd(),
|
||||||
|
runtime_anchor_label: launch.runtime_anchor_label(),
|
||||||
|
expected_lower,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn prepared_artifact(lower: &Path) -> PreparedArtifact {
|
fn prepared_artifact(lower: &Path) -> PreparedArtifact {
|
||||||
let metadata = lower.metadata().unwrap();
|
let metadata = lower.metadata().unwrap();
|
||||||
PreparedArtifact {
|
PreparedArtifact {
|
||||||
|
|
|
||||||
|
|
@ -19,9 +19,9 @@ use lazaret_linux::process::{
|
||||||
ProcessOwnershipContext, current_process_identity, dispatch_private_process_role,
|
ProcessOwnershipContext, current_process_identity, dispatch_private_process_role,
|
||||||
};
|
};
|
||||||
use lazaret_state_anchor::{
|
use lazaret_state_anchor::{
|
||||||
FuseOverlayfsMount, ProtectedRuntimeFiles, RuntimeStateAnchorAuthority, StateAnchorAcquisition,
|
FuseOverlayfsMount, ProtectedRuntimeFiles, RuntimeStateAnchorAuthority,
|
||||||
StateAnchorLaunchAuthority, StateAnchorLayout, StateAnchorSandboxView,
|
RuntimeStateAnchorInherited, StateAnchorAcquisition, StateAnchorLaunchAuthority,
|
||||||
unmount_after_coordinator_loss,
|
StateAnchorLayout, StateAnchorSandboxView, unmount_after_coordinator_loss,
|
||||||
};
|
};
|
||||||
use rustix::process::Signal;
|
use rustix::process::Signal;
|
||||||
|
|
||||||
|
|
@ -80,17 +80,18 @@ fn run() -> io::Result<()> {
|
||||||
|
|
||||||
let launch =
|
let launch =
|
||||||
StateAnchorLaunchAuthority::capture(&layout, &artifact).map_err(io::Error::other)?;
|
StateAnchorLaunchAuthority::capture(&layout, &artifact).map_err(io::Error::other)?;
|
||||||
let runtime_authority = RuntimeStateAnchorAuthority::from_inherited(
|
let runtime_authority =
|
||||||
launch.lower_fd(),
|
RuntimeStateAnchorAuthority::from_inherited(RuntimeStateAnchorInherited {
|
||||||
launch.lower_label(),
|
lower_fd: launch.lower_fd(),
|
||||||
launch.persistent_root_fd(),
|
lower_label: launch.lower_label(),
|
||||||
launch.persistent_anchor_fd(),
|
persistent_root_fd: launch.persistent_root_fd(),
|
||||||
launch.persistent_anchor_label(),
|
persistent_anchor_fd: launch.persistent_anchor_fd(),
|
||||||
launch.runtime_root_fd(),
|
persistent_anchor_label: launch.persistent_anchor_label(),
|
||||||
launch.runtime_anchor_fd(),
|
runtime_root_fd: launch.runtime_root_fd(),
|
||||||
launch.runtime_anchor_label(),
|
runtime_anchor_fd: launch.runtime_anchor_fd(),
|
||||||
launch.expected_lower(),
|
runtime_anchor_label: launch.runtime_anchor_label(),
|
||||||
)
|
expected_lower: launch.expected_lower(),
|
||||||
|
})
|
||||||
.map_err(io::Error::other)?;
|
.map_err(io::Error::other)?;
|
||||||
let expected = std::cell::Cell::new(None);
|
let expected = std::cell::Cell::new(None);
|
||||||
let runtime_mount = runtime_authority
|
let runtime_mount = runtime_authority
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue