Require fresh State Anchor absence before lease release

This commit is contained in:
ms2k 2026-07-17 04:40:02 +09:00
commit 3b76b59bdf
4 changed files with 543 additions and 194 deletions

View file

@ -26,7 +26,10 @@ use crate::lifecycle::{
SessionLifecycleAdapter,
};
use crate::network::NetworkCoordinator;
use crate::runtime::{AwaitingAttachedRunsc, CommittedSandboxRuntime, SandboxCoordinator};
use crate::runtime::{
AwaitingAttachedRunsc, CommittedSandboxRuntime, SandboxCoordinator,
StateAnchorAbsenceObservation,
};
use crate::state_anchor::{StateAnchorCoordinator, StateAnchorReleaseAuthority};
#[derive(Debug)]
@ -80,6 +83,7 @@ pub struct ProductionLifecycleAdapter<'a> {
runtime: Option<CommittedSandboxRuntime>,
state_anchor_mount: Option<MountIdentity>,
state_anchor_release: Option<StateAnchorReleaseAuthority>,
state_anchor_absence: Option<StateAnchorAbsenceObservation>,
}
pub type ProductionHostCoordinatorLifecycle<'store, 'resources> = HostCoordinatorLifecycle<
@ -175,6 +179,7 @@ impl<'a> ProductionLifecycleAdapter<'a> {
runtime: None,
state_anchor_mount: None,
state_anchor_release: None,
state_anchor_absence: None,
},
ProductionSessionRegistry { registry },
)
@ -409,18 +414,26 @@ impl<'a> ProductionLifecycleAdapter<'a> {
)
.map_err(failure)?,
);
self.runtime
let runtime = self
.runtime
.as_mut()
.ok_or_else(|| ProductionLifecycleError("committed runtime is absent".into()))?
.unmount_state_anchor(
self.state_anchor_release
.as_ref()
.ok_or_else(|| {
ProductionLifecycleError("State Anchor release is absent".into())
})?
.mount(),
)
.ok_or_else(|| ProductionLifecycleError("committed runtime is absent".into()))?;
let release = self
.state_anchor_release
.as_ref()
.ok_or_else(|| ProductionLifecycleError("State Anchor release is absent".into()))?;
runtime
.unmount_state_anchor(release.mount())
.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;
Ok(())
}
@ -428,11 +441,44 @@ impl<'a> ProductionLifecycleAdapter<'a> {
fn release_prepared_lower(&mut self) -> Result<(), ProductionLifecycleError> {
let release = self
.state_anchor_release
.take()
.as_ref()
.ok_or_else(|| ProductionLifecycleError("State Anchor release is absent".into()))?;
self.state_anchor()
.complete_release_after_unmount(&release, self.plan.state_anchor_layout)
.map_err(failure)
let first_observation = self.state_anchor_absence.ok_or_else(|| {
ProductionLifecycleError("State Anchor absence evidence is absent".into())
})?;
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> {

View file

@ -30,6 +30,14 @@ use crate::network::{NetworkCoordinator, NetworkCoordinatorError};
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)]
pub enum SandboxCoordinatorError {
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.
///
/// # Errors

View file

@ -15,6 +15,7 @@ use lazaret_state_anchor::{StateAnchorError, StateAnchorLayout, VerifiedStateAnc
use lazaret_storage_catalog::{Catalog, CatalogError};
use crate::domain_store::{DomainStoreError, DomainStoreMutation, HostCoordinatorDomainStore};
use crate::runtime::StateAnchorAbsenceObservation;
#[derive(Debug)]
pub enum StateAnchorCoordinatorError {
@ -367,11 +368,17 @@ impl<'a> StateAnchorCoordinator<'a> {
///
/// # Errors
/// 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,
authority: &StateAnchorReleaseAuthority,
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_merged = expected_layout.merged_path.as_ref().ok_or(
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
.apply(&[DomainStoreMutation::CleanParticipantAfterAbsence(
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,
authority: &StateAnchorReleaseAuthority,
) -> Result<(), StateAnchorCoordinatorError> {
observe_again: &mut Observe,
) -> Result<(), StateAnchorCoordinatorError>
where
Observe: FnMut() -> Result<StateAnchorAbsenceObservation, StateAnchorCoordinatorError>,
{
let snapshot = self.catalog.snapshot()?;
let generation_lease = snapshot.artifact_leases.iter().find(|lease| {
lease.domain_id == authority.lease.domain_id
@ -570,12 +581,33 @@ impl<'a> StateAnchorCoordinator<'a> {
"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(())
}
}
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(
domain: &lazaret_core::records::Domain,
anchor: &Participant,

View file

@ -182,6 +182,44 @@ fn durable_phases_resume_release_without_collapsing_lease_order() {
ParticipantState::ReleaseIntent,
);
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 prepared = role(
&snapshot.participants,
@ -197,9 +235,42 @@ fn durable_phases_resume_release_without_collapsing_lease_order() {
DomainStoreMutation::PutParticipant(prepared_release_intent(prepared).unwrap()),
])
.unwrap();
for invalid in invalid_observations {
assert!(
coordinator
.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)
.complete_release_after_unmount(&authority, &layout, valid, || {
observations += 1;
Ok(valid)
})
.unwrap();
assert_eq!(observations, 1);
assert_states(
&domain_store,
ParticipantState::Clean,
@ -208,6 +279,19 @@ fn durable_phases_resume_release_without_collapsing_lease_order() {
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]
fn acquisition_resumes_after_prepared_intent_reopen() {
let fixture = AcquisitionFixture::new(17);