Make State Anchor acquisition resumable and descriptor-confined
This commit is contained in:
parent
e6d6406c64
commit
48c10e9a7a
6 changed files with 1030 additions and 197 deletions
|
|
@ -9,7 +9,9 @@ use lazaret_core::records::{
|
||||||
ParticipantState, PersistedHostProcessOwnership, PreparedArtifact, PreparedLowerOwnership,
|
ParticipantState, PersistedHostProcessOwnership, PreparedArtifact, PreparedLowerOwnership,
|
||||||
ProcessOwnershipCommitAck, StagedProcessOwnership, StateAnchorOwnership,
|
ProcessOwnershipCommitAck, StagedProcessOwnership, StateAnchorOwnership,
|
||||||
};
|
};
|
||||||
use lazaret_state_anchor::{StateAnchorError, StateAnchorLayout, require_mount_absent};
|
use lazaret_state_anchor::{
|
||||||
|
StateAnchorError, StateAnchorLayout, VerifiedStateAnchorReadiness, require_mount_absent,
|
||||||
|
};
|
||||||
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};
|
||||||
|
|
@ -66,7 +68,7 @@ impl From<StateAnchorError> for StateAnchorCoordinatorError {
|
||||||
pub struct StateAnchorReleaseAuthority {
|
pub struct StateAnchorReleaseAuthority {
|
||||||
generation_id: GenerationId,
|
generation_id: GenerationId,
|
||||||
lease: ArtifactLease,
|
lease: ArtifactLease,
|
||||||
merged: PathBuf,
|
merged_label: PathBuf,
|
||||||
mount: MountIdentity,
|
mount: MountIdentity,
|
||||||
daemon: PidfsProcessIdentityV1,
|
daemon: PidfsProcessIdentityV1,
|
||||||
}
|
}
|
||||||
|
|
@ -77,11 +79,6 @@ impl StateAnchorReleaseAuthority {
|
||||||
self.generation_id
|
self.generation_id
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub fn merged(&self) -> &Path {
|
|
||||||
&self.merged
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub const fn mount(&self) -> MountIdentity {
|
pub const fn mount(&self) -> MountIdentity {
|
||||||
self.mount
|
self.mount
|
||||||
|
|
@ -110,7 +107,7 @@ impl<'a> StateAnchorCoordinator<'a> {
|
||||||
/// Commits prepared-lower intent, exact catalog lease, `LeaseHeld`, and State Anchor intent.
|
/// Commits prepared-lower intent, exact catalog lease, `LeaseHeld`, and State Anchor intent.
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// Refuses existing participants, artifact drift, or any durable transition failure.
|
/// Refuses ownership drift or any durable transition failure.
|
||||||
pub fn begin_acquisition(
|
pub fn begin_acquisition(
|
||||||
&self,
|
&self,
|
||||||
generation_id: GenerationId,
|
generation_id: GenerationId,
|
||||||
|
|
@ -118,16 +115,6 @@ impl<'a> StateAnchorCoordinator<'a> {
|
||||||
layout: &StateAnchorLayout,
|
layout: &StateAnchorLayout,
|
||||||
) -> Result<ArtifactLease, StateAnchorCoordinatorError> {
|
) -> Result<ArtifactLease, StateAnchorCoordinatorError> {
|
||||||
let snapshot = self.domain_store.snapshot()?;
|
let snapshot = self.domain_store.snapshot()?;
|
||||||
require_absent_role(
|
|
||||||
&snapshot.participants,
|
|
||||||
generation_id,
|
|
||||||
ParticipantRole::PreparedLower,
|
|
||||||
)?;
|
|
||||||
require_absent_role(
|
|
||||||
&snapshot.participants,
|
|
||||||
generation_id,
|
|
||||||
ParticipantRole::StateAnchor,
|
|
||||||
)?;
|
|
||||||
let lease = ArtifactLease {
|
let lease = ArtifactLease {
|
||||||
prepared_artifact_id: artifact.prepared_artifact_id,
|
prepared_artifact_id: artifact.prepared_artifact_id,
|
||||||
domain_id: snapshot.domain.domain_id,
|
domain_id: snapshot.domain.domain_id,
|
||||||
|
|
@ -141,42 +128,60 @@ impl<'a> StateAnchorCoordinator<'a> {
|
||||||
device: Some(artifact.device),
|
device: Some(artifact.device),
|
||||||
inode: Some(artifact.inode),
|
inode: Some(artifact.inode),
|
||||||
};
|
};
|
||||||
self.domain_store.apply(&[
|
let expected_prepared_intent = participant(
|
||||||
DomainStoreMutation::PutParticipant(Participant::bare_intent(
|
|
||||||
generation_id,
|
|
||||||
ParticipantRole::PreparedLower,
|
|
||||||
)),
|
|
||||||
DomainStoreMutation::PutParticipant(participant(
|
|
||||||
generation_id,
|
generation_id,
|
||||||
ParticipantRole::PreparedLower,
|
ParticipantRole::PreparedLower,
|
||||||
ParticipantState::Intent,
|
ParticipantState::Intent,
|
||||||
ParticipantOwnership::PreparedLower(prepared_intent.clone()),
|
ParticipantOwnership::PreparedLower(prepared_intent.clone()),
|
||||||
)?),
|
)?;
|
||||||
])?;
|
|
||||||
self.catalog.acquire_lease_checked(artifact, &lease)?;
|
|
||||||
let lease_held = PreparedLowerOwnership {
|
let lease_held = PreparedLowerOwnership {
|
||||||
lease_state: Some(ParticipantState::LeaseHeld),
|
lease_state: Some(ParticipantState::LeaseHeld),
|
||||||
..prepared_intent
|
..prepared_intent
|
||||||
};
|
};
|
||||||
self.domain_store.apply(&[
|
let expected_lease_held = participant(
|
||||||
DomainStoreMutation::PutParticipant(participant(
|
|
||||||
generation_id,
|
generation_id,
|
||||||
ParticipantRole::PreparedLower,
|
ParticipantRole::PreparedLower,
|
||||||
ParticipantState::LeaseHeld,
|
ParticipantState::LeaseHeld,
|
||||||
ParticipantOwnership::PreparedLower(lease_held),
|
ParticipantOwnership::PreparedLower(lease_held.clone()),
|
||||||
)?),
|
)?;
|
||||||
DomainStoreMutation::PutParticipant(Participant::bare_intent(
|
let expected_anchor_intent = participant(
|
||||||
generation_id,
|
|
||||||
ParticipantRole::StateAnchor,
|
|
||||||
)),
|
|
||||||
DomainStoreMutation::PutParticipant(participant(
|
|
||||||
generation_id,
|
generation_id,
|
||||||
ParticipantRole::StateAnchor,
|
ParticipantRole::StateAnchor,
|
||||||
ParticipantState::Intent,
|
ParticipantState::Intent,
|
||||||
ParticipantOwnership::StateAnchor(
|
ParticipantOwnership::StateAnchor(
|
||||||
layout.ownership_intent(artifact.prepared_artifact_id),
|
layout.ownership_intent(artifact.prepared_artifact_id),
|
||||||
),
|
),
|
||||||
)?),
|
)?;
|
||||||
|
match acquisition_checkpoint(
|
||||||
|
&snapshot.participants,
|
||||||
|
generation_id,
|
||||||
|
&expected_prepared_intent,
|
||||||
|
&expected_lease_held,
|
||||||
|
&expected_anchor_intent,
|
||||||
|
)? {
|
||||||
|
AcquisitionCheckpoint::Absent => self.domain_store.apply(&[
|
||||||
|
DomainStoreMutation::PutParticipant(Participant::bare_intent(
|
||||||
|
generation_id,
|
||||||
|
ParticipantRole::PreparedLower,
|
||||||
|
)),
|
||||||
|
DomainStoreMutation::PutParticipant(expected_prepared_intent.clone()),
|
||||||
|
])?,
|
||||||
|
AcquisitionCheckpoint::PreparedIntent => {}
|
||||||
|
AcquisitionCheckpoint::Complete => {
|
||||||
|
self.catalog
|
||||||
|
.acquire_or_resume_lease_checked(artifact, &lease)?;
|
||||||
|
return Ok(lease);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.catalog
|
||||||
|
.acquire_or_resume_lease_checked(artifact, &lease)?;
|
||||||
|
self.domain_store.apply(&[
|
||||||
|
DomainStoreMutation::PutParticipant(expected_lease_held),
|
||||||
|
DomainStoreMutation::PutParticipant(Participant::bare_intent(
|
||||||
|
generation_id,
|
||||||
|
ParticipantRole::StateAnchor,
|
||||||
|
)),
|
||||||
|
DomainStoreMutation::PutParticipant(expected_anchor_intent),
|
||||||
])?;
|
])?;
|
||||||
Ok(lease)
|
Ok(lease)
|
||||||
}
|
}
|
||||||
|
|
@ -190,28 +195,66 @@ impl<'a> StateAnchorCoordinator<'a> {
|
||||||
staged: StagedProcessOwnership,
|
staged: StagedProcessOwnership,
|
||||||
) -> Result<(), StateAnchorCoordinatorError> {
|
) -> Result<(), StateAnchorCoordinatorError> {
|
||||||
self.require_staged_identity(staged)?;
|
self.require_staged_identity(staged)?;
|
||||||
self.update_daemon(
|
let expected = PersistedHostProcessOwnership::staged(staged.identity());
|
||||||
staged.generation_id(),
|
let current = self.participant(staged.generation_id(), ParticipantRole::StateAnchor)?;
|
||||||
PersistedHostProcessOwnership::staged(staged.identity()),
|
let Some(ParticipantOwnership::StateAnchor(ownership)) = current.ownership() else {
|
||||||
)
|
return Err(StateAnchorCoordinatorError::InvalidState(
|
||||||
|
"State Anchor ownership is invalid",
|
||||||
|
));
|
||||||
|
};
|
||||||
|
if current.state() != ParticipantState::Intent {
|
||||||
|
return Err(StateAnchorCoordinatorError::InvalidState(
|
||||||
|
"State Anchor daemon acquisition is not in Intent",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if ownership.daemon == expected && ownership.mount.is_none() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
if ownership.daemon != PersistedHostProcessOwnership::unassigned() {
|
||||||
|
return Err(StateAnchorCoordinatorError::InvalidState(
|
||||||
|
"State Anchor daemon ownership changed",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
self.update_daemon(staged.generation_id(), expected)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Commits the exact daemon and returns the acknowledgment that releases its mount command.
|
/// Acknowledges that the exact staged daemon is durable so its mount command may start.
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// Refuses a staged identity mismatch or participant transition failure.
|
/// Refuses a staged identity mismatch or participant transition failure.
|
||||||
pub fn commit_daemon(
|
pub fn acknowledge_staged_daemon(
|
||||||
&self,
|
&self,
|
||||||
staged: StagedProcessOwnership,
|
staged: StagedProcessOwnership,
|
||||||
) -> Result<ProcessOwnershipCommitAck, StateAnchorCoordinatorError> {
|
) -> Result<ProcessOwnershipCommitAck, StateAnchorCoordinatorError> {
|
||||||
self.require_staged_identity(staged)?;
|
self.require_staged_identity(staged)?;
|
||||||
self.update_daemon(
|
self.require_exact_daemon(
|
||||||
staged.generation_id(),
|
staged.generation_id(),
|
||||||
PersistedHostProcessOwnership::committed(staged.identity()),
|
PersistedHostProcessOwnership::staged(staged.identity()),
|
||||||
)?;
|
)?;
|
||||||
Ok(ProcessOwnershipCommitAck::v1(staged))
|
Ok(ProcessOwnershipCommitAck::v1(staged))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Promotes the exact staged daemon only after physical target readiness was verified.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Refuses a forged readiness witness, staged ownership drift, or transition failure.
|
||||||
|
pub fn commit_daemon_ready(
|
||||||
|
&self,
|
||||||
|
readiness: VerifiedStateAnchorReadiness,
|
||||||
|
) -> Result<(), StateAnchorCoordinatorError> {
|
||||||
|
let staged = readiness.staged();
|
||||||
|
self.require_staged_identity(staged)?;
|
||||||
|
self.require_exact_daemon(
|
||||||
|
staged.generation_id(),
|
||||||
|
PersistedHostProcessOwnership::staged(staged.identity()),
|
||||||
|
)?;
|
||||||
|
self.update_daemon_and_mount(
|
||||||
|
staged.generation_id(),
|
||||||
|
PersistedHostProcessOwnership::committed(staged.identity()),
|
||||||
|
Some(readiness.mount()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/// Commits Prepared only after the Runtime Supervisor reports exact mount identity.
|
/// Commits Prepared only after the Runtime Supervisor reports exact mount identity.
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
|
|
@ -231,22 +274,18 @@ impl<'a> StateAnchorCoordinator<'a> {
|
||||||
if current.state() != ParticipantState::Intent
|
if current.state() != ParticipantState::Intent
|
||||||
|| ownership.lower_artifact_id != Some(prepared_artifact_id)
|
|| ownership.lower_artifact_id != Some(prepared_artifact_id)
|
||||||
|| !ownership.daemon.is_current_committed()
|
|| !ownership.daemon.is_current_committed()
|
||||||
|| ownership.mount.is_some()
|
|| ownership.mount != Some(mount)
|
||||||
{
|
{
|
||||||
return Err(StateAnchorCoordinatorError::InvalidState(
|
return Err(StateAnchorCoordinatorError::InvalidState(
|
||||||
"State Anchor is not ready for mount commit",
|
"State Anchor is not ready for mount commit",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
let prepared = StateAnchorOwnership {
|
|
||||||
mount: Some(mount),
|
|
||||||
..ownership.clone()
|
|
||||||
};
|
|
||||||
self.domain_store
|
self.domain_store
|
||||||
.apply(&[DomainStoreMutation::PutParticipant(participant(
|
.apply(&[DomainStoreMutation::PutParticipant(participant(
|
||||||
generation_id,
|
generation_id,
|
||||||
ParticipantRole::StateAnchor,
|
ParticipantRole::StateAnchor,
|
||||||
ParticipantState::Prepared,
|
ParticipantState::Prepared,
|
||||||
ParticipantOwnership::StateAnchor(prepared),
|
ParticipantOwnership::StateAnchor(ownership.clone()),
|
||||||
)?)])?;
|
)?)])?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -258,6 +297,7 @@ impl<'a> StateAnchorCoordinator<'a> {
|
||||||
pub fn begin_release(
|
pub fn begin_release(
|
||||||
&self,
|
&self,
|
||||||
generation_id: GenerationId,
|
generation_id: GenerationId,
|
||||||
|
layout: &StateAnchorLayout<'_>,
|
||||||
) -> Result<StateAnchorReleaseAuthority, StateAnchorCoordinatorError> {
|
) -> Result<StateAnchorReleaseAuthority, StateAnchorCoordinatorError> {
|
||||||
let snapshot = self.domain_store.snapshot()?;
|
let snapshot = self.domain_store.snapshot()?;
|
||||||
let anchor = role(
|
let anchor = role(
|
||||||
|
|
@ -271,6 +311,16 @@ impl<'a> StateAnchorCoordinator<'a> {
|
||||||
ParticipantRole::PreparedLower,
|
ParticipantRole::PreparedLower,
|
||||||
)?;
|
)?;
|
||||||
let authority = release_authority(&snapshot.domain, anchor, prepared)?;
|
let authority = release_authority(&snapshot.domain, anchor, prepared)?;
|
||||||
|
let Some(ParticipantOwnership::StateAnchor(ownership)) = anchor.ownership() else {
|
||||||
|
return Err(StateAnchorCoordinatorError::InvalidState(
|
||||||
|
"State Anchor ownership is invalid",
|
||||||
|
));
|
||||||
|
};
|
||||||
|
if !layout.matches_recorded_paths(ownership) {
|
||||||
|
return Err(StateAnchorCoordinatorError::InvalidState(
|
||||||
|
"State Anchor layout changed",
|
||||||
|
));
|
||||||
|
}
|
||||||
self.domain_store
|
self.domain_store
|
||||||
.apply(&[DomainStoreMutation::PutParticipant(participant(
|
.apply(&[DomainStoreMutation::PutParticipant(participant(
|
||||||
generation_id,
|
generation_id,
|
||||||
|
|
@ -293,8 +343,18 @@ impl<'a> StateAnchorCoordinator<'a> {
|
||||||
pub fn complete_release_after_unmount(
|
pub fn complete_release_after_unmount(
|
||||||
&self,
|
&self,
|
||||||
authority: &StateAnchorReleaseAuthority,
|
authority: &StateAnchorReleaseAuthority,
|
||||||
|
layout: &StateAnchorLayout<'_>,
|
||||||
) -> Result<(), StateAnchorCoordinatorError> {
|
) -> Result<(), StateAnchorCoordinatorError> {
|
||||||
require_mount_absent(&authority.merged)?;
|
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"),
|
||||||
|
)?;
|
||||||
|
if path_from_record(expected_merged) != authority.merged_label {
|
||||||
|
return Err(StateAnchorCoordinatorError::InvalidState(
|
||||||
|
"release authority layout changed",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
require_mount_absent(layout)?;
|
||||||
let anchor = self.participant(authority.generation_id, ParticipantRole::StateAnchor)?;
|
let anchor = self.participant(authority.generation_id, ParticipantRole::StateAnchor)?;
|
||||||
let prepared = self.participant(authority.generation_id, ParticipantRole::PreparedLower)?;
|
let prepared = self.participant(authority.generation_id, ParticipantRole::PreparedLower)?;
|
||||||
match (anchor.state(), prepared.state()) {
|
match (anchor.state(), prepared.state()) {
|
||||||
|
|
@ -321,7 +381,7 @@ impl<'a> StateAnchorCoordinator<'a> {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.release_catalog_lease_if_present(authority)?;
|
self.release_catalog_lease_if_present(authority, layout)?;
|
||||||
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)?,
|
||||||
|
|
@ -354,6 +414,15 @@ impl<'a> StateAnchorCoordinator<'a> {
|
||||||
&self,
|
&self,
|
||||||
generation_id: GenerationId,
|
generation_id: GenerationId,
|
||||||
daemon: PersistedHostProcessOwnership,
|
daemon: PersistedHostProcessOwnership,
|
||||||
|
) -> Result<(), StateAnchorCoordinatorError> {
|
||||||
|
self.update_daemon_and_mount(generation_id, daemon, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_daemon_and_mount(
|
||||||
|
&self,
|
||||||
|
generation_id: GenerationId,
|
||||||
|
daemon: PersistedHostProcessOwnership,
|
||||||
|
mount: Option<MountIdentity>,
|
||||||
) -> Result<(), StateAnchorCoordinatorError> {
|
) -> Result<(), StateAnchorCoordinatorError> {
|
||||||
let current = self.participant(generation_id, ParticipantRole::StateAnchor)?;
|
let current = self.participant(generation_id, ParticipantRole::StateAnchor)?;
|
||||||
let Some(ParticipantOwnership::StateAnchor(ownership)) = current.ownership() else {
|
let Some(ParticipantOwnership::StateAnchor(ownership)) = current.ownership() else {
|
||||||
|
|
@ -368,6 +437,7 @@ impl<'a> StateAnchorCoordinator<'a> {
|
||||||
}
|
}
|
||||||
let next = StateAnchorOwnership {
|
let next = StateAnchorOwnership {
|
||||||
daemon,
|
daemon,
|
||||||
|
mount,
|
||||||
..ownership.clone()
|
..ownership.clone()
|
||||||
};
|
};
|
||||||
self.domain_store
|
self.domain_store
|
||||||
|
|
@ -380,6 +450,29 @@ impl<'a> StateAnchorCoordinator<'a> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn require_exact_daemon(
|
||||||
|
&self,
|
||||||
|
generation_id: GenerationId,
|
||||||
|
expected: PersistedHostProcessOwnership,
|
||||||
|
) -> Result<(), StateAnchorCoordinatorError> {
|
||||||
|
let current = self.participant(generation_id, ParticipantRole::StateAnchor)?;
|
||||||
|
let Some(ParticipantOwnership::StateAnchor(ownership)) = current.ownership() else {
|
||||||
|
return Err(StateAnchorCoordinatorError::InvalidState(
|
||||||
|
"State Anchor ownership is invalid",
|
||||||
|
));
|
||||||
|
};
|
||||||
|
if current.state() == ParticipantState::Intent
|
||||||
|
&& ownership.daemon == expected
|
||||||
|
&& ownership.mount.is_none()
|
||||||
|
{
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(StateAnchorCoordinatorError::InvalidState(
|
||||||
|
"State Anchor daemon ownership changed",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn participant(
|
fn participant(
|
||||||
&self,
|
&self,
|
||||||
generation_id: GenerationId,
|
generation_id: GenerationId,
|
||||||
|
|
@ -392,6 +485,7 @@ impl<'a> StateAnchorCoordinator<'a> {
|
||||||
fn release_catalog_lease_if_present(
|
fn release_catalog_lease_if_present(
|
||||||
&self,
|
&self,
|
||||||
authority: &StateAnchorReleaseAuthority,
|
authority: &StateAnchorReleaseAuthority,
|
||||||
|
layout: &StateAnchorLayout<'_>,
|
||||||
) -> Result<(), StateAnchorCoordinatorError> {
|
) -> Result<(), 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| {
|
||||||
|
|
@ -431,8 +525,7 @@ impl<'a> StateAnchorCoordinator<'a> {
|
||||||
"Domain release evidence is not committed",
|
"Domain release evidence is not committed",
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
require_mount_absent(&authority.merged)
|
require_mount_absent(layout).map_err(|error| CatalogError::Io(io::Error::other(error)))
|
||||||
.map_err(|error| CatalogError::Io(io::Error::other(error)))
|
|
||||||
})?;
|
})?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -477,7 +570,7 @@ fn release_authority(
|
||||||
domain_id: domain.domain_id,
|
domain_id: domain.domain_id,
|
||||||
generation_id: anchor.generation_id(),
|
generation_id: anchor.generation_id(),
|
||||||
},
|
},
|
||||||
merged: path_from_record(anchor_ownership.merged_path.as_ref().ok_or(
|
merged_label: path_from_record(anchor_ownership.merged_path.as_ref().ok_or(
|
||||||
StateAnchorCoordinatorError::InvalidState("State Anchor merged path is absent"),
|
StateAnchorCoordinatorError::InvalidState("State Anchor merged path is absent"),
|
||||||
)?)
|
)?)
|
||||||
.to_owned(),
|
.to_owned(),
|
||||||
|
|
@ -493,19 +586,42 @@ fn release_authority(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn require_absent_role(
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
enum AcquisitionCheckpoint {
|
||||||
|
Absent,
|
||||||
|
PreparedIntent,
|
||||||
|
Complete,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn acquisition_checkpoint(
|
||||||
participants: &[Participant],
|
participants: &[Participant],
|
||||||
generation_id: GenerationId,
|
generation_id: GenerationId,
|
||||||
participant_role: ParticipantRole,
|
expected_prepared_intent: &Participant,
|
||||||
) -> Result<(), StateAnchorCoordinatorError> {
|
expected_lease_held: &Participant,
|
||||||
if participants.iter().any(|participant| {
|
expected_anchor_intent: &Participant,
|
||||||
participant.generation_id() == generation_id && participant.role() == participant_role
|
) -> Result<AcquisitionCheckpoint, StateAnchorCoordinatorError> {
|
||||||
}) {
|
let prepared = participants.iter().find(|participant| {
|
||||||
Err(StateAnchorCoordinatorError::InvalidState(
|
participant.generation_id() == generation_id
|
||||||
"participant already exists",
|
&& participant.role() == ParticipantRole::PreparedLower
|
||||||
))
|
});
|
||||||
} else {
|
let anchor = participants.iter().find(|participant| {
|
||||||
Ok(())
|
participant.generation_id() == generation_id
|
||||||
|
&& participant.role() == ParticipantRole::StateAnchor
|
||||||
|
});
|
||||||
|
match (prepared, anchor) {
|
||||||
|
(None, None) => Ok(AcquisitionCheckpoint::Absent),
|
||||||
|
(Some(observed), None) if observed == expected_prepared_intent => {
|
||||||
|
Ok(AcquisitionCheckpoint::PreparedIntent)
|
||||||
|
}
|
||||||
|
(Some(observed_prepared), Some(observed_anchor))
|
||||||
|
if observed_prepared == expected_lease_held
|
||||||
|
&& observed_anchor == expected_anchor_intent =>
|
||||||
|
{
|
||||||
|
Ok(AcquisitionCheckpoint::Complete)
|
||||||
|
}
|
||||||
|
_ => Err(StateAnchorCoordinatorError::InvalidState(
|
||||||
|
"acquisition checkpoint ownership changed",
|
||||||
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -98,8 +98,57 @@ fn durable_phases_resume_release_without_collapsing_lease_order() {
|
||||||
);
|
);
|
||||||
assert!(coordinator.stage_daemon(wrong_coordinator).is_err());
|
assert!(coordinator.stage_daemon(wrong_coordinator).is_err());
|
||||||
coordinator.stage_daemon(staged).unwrap();
|
coordinator.stage_daemon(staged).unwrap();
|
||||||
let acknowledgment = coordinator.commit_daemon(staged).unwrap();
|
let acknowledgment = coordinator.acknowledge_staged_daemon(staged).unwrap();
|
||||||
assert!(acknowledgment.acknowledges(&staged));
|
assert!(acknowledgment.acknowledges(&staged));
|
||||||
|
let staged_anchor = coordinator
|
||||||
|
.participant(generation_id, ParticipantRole::StateAnchor)
|
||||||
|
.unwrap();
|
||||||
|
let ParticipantOwnership::StateAnchor(staged_ownership) = staged_anchor.ownership().unwrap()
|
||||||
|
else {
|
||||||
|
panic!("wrong ownership")
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
staged_ownership.daemon,
|
||||||
|
PersistedHostProcessOwnership::staged(staged.identity())
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
coordinator
|
||||||
|
.commit_prepared(
|
||||||
|
generation_id,
|
||||||
|
artifact.prepared_artifact_id,
|
||||||
|
MountIdentity {
|
||||||
|
mount_id: MountId::new(90),
|
||||||
|
device: DeviceId::new(91),
|
||||||
|
inode: InodeId::new(92),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.is_err()
|
||||||
|
);
|
||||||
|
let anchor = coordinator
|
||||||
|
.participant(generation_id, ParticipantRole::StateAnchor)
|
||||||
|
.unwrap();
|
||||||
|
let ParticipantOwnership::StateAnchor(ownership) = anchor.ownership().unwrap() else {
|
||||||
|
panic!("wrong ownership")
|
||||||
|
};
|
||||||
|
domain_store
|
||||||
|
.apply(&[DomainStoreMutation::PutParticipant(
|
||||||
|
participant(
|
||||||
|
generation_id,
|
||||||
|
ParticipantRole::StateAnchor,
|
||||||
|
ParticipantState::Intent,
|
||||||
|
ParticipantOwnership::StateAnchor(StateAnchorOwnership {
|
||||||
|
daemon: PersistedHostProcessOwnership::committed(staged.identity()),
|
||||||
|
mount: Some(MountIdentity {
|
||||||
|
mount_id: MountId::new(90),
|
||||||
|
device: DeviceId::new(91),
|
||||||
|
inode: InodeId::new(92),
|
||||||
|
}),
|
||||||
|
..ownership.clone()
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
)])
|
||||||
|
.unwrap();
|
||||||
coordinator
|
coordinator
|
||||||
.commit_prepared(
|
.commit_prepared(
|
||||||
generation_id,
|
generation_id,
|
||||||
|
|
@ -117,7 +166,7 @@ fn durable_phases_resume_release_without_collapsing_lease_order() {
|
||||||
ParticipantState::Prepared,
|
ParticipantState::Prepared,
|
||||||
);
|
);
|
||||||
|
|
||||||
let authority = coordinator.begin_release(generation_id).unwrap();
|
let authority = coordinator.begin_release(generation_id, &layout).unwrap();
|
||||||
assert_states(
|
assert_states(
|
||||||
&domain_store,
|
&domain_store,
|
||||||
ParticipantState::LeaseHeld,
|
ParticipantState::LeaseHeld,
|
||||||
|
|
@ -140,7 +189,7 @@ fn durable_phases_resume_release_without_collapsing_lease_order() {
|
||||||
])
|
])
|
||||||
.unwrap();
|
.unwrap();
|
||||||
coordinator
|
coordinator
|
||||||
.complete_release_after_unmount(&authority)
|
.complete_release_after_unmount(&authority, &layout)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_states(
|
assert_states(
|
||||||
&domain_store,
|
&domain_store,
|
||||||
|
|
@ -150,6 +199,211 @@ 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());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn acquisition_resumes_after_prepared_intent_reopen() {
|
||||||
|
let fixture = AcquisitionFixture::new(17);
|
||||||
|
let layout = StateAnchorLayout::create(&fixture.configuration).unwrap();
|
||||||
|
let store = fixture.open_store();
|
||||||
|
store
|
||||||
|
.apply(&[
|
||||||
|
DomainStoreMutation::PutParticipant(Participant::bare_intent(
|
||||||
|
fixture.generation_id,
|
||||||
|
ParticipantRole::PreparedLower,
|
||||||
|
)),
|
||||||
|
DomainStoreMutation::PutParticipant(fixture.prepared_intent()),
|
||||||
|
])
|
||||||
|
.unwrap();
|
||||||
|
drop(store);
|
||||||
|
|
||||||
|
let store = fixture.open_store();
|
||||||
|
let coordinator = StateAnchorCoordinator::new(&store, &fixture.catalog);
|
||||||
|
let lease = coordinator
|
||||||
|
.begin_acquisition(fixture.generation_id, &fixture.artifact, &layout)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
fixture.catalog.snapshot().unwrap().artifact_leases,
|
||||||
|
vec![lease]
|
||||||
|
);
|
||||||
|
assert_fixture_states(&store, fixture.generation_id);
|
||||||
|
assert_eq!(
|
||||||
|
coordinator
|
||||||
|
.begin_acquisition(fixture.generation_id, &fixture.artifact, &layout)
|
||||||
|
.unwrap(),
|
||||||
|
lease
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn acquisition_resumes_after_catalog_lease_reopen() {
|
||||||
|
let fixture = AcquisitionFixture::new(18);
|
||||||
|
let layout = StateAnchorLayout::create(&fixture.configuration).unwrap();
|
||||||
|
let store = fixture.open_store();
|
||||||
|
store
|
||||||
|
.apply(&[
|
||||||
|
DomainStoreMutation::PutParticipant(Participant::bare_intent(
|
||||||
|
fixture.generation_id,
|
||||||
|
ParticipantRole::PreparedLower,
|
||||||
|
)),
|
||||||
|
DomainStoreMutation::PutParticipant(fixture.prepared_intent()),
|
||||||
|
])
|
||||||
|
.unwrap();
|
||||||
|
let lease = fixture.lease();
|
||||||
|
fixture
|
||||||
|
.catalog
|
||||||
|
.acquire_lease_checked(&fixture.artifact, &lease)
|
||||||
|
.unwrap();
|
||||||
|
drop(store);
|
||||||
|
|
||||||
|
let store = fixture.open_store();
|
||||||
|
let coordinator = StateAnchorCoordinator::new(&store, &fixture.catalog);
|
||||||
|
assert_eq!(
|
||||||
|
coordinator
|
||||||
|
.begin_acquisition(fixture.generation_id, &fixture.artifact, &layout)
|
||||||
|
.unwrap(),
|
||||||
|
lease
|
||||||
|
);
|
||||||
|
assert_fixture_states(&store, fixture.generation_id);
|
||||||
|
|
||||||
|
let mut changed_artifact = fixture.artifact.clone();
|
||||||
|
changed_artifact.inode = InodeId::new(changed_artifact.inode.get() + 1);
|
||||||
|
assert!(
|
||||||
|
coordinator
|
||||||
|
.begin_acquisition(fixture.generation_id, &changed_artifact, &layout)
|
||||||
|
.is_err()
|
||||||
|
);
|
||||||
|
let other_tree = TestTree::new();
|
||||||
|
let other_configuration = configuration(&other_tree);
|
||||||
|
let other_layout = StateAnchorLayout::create(&other_configuration).unwrap();
|
||||||
|
assert!(
|
||||||
|
coordinator
|
||||||
|
.begin_acquisition(fixture.generation_id, &fixture.artifact, &other_layout)
|
||||||
|
.is_err()
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
coordinator
|
||||||
|
.begin_acquisition(GenerationId::new(999), &fixture.artifact, &layout)
|
||||||
|
.is_err()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn assert_fixture_states(store: &HostCoordinatorDomainStore, generation_id: GenerationId) {
|
||||||
|
let snapshot = store.snapshot().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
role(
|
||||||
|
&snapshot.participants,
|
||||||
|
generation_id,
|
||||||
|
ParticipantRole::PreparedLower,
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
.state(),
|
||||||
|
ParticipantState::LeaseHeld
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
role(
|
||||||
|
&snapshot.participants,
|
||||||
|
generation_id,
|
||||||
|
ParticipantRole::StateAnchor,
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
.state(),
|
||||||
|
ParticipantState::Intent
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AcquisitionFixture {
|
||||||
|
tree: TestTree,
|
||||||
|
configuration: lazaret_core::configuration::ResolvedDomainConfiguration,
|
||||||
|
artifact: PreparedArtifact,
|
||||||
|
catalog: Catalog,
|
||||||
|
domain: Domain,
|
||||||
|
generation_id: GenerationId,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AcquisitionFixture {
|
||||||
|
fn new(generation: u64) -> Self {
|
||||||
|
let tree = TestTree::new();
|
||||||
|
let configuration = configuration(&tree);
|
||||||
|
let lower = tree.path.join("prepared-lower");
|
||||||
|
fs::create_dir(&lower).unwrap();
|
||||||
|
let artifact = artifact(&lower);
|
||||||
|
let catalog = Catalog::new(
|
||||||
|
tree.path.join("catalog.redb"),
|
||||||
|
tree.path.join("storage-catalog.lock"),
|
||||||
|
);
|
||||||
|
catalog.initialize().unwrap();
|
||||||
|
publish(&catalog, &artifact);
|
||||||
|
let domain = Domain {
|
||||||
|
domain_id: configuration.domain_id(),
|
||||||
|
profile_name: configuration.selected_profile().to_owned(),
|
||||||
|
canonical_workspace: configuration.workspace().canonical_label().clone(),
|
||||||
|
};
|
||||||
|
let generation_id = GenerationId::new(generation);
|
||||||
|
let fixture = Self {
|
||||||
|
tree,
|
||||||
|
configuration,
|
||||||
|
artifact,
|
||||||
|
catalog,
|
||||||
|
domain,
|
||||||
|
generation_id,
|
||||||
|
};
|
||||||
|
let store = fixture.open_store();
|
||||||
|
store
|
||||||
|
.apply(&[DomainStoreMutation::PutGeneration(Generation {
|
||||||
|
domain_id: fixture.domain.domain_id,
|
||||||
|
generation_id,
|
||||||
|
configuration_fingerprint: ConfigurationFingerprint::new([2; 32]),
|
||||||
|
coordinator_id: CoordinatorId::new([3; 32]),
|
||||||
|
network_sidecar_id: NetworkSidecarId::new([4; 32]),
|
||||||
|
sandbox_id: SandboxId::new([5; 32]),
|
||||||
|
durable_decision: DurableDecision::None,
|
||||||
|
})])
|
||||||
|
.unwrap();
|
||||||
|
drop(store);
|
||||||
|
fixture
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_store(&self) -> HostCoordinatorDomainStore {
|
||||||
|
let lock_file = OpenOptions::new()
|
||||||
|
.create(true)
|
||||||
|
.read(true)
|
||||||
|
.truncate(false)
|
||||||
|
.write(true)
|
||||||
|
.open(self.tree.path.join("domain.lock"))
|
||||||
|
.unwrap();
|
||||||
|
HostCoordinatorDomainStore::open(
|
||||||
|
try_exclusive_flock(lock_file).unwrap(),
|
||||||
|
&self.tree.path.join("domain.redb"),
|
||||||
|
&self.domain,
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lease(&self) -> ArtifactLease {
|
||||||
|
ArtifactLease {
|
||||||
|
prepared_artifact_id: self.artifact.prepared_artifact_id,
|
||||||
|
domain_id: self.domain.domain_id,
|
||||||
|
generation_id: self.generation_id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepared_intent(&self) -> Participant {
|
||||||
|
participant(
|
||||||
|
self.generation_id,
|
||||||
|
ParticipantRole::PreparedLower,
|
||||||
|
ParticipantState::Intent,
|
||||||
|
ParticipantOwnership::PreparedLower(PreparedLowerOwnership {
|
||||||
|
prepared_artifact_id: Some(self.artifact.prepared_artifact_id),
|
||||||
|
source_identity: Some(self.artifact.source_identity.clone()),
|
||||||
|
lease_state: Some(ParticipantState::Intent),
|
||||||
|
path: Some(self.artifact.path.clone()),
|
||||||
|
device: Some(self.artifact.device),
|
||||||
|
inode: Some(self.artifact.inode),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn assert_states(
|
fn assert_states(
|
||||||
store: &HostCoordinatorDomainStore,
|
store: &HostCoordinatorDomainStore,
|
||||||
prepared: ParticipantState,
|
prepared: ParticipantState,
|
||||||
|
|
|
||||||
|
|
@ -2,28 +2,33 @@
|
||||||
|
|
||||||
use std::ffi::OsString;
|
use std::ffi::OsString;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::fs::{File, OpenOptions};
|
use std::fs::File;
|
||||||
use std::io;
|
use std::io::{self, Write};
|
||||||
use std::os::fd::{AsFd, OwnedFd};
|
use std::os::fd::{AsFd, AsRawFd, OwnedFd};
|
||||||
use std::os::unix::ffi::{OsStrExt, OsStringExt};
|
use std::os::unix::ffi::{OsStrExt, OsStringExt};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
use std::thread;
|
use std::thread;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use lazaret_core::configuration::ResolvedDomainConfiguration;
|
use lazaret_core::configuration::{ConfinedPathPlan, ResolvedDomainConfiguration};
|
||||||
use lazaret_core::identity::{HostPath, MountIdentity, PreparedArtifactId};
|
use lazaret_core::identity::{HostPath, MountIdentity, PreparedArtifactId};
|
||||||
use lazaret_core::records::{
|
use lazaret_core::records::{
|
||||||
HostProcessOwnershipSlot, PersistedHostProcessOwnership, PreparedArtifact,
|
HostProcessOwnershipSlot, PersistedHostProcessOwnership, PreparedArtifact,
|
||||||
ProcessOwnershipCommitAck, StagedProcessOwnership, StateAnchorOwnership,
|
ProcessOwnershipCommitAck, StagedProcessOwnership, StateAnchorOwnership,
|
||||||
};
|
};
|
||||||
use lazaret_linux::filesystem::{atomic_replace, current_mount_identity, require_mount_identity};
|
use lazaret_linux::filesystem::mount_identity;
|
||||||
use lazaret_linux::process::{
|
use lazaret_linux::process::{
|
||||||
ProcessOwnershipContext, RetainedProcess, TargetCommand, child_broker,
|
ProcessOwnershipContext, RetainedProcess, TargetCommand, child_broker,
|
||||||
};
|
};
|
||||||
use rustix::fs::{FileType, Mode, OFlags, ResolveFlags, mkdirat, openat2};
|
use rustix::fs::{
|
||||||
|
AtFlags, FileType, Mode, OFlags, ResolveFlags, mkdirat, openat2, renameat, unlinkat,
|
||||||
|
};
|
||||||
|
use rustix::io::dup;
|
||||||
|
|
||||||
const MOUNT_TIMEOUT: Duration = Duration::from_secs(5);
|
const MOUNT_TIMEOUT: Duration = Duration::from_secs(5);
|
||||||
const MOUNT_POLL_INTERVAL: Duration = Duration::from_millis(2);
|
const MOUNT_POLL_INTERVAL: Duration = Duration::from_millis(2);
|
||||||
|
static TEMPORARY_SEQUENCE: AtomicU64 = AtomicU64::new(0);
|
||||||
pub const PROTECTED_RUNTIME_FILES: [&str; 3] = ["resolv.conf", "hosts", "hostname"];
|
pub const PROTECTED_RUNTIME_FILES: [&str; 3] = ["resolv.conf", "hosts", "hostname"];
|
||||||
|
|
||||||
pub struct ProtectedRuntimeFiles<'a> {
|
pub struct ProtectedRuntimeFiles<'a> {
|
||||||
|
|
@ -74,22 +79,35 @@ impl From<io::Error> for StateAnchorError {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct StateAnchorLayout {
|
pub struct StateAnchorLayout<'configuration> {
|
||||||
|
persistent_plan: &'configuration ConfinedPathPlan,
|
||||||
|
runtime_plan: &'configuration ConfinedPathPlan,
|
||||||
persistent_root: OwnedFd,
|
persistent_root: OwnedFd,
|
||||||
runtime_root: OwnedFd,
|
runtime_root: OwnedFd,
|
||||||
|
persistent_anchor: OwnedFd,
|
||||||
|
runtime_anchor: OwnedFd,
|
||||||
|
upper_descriptor: OwnedFd,
|
||||||
|
work_descriptor: OwnedFd,
|
||||||
|
merged_descriptor: OwnedFd,
|
||||||
|
runtime_files_descriptor: OwnedFd,
|
||||||
upper: PathBuf,
|
upper: PathBuf,
|
||||||
work: PathBuf,
|
work: PathBuf,
|
||||||
merged: PathBuf,
|
merged: PathBuf,
|
||||||
runtime_files: PathBuf,
|
runtime_files: PathBuf,
|
||||||
|
upper_label: PathBuf,
|
||||||
|
work_label: PathBuf,
|
||||||
|
merged_label: PathBuf,
|
||||||
persistent_destinations: Vec<PathBuf>,
|
persistent_destinations: Vec<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StateAnchorLayout {
|
impl<'configuration> StateAnchorLayout<'configuration> {
|
||||||
/// Creates the fixed layout beneath retained, revalidated Domain authorities.
|
/// Creates the fixed layout beneath retained, revalidated Domain authorities.
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// Refuses changed roots, non-directory fixed paths, and unsafe persistent destinations.
|
/// Refuses changed roots, non-directory fixed paths, and unsafe persistent destinations.
|
||||||
pub fn create(configuration: &ResolvedDomainConfiguration) -> Result<Self, StateAnchorError> {
|
pub fn create(
|
||||||
|
configuration: &'configuration ResolvedDomainConfiguration,
|
||||||
|
) -> Result<Self, StateAnchorError> {
|
||||||
let persistent_root = configuration
|
let persistent_root = configuration
|
||||||
.persistent_domain_root()
|
.persistent_domain_root()
|
||||||
.open_authority()
|
.open_authority()
|
||||||
|
|
@ -98,9 +116,6 @@ impl StateAnchorLayout {
|
||||||
.runtime_domain_root()
|
.runtime_domain_root()
|
||||||
.open_authority()
|
.open_authority()
|
||||||
.map_err(|_| StateAnchorError::InvalidLayout("runtime Domain authority changed"))?;
|
.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")?;
|
create_directory(&persistent_root, "state-anchor")?;
|
||||||
let persistent_anchor = open_directory(&persistent_root, Path::new("state-anchor"))?;
|
let persistent_anchor = open_directory(&persistent_root, Path::new("state-anchor"))?;
|
||||||
for name in ["upper", "work"] {
|
for name in ["upper", "work"] {
|
||||||
|
|
@ -111,19 +126,18 @@ impl StateAnchorLayout {
|
||||||
for name in ["merged", "runtime-files"] {
|
for name in ["merged", "runtime-files"] {
|
||||||
create_directory(&runtime_anchor, name)?;
|
create_directory(&runtime_anchor, name)?;
|
||||||
}
|
}
|
||||||
let runtime_files = runtime_base.join("state-anchor/runtime-files");
|
let upper_descriptor = open_directory(&persistent_anchor, Path::new("upper"))?;
|
||||||
|
let work_descriptor = open_directory(&persistent_anchor, Path::new("work"))?;
|
||||||
|
let merged_descriptor = open_directory(&runtime_anchor, Path::new("merged"))?;
|
||||||
|
let runtime_files_descriptor = open_directory(&runtime_anchor, Path::new("runtime-files"))?;
|
||||||
for name in PROTECTED_RUNTIME_FILES {
|
for name in PROTECTED_RUNTIME_FILES {
|
||||||
let path = runtime_files.join(name);
|
create_regular_file(&runtime_files_descriptor, 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(&runtime_anchor).map_err(io::Error::from)?;
|
||||||
rustix::fs::fsync(&persistent_anchor).map_err(io::Error::from)?;
|
rustix::fs::fsync(&persistent_anchor).map_err(io::Error::from)?;
|
||||||
|
let persistent_base =
|
||||||
|
path_from_host(configuration.persistent_domain_root().canonical_label());
|
||||||
|
let runtime_base = path_from_host(configuration.runtime_domain_root().canonical_label());
|
||||||
let persistent_destinations = configuration
|
let persistent_destinations = configuration
|
||||||
.fingerprint_record()
|
.fingerprint_record()
|
||||||
.persistent_dirs
|
.persistent_dirs
|
||||||
|
|
@ -131,12 +145,23 @@ impl StateAnchorLayout {
|
||||||
.map(|destination| PathBuf::from(destination.strip_prefix('/').unwrap_or(destination)))
|
.map(|destination| PathBuf::from(destination.strip_prefix('/').unwrap_or(destination)))
|
||||||
.collect();
|
.collect();
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
|
persistent_plan: configuration.persistent_domain_root(),
|
||||||
|
runtime_plan: configuration.runtime_domain_root(),
|
||||||
persistent_root,
|
persistent_root,
|
||||||
runtime_root,
|
runtime_root,
|
||||||
upper: persistent_base.join("state-anchor/upper"),
|
upper: descriptor_child_path(&persistent_anchor, "upper"),
|
||||||
work: persistent_base.join("state-anchor/work"),
|
work: descriptor_child_path(&persistent_anchor, "work"),
|
||||||
merged: runtime_base.join("state-anchor/merged"),
|
merged: descriptor_child_path(&runtime_anchor, "merged"),
|
||||||
runtime_files,
|
runtime_files: descriptor_child_path(&runtime_anchor, "runtime-files"),
|
||||||
|
persistent_anchor,
|
||||||
|
runtime_anchor,
|
||||||
|
upper_descriptor,
|
||||||
|
work_descriptor,
|
||||||
|
merged_descriptor,
|
||||||
|
runtime_files_descriptor,
|
||||||
|
upper_label: persistent_base.join("state-anchor/upper"),
|
||||||
|
work_label: persistent_base.join("state-anchor/work"),
|
||||||
|
merged_label: runtime_base.join("state-anchor/merged"),
|
||||||
persistent_destinations,
|
persistent_destinations,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -171,18 +196,59 @@ impl StateAnchorLayout {
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// Refuses replacement, symlink, mount crossing, or a non-directory fixed path.
|
/// Refuses replacement, symlink, mount crossing, or a non-directory fixed path.
|
||||||
pub fn revalidate(&self) -> Result<(), StateAnchorError> {
|
pub fn revalidate(&self) -> Result<(), StateAnchorError> {
|
||||||
for path in ["state-anchor/upper", "state-anchor/work"] {
|
self.revalidate_authorities()?;
|
||||||
drop(open_directory(&self.persistent_root, Path::new(path))?);
|
let merged = open_directory(&self.runtime_anchor, Path::new("merged"))?;
|
||||||
|
require_same_descriptor(&merged, &self.merged_descriptor)
|
||||||
}
|
}
|
||||||
for path in ["state-anchor/merged", "state-anchor/runtime-files"] {
|
|
||||||
drop(open_directory(&self.runtime_root, Path::new(path))?);
|
fn revalidate_authorities(&self) -> Result<(), StateAnchorError> {
|
||||||
|
let persistent_root = self
|
||||||
|
.persistent_plan
|
||||||
|
.open_authority()
|
||||||
|
.map_err(|_| StateAnchorError::InvalidLayout("persistent Domain authority changed"))?;
|
||||||
|
let runtime_root = self
|
||||||
|
.runtime_plan
|
||||||
|
.open_authority()
|
||||||
|
.map_err(|_| StateAnchorError::InvalidLayout("runtime Domain authority changed"))?;
|
||||||
|
require_same_descriptor(&persistent_root, &self.persistent_root)?;
|
||||||
|
require_same_descriptor(&runtime_root, &self.runtime_root)?;
|
||||||
|
let persistent_anchor = open_directory(&self.persistent_root, Path::new("state-anchor"))?;
|
||||||
|
let runtime_anchor = open_directory(&self.runtime_root, Path::new("state-anchor"))?;
|
||||||
|
require_same_descriptor(&persistent_anchor, &self.persistent_anchor)?;
|
||||||
|
require_same_descriptor(&runtime_anchor, &self.runtime_anchor)?;
|
||||||
|
for (name, expected) in [
|
||||||
|
("upper", &self.upper_descriptor),
|
||||||
|
("work", &self.work_descriptor),
|
||||||
|
] {
|
||||||
|
let observed = open_directory(&self.persistent_anchor, Path::new(name))?;
|
||||||
|
require_same_descriptor(&observed, expected)?;
|
||||||
}
|
}
|
||||||
|
let runtime_files = open_directory(&self.runtime_anchor, Path::new("runtime-files"))?;
|
||||||
|
require_same_descriptor(&runtime_files, &self.runtime_files_descriptor)?;
|
||||||
for name in PROTECTED_RUNTIME_FILES {
|
for name in PROTECTED_RUNTIME_FILES {
|
||||||
require_regular_file(&self.runtime_files.join(name))?;
|
drop(open_regular_file(&self.runtime_files_descriptor, name)?);
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn current_merged_identity(&self) -> Result<MountIdentity, StateAnchorError> {
|
||||||
|
self.revalidate_authorities()?;
|
||||||
|
let merged = open_mounted_directory(&self.runtime_anchor, Path::new("merged"))?;
|
||||||
|
mount_identity(&merged).map_err(StateAnchorError::from)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn require_mounted(&self, expected: MountIdentity) -> Result<(), StateAnchorError> {
|
||||||
|
if self.current_merged_identity()? == expected {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(StateAnchorError::OwnershipMismatch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn require_unmounted(&self) -> Result<(), StateAnchorError> {
|
||||||
|
self.revalidate()
|
||||||
|
}
|
||||||
|
|
||||||
/// Atomically replaces sandbox runtime files in host-private transient storage.
|
/// Atomically replaces sandbox runtime files in host-private transient storage.
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
|
|
@ -191,32 +257,43 @@ impl StateAnchorLayout {
|
||||||
&self,
|
&self,
|
||||||
files: &ProtectedRuntimeFiles<'_>,
|
files: &ProtectedRuntimeFiles<'_>,
|
||||||
) -> Result<(), StateAnchorError> {
|
) -> Result<(), StateAnchorError> {
|
||||||
self.revalidate()?;
|
self.revalidate_authorities()?;
|
||||||
for (name, bytes) in [
|
for (name, bytes) in [
|
||||||
("resolv.conf", files.resolv_conf),
|
("resolv.conf", files.resolv_conf),
|
||||||
("hosts", files.hosts),
|
("hosts", files.hosts),
|
||||||
("hostname", files.hostname),
|
("hostname", files.hostname),
|
||||||
] {
|
] {
|
||||||
atomic_replace(&self.runtime_files.join(name), bytes)?;
|
atomic_replace_at(&self.runtime_files_descriptor, name, bytes)?;
|
||||||
}
|
}
|
||||||
self.revalidate()
|
self.revalidate_authorities()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn ownership_intent(&self, artifact_id: PreparedArtifactId) -> StateAnchorOwnership {
|
pub fn ownership_intent(&self, artifact_id: PreparedArtifactId) -> StateAnchorOwnership {
|
||||||
StateAnchorOwnership {
|
StateAnchorOwnership {
|
||||||
lower_artifact_id: Some(artifact_id),
|
lower_artifact_id: Some(artifact_id),
|
||||||
upper_path: Some(host_path(&self.upper)),
|
upper_path: Some(host_path(&self.upper_label)),
|
||||||
work_path: Some(host_path(&self.work)),
|
work_path: Some(host_path(&self.work_label)),
|
||||||
merged_path: Some(host_path(&self.merged)),
|
merged_path: Some(host_path(&self.merged_label)),
|
||||||
daemon: PersistedHostProcessOwnership::unassigned(),
|
daemon: PersistedHostProcessOwnership::unassigned(),
|
||||||
mount: None,
|
mount: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn matches_recorded_paths(&self, ownership: &StateAnchorOwnership) -> bool {
|
||||||
|
ownership.upper_path.as_ref() == Some(&host_path(&self.upper_label))
|
||||||
|
&& ownership.work_path.as_ref() == Some(&host_path(&self.work_label))
|
||||||
|
&& ownership.merged_path.as_ref() == Some(&host_path(&self.merged_label))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Pins the exact mounted and lower authorities until the view is dropped before unmount.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct StateAnchorSandboxView {
|
pub struct StateAnchorSandboxView<'layout, 'configuration> {
|
||||||
|
layout: &'layout StateAnchorLayout<'configuration>,
|
||||||
|
lower_descriptor: OwnedFd,
|
||||||
|
merged_descriptor: OwnedFd,
|
||||||
lower: PathBuf,
|
lower: PathBuf,
|
||||||
lower_identity: (u64, u64),
|
lower_identity: (u64, u64),
|
||||||
merged: PathBuf,
|
merged: PathBuf,
|
||||||
|
|
@ -225,25 +302,34 @@ pub struct StateAnchorSandboxView {
|
||||||
persistent_destinations: Vec<PathBuf>,
|
persistent_destinations: Vec<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StateAnchorSandboxView {
|
impl<'layout, 'configuration> StateAnchorSandboxView<'layout, 'configuration> {
|
||||||
/// Binds a serviceable State Anchor view to exact prepared-lower and mount evidence.
|
/// Binds a serviceable State Anchor view to exact prepared-lower and mount evidence.
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// Refuses changed layout, prepared lower, or mount identity.
|
/// Refuses changed layout, prepared lower, or mount identity.
|
||||||
pub fn from_mounted(
|
pub fn from_mounted(
|
||||||
layout: &StateAnchorLayout,
|
layout: &'layout StateAnchorLayout<'configuration>,
|
||||||
artifact: &PreparedArtifact,
|
artifact: &PreparedArtifact,
|
||||||
mount: MountIdentity,
|
mount: MountIdentity,
|
||||||
) -> Result<Self, StateAnchorError> {
|
) -> Result<Self, StateAnchorError> {
|
||||||
|
let lower_descriptor = open_exact_directory_path(
|
||||||
|
&path_from_host(&artifact.path),
|
||||||
|
(artifact.device.get(), artifact.inode.get()),
|
||||||
|
)?;
|
||||||
|
layout.require_mounted(mount)?;
|
||||||
|
let merged_descriptor =
|
||||||
|
open_mounted_directory(&layout.runtime_anchor, Path::new("merged"))?;
|
||||||
let view = Self {
|
let view = Self {
|
||||||
lower: path_from_host(&artifact.path),
|
layout,
|
||||||
|
lower: descriptor_path(&lower_descriptor),
|
||||||
lower_identity: (artifact.device.get(), artifact.inode.get()),
|
lower_identity: (artifact.device.get(), artifact.inode.get()),
|
||||||
merged: layout.merged.clone(),
|
merged: descriptor_path(&merged_descriptor),
|
||||||
mount,
|
mount,
|
||||||
runtime_files: layout.runtime_files.clone(),
|
runtime_files: descriptor_path(&layout.runtime_files_descriptor),
|
||||||
persistent_destinations: layout.persistent_destinations.clone(),
|
persistent_destinations: layout.persistent_destinations.clone(),
|
||||||
|
lower_descriptor,
|
||||||
|
merged_descriptor,
|
||||||
};
|
};
|
||||||
layout.revalidate()?;
|
|
||||||
view.revalidate()?;
|
view.revalidate()?;
|
||||||
Ok(view)
|
Ok(view)
|
||||||
}
|
}
|
||||||
|
|
@ -253,11 +339,16 @@ impl StateAnchorSandboxView {
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// Refuses replacement or unreadable physical evidence.
|
/// Refuses replacement or unreadable physical evidence.
|
||||||
pub fn revalidate(&self) -> Result<(), StateAnchorError> {
|
pub fn revalidate(&self) -> Result<(), StateAnchorError> {
|
||||||
require_directory_identity(&self.lower, self.lower_identity)?;
|
self.layout.require_mounted(self.mount)?;
|
||||||
require_mount_identity(&self.merged, self.mount)
|
require_directory_descriptor_identity(&self.lower_descriptor, self.lower_identity)?;
|
||||||
.map_err(|_| StateAnchorError::OwnershipMismatch)?;
|
if mount_identity(&self.merged_descriptor)? != self.mount {
|
||||||
|
return Err(StateAnchorError::OwnershipMismatch);
|
||||||
|
}
|
||||||
for name in PROTECTED_RUNTIME_FILES {
|
for name in PROTECTED_RUNTIME_FILES {
|
||||||
require_regular_file(&self.runtime_files.join(name))?;
|
drop(open_regular_file(
|
||||||
|
&self.layout.runtime_files_descriptor,
|
||||||
|
name,
|
||||||
|
)?);
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -283,59 +374,120 @@ impl StateAnchorSandboxView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct FuseOverlayfsMount {
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
process: RetainedProcess,
|
pub struct VerifiedStateAnchorReadiness {
|
||||||
|
staged: StagedProcessOwnership,
|
||||||
mount: MountIdentity,
|
mount: MountIdentity,
|
||||||
merged: PathBuf,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FuseOverlayfsMount {
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub struct StateAnchorAcquisition<'path> {
|
||||||
|
pub lower: &'path Path,
|
||||||
|
pub expected_lower: (u64, u64),
|
||||||
|
pub expected_parent: lazaret_core::identity::PidfsProcessIdentityV1,
|
||||||
|
pub ownership: ProcessOwnershipContext,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VerifiedStateAnchorReadiness {
|
||||||
|
#[must_use]
|
||||||
|
pub const fn staged(self) -> StagedProcessOwnership {
|
||||||
|
self.staged
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub const fn mount(self) -> MountIdentity {
|
||||||
|
self.mount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct FuseOverlayfsMount<'layout, 'configuration> {
|
||||||
|
process: RetainedProcess,
|
||||||
|
mount: MountIdentity,
|
||||||
|
layout: &'layout StateAnchorLayout<'configuration>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'layout, 'configuration> FuseOverlayfsMount<'layout, 'configuration> {
|
||||||
/// Starts fuse-overlayfs through causal child acquisition and records staged/committed identity
|
/// Starts fuse-overlayfs through causal child acquisition and records staged/committed identity
|
||||||
/// through caller-owned durable callbacks.
|
/// through caller-owned durable callbacks.
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// Refuses changed layout or lower identity, callback failure, daemon loss, and mount timeout.
|
/// Refuses changed layout or lower identity, callback failure, daemon loss, and mount timeout.
|
||||||
pub fn acquire<Stage, Acknowledge>(
|
pub fn acquire<Stage, Acknowledge, Ready>(
|
||||||
layout: &StateAnchorLayout,
|
layout: &'layout StateAnchorLayout<'configuration>,
|
||||||
lower: &Path,
|
acquisition: StateAnchorAcquisition<'_>,
|
||||||
expected_lower: (u64, u64),
|
|
||||||
expected_parent: lazaret_core::identity::PidfsProcessIdentityV1,
|
|
||||||
ownership: ProcessOwnershipContext,
|
|
||||||
stage: Stage,
|
stage: Stage,
|
||||||
acknowledge: Acknowledge,
|
acknowledge: Acknowledge,
|
||||||
|
ready: Ready,
|
||||||
) -> Result<Self, StateAnchorError>
|
) -> Result<Self, StateAnchorError>
|
||||||
where
|
where
|
||||||
Stage: FnOnce(StagedProcessOwnership) -> io::Result<()>,
|
Stage: FnOnce(StagedProcessOwnership) -> io::Result<()>,
|
||||||
Acknowledge: FnOnce(StagedProcessOwnership) -> io::Result<ProcessOwnershipCommitAck>,
|
Acknowledge: FnOnce(StagedProcessOwnership) -> io::Result<ProcessOwnershipCommitAck>,
|
||||||
|
Ready: FnOnce(VerifiedStateAnchorReadiness) -> io::Result<()>,
|
||||||
{
|
{
|
||||||
if ownership.slot != HostProcessOwnershipSlot::StateAnchorDaemon {
|
if acquisition.ownership.slot != HostProcessOwnershipSlot::StateAnchorDaemon {
|
||||||
return Err(StateAnchorError::InvalidLayout(
|
return Err(StateAnchorError::InvalidLayout(
|
||||||
"process slot is not State Anchor daemon",
|
"process slot is not State Anchor daemon",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
layout.revalidate()?;
|
layout.revalidate()?;
|
||||||
require_directory_identity(lower, expected_lower)?;
|
let lower_descriptor =
|
||||||
let baseline = current_mount_identity(layout.merged())?;
|
open_exact_directory_path(acquisition.lower, acquisition.expected_lower)?;
|
||||||
let options = overlay_options(lower, layout.upper(), layout.work());
|
let lower_launch = inherit_for_exec(&lower_descriptor)?;
|
||||||
|
let persistent_launch = inherit_for_exec(&layout.persistent_anchor)?;
|
||||||
|
let runtime_launch = inherit_for_exec(&layout.runtime_anchor)?;
|
||||||
|
let lower_path = descriptor_path(&lower_launch);
|
||||||
|
let upper_path = descriptor_child_path(&persistent_launch, "upper");
|
||||||
|
let work_path = descriptor_child_path(&persistent_launch, "work");
|
||||||
|
let merged_path = descriptor_child_path(&runtime_launch, "merged");
|
||||||
|
let baseline = layout.current_merged_identity()?;
|
||||||
|
if baseline != mount_identity(&layout.merged_descriptor)? {
|
||||||
|
return Err(StateAnchorError::OwnershipMismatch);
|
||||||
|
}
|
||||||
|
let options = overlay_options(&lower_path, &upper_path, &work_path);
|
||||||
let target = TargetCommand::new("fuse-overlayfs")
|
let target = TargetCommand::new("fuse-overlayfs")
|
||||||
.argument("-f")
|
.argument("-f")
|
||||||
.argument("-o")
|
.argument("-o")
|
||||||
.argument(options)
|
.argument(options)
|
||||||
.argument(layout.merged().as_os_str());
|
.argument(merged_path.as_os_str());
|
||||||
let process =
|
let process = child_broker().acquire(
|
||||||
child_broker().acquire(expected_parent, ownership, &target, stage, acknowledge)?;
|
acquisition.expected_parent,
|
||||||
|
acquisition.ownership,
|
||||||
|
&target,
|
||||||
|
stage,
|
||||||
|
acknowledge,
|
||||||
|
)?;
|
||||||
|
let staged = StagedProcessOwnership::v1(
|
||||||
|
acquisition.ownership.domain_id,
|
||||||
|
acquisition.ownership.generation_id,
|
||||||
|
acquisition.ownership.coordinator_id,
|
||||||
|
acquisition.ownership.slot,
|
||||||
|
process.identity(),
|
||||||
|
);
|
||||||
let deadline = Instant::now() + MOUNT_TIMEOUT;
|
let deadline = Instant::now() + MOUNT_TIMEOUT;
|
||||||
loop {
|
loop {
|
||||||
if process.has_exited()? {
|
if process.has_exited()? {
|
||||||
child_broker().reap(process)?;
|
child_broker().reap(process)?;
|
||||||
return Err(StateAnchorError::DaemonExited);
|
return Err(StateAnchorError::DaemonExited);
|
||||||
}
|
}
|
||||||
let observed = current_mount_identity(layout.merged())?;
|
let observed = layout.current_merged_identity()?;
|
||||||
if observed.mount_id != baseline.mount_id {
|
if observed.mount_id != baseline.mount_id {
|
||||||
|
let readiness = VerifiedStateAnchorReadiness {
|
||||||
|
staged,
|
||||||
|
mount: observed,
|
||||||
|
};
|
||||||
|
if let Err(error) = ready(readiness) {
|
||||||
|
process.send_signal(rustix::process::Signal::KILL)?;
|
||||||
|
while !process.has_exited()? {
|
||||||
|
thread::yield_now();
|
||||||
|
}
|
||||||
|
rootless_unmount(layout)?;
|
||||||
|
child_broker().reap(process)?;
|
||||||
|
return Err(error.into());
|
||||||
|
}
|
||||||
return Ok(Self {
|
return Ok(Self {
|
||||||
process,
|
process,
|
||||||
mount: observed,
|
mount: observed,
|
||||||
merged: layout.merged().to_owned(),
|
layout,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if Instant::now() >= deadline {
|
if Instant::now() >= deadline {
|
||||||
|
|
@ -343,11 +495,11 @@ impl FuseOverlayfsMount {
|
||||||
while !process.has_exited()? {
|
while !process.has_exited()? {
|
||||||
thread::yield_now();
|
thread::yield_now();
|
||||||
}
|
}
|
||||||
if match current_mount_identity(layout.merged()) {
|
if match layout.current_merged_identity() {
|
||||||
Ok(identity) => identity.mount_id != baseline.mount_id,
|
Ok(identity) => identity.mount_id != baseline.mount_id,
|
||||||
Err(_) => true,
|
Err(_) => true,
|
||||||
} {
|
} {
|
||||||
let _ = rootless_unmount(layout.merged());
|
let _ = rootless_unmount(layout);
|
||||||
}
|
}
|
||||||
child_broker().reap(process)?;
|
child_broker().reap(process)?;
|
||||||
return Err(StateAnchorError::MountTimeout);
|
return Err(StateAnchorError::MountTimeout);
|
||||||
|
|
@ -376,11 +528,13 @@ impl FuseOverlayfsMount {
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// A missing, replaced, renamed, or remounted target is left untouched.
|
/// A missing, replaced, renamed, or remounted target is left untouched.
|
||||||
pub fn unmount(self) -> Result<(), StateAnchorError> {
|
pub fn unmount(self) -> Result<(), StateAnchorError> {
|
||||||
require_mount_record(&self.merged, self.mount)?;
|
self.layout.revalidate_authorities()?;
|
||||||
rootless_unmount(&self.merged)?;
|
require_mount_record(self.layout, self.mount)?;
|
||||||
if has_mount_record(&self.merged, self.mount)? {
|
rootless_unmount(self.layout)?;
|
||||||
|
if has_mount_record(self.layout, self.mount)? {
|
||||||
return Err(StateAnchorError::OwnershipMismatch);
|
return Err(StateAnchorError::OwnershipMismatch);
|
||||||
}
|
}
|
||||||
|
self.layout.require_unmounted()?;
|
||||||
child_broker().reap(self.process)?;
|
child_broker().reap(self.process)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -390,9 +544,11 @@ impl FuseOverlayfsMount {
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// Refuses while the exact recorded mount remains or when exact reaping fails.
|
/// Refuses while the exact recorded mount remains or when exact reaping fails.
|
||||||
pub fn finish_after_external_unmount(self) -> Result<(), StateAnchorError> {
|
pub fn finish_after_external_unmount(self) -> Result<(), StateAnchorError> {
|
||||||
if has_mount_record(&self.merged, self.mount)? {
|
self.layout.revalidate_authorities()?;
|
||||||
|
if has_mount_record(self.layout, self.mount)? {
|
||||||
return Err(StateAnchorError::OwnershipMismatch);
|
return Err(StateAnchorError::OwnershipMismatch);
|
||||||
}
|
}
|
||||||
|
self.layout.require_unmounted()?;
|
||||||
child_broker().reap(self.process)?;
|
child_broker().reap(self.process)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -403,14 +559,16 @@ impl FuseOverlayfsMount {
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// A missing or mismatched mount is left untouched.
|
/// A missing or mismatched mount is left untouched.
|
||||||
pub fn unmount_after_coordinator_loss(
|
pub fn unmount_after_coordinator_loss(
|
||||||
merged: &Path,
|
layout: &StateAnchorLayout<'_>,
|
||||||
expected: MountIdentity,
|
expected: MountIdentity,
|
||||||
) -> Result<(), StateAnchorError> {
|
) -> Result<(), StateAnchorError> {
|
||||||
require_mount_record(merged, expected)?;
|
layout.revalidate_authorities()?;
|
||||||
rootless_unmount(merged)?;
|
require_mount_record(layout, expected)?;
|
||||||
if has_mount_record(merged, expected)? {
|
rootless_unmount(layout)?;
|
||||||
|
if has_mount_record(layout, expected)? {
|
||||||
return Err(StateAnchorError::OwnershipMismatch);
|
return Err(StateAnchorError::OwnershipMismatch);
|
||||||
}
|
}
|
||||||
|
layout.require_unmounted()?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -418,16 +576,8 @@ pub fn unmount_after_coordinator_loss(
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// Refuses unreadable mount information or an inaccessible merged directory.
|
/// Refuses unreadable mount information or an inaccessible merged directory.
|
||||||
pub fn require_mount_absent(merged: &Path) -> Result<(), StateAnchorError> {
|
pub fn require_mount_absent(layout: &StateAnchorLayout<'_>) -> Result<(), StateAnchorError> {
|
||||||
current_mount_identity(merged)?;
|
layout.require_unmounted()
|
||||||
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> {
|
fn create_directory(parent: impl AsFd, name: &str) -> Result<(), StateAnchorError> {
|
||||||
|
|
@ -445,7 +595,10 @@ fn open_directory(parent: impl AsFd, path: &Path) -> Result<OwnedFd, StateAnchor
|
||||||
path,
|
path,
|
||||||
OFlags::RDONLY | OFlags::DIRECTORY | OFlags::CLOEXEC | OFlags::NOFOLLOW,
|
OFlags::RDONLY | OFlags::DIRECTORY | OFlags::CLOEXEC | OFlags::NOFOLLOW,
|
||||||
Mode::empty(),
|
Mode::empty(),
|
||||||
ResolveFlags::BENEATH | ResolveFlags::NO_SYMLINKS | ResolveFlags::NO_MAGICLINKS,
|
ResolveFlags::BENEATH
|
||||||
|
| ResolveFlags::NO_SYMLINKS
|
||||||
|
| ResolveFlags::NO_MAGICLINKS
|
||||||
|
| ResolveFlags::NO_XDEV,
|
||||||
)
|
)
|
||||||
.map_err(io::Error::from)?;
|
.map_err(io::Error::from)?;
|
||||||
let metadata = rustix::fs::fstat(&descriptor).map_err(io::Error::from)?;
|
let metadata = rustix::fs::fstat(&descriptor).map_err(io::Error::from)?;
|
||||||
|
|
@ -457,10 +610,88 @@ fn open_directory(parent: impl AsFd, path: &Path) -> Result<OwnedFd, StateAnchor
|
||||||
Ok(descriptor)
|
Ok(descriptor)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn require_regular_file(path: &Path) -> Result<(), StateAnchorError> {
|
fn open_mounted_directory(parent: impl AsFd, path: &Path) -> Result<OwnedFd, StateAnchorError> {
|
||||||
let metadata = path.symlink_metadata()?;
|
let descriptor = openat2(
|
||||||
if metadata.file_type().is_file() {
|
parent.as_fd(),
|
||||||
|
path,
|
||||||
|
OFlags::PATH | OFlags::DIRECTORY | OFlags::CLOEXEC | OFlags::NOFOLLOW,
|
||||||
|
Mode::empty(),
|
||||||
|
ResolveFlags::BENEATH | ResolveFlags::NO_SYMLINKS | ResolveFlags::NO_MAGICLINKS,
|
||||||
|
)
|
||||||
|
.map_err(io::Error::from)?;
|
||||||
|
Ok(descriptor)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_exact_directory_path(
|
||||||
|
path: &Path,
|
||||||
|
expected: (u64, u64),
|
||||||
|
) -> Result<OwnedFd, StateAnchorError> {
|
||||||
|
let descriptor = openat2(
|
||||||
|
rustix::fs::CWD,
|
||||||
|
path,
|
||||||
|
OFlags::PATH | OFlags::DIRECTORY | OFlags::CLOEXEC | OFlags::NOFOLLOW,
|
||||||
|
Mode::empty(),
|
||||||
|
ResolveFlags::NO_SYMLINKS | ResolveFlags::NO_MAGICLINKS,
|
||||||
|
)
|
||||||
|
.map_err(io::Error::from)?;
|
||||||
|
require_directory_descriptor_identity(&descriptor, expected)?;
|
||||||
|
Ok(descriptor)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn require_directory_descriptor_identity(
|
||||||
|
descriptor: impl AsFd,
|
||||||
|
expected: (u64, u64),
|
||||||
|
) -> Result<(), StateAnchorError> {
|
||||||
|
let metadata = rustix::fs::fstat(descriptor).map_err(io::Error::from)?;
|
||||||
|
if FileType::from_raw_mode(metadata.st_mode).is_dir()
|
||||||
|
&& (metadata.st_dev, metadata.st_ino) == expected
|
||||||
|
{
|
||||||
Ok(())
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(StateAnchorError::OwnershipMismatch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn require_same_descriptor(
|
||||||
|
observed: impl AsFd,
|
||||||
|
expected: impl AsFd,
|
||||||
|
) -> Result<(), StateAnchorError> {
|
||||||
|
let observed = mount_identity(observed)?;
|
||||||
|
let expected = mount_identity(expected)?;
|
||||||
|
if observed == expected {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(StateAnchorError::OwnershipMismatch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_regular_file(parent: impl AsFd, name: &str) -> Result<(), StateAnchorError> {
|
||||||
|
match openat2(
|
||||||
|
parent.as_fd(),
|
||||||
|
name,
|
||||||
|
OFlags::WRONLY | OFlags::CREATE | OFlags::EXCL | OFlags::CLOEXEC | OFlags::NOFOLLOW,
|
||||||
|
Mode::from_raw_mode(0o600),
|
||||||
|
confined_resolve_flags(),
|
||||||
|
) {
|
||||||
|
Ok(descriptor) => File::from(descriptor).sync_all()?,
|
||||||
|
Err(rustix::io::Errno::EXIST) => drop(open_regular_file(parent, name)?),
|
||||||
|
Err(error) => return Err(io::Error::from(error).into()),
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_regular_file(parent: impl AsFd, name: &str) -> Result<OwnedFd, StateAnchorError> {
|
||||||
|
let descriptor = openat2(
|
||||||
|
parent.as_fd(),
|
||||||
|
name,
|
||||||
|
OFlags::PATH | OFlags::CLOEXEC | OFlags::NOFOLLOW,
|
||||||
|
Mode::empty(),
|
||||||
|
confined_resolve_flags(),
|
||||||
|
)
|
||||||
|
.map_err(io::Error::from)?;
|
||||||
|
let metadata = rustix::fs::fstat(&descriptor).map_err(io::Error::from)?;
|
||||||
|
if FileType::from_raw_mode(metadata.st_mode).is_file() {
|
||||||
|
Ok(descriptor)
|
||||||
} else {
|
} else {
|
||||||
Err(StateAnchorError::InvalidLayout(
|
Err(StateAnchorError::InvalidLayout(
|
||||||
"protected runtime path is not a regular file",
|
"protected runtime path is not a regular file",
|
||||||
|
|
@ -468,15 +699,62 @@ fn require_regular_file(path: &Path) -> Result<(), StateAnchorError> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn require_directory_identity(path: &Path, expected: (u64, u64)) -> Result<(), StateAnchorError> {
|
fn atomic_replace_at(parent: impl AsFd, name: &str, bytes: &[u8]) -> Result<(), StateAnchorError> {
|
||||||
use std::os::unix::fs::MetadataExt;
|
let temporary_name = loop {
|
||||||
let file = File::open(path)?;
|
let sequence = TEMPORARY_SEQUENCE.fetch_add(1, Ordering::Relaxed);
|
||||||
let metadata = file.metadata()?;
|
let candidate =
|
||||||
if metadata.is_dir() && (metadata.dev(), metadata.ino()) == expected {
|
OsString::from(format!(".{name}.lazaret-{}-{sequence}", std::process::id()));
|
||||||
Ok(())
|
match openat2(
|
||||||
} else {
|
parent.as_fd(),
|
||||||
Err(StateAnchorError::OwnershipMismatch)
|
Path::new(&candidate),
|
||||||
|
OFlags::WRONLY | OFlags::CREATE | OFlags::EXCL | OFlags::CLOEXEC | OFlags::NOFOLLOW,
|
||||||
|
Mode::from_raw_mode(0o600),
|
||||||
|
confined_resolve_flags(),
|
||||||
|
) {
|
||||||
|
Ok(descriptor) => {
|
||||||
|
let mut file = File::from(descriptor);
|
||||||
|
if let Err(error) = file.write_all(bytes).and_then(|()| file.sync_all()) {
|
||||||
|
let _ = unlinkat(parent.as_fd(), Path::new(&candidate), AtFlags::empty());
|
||||||
|
return Err(error.into());
|
||||||
}
|
}
|
||||||
|
break candidate;
|
||||||
|
}
|
||||||
|
Err(rustix::io::Errno::EXIST) => {}
|
||||||
|
Err(error) => return Err(io::Error::from(error).into()),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let result = renameat(
|
||||||
|
parent.as_fd(),
|
||||||
|
Path::new(&temporary_name),
|
||||||
|
parent.as_fd(),
|
||||||
|
name,
|
||||||
|
)
|
||||||
|
.and_then(|()| rustix::fs::fsync(parent.as_fd()));
|
||||||
|
if result.is_err() {
|
||||||
|
let _ = unlinkat(parent.as_fd(), Path::new(&temporary_name), AtFlags::empty());
|
||||||
|
}
|
||||||
|
result
|
||||||
|
.map_err(io::Error::from)
|
||||||
|
.map_err(StateAnchorError::from)
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn confined_resolve_flags() -> ResolveFlags {
|
||||||
|
ResolveFlags::BENEATH
|
||||||
|
.union(ResolveFlags::NO_SYMLINKS)
|
||||||
|
.union(ResolveFlags::NO_MAGICLINKS)
|
||||||
|
.union(ResolveFlags::NO_XDEV)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn descriptor_path(descriptor: impl AsFd) -> PathBuf {
|
||||||
|
PathBuf::from(format!("/proc/self/fd/{}", descriptor.as_fd().as_raw_fd()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn descriptor_child_path(descriptor: impl AsFd, child: &str) -> PathBuf {
|
||||||
|
descriptor_path(descriptor).join(child)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn inherit_for_exec(descriptor: impl AsFd) -> Result<OwnedFd, StateAnchorError> {
|
||||||
|
dup(descriptor).map_err(io::Error::from).map_err(Into::into)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn overlay_options(lower: &Path, upper: &Path, work: &Path) -> OsString {
|
fn overlay_options(lower: &Path, upper: &Path, work: &Path) -> OsString {
|
||||||
|
|
@ -502,10 +780,12 @@ fn append_fuse_option_path(target: &mut Vec<u8>, path: &Path) {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::disallowed_methods, clippy::disallowed_types)]
|
#[allow(clippy::disallowed_methods, clippy::disallowed_types)]
|
||||||
fn rootless_unmount(path: &Path) -> Result<(), StateAnchorError> {
|
fn rootless_unmount(layout: &StateAnchorLayout<'_>) -> Result<(), StateAnchorError> {
|
||||||
|
let inherited_anchor = inherit_for_exec(&layout.runtime_anchor)?;
|
||||||
|
let path = descriptor_child_path(&inherited_anchor, "merged");
|
||||||
let status = std::process::Command::new("fusermount3")
|
let status = std::process::Command::new("fusermount3")
|
||||||
.arg("-u")
|
.arg("-u")
|
||||||
.arg(path)
|
.arg(&path)
|
||||||
.status()?;
|
.status()?;
|
||||||
if status.success() {
|
if status.success() {
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -516,16 +796,25 @@ fn rootless_unmount(path: &Path) -> Result<(), StateAnchorError> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn require_mount_record(path: &Path, expected: MountIdentity) -> Result<(), StateAnchorError> {
|
fn require_mount_record(
|
||||||
if has_mount_record(path, expected)? {
|
layout: &StateAnchorLayout<'_>,
|
||||||
|
expected: MountIdentity,
|
||||||
|
) -> Result<(), StateAnchorError> {
|
||||||
|
if has_mount_record(layout, expected)? {
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
Err(StateAnchorError::OwnershipMismatch)
|
Err(StateAnchorError::OwnershipMismatch)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn has_mount_record(path: &Path, expected: MountIdentity) -> Result<bool, StateAnchorError> {
|
fn has_mount_record(
|
||||||
let expected_path = path.as_os_str().as_bytes();
|
layout: &StateAnchorLayout<'_>,
|
||||||
|
expected: MountIdentity,
|
||||||
|
) -> Result<bool, StateAnchorError> {
|
||||||
|
let expected_path = std::fs::read_link(descriptor_path(&layout.runtime_anchor))?
|
||||||
|
.join("merged")
|
||||||
|
.into_os_string()
|
||||||
|
.into_vec();
|
||||||
for line in std::fs::read("/proc/self/mountinfo")?.split(|byte| *byte == b'\n') {
|
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<_>>();
|
let fields = line.split(|byte| *byte == b' ').take(5).collect::<Vec<_>>();
|
||||||
if fields.len() != 5 {
|
if fields.len() != 5 {
|
||||||
|
|
|
||||||
109
crates/lazaret-state-anchor/tests/authority.rs
Normal file
109
crates/lazaret-state-anchor/tests/authority.rs
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use std::ffi::OsString;
|
||||||
|
use std::fs;
|
||||||
|
use std::os::unix::ffi::OsStringExt;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
|
|
||||||
|
use lazaret_core::configuration::{
|
||||||
|
ConfigurationLoadRequest, SandboxLayoutV1, load_trusted_configuration,
|
||||||
|
};
|
||||||
|
use lazaret_state_anchor::{ProtectedRuntimeFiles, StateAnchorLayout};
|
||||||
|
|
||||||
|
static SEQUENCE: AtomicU64 = AtomicU64::new(0);
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn replaced_runtime_root_is_refused_before_runtime_file_write() {
|
||||||
|
let tree = TestTree::new();
|
||||||
|
let configuration = configuration(&tree);
|
||||||
|
let layout = StateAnchorLayout::create(&configuration).unwrap();
|
||||||
|
let runtime_root = PathBuf::from(OsString::from_vec(
|
||||||
|
configuration
|
||||||
|
.runtime_domain_root()
|
||||||
|
.canonical_label()
|
||||||
|
.as_bytes()
|
||||||
|
.to_vec(),
|
||||||
|
));
|
||||||
|
let moved_root = runtime_root.with_extension("retained");
|
||||||
|
fs::rename(&runtime_root, &moved_root).unwrap();
|
||||||
|
let replacement_files = runtime_root.join("state-anchor/runtime-files");
|
||||||
|
fs::create_dir_all(&replacement_files).unwrap();
|
||||||
|
fs::write(replacement_files.join("hostname"), b"replacement\n").unwrap();
|
||||||
|
|
||||||
|
assert!(layout.revalidate().is_err());
|
||||||
|
assert!(
|
||||||
|
layout
|
||||||
|
.replace_protected_runtime_files(&ProtectedRuntimeFiles {
|
||||||
|
resolv_conf: b"nameserver 192.0.2.1\n",
|
||||||
|
hosts: b"127.0.0.1 localhost\n",
|
||||||
|
hostname: b"changed\n",
|
||||||
|
})
|
||||||
|
.is_err()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
fs::read(replacement_files.join("hostname")).unwrap(),
|
||||||
|
b"replacement\n"
|
||||||
|
);
|
||||||
|
assert!(!replacement_files.join("resolv.conf").exists());
|
||||||
|
assert!(!replacement_files.join("hosts").exists());
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TestTree {
|
||||||
|
path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TestTree {
|
||||||
|
fn new() -> Self {
|
||||||
|
let path = std::env::temp_dir().join(format!(
|
||||||
|
"lazaret-state-anchor-authority-{}-{}",
|
||||||
|
std::process::id(),
|
||||||
|
SEQUENCE.fetch_add(1, Ordering::Relaxed)
|
||||||
|
));
|
||||||
|
fs::create_dir(&path).unwrap();
|
||||||
|
Self { path }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for TestTree {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
fs::remove_dir_all(&self.path).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn configuration(tree: &TestTree) -> 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).unwrap();
|
||||||
|
fs::create_dir(&workspace).unwrap();
|
||||||
|
fs::create_dir(&runtime).unwrap();
|
||||||
|
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\"]\n",
|
||||||
|
tree.path.join("data").display()
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let layout = SandboxLayoutV1::from_production_layout([
|
||||||
|
"/workspace".to_owned(),
|
||||||
|
"/proc".to_owned(),
|
||||||
|
"/sys".to_owned(),
|
||||||
|
"/dev".to_owned(),
|
||||||
|
"/run/lazaret".to_owned(),
|
||||||
|
])
|
||||||
|
.unwrap();
|
||||||
|
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,
|
||||||
|
})
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
@ -19,8 +19,8 @@ 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, StateAnchorLayout, StateAnchorSandboxView,
|
FuseOverlayfsMount, ProtectedRuntimeFiles, StateAnchorAcquisition, StateAnchorLayout,
|
||||||
unmount_after_coordinator_loss,
|
StateAnchorSandboxView, unmount_after_coordinator_loss,
|
||||||
};
|
};
|
||||||
use rustix::process::Signal;
|
use rustix::process::Signal;
|
||||||
|
|
||||||
|
|
@ -121,8 +121,9 @@ fn run() -> io::Result<()> {
|
||||||
"runtime-file replacement escaped transient storage",
|
"runtime-file replacement escaped transient storage",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
drop(view);
|
||||||
let wrong = lazaret_linux::filesystem::current_mount_identity(layout.upper())?;
|
let wrong = lazaret_linux::filesystem::current_mount_identity(layout.upper())?;
|
||||||
if unmount_after_coordinator_loss(layout.merged(), wrong).is_ok() {
|
if unmount_after_coordinator_loss(&layout, wrong).is_ok() {
|
||||||
return Err(io::Error::other(
|
return Err(io::Error::other(
|
||||||
"ownership mismatch unmounted foreign identity",
|
"ownership mismatch unmounted foreign identity",
|
||||||
));
|
));
|
||||||
|
|
@ -142,7 +143,7 @@ fn run() -> io::Result<()> {
|
||||||
while !second.process().has_exited()? {
|
while !second.process().has_exited()? {
|
||||||
std::thread::yield_now();
|
std::thread::yield_now();
|
||||||
}
|
}
|
||||||
unmount_after_coordinator_loss(layout.merged(), identity).map_err(io::Error::other)?;
|
unmount_after_coordinator_loss(&layout, identity).map_err(io::Error::other)?;
|
||||||
second
|
second
|
||||||
.finish_after_external_unmount()
|
.finish_after_external_unmount()
|
||||||
.map_err(io::Error::other)?;
|
.map_err(io::Error::other)?;
|
||||||
|
|
@ -150,22 +151,24 @@ fn run() -> io::Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn acquire(
|
fn acquire<'layout, 'configuration>(
|
||||||
layout: &StateAnchorLayout,
|
layout: &'layout StateAnchorLayout<'configuration>,
|
||||||
artifact: &PreparedArtifact,
|
artifact: &PreparedArtifact,
|
||||||
) -> io::Result<FuseOverlayfsMount> {
|
) -> io::Result<FuseOverlayfsMount<'layout, 'configuration>> {
|
||||||
let expected = std::cell::Cell::new(None);
|
let expected = std::cell::Cell::new(None);
|
||||||
FuseOverlayfsMount::acquire(
|
FuseOverlayfsMount::acquire(
|
||||||
layout,
|
layout,
|
||||||
Path::new(std::ffi::OsStr::from_bytes(artifact.path.as_bytes())),
|
StateAnchorAcquisition {
|
||||||
(artifact.device.get(), artifact.inode.get()),
|
lower: Path::new(std::ffi::OsStr::from_bytes(artifact.path.as_bytes())),
|
||||||
current_process_identity()?,
|
expected_lower: (artifact.device.get(), artifact.inode.get()),
|
||||||
ProcessOwnershipContext {
|
expected_parent: current_process_identity()?,
|
||||||
|
ownership: ProcessOwnershipContext {
|
||||||
domain_id: DomainId::new([1; 32]),
|
domain_id: DomainId::new([1; 32]),
|
||||||
generation_id: GenerationId::new(2),
|
generation_id: GenerationId::new(2),
|
||||||
coordinator_id: CoordinatorId::new([3; 32]),
|
coordinator_id: CoordinatorId::new([3; 32]),
|
||||||
slot: HostProcessOwnershipSlot::StateAnchorDaemon,
|
slot: HostProcessOwnershipSlot::StateAnchorDaemon,
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|staged| {
|
|staged| {
|
||||||
expected.set(Some(staged));
|
expected.set(Some(staged));
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -176,6 +179,19 @@ fn acquire(
|
||||||
}
|
}
|
||||||
Ok(ProcessOwnershipCommitAck::v1(staged))
|
Ok(ProcessOwnershipCommitAck::v1(staged))
|
||||||
},
|
},
|
||||||
|
|readiness| {
|
||||||
|
if expected.get() != Some(readiness.staged()) {
|
||||||
|
return Err(io::Error::other("ready process identity changed"));
|
||||||
|
}
|
||||||
|
if lazaret_linux::filesystem::current_mount_identity(layout.merged())?
|
||||||
|
!= readiness.mount()
|
||||||
|
{
|
||||||
|
return Err(io::Error::other(
|
||||||
|
"readiness preceded exact mount verification",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
)
|
)
|
||||||
.map_err(io::Error::other)
|
.map_err(io::Error::other)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -351,6 +351,55 @@ impl Catalog {
|
||||||
self.acquire_lease_with(lease, Some(expected_artifact))
|
self.acquire_lease_with(lease, Some(expected_artifact))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Inserts an exact lease or accepts the same already-committed lease checkpoint.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Returns an error for artifact or lease ownership drift, absence, reclaim, or a transaction
|
||||||
|
/// failure.
|
||||||
|
pub fn acquire_or_resume_lease_checked(
|
||||||
|
&self,
|
||||||
|
expected_artifact: &PreparedArtifact,
|
||||||
|
lease: &ArtifactLease,
|
||||||
|
) -> Result<(), CatalogError> {
|
||||||
|
if expected_artifact.prepared_artifact_id != lease.prepared_artifact_id {
|
||||||
|
return Err(CatalogError::OwnershipMismatch {
|
||||||
|
row: CatalogRow::PreparedArtifact,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
self.write(|transaction| {
|
||||||
|
let artifact_key = artifact_key(lease.prepared_artifact_id);
|
||||||
|
let artifacts = transaction
|
||||||
|
.open_table(ARTIFACTS)
|
||||||
|
.map_err(CatalogError::redb)?;
|
||||||
|
let artifact: PreparedArtifact =
|
||||||
|
require_row(&artifacts, &artifact_key, CatalogRow::PreparedArtifact)?;
|
||||||
|
if artifact != *expected_artifact {
|
||||||
|
return Err(CatalogError::OwnershipMismatch {
|
||||||
|
row: CatalogRow::PreparedArtifact,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if artifact.state != ArtifactState::Available {
|
||||||
|
return Err(CatalogError::ArtifactUnavailable);
|
||||||
|
}
|
||||||
|
drop(artifacts);
|
||||||
|
|
||||||
|
let key = lease_key(lease);
|
||||||
|
let mut leases = transaction.open_table(LEASES).map_err(CatalogError::redb)?;
|
||||||
|
if leases
|
||||||
|
.get(key.as_slice())
|
||||||
|
.map_err(CatalogError::redb)?
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
|
return require_exact(&leases, &key, lease, CatalogRow::ArtifactLease);
|
||||||
|
}
|
||||||
|
let encoded = encode(lease)?;
|
||||||
|
leases
|
||||||
|
.insert(key.as_slice(), encoded.as_slice())
|
||||||
|
.map_err(CatalogError::redb)?;
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn acquire_lease_with(
|
fn acquire_lease_with(
|
||||||
&self,
|
&self,
|
||||||
lease: &ArtifactLease,
|
lease: &ArtifactLease,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue