Compose cold-start resources under Runtime Supervisor ownership
This commit is contained in:
parent
510dfccf5a
commit
33ac0ba4ee
15 changed files with 1827 additions and 139 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -1309,6 +1309,7 @@ version = "0.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"lazaret-core",
|
"lazaret-core",
|
||||||
"lazaret-linux",
|
"lazaret-linux",
|
||||||
|
"lazaret-state-anchor",
|
||||||
"oci-spec 0.10.0",
|
"oci-spec 0.10.0",
|
||||||
"rustix",
|
"rustix",
|
||||||
"serde",
|
"serde",
|
||||||
|
|
|
||||||
483
crates/lazaret-host-coordinator/src/bootstrap.rs
Normal file
483
crates/lazaret-host-coordinator/src/bootstrap.rs
Normal file
|
|
@ -0,0 +1,483 @@
|
||||||
|
use std::fmt;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use lazaret_core::configuration::ResolvedDomainConfiguration;
|
||||||
|
use lazaret_core::identity::{BundleDigest, ContainerId, MountIdentity};
|
||||||
|
use lazaret_core::network::{CanonicalDerivedNetworkPolicy, ReachabilityExpectation};
|
||||||
|
use lazaret_core::records::Session;
|
||||||
|
use lazaret_core::records::{Generation, GenerationIdentity, PreparedArtifact};
|
||||||
|
use lazaret_linux::process::current_process_identity;
|
||||||
|
use lazaret_runtime_supervisor::network::NetworkProbeRequest;
|
||||||
|
use lazaret_runtime_supervisor::sandbox::SandboxBundle;
|
||||||
|
use lazaret_runtime_supervisor::supervision::RuntimeSupervisorLaunch;
|
||||||
|
use lazaret_state_anchor::StateAnchorLayout;
|
||||||
|
use lazaret_storage_catalog::Catalog;
|
||||||
|
|
||||||
|
use crate::control::ControlSessionAdmission;
|
||||||
|
use crate::control::{PublishedControlFace, SessionRegistry, StagedControlFace, TrustAnchor};
|
||||||
|
use crate::domain_store::HostCoordinatorDomainStore;
|
||||||
|
use crate::lifecycle::{
|
||||||
|
HostCoordinatorLifecycle, LifecycleAction, LifecycleAdapter, SessionAdmissionIdentity,
|
||||||
|
SessionLifecycleAdapter,
|
||||||
|
};
|
||||||
|
use crate::network::NetworkCoordinator;
|
||||||
|
use crate::runtime::{AwaitingAttachedRunsc, CommittedSandboxRuntime, SandboxCoordinator};
|
||||||
|
use crate::state_anchor::{StateAnchorCoordinator, StateAnchorReleaseAuthority};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ProductionLifecycleError(String);
|
||||||
|
|
||||||
|
impl fmt::Display for ProductionLifecycleError {
|
||||||
|
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
formatter.write_str(&self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for ProductionLifecycleError {}
|
||||||
|
|
||||||
|
fn failure(error: impl fmt::Display) -> ProductionLifecycleError {
|
||||||
|
ProductionLifecycleError(error.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ProductionBootstrapPlan<'a> {
|
||||||
|
pub store: Arc<HostCoordinatorDomainStore>,
|
||||||
|
pub catalog: &'a Catalog,
|
||||||
|
pub configuration: &'a ResolvedDomainConfiguration,
|
||||||
|
pub generation_identity: GenerationIdentity,
|
||||||
|
pub prepared_artifact: &'a PreparedArtifact,
|
||||||
|
pub state_anchor_layout: &'a StateAnchorLayout<'a>,
|
||||||
|
pub runtime_launch: RuntimeSupervisorLaunch,
|
||||||
|
pub sandbox_bundle: &'a SandboxBundle,
|
||||||
|
pub network_policy: &'a CanonicalDerivedNetworkPolicy,
|
||||||
|
pub network_probe: NetworkProbeRequest,
|
||||||
|
pub reachability: &'a ReachabilityExpectation,
|
||||||
|
pub container_id: ContainerId,
|
||||||
|
pub runtime_root: PathBuf,
|
||||||
|
pub bundle_digest: BundleDigest,
|
||||||
|
pub invoking_uid: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ProductionLifecycleAdapter<'a> {
|
||||||
|
plan: ProductionBootstrapPlan<'a>,
|
||||||
|
registry: Arc<SessionRegistry>,
|
||||||
|
trust_anchor: Option<TrustAnchor>,
|
||||||
|
staged_control: Option<StagedControlFace>,
|
||||||
|
published_control: Option<PublishedControlFace>,
|
||||||
|
awaiting_runtime: Option<AwaitingAttachedRunsc>,
|
||||||
|
runtime: Option<CommittedSandboxRuntime>,
|
||||||
|
state_anchor_mount: Option<MountIdentity>,
|
||||||
|
state_anchor_release: Option<StateAnchorReleaseAuthority>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type ProductionHostCoordinatorLifecycle<'store, 'resources> = HostCoordinatorLifecycle<
|
||||||
|
'store,
|
||||||
|
ProductionLifecycleAdapter<'resources>,
|
||||||
|
ProductionSessionRegistry,
|
||||||
|
>;
|
||||||
|
|
||||||
|
/// Composes the cold-start lifecycle with the exact shared production session registry.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Refuses a plan whose Domain Store is not the caller-retained lock-owning store.
|
||||||
|
pub fn production_lifecycle<'store, 'resources>(
|
||||||
|
store: &'store Arc<HostCoordinatorDomainStore>,
|
||||||
|
plan: ProductionBootstrapPlan<'resources>,
|
||||||
|
) -> Result<ProductionHostCoordinatorLifecycle<'store, 'resources>, ProductionLifecycleError> {
|
||||||
|
if !Arc::ptr_eq(store, &plan.store) {
|
||||||
|
return Err(ProductionLifecycleError(
|
||||||
|
"production bootstrap Domain Store authority differs".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let (adapter, registry) = ProductionLifecycleAdapter::new(plan);
|
||||||
|
Ok(HostCoordinatorLifecycle::new(store, adapter, registry))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct ProductionSessionRegistry {
|
||||||
|
registry: Arc<SessionRegistry>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProductionSessionRegistry {
|
||||||
|
#[must_use]
|
||||||
|
pub fn registry(&self) -> Arc<SessionRegistry> {
|
||||||
|
Arc::clone(&self.registry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SessionLifecycleAdapter for ProductionSessionRegistry {
|
||||||
|
type Error = ProductionLifecycleError;
|
||||||
|
type Admission = ControlSessionAdmission;
|
||||||
|
|
||||||
|
fn admission_identity(&self, admission: &Self::Admission) -> SessionAdmissionIdentity {
|
||||||
|
<Arc<SessionRegistry> as SessionLifecycleAdapter>::admission_identity(
|
||||||
|
&self.registry,
|
||||||
|
admission,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn register_session(
|
||||||
|
&self,
|
||||||
|
store: &HostCoordinatorDomainStore,
|
||||||
|
entry_action: LifecycleAction,
|
||||||
|
admission: Self::Admission,
|
||||||
|
) -> Result<Session, Self::Error> {
|
||||||
|
<Arc<SessionRegistry> as SessionLifecycleAdapter>::register_session(
|
||||||
|
&self.registry,
|
||||||
|
store,
|
||||||
|
entry_action,
|
||||||
|
admission,
|
||||||
|
)
|
||||||
|
.map_err(failure)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn disconnect_session(
|
||||||
|
&self,
|
||||||
|
store: &HostCoordinatorDomainStore,
|
||||||
|
session: &Session,
|
||||||
|
) -> Result<(), Self::Error> {
|
||||||
|
<Arc<SessionRegistry> as SessionLifecycleAdapter>::disconnect_session(
|
||||||
|
&self.registry,
|
||||||
|
store,
|
||||||
|
session,
|
||||||
|
)
|
||||||
|
.map_err(failure)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> ProductionLifecycleAdapter<'a> {
|
||||||
|
#[must_use]
|
||||||
|
pub fn new(plan: ProductionBootstrapPlan<'a>) -> (Self, ProductionSessionRegistry) {
|
||||||
|
let registry = Arc::new(SessionRegistry::new(
|
||||||
|
Arc::clone(&plan.store),
|
||||||
|
plan.generation_identity.clone(),
|
||||||
|
));
|
||||||
|
(
|
||||||
|
Self {
|
||||||
|
plan,
|
||||||
|
registry: Arc::clone(®istry),
|
||||||
|
trust_anchor: None,
|
||||||
|
staged_control: None,
|
||||||
|
published_control: None,
|
||||||
|
awaiting_runtime: None,
|
||||||
|
runtime: None,
|
||||||
|
state_anchor_mount: None,
|
||||||
|
state_anchor_release: None,
|
||||||
|
},
|
||||||
|
ProductionSessionRegistry { registry },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn published_control(&self) -> Option<&PublishedControlFace> {
|
||||||
|
self.published_control.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn require_generation(&self, generation: &Generation) -> Result<(), ProductionLifecycleError> {
|
||||||
|
if generation.domain_id == self.plan.generation_identity.domain_id
|
||||||
|
&& generation.generation_id == self.plan.generation_identity.generation_id
|
||||||
|
&& generation.configuration_fingerprint
|
||||||
|
== self.plan.generation_identity.configuration_fingerprint
|
||||||
|
&& generation.coordinator_id == self.plan.generation_identity.coordinator_id
|
||||||
|
&& generation.sandbox_id == self.plan.generation_identity.sandbox_id
|
||||||
|
{
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(ProductionLifecycleError(
|
||||||
|
"production bootstrap generation identity changed".into(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn store(&self) -> &HostCoordinatorDomainStore {
|
||||||
|
&self.plan.store
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sandbox(&self) -> SandboxCoordinator<'_> {
|
||||||
|
SandboxCoordinator::new(self.store())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn network(&self) -> NetworkCoordinator<'_> {
|
||||||
|
NetworkCoordinator::new(self.store())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn state_anchor(&self) -> StateAnchorCoordinator<'_> {
|
||||||
|
StateAnchorCoordinator::new(self.store(), self.plan.catalog)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_trust_anchor(&mut self) -> Result<(), ProductionLifecycleError> {
|
||||||
|
if self.trust_anchor.is_some() {
|
||||||
|
return Err(ProductionLifecycleError(
|
||||||
|
"Trust Anchor already exists".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
self.trust_anchor =
|
||||||
|
Some(TrustAnchor::create(self.plan.generation_identity.clone()).map_err(failure)?);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start_runtime_supervisor(&mut self) -> Result<(), ProductionLifecycleError> {
|
||||||
|
let sandbox = self.sandbox();
|
||||||
|
sandbox
|
||||||
|
.begin_intent(
|
||||||
|
self.plan.generation_identity.generation_id,
|
||||||
|
self.plan.container_id,
|
||||||
|
&self.plan.runtime_root,
|
||||||
|
self.plan.bundle_digest,
|
||||||
|
current_process_identity().map_err(failure)?,
|
||||||
|
)
|
||||||
|
.map_err(failure)?;
|
||||||
|
let launch = &self.plan.runtime_launch;
|
||||||
|
self.awaiting_runtime = Some(
|
||||||
|
AwaitingAttachedRunsc::launch_networked(&sandbox, &self.network(), launch)
|
||||||
|
.map_err(failure)?,
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lease_prepared_lower(&self) -> Result<(), ProductionLifecycleError> {
|
||||||
|
self.state_anchor()
|
||||||
|
.begin_acquisition(
|
||||||
|
self.plan.generation_identity.generation_id,
|
||||||
|
self.plan.prepared_artifact,
|
||||||
|
self.plan.state_anchor_layout,
|
||||||
|
)
|
||||||
|
.map(|_| ())
|
||||||
|
.map_err(failure)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mount_state_anchor(&mut self) -> Result<(), ProductionLifecycleError> {
|
||||||
|
let store = Arc::clone(&self.plan.store);
|
||||||
|
let coordinator = StateAnchorCoordinator::new(&store, self.plan.catalog);
|
||||||
|
let mount = self
|
||||||
|
.awaiting_runtime
|
||||||
|
.as_mut()
|
||||||
|
.ok_or_else(|| ProductionLifecycleError("Runtime Supervisor is absent".into()))?
|
||||||
|
.mount_state_anchor(
|
||||||
|
&coordinator,
|
||||||
|
self.plan.prepared_artifact.prepared_artifact_id,
|
||||||
|
)
|
||||||
|
.map_err(failure)?;
|
||||||
|
self.state_anchor_mount = Some(mount);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start_connectivity_helper(&mut self) -> Result<(), ProductionLifecycleError> {
|
||||||
|
let store = Arc::clone(&self.plan.store);
|
||||||
|
let network = NetworkCoordinator::new(&store);
|
||||||
|
self.awaiting_runtime
|
||||||
|
.as_mut()
|
||||||
|
.ok_or_else(|| ProductionLifecycleError("Runtime Supervisor is absent".into()))?
|
||||||
|
.receive_helper_staged(&network)
|
||||||
|
.map(|_| ())
|
||||||
|
.map_err(failure)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start_sidecar(&mut self) -> Result<(), ProductionLifecycleError> {
|
||||||
|
self.awaiting_runtime
|
||||||
|
.as_mut()
|
||||||
|
.ok_or_else(|| ProductionLifecycleError("Runtime Supervisor is absent".into()))?
|
||||||
|
.start_sidecar()
|
||||||
|
.map_err(failure)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mark_network_ready(&mut self) -> Result<(), ProductionLifecycleError> {
|
||||||
|
let store = Arc::clone(&self.plan.store);
|
||||||
|
let network = NetworkCoordinator::new(&store);
|
||||||
|
let mut probe = self.plan.network_probe.clone();
|
||||||
|
self.awaiting_runtime
|
||||||
|
.as_mut()
|
||||||
|
.ok_or_else(|| ProductionLifecycleError("Runtime Supervisor is absent".into()))?
|
||||||
|
.receive_network_readiness(
|
||||||
|
&network,
|
||||||
|
self.plan.network_policy,
|
||||||
|
|namespace| {
|
||||||
|
probe.enforcement_namespace = namespace;
|
||||||
|
probe
|
||||||
|
},
|
||||||
|
self.plan.reachability,
|
||||||
|
)
|
||||||
|
.map(|_| ())
|
||||||
|
.map_err(failure)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start_sandbox(&mut self) -> Result<(), ProductionLifecycleError> {
|
||||||
|
let store = Arc::clone(&self.plan.store);
|
||||||
|
let sandbox = SandboxCoordinator::new(&store);
|
||||||
|
let mut pending = self
|
||||||
|
.awaiting_runtime
|
||||||
|
.take()
|
||||||
|
.ok_or_else(|| ProductionLifecycleError("Runtime Supervisor is absent".into()))?;
|
||||||
|
let staged = pending.receive_staged(&sandbox).map_err(failure)?;
|
||||||
|
let handoff = self
|
||||||
|
.plan
|
||||||
|
.sandbox_bundle
|
||||||
|
.capture_handoff_authority(self.plan.configuration)
|
||||||
|
.map_err(failure)?;
|
||||||
|
let (runtime, _) = pending
|
||||||
|
.acknowledge(staged, &handoff)
|
||||||
|
.map_err(failure)?
|
||||||
|
.commit_networked(&sandbox)
|
||||||
|
.map_err(failure)?;
|
||||||
|
self.runtime = Some(runtime);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stage_publication(&mut self) -> Result<(), ProductionLifecycleError> {
|
||||||
|
let anchor = self
|
||||||
|
.trust_anchor
|
||||||
|
.take()
|
||||||
|
.ok_or_else(|| ProductionLifecycleError("Trust Anchor is absent".into()))?;
|
||||||
|
self.staged_control = Some(
|
||||||
|
StagedControlFace::stage(
|
||||||
|
path_from_host(
|
||||||
|
self.plan
|
||||||
|
.configuration
|
||||||
|
.runtime_domain_root()
|
||||||
|
.canonical_label()
|
||||||
|
.as_bytes(),
|
||||||
|
),
|
||||||
|
self.plan.invoking_uid,
|
||||||
|
anchor,
|
||||||
|
Arc::clone(&self.registry),
|
||||||
|
)
|
||||||
|
.map_err(failure)?,
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expose_publication(&mut self) -> Result<(), ProductionLifecycleError> {
|
||||||
|
self.published_control = Some(
|
||||||
|
self.staged_control
|
||||||
|
.take()
|
||||||
|
.ok_or_else(|| ProductionLifecycleError("staged control face is absent".into()))?
|
||||||
|
.expose()
|
||||||
|
.map_err(failure)?,
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stop_sandbox(&mut self) -> Result<(), ProductionLifecycleError> {
|
||||||
|
let store = Arc::clone(&self.plan.store);
|
||||||
|
let sandbox = SandboxCoordinator::new(&store);
|
||||||
|
self.runtime
|
||||||
|
.as_mut()
|
||||||
|
.ok_or_else(|| ProductionLifecycleError("committed runtime is absent".into()))?
|
||||||
|
.stop_sandbox(&sandbox)
|
||||||
|
.map_err(failure)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stop_sidecar(&mut self) -> Result<(), ProductionLifecycleError> {
|
||||||
|
let store = Arc::clone(&self.plan.store);
|
||||||
|
let network = NetworkCoordinator::new(&store);
|
||||||
|
network
|
||||||
|
.begin_release(self.plan.generation_identity.generation_id)
|
||||||
|
.map_err(failure)?;
|
||||||
|
self.runtime
|
||||||
|
.as_mut()
|
||||||
|
.ok_or_else(|| ProductionLifecycleError("committed runtime is absent".into()))?
|
||||||
|
.stop_sidecar()
|
||||||
|
.map_err(failure)?;
|
||||||
|
network
|
||||||
|
.complete_clean_after_absence(self.plan.generation_identity.generation_id)
|
||||||
|
.map_err(failure)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unmount_state_anchor(&mut self) -> Result<(), ProductionLifecycleError> {
|
||||||
|
let coordinator = self.state_anchor();
|
||||||
|
self.state_anchor_release = Some(
|
||||||
|
coordinator
|
||||||
|
.begin_release(
|
||||||
|
self.plan.generation_identity.generation_id,
|
||||||
|
self.plan.state_anchor_layout,
|
||||||
|
)
|
||||||
|
.map_err(failure)?,
|
||||||
|
);
|
||||||
|
self.runtime
|
||||||
|
.as_mut()
|
||||||
|
.ok_or_else(|| ProductionLifecycleError("committed runtime is absent".into()))?
|
||||||
|
.unmount_state_anchor()
|
||||||
|
.map_err(failure)?;
|
||||||
|
self.state_anchor_mount = None;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn release_prepared_lower(&mut self) -> Result<(), ProductionLifecycleError> {
|
||||||
|
let release = self
|
||||||
|
.state_anchor_release
|
||||||
|
.take()
|
||||||
|
.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)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn graceful_exit(&mut self) -> Result<(), ProductionLifecycleError> {
|
||||||
|
self.runtime
|
||||||
|
.take()
|
||||||
|
.ok_or_else(|| ProductionLifecycleError("committed runtime is absent".into()))?
|
||||||
|
.graceful_exit()
|
||||||
|
.map_err(failure)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LifecycleAdapter for ProductionLifecycleAdapter<'_> {
|
||||||
|
type Error = ProductionLifecycleError;
|
||||||
|
|
||||||
|
fn execute(
|
||||||
|
&mut self,
|
||||||
|
action: LifecycleAction,
|
||||||
|
generation: &Generation,
|
||||||
|
) -> Result<(), Self::Error> {
|
||||||
|
self.require_generation(generation)?;
|
||||||
|
match action {
|
||||||
|
LifecycleAction::CreateTrustAnchor => self.create_trust_anchor(),
|
||||||
|
LifecycleAction::StartRuntimeSupervisor => self.start_runtime_supervisor(),
|
||||||
|
LifecycleAction::LeasePreparedLower => self.lease_prepared_lower(),
|
||||||
|
LifecycleAction::MountStateAnchor => self.mount_state_anchor(),
|
||||||
|
LifecycleAction::StartSidecar => self.start_sidecar(),
|
||||||
|
LifecycleAction::StartConnectivityHelper => self.start_connectivity_helper(),
|
||||||
|
LifecycleAction::MarkNetworkReady => self.mark_network_ready(),
|
||||||
|
LifecycleAction::StartSandbox => self.start_sandbox(),
|
||||||
|
LifecycleAction::PublishDomain => self.stage_publication(),
|
||||||
|
LifecycleAction::StopSandbox => self.stop_sandbox(),
|
||||||
|
LifecycleAction::StopSidecar => self.stop_sidecar(),
|
||||||
|
LifecycleAction::UnmountStateAnchor => self.unmount_state_anchor(),
|
||||||
|
LifecycleAction::ReleasePreparedLowerLease => self.release_prepared_lower(),
|
||||||
|
LifecycleAction::GracefulSupervisorExit => self.graceful_exit(),
|
||||||
|
LifecycleAction::FinishTeardown => {
|
||||||
|
self.published_control.take();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
LifecycleAction::StartNewDomain
|
||||||
|
| LifecycleAction::StartReadySession
|
||||||
|
| LifecycleAction::WaitForDomain
|
||||||
|
| LifecycleAction::RestartWaitingDomain
|
||||||
|
| LifecycleAction::ArmRuntimeCausalEdge
|
||||||
|
| LifecycleAction::AdvanceWaitingSession
|
||||||
|
| LifecycleAction::RejectConfigurationMismatch
|
||||||
|
| LifecycleAction::RegisterSession
|
||||||
|
| LifecycleAction::DisconnectSession
|
||||||
|
| LifecycleAction::BeginTeardown => Ok(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn after_commit(
|
||||||
|
&mut self,
|
||||||
|
action: LifecycleAction,
|
||||||
|
generation: &Generation,
|
||||||
|
) -> Result<(), Self::Error> {
|
||||||
|
self.require_generation(generation)?;
|
||||||
|
if action == LifecycleAction::PublishDomain {
|
||||||
|
self.expose_publication()
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn path_from_host(bytes: &[u8]) -> &Path {
|
||||||
|
use std::os::unix::ffi::OsStrExt;
|
||||||
|
|
||||||
|
Path::new(std::ffi::OsStr::from_bytes(bytes))
|
||||||
|
}
|
||||||
|
|
@ -49,6 +49,8 @@ pub const MAX_ENROLLMENT_BYTES: usize = 8_192;
|
||||||
const MAX_CONTROL_BYTES: usize = 65_536;
|
const MAX_CONTROL_BYTES: usize = 65_536;
|
||||||
const CONTROL_SOCKET_NAME: &str = "control.sock";
|
const CONTROL_SOCKET_NAME: &str = "control.sock";
|
||||||
const PUBLICATION_IDENTITY_NAME: &str = "protocol-identity.postcard";
|
const PUBLICATION_IDENTITY_NAME: &str = "protocol-identity.postcard";
|
||||||
|
const STAGED_CONTROL_SOCKET_NAME: &str = ".control.sock.staged";
|
||||||
|
const STAGED_PUBLICATION_IDENTITY_NAME: &str = ".protocol-identity.staged";
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum ControlError {
|
pub enum ControlError {
|
||||||
|
|
@ -207,6 +209,11 @@ pub struct TrustAnchor {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TrustAnchor {
|
impl TrustAnchor {
|
||||||
|
#[must_use]
|
||||||
|
pub const fn generation_identity(&self) -> &GenerationIdentity {
|
||||||
|
&self.generation_identity
|
||||||
|
}
|
||||||
|
|
||||||
/// Creates one ephemeral generation Trust Anchor with role-separated Ed25519 keys.
|
/// Creates one ephemeral generation Trust Anchor with role-separated Ed25519 keys.
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
|
|
@ -412,7 +419,8 @@ pub struct SessionRegistry {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SessionRegistry {
|
impl SessionRegistry {
|
||||||
fn new(
|
#[must_use]
|
||||||
|
pub fn new(
|
||||||
store: Arc<HostCoordinatorDomainStore>,
|
store: Arc<HostCoordinatorDomainStore>,
|
||||||
generation_identity: GenerationIdentity,
|
generation_identity: GenerationIdentity,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
|
|
@ -676,6 +684,8 @@ struct ControlPaths {
|
||||||
root_identity: FileIdentity,
|
root_identity: FileIdentity,
|
||||||
socket: PathBuf,
|
socket: PathBuf,
|
||||||
identity: PathBuf,
|
identity: PathBuf,
|
||||||
|
staged_socket: PathBuf,
|
||||||
|
staged_identity: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ControlPaths {
|
impl ControlPaths {
|
||||||
|
|
@ -708,6 +718,8 @@ impl ControlPaths {
|
||||||
root_identity,
|
root_identity,
|
||||||
socket: root.join(CONTROL_SOCKET_NAME),
|
socket: root.join(CONTROL_SOCKET_NAME),
|
||||||
identity: root.join(PUBLICATION_IDENTITY_NAME),
|
identity: root.join(PUBLICATION_IDENTITY_NAME),
|
||||||
|
staged_socket: root.join(STAGED_CONTROL_SOCKET_NAME),
|
||||||
|
staged_identity: root.join(STAGED_PUBLICATION_IDENTITY_NAME),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -725,15 +737,15 @@ impl ControlPaths {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_identity(
|
fn create_named_identity(
|
||||||
&self,
|
&self,
|
||||||
|
name: &str,
|
||||||
bytes: &[u8],
|
bytes: &[u8],
|
||||||
uid: u32,
|
uid: u32,
|
||||||
) -> Result<(File, FileIdentity), ControlError> {
|
) -> Result<(File, FileIdentity), ControlError> {
|
||||||
self.validate_root(uid)?;
|
|
||||||
let fd = openat(
|
let fd = openat(
|
||||||
self.root_file.as_fd(),
|
self.root_file.as_fd(),
|
||||||
PUBLICATION_IDENTITY_NAME,
|
name,
|
||||||
OFlags::CREATE | OFlags::EXCL | OFlags::RDWR | OFlags::CLOEXEC | OFlags::NOFOLLOW,
|
OFlags::CREATE | OFlags::EXCL | OFlags::RDWR | OFlags::CLOEXEC | OFlags::NOFOLLOW,
|
||||||
Mode::RUSR | Mode::WUSR,
|
Mode::RUSR | Mode::WUSR,
|
||||||
)
|
)
|
||||||
|
|
@ -749,10 +761,10 @@ impl ControlPaths {
|
||||||
self.root_file
|
self.root_file
|
||||||
.sync_all()
|
.sync_all()
|
||||||
.map_err(|_| ControlError::InvalidPublication("directory sync failed"))?;
|
.map_err(|_| ControlError::InvalidPublication("directory sync failed"))?;
|
||||||
self.validate_identity(&file, identity, uid, bytes.len())
|
self.validate_identity_at(&self.root.join(name), &file, identity, uid, bytes.len())
|
||||||
})();
|
})();
|
||||||
if let Err(error) = result {
|
if let Err(error) = result {
|
||||||
remove_if_identity_matches(&self.identity, identity);
|
remove_if_identity_matches(&self.root.join(name), identity);
|
||||||
return Err(error);
|
return Err(error);
|
||||||
}
|
}
|
||||||
Ok((file, identity))
|
Ok((file, identity))
|
||||||
|
|
@ -764,10 +776,21 @@ impl ControlPaths {
|
||||||
expected: FileIdentity,
|
expected: FileIdentity,
|
||||||
uid: u32,
|
uid: u32,
|
||||||
expected_length: usize,
|
expected_length: usize,
|
||||||
|
) -> Result<(), ControlError> {
|
||||||
|
self.validate_identity_at(&self.identity, file, expected, uid, expected_length)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_identity_at(
|
||||||
|
&self,
|
||||||
|
path: &Path,
|
||||||
|
file: &File,
|
||||||
|
expected: FileIdentity,
|
||||||
|
uid: u32,
|
||||||
|
expected_length: usize,
|
||||||
) -> Result<(), ControlError> {
|
) -> Result<(), ControlError> {
|
||||||
self.validate_root(uid)?;
|
self.validate_root(uid)?;
|
||||||
let descriptor = file.metadata()?;
|
let descriptor = file.metadata()?;
|
||||||
let current = std::fs::symlink_metadata(&self.identity)?;
|
let current = std::fs::symlink_metadata(path)?;
|
||||||
if FileIdentity::from_metadata(&descriptor) != expected
|
if FileIdentity::from_metadata(&descriptor) != expected
|
||||||
|| FileIdentity::from_metadata(¤t) != expected
|
|| FileIdentity::from_metadata(¤t) != expected
|
||||||
|| !descriptor.file_type().is_file()
|
|| !descriptor.file_type().is_file()
|
||||||
|
|
@ -801,6 +824,150 @@ pub struct PublishedControlFace {
|
||||||
registry: Arc<SessionRegistry>,
|
registry: Arc<SessionRegistry>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct StagedControlFace {
|
||||||
|
listener: Option<std::os::unix::net::UnixListener>,
|
||||||
|
paths: ControlPaths,
|
||||||
|
staged_socket_identity: FileIdentity,
|
||||||
|
identity_file: File,
|
||||||
|
identity_file_identity: FileIdentity,
|
||||||
|
publication_identity: ProtocolPublicationIdentity,
|
||||||
|
publication_bytes: Vec<u8>,
|
||||||
|
uid: u32,
|
||||||
|
anchor: Option<TrustAnchor>,
|
||||||
|
server_config: Arc<ServerConfig>,
|
||||||
|
registry: Arc<SessionRegistry>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StagedControlFace {
|
||||||
|
/// Stages an undiscoverable private control face before durable publication.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Refuses existing artifacts, changed paths, ownership/mode errors, or TLS failure.
|
||||||
|
pub fn stage(
|
||||||
|
runtime_domain_root: &Path,
|
||||||
|
uid: u32,
|
||||||
|
anchor: TrustAnchor,
|
||||||
|
registry: Arc<SessionRegistry>,
|
||||||
|
) -> Result<Self, ControlError> {
|
||||||
|
if registry.generation_identity != anchor.generation_identity {
|
||||||
|
return Err(ControlError::StaleGeneration);
|
||||||
|
}
|
||||||
|
let paths = ControlPaths::open(runtime_domain_root, uid)?;
|
||||||
|
if [
|
||||||
|
&paths.socket,
|
||||||
|
&paths.identity,
|
||||||
|
&paths.staged_socket,
|
||||||
|
&paths.staged_identity,
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.any(|path| path.try_exists().unwrap_or(true))
|
||||||
|
{
|
||||||
|
return Err(ControlError::InvalidPublication(
|
||||||
|
"publication path already exists",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let publication_identity = anchor.publication_identity();
|
||||||
|
let publication_bytes = encode(&publication_identity)?;
|
||||||
|
let server_config = Arc::new(anchor.server_config()?);
|
||||||
|
let (identity_file, identity_file_identity) = paths.create_named_identity(
|
||||||
|
STAGED_PUBLICATION_IDENTITY_NAME,
|
||||||
|
&publication_bytes,
|
||||||
|
uid,
|
||||||
|
)?;
|
||||||
|
let listener = std::os::unix::net::UnixListener::bind(&paths.staged_socket)
|
||||||
|
.map_err(|_| ControlError::InvalidPublication("staged control socket bind failed"))?;
|
||||||
|
std::fs::set_permissions(&paths.staged_socket, std::fs::Permissions::from_mode(0o600))?;
|
||||||
|
let socket_metadata = std::fs::symlink_metadata(&paths.staged_socket)?;
|
||||||
|
let staged_socket_identity = FileIdentity::from_metadata(&socket_metadata);
|
||||||
|
if !socket_metadata.file_type().is_socket()
|
||||||
|
|| socket_metadata.uid() != uid
|
||||||
|
|| socket_metadata.mode() & 0o7777 != 0o600
|
||||||
|
{
|
||||||
|
return Err(ControlError::InvalidPublication(
|
||||||
|
"staged control socket ownership or mode is invalid",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
paths.root_file.sync_all()?;
|
||||||
|
Ok(Self {
|
||||||
|
listener: Some(listener),
|
||||||
|
paths,
|
||||||
|
staged_socket_identity,
|
||||||
|
identity_file,
|
||||||
|
identity_file_identity,
|
||||||
|
publication_identity,
|
||||||
|
publication_bytes,
|
||||||
|
uid,
|
||||||
|
anchor: Some(anchor),
|
||||||
|
server_config,
|
||||||
|
registry,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Exposes the staged face after the exact generation is durably Published.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Refuses stale durable state, changed staged artifacts, or exposure failure.
|
||||||
|
pub fn expose(mut self) -> Result<PublishedControlFace, ControlError> {
|
||||||
|
self.registry.require_serviceable_generation()?;
|
||||||
|
self.paths.validate_root(self.uid)?;
|
||||||
|
let staged_socket = std::fs::symlink_metadata(&self.paths.staged_socket)?;
|
||||||
|
if FileIdentity::from_metadata(&staged_socket) != self.staged_socket_identity {
|
||||||
|
return Err(ControlError::InvalidPublication(
|
||||||
|
"staged control socket changed",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
self.paths.validate_identity_at(
|
||||||
|
&self.paths.staged_identity,
|
||||||
|
&self.identity_file,
|
||||||
|
self.identity_file_identity,
|
||||||
|
self.uid,
|
||||||
|
self.publication_bytes.len(),
|
||||||
|
)?;
|
||||||
|
std::fs::rename(&self.paths.staged_socket, &self.paths.socket)?;
|
||||||
|
if let Err(error) = std::fs::rename(&self.paths.staged_identity, &self.paths.identity) {
|
||||||
|
remove_if_identity_matches(&self.paths.socket, self.staged_socket_identity);
|
||||||
|
return Err(error.into());
|
||||||
|
}
|
||||||
|
self.paths.root_file.sync_all()?;
|
||||||
|
let socket_identity =
|
||||||
|
FileIdentity::from_metadata(&std::fs::symlink_metadata(&self.paths.socket)?);
|
||||||
|
self.paths.validate_identity(
|
||||||
|
&self.identity_file,
|
||||||
|
self.identity_file_identity,
|
||||||
|
self.uid,
|
||||||
|
self.publication_bytes.len(),
|
||||||
|
)?;
|
||||||
|
let paths = ControlPaths::open(&self.paths.root, self.uid)?;
|
||||||
|
Ok(PublishedControlFace {
|
||||||
|
listener: self
|
||||||
|
.listener
|
||||||
|
.take()
|
||||||
|
.ok_or(ControlError::InvalidPublication(
|
||||||
|
"staged listener is absent",
|
||||||
|
))?,
|
||||||
|
paths,
|
||||||
|
socket_identity,
|
||||||
|
identity_file: self.identity_file.try_clone()?,
|
||||||
|
identity_file_identity: self.identity_file_identity,
|
||||||
|
publication_identity: self.publication_identity.clone(),
|
||||||
|
publication_bytes: self.publication_bytes.clone(),
|
||||||
|
uid: self.uid,
|
||||||
|
anchor: self.anchor.take().ok_or(ControlError::InvalidPublication(
|
||||||
|
"staged Trust Anchor is absent",
|
||||||
|
))?,
|
||||||
|
server_config: Arc::clone(&self.server_config),
|
||||||
|
registry: Arc::clone(&self.registry),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for StagedControlFace {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
remove_if_identity_matches(&self.paths.staged_socket, self.staged_socket_identity);
|
||||||
|
remove_if_identity_matches(&self.paths.staged_identity, self.identity_file_identity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl PublishedControlFace {
|
impl PublishedControlFace {
|
||||||
/// Publishes one generation control face in the fixed host-private runtime path.
|
/// Publishes one generation control face in the fixed host-private runtime path.
|
||||||
///
|
///
|
||||||
|
|
@ -813,83 +980,11 @@ impl PublishedControlFace {
|
||||||
anchor: TrustAnchor,
|
anchor: TrustAnchor,
|
||||||
store: Arc<HostCoordinatorDomainStore>,
|
store: Arc<HostCoordinatorDomainStore>,
|
||||||
) -> Result<Self, ControlError> {
|
) -> Result<Self, ControlError> {
|
||||||
let paths = ControlPaths::open(runtime_domain_root, uid)?;
|
|
||||||
if paths.socket.try_exists()? || paths.identity.try_exists()? {
|
|
||||||
return Err(ControlError::InvalidPublication(
|
|
||||||
"publication path already exists",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
let registry = Arc::new(SessionRegistry::new(
|
let registry = Arc::new(SessionRegistry::new(
|
||||||
store,
|
store,
|
||||||
anchor.generation_identity.clone(),
|
anchor.generation_identity.clone(),
|
||||||
));
|
));
|
||||||
registry.require_generation_authority()?;
|
StagedControlFace::stage(runtime_domain_root, uid, anchor, registry)?.expose()
|
||||||
let publication_identity = anchor.publication_identity();
|
|
||||||
let publication_bytes = encode(&publication_identity)?;
|
|
||||||
let server_config = Arc::new(anchor.server_config()?);
|
|
||||||
let (identity_file, identity_file_identity) =
|
|
||||||
paths.create_identity(&publication_bytes, uid)?;
|
|
||||||
let listener = match std::os::unix::net::UnixListener::bind(&paths.socket) {
|
|
||||||
Ok(listener) => listener,
|
|
||||||
Err(error) => {
|
|
||||||
let _ = std::fs::remove_file(&paths.identity);
|
|
||||||
return Err(ControlError::InvalidPublication(
|
|
||||||
if error.kind() == io::ErrorKind::PermissionDenied {
|
|
||||||
"control socket bind was denied"
|
|
||||||
} else {
|
|
||||||
"control socket bind failed"
|
|
||||||
},
|
|
||||||
));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let finalize = (|| {
|
|
||||||
std::fs::set_permissions(&paths.socket, std::fs::Permissions::from_mode(0o600))
|
|
||||||
.map_err(|_| ControlError::InvalidPublication("control socket chmod failed"))?;
|
|
||||||
let socket_metadata = std::fs::symlink_metadata(&paths.socket)?;
|
|
||||||
if !socket_metadata.file_type().is_socket()
|
|
||||||
|| socket_metadata.uid() != uid
|
|
||||||
|| socket_metadata.mode() & 0o7777 != 0o600
|
|
||||||
{
|
|
||||||
return Err(ControlError::InvalidPublication(
|
|
||||||
"control socket ownership or mode is invalid",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
paths.validate_root(uid)?;
|
|
||||||
Ok(FileIdentity::from_metadata(&socket_metadata))
|
|
||||||
})();
|
|
||||||
let socket_identity = match finalize {
|
|
||||||
Ok(identity) => identity,
|
|
||||||
Err(error) => {
|
|
||||||
drop(listener);
|
|
||||||
let _ = std::fs::remove_file(&paths.socket);
|
|
||||||
let _ = std::fs::remove_file(&paths.identity);
|
|
||||||
return Err(error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if let Err(error) = paths.validate_identity(
|
|
||||||
&identity_file,
|
|
||||||
identity_file_identity,
|
|
||||||
uid,
|
|
||||||
publication_bytes.len(),
|
|
||||||
) {
|
|
||||||
drop(listener);
|
|
||||||
let _ = std::fs::remove_file(&paths.socket);
|
|
||||||
remove_if_identity_matches(&paths.identity, identity_file_identity);
|
|
||||||
return Err(error);
|
|
||||||
}
|
|
||||||
Ok(Self {
|
|
||||||
listener,
|
|
||||||
paths,
|
|
||||||
socket_identity,
|
|
||||||
identity_file,
|
|
||||||
identity_file_identity,
|
|
||||||
publication_identity,
|
|
||||||
publication_bytes,
|
|
||||||
uid,
|
|
||||||
anchor,
|
|
||||||
server_config,
|
|
||||||
registry,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
#![doc = "Host-side coordination authority for one Lazaret Domain."]
|
#![doc = "Host-side coordination authority for one Lazaret Domain."]
|
||||||
|
|
||||||
|
pub mod bootstrap;
|
||||||
pub mod control;
|
pub mod control;
|
||||||
pub mod domain_store;
|
pub mod domain_store;
|
||||||
pub mod escalation;
|
pub mod escalation;
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,18 @@ pub trait LifecycleAdapter {
|
||||||
action: LifecycleAction,
|
action: LifecycleAction,
|
||||||
generation: &Generation,
|
generation: &Generation,
|
||||||
) -> Result<(), Self::Error>;
|
) -> Result<(), Self::Error>;
|
||||||
|
|
||||||
|
/// Completes a physical boundary that must follow its durable lifecycle commit.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Returns the adapter's post-commit publication or cleanup failure.
|
||||||
|
fn after_commit(
|
||||||
|
&mut self,
|
||||||
|
_action: LifecycleAction,
|
||||||
|
_generation: &Generation,
|
||||||
|
) -> Result<(), Self::Error> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
|
@ -209,6 +221,16 @@ where
|
||||||
self.registered.len()
|
self.registered.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub const fn adapter(&self) -> &A {
|
||||||
|
&self.adapter
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub const fn adapter_mut(&mut self) -> &mut A {
|
||||||
|
&mut self.adapter
|
||||||
|
}
|
||||||
|
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// Refuses a non-absent Domain, invalid initial records, or adapter/store failure.
|
/// Refuses a non-absent Domain, invalid initial records, or adapter/store failure.
|
||||||
pub fn start_new_domain(
|
pub fn start_new_domain(
|
||||||
|
|
@ -251,6 +273,28 @@ where
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Drives one cold-start leader through the complete normal publication boundary.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Returns the first invalid transition, durable update, or production adapter failure.
|
||||||
|
pub fn bootstrap_new_domain(
|
||||||
|
&mut self,
|
||||||
|
generation: Generation,
|
||||||
|
session: Session,
|
||||||
|
) -> Result<(), LifecycleError<A::Error>> {
|
||||||
|
self.start_new_domain(generation, session)?;
|
||||||
|
self.create_trust_anchor()?;
|
||||||
|
self.start_runtime_supervisor()?;
|
||||||
|
self.lease_prepared_lower()?;
|
||||||
|
self.mount_state_anchor()?;
|
||||||
|
self.start_sidecar()?;
|
||||||
|
self.start_connectivity_helper()?;
|
||||||
|
self.mark_network_ready()?;
|
||||||
|
self.arm_runtime_causal_edge()?;
|
||||||
|
self.start_sandbox()?;
|
||||||
|
self.publish_domain()
|
||||||
|
}
|
||||||
|
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// Refuses an unpublished, stale, mismatched, duplicate, or unauthenticated session.
|
/// Refuses an unpublished, stale, mismatched, duplicate, or unauthenticated session.
|
||||||
pub fn admit_session(
|
pub fn admit_session(
|
||||||
|
|
@ -529,7 +573,16 @@ where
|
||||||
generation.durable_decision = DurableDecision::Published;
|
generation.durable_decision = DurableDecision::Published;
|
||||||
self.store
|
self.store
|
||||||
.apply(&[DomainStoreMutation::PutGeneration(generation.clone())])?;
|
.apply(&[DomainStoreMutation::PutGeneration(generation.clone())])?;
|
||||||
self.generation = Some(generation);
|
self.generation = Some(generation.clone());
|
||||||
|
if let Err(error) = self
|
||||||
|
.adapter
|
||||||
|
.after_commit(LifecycleAction::PublishDomain, &generation)
|
||||||
|
{
|
||||||
|
self.phase = DomainPhase::Failed;
|
||||||
|
self.bootstrap_owner = None;
|
||||||
|
self.published = false;
|
||||||
|
return Err(LifecycleError::Adapter(error));
|
||||||
|
}
|
||||||
self.phase = DomainPhase::Ready;
|
self.phase = DomainPhase::Ready;
|
||||||
self.bootstrap_owner = None;
|
self.bootstrap_owner = None;
|
||||||
self.published = true;
|
self.published = true;
|
||||||
|
|
|
||||||
|
|
@ -368,6 +368,92 @@ impl<'store> NetworkCoordinator<'store> {
|
||||||
Ok(evidence)
|
Ok(evidence)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Records exact helper and network release intent before RS resource stop.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Refuses noncommitted or incomplete role ownership.
|
||||||
|
pub fn begin_release(
|
||||||
|
&self,
|
||||||
|
generation_id: GenerationId,
|
||||||
|
) -> Result<(), NetworkCoordinatorError> {
|
||||||
|
let network = self.participant(generation_id, ParticipantRole::Network)?;
|
||||||
|
let helper = self.participant(generation_id, ParticipantRole::Helper)?;
|
||||||
|
if network.state() != ParticipantState::Committed
|
||||||
|
|| helper.state() != ParticipantState::Committed
|
||||||
|
{
|
||||||
|
return Err(NetworkCoordinatorError::InvalidState(
|
||||||
|
"network roles are not committed",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let network_ownership =
|
||||||
|
network
|
||||||
|
.ownership()
|
||||||
|
.cloned()
|
||||||
|
.ok_or(NetworkCoordinatorError::InvalidState(
|
||||||
|
"network ownership is absent",
|
||||||
|
))?;
|
||||||
|
let helper_ownership =
|
||||||
|
helper
|
||||||
|
.ownership()
|
||||||
|
.cloned()
|
||||||
|
.ok_or(NetworkCoordinatorError::InvalidState(
|
||||||
|
"helper ownership is absent",
|
||||||
|
))?;
|
||||||
|
self.domain_store.apply(&[
|
||||||
|
DomainStoreMutation::PutParticipant(
|
||||||
|
Participant::snapshot(
|
||||||
|
generation_id,
|
||||||
|
ParticipantRole::Helper,
|
||||||
|
ParticipantState::ReleaseIntent,
|
||||||
|
Some(helper_ownership),
|
||||||
|
)
|
||||||
|
.map_err(|_| NetworkCoordinatorError::InvalidState("helper release is invalid"))?,
|
||||||
|
),
|
||||||
|
DomainStoreMutation::PutParticipant(
|
||||||
|
Participant::snapshot(
|
||||||
|
generation_id,
|
||||||
|
ParticipantRole::Network,
|
||||||
|
ParticipantState::ReleaseIntent,
|
||||||
|
Some(network_ownership),
|
||||||
|
)
|
||||||
|
.map_err(|_| NetworkCoordinatorError::InvalidState("network release is invalid"))?,
|
||||||
|
),
|
||||||
|
])?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cleans helper and network roles only after RS reports their physical absence.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Refuses cleanup without exact release intent.
|
||||||
|
pub fn complete_clean_after_absence(
|
||||||
|
&self,
|
||||||
|
generation_id: GenerationId,
|
||||||
|
) -> Result<(), NetworkCoordinatorError> {
|
||||||
|
for role in [ParticipantRole::Helper, ParticipantRole::Network] {
|
||||||
|
if self.participant(generation_id, role)?.state() != ParticipantState::ReleaseIntent {
|
||||||
|
return Err(NetworkCoordinatorError::InvalidState(
|
||||||
|
"network release intent is absent",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let clean = [ParticipantRole::Helper, ParticipantRole::Network].map(|role| {
|
||||||
|
Participant::snapshot(
|
||||||
|
generation_id,
|
||||||
|
role,
|
||||||
|
ParticipantState::Clean,
|
||||||
|
Some(ParticipantOwnership::empty(role)),
|
||||||
|
)
|
||||||
|
.map_err(|_| NetworkCoordinatorError::InvalidState("network Clean is invalid"))
|
||||||
|
});
|
||||||
|
let [helper, network] = clean;
|
||||||
|
self.domain_store.apply(&[
|
||||||
|
DomainStoreMutation::CleanParticipantAfterAbsence(helper?),
|
||||||
|
DomainStoreMutation::CleanParticipantAfterAbsence(network?),
|
||||||
|
])?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn require_staged(
|
fn require_staged(
|
||||||
&self,
|
&self,
|
||||||
staged: StagedProcessOwnership,
|
staged: StagedProcessOwnership,
|
||||||
|
|
|
||||||
|
|
@ -155,6 +155,60 @@ fn independently_observed_namespace_refuses_attestation_mutation() {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn committed_network_roles_release_together_only_after_absence() {
|
||||||
|
let fixture = Fixture::new();
|
||||||
|
let (policy, runtime) = fixture.prepare_roles();
|
||||||
|
let namespace = namespace_identity(std::path::Path::new("/proc/self/ns/net")).unwrap();
|
||||||
|
let attestation = attestation(&fixture.generation, policy.digest(), namespace, runtime);
|
||||||
|
let probe = NetworkReachabilityProbe {
|
||||||
|
domain_id: fixture.generation.domain_id,
|
||||||
|
generation_id: fixture.generation.generation_id,
|
||||||
|
enforcement_namespace: namespace,
|
||||||
|
denied_destinations: vec![DeniedDestinationObservation {
|
||||||
|
destination: IpAddress::V4([10, 1, 2, 3]),
|
||||||
|
unreachable: true,
|
||||||
|
}],
|
||||||
|
permitted_destinations: vec![PermittedDestinationObservation {
|
||||||
|
destination: IpAddress::V4([1, 1, 1, 1]),
|
||||||
|
reachable: true,
|
||||||
|
}],
|
||||||
|
dns_observations: vec![DnsObservation {
|
||||||
|
resolver: IpAddress::V4([8, 8, 8, 8]),
|
||||||
|
query_name: "example.com".into(),
|
||||||
|
resolved_addresses: vec![IpAddress::V4([93, 184, 216, 34])],
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
let coordinator = NetworkCoordinator::new(&fixture.store);
|
||||||
|
coordinator
|
||||||
|
.commit_readiness(&policy, attestation, probe, &reachability())
|
||||||
|
.unwrap();
|
||||||
|
assert!(
|
||||||
|
coordinator
|
||||||
|
.complete_clean_after_absence(fixture.generation.generation_id)
|
||||||
|
.is_err()
|
||||||
|
);
|
||||||
|
coordinator
|
||||||
|
.begin_release(fixture.generation.generation_id)
|
||||||
|
.unwrap();
|
||||||
|
coordinator
|
||||||
|
.complete_clean_after_absence(fixture.generation.generation_id)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let participants = fixture.store.snapshot().unwrap().participants;
|
||||||
|
for role in [ParticipantRole::Helper, ParticipantRole::Network] {
|
||||||
|
let participant = participants
|
||||||
|
.iter()
|
||||||
|
.find(|participant| participant.role() == role)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(participant.state(), ParticipantState::Clean);
|
||||||
|
assert_eq!(
|
||||||
|
participant.ownership(),
|
||||||
|
Some(&ParticipantOwnership::empty(role))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn policy() -> CanonicalDerivedNetworkPolicy {
|
fn policy() -> CanonicalDerivedNetworkPolicy {
|
||||||
derive_policy(DerivedNetworkPolicyFields {
|
derive_policy(DerivedNetworkPolicyFields {
|
||||||
address_families: vec![
|
address_families: vec![
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,10 @@ use std::os::unix::ffi::OsStrExt;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
use crate::state_anchor::StateAnchorCoordinator;
|
||||||
use lazaret_core::identity::{
|
use lazaret_core::identity::{
|
||||||
BundleDigest, ContainerId, GenerationId, HostPath, NamespaceIdentity, PidfsProcessIdentityV1,
|
BundleDigest, ContainerId, GenerationId, HostPath, MountIdentity, NamespaceIdentity,
|
||||||
|
PidfsProcessIdentityV1, PreparedArtifactId,
|
||||||
};
|
};
|
||||||
use lazaret_core::network::{CanonicalDerivedNetworkPolicy, ReachabilityExpectation};
|
use lazaret_core::network::{CanonicalDerivedNetworkPolicy, ReachabilityExpectation};
|
||||||
use lazaret_core::records::{
|
use lazaret_core::records::{
|
||||||
|
|
@ -375,6 +377,88 @@ impl AwaitingAttachedRunsc {
|
||||||
self.supervisor.as_ref().map(RetainedProcess::identity)
|
self.supervisor.as_ref().map(RetainedProcess::identity)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Requests RS-owned State Anchor acquisition and commits exact staged and mount evidence.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Refuses missing intent, event mismatch, changed ownership, or channel failure.
|
||||||
|
pub fn mount_state_anchor(
|
||||||
|
&mut self,
|
||||||
|
coordinator: &StateAnchorCoordinator<'_>,
|
||||||
|
prepared_artifact_id: PreparedArtifactId,
|
||||||
|
) -> Result<MountIdentity, SandboxCoordinatorError> {
|
||||||
|
let control = self
|
||||||
|
.control
|
||||||
|
.as_ref()
|
||||||
|
.ok_or(SandboxCoordinatorError::InvalidState(
|
||||||
|
"Runtime Supervisor control channel is absent",
|
||||||
|
))?;
|
||||||
|
control.mount_state_anchor()?;
|
||||||
|
let staged = match control.receive()? {
|
||||||
|
SupervisorEvent::StateAnchorStaged(staged) => staged,
|
||||||
|
SupervisorEvent::Refused(detail) => {
|
||||||
|
return Err(SandboxCoordinatorError::Io(io::Error::other(detail)));
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
return Err(SandboxCoordinatorError::InvalidState(
|
||||||
|
"Runtime Supervisor did not stage the State Anchor daemon",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
coordinator
|
||||||
|
.stage_daemon(staged)
|
||||||
|
.map_err(|error| SandboxCoordinatorError::Io(io::Error::other(error)))?;
|
||||||
|
let acknowledgment = coordinator
|
||||||
|
.acknowledge_staged_daemon(staged)
|
||||||
|
.map_err(|error| SandboxCoordinatorError::Io(io::Error::other(error)))?;
|
||||||
|
control.acknowledge_state_anchor(acknowledgment)?;
|
||||||
|
let (ready_staged, mount) = match control.receive()? {
|
||||||
|
SupervisorEvent::StateAnchorReady { staged, mount } => (staged, mount),
|
||||||
|
SupervisorEvent::Refused(detail) => {
|
||||||
|
return Err(SandboxCoordinatorError::Io(io::Error::other(detail)));
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
return Err(SandboxCoordinatorError::InvalidState(
|
||||||
|
"Runtime Supervisor did not report State Anchor readiness",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if ready_staged != staged {
|
||||||
|
return Err(SandboxCoordinatorError::InvalidState(
|
||||||
|
"State Anchor readiness changed daemon ownership",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let readiness =
|
||||||
|
lazaret_state_anchor::VerifiedStateAnchorReadiness::from_parts(ready_staged, mount);
|
||||||
|
coordinator
|
||||||
|
.commit_daemon_ready(readiness)
|
||||||
|
.and_then(|()| {
|
||||||
|
coordinator.commit_prepared(staged.generation_id(), prepared_artifact_id, mount)
|
||||||
|
})
|
||||||
|
.map_err(|error| SandboxCoordinatorError::Io(io::Error::other(error)))?;
|
||||||
|
Ok(mount)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Releases the RS-owned sidecar install path after exact State Anchor readiness.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Refuses an unexpected RS response or channel failure.
|
||||||
|
pub fn start_sidecar(&mut self) -> Result<(), SandboxCoordinatorError> {
|
||||||
|
let control = self
|
||||||
|
.control
|
||||||
|
.as_ref()
|
||||||
|
.ok_or(SandboxCoordinatorError::InvalidState(
|
||||||
|
"Runtime Supervisor control channel is absent",
|
||||||
|
))?;
|
||||||
|
control.start_sidecar()?;
|
||||||
|
if control.receive()? == SupervisorEvent::SidecarStarted {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(SandboxCoordinatorError::InvalidState(
|
||||||
|
"Runtime Supervisor did not start the sidecar path",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Launches the RS through the HC broker and commits its staged ownership before execution.
|
/// Launches the RS through the HC broker and commits its staged ownership before execution.
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
|
|
@ -827,6 +911,70 @@ impl CommittedSandboxRuntime {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Stops and durably cleans only the exact attached sandbox runtime.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Refuses missing release intent, unexpected RS response, or exact cleanup failure.
|
||||||
|
pub fn stop_sandbox(
|
||||||
|
&mut self,
|
||||||
|
coordinator: &SandboxCoordinator<'_>,
|
||||||
|
) -> Result<(), SandboxCoordinatorError> {
|
||||||
|
coordinator.begin_release(self.generation_id)?;
|
||||||
|
self.control.stop_sandbox()?;
|
||||||
|
if self.control.receive()? != SupervisorEvent::SandboxStopped {
|
||||||
|
return Err(SandboxCoordinatorError::InvalidState(
|
||||||
|
"Runtime Supervisor did not stop the sandbox",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
coordinator.complete_clean_after_absence(self.generation_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stops only the RS-owned network resources after sandbox absence.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Refuses an unexpected RS response or channel failure.
|
||||||
|
pub fn stop_sidecar(&mut self) -> Result<(), SandboxCoordinatorError> {
|
||||||
|
self.control.stop_sidecar()?;
|
||||||
|
if self.control.receive()? == SupervisorEvent::SidecarStopped {
|
||||||
|
self.network_evidence = None;
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(SandboxCoordinatorError::InvalidState(
|
||||||
|
"Runtime Supervisor did not stop the sidecar",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Requests exact RS-owned State Anchor unmount after dependent runtime resources stop.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Refuses an unexpected RS response or channel failure.
|
||||||
|
pub fn unmount_state_anchor(&mut self) -> Result<(), SandboxCoordinatorError> {
|
||||||
|
self.control.unmount_state_anchor()?;
|
||||||
|
if self.control.receive()? == SupervisorEvent::StateAnchorUnmounted {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(SandboxCoordinatorError::InvalidState(
|
||||||
|
"Runtime Supervisor did not unmount the State Anchor",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gracefully exits and reaps the resource-empty exact Runtime Supervisor.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Refuses an unexpected response or an RS that does not exit.
|
||||||
|
pub fn graceful_exit(mut self) -> Result<(), SandboxCoordinatorError> {
|
||||||
|
self.control.exit()?;
|
||||||
|
if self.control.receive()? != SupervisorEvent::Exited {
|
||||||
|
return Err(SandboxCoordinatorError::InvalidState(
|
||||||
|
"Runtime Supervisor did not acknowledge graceful exit",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
self.lifetime.take();
|
||||||
|
wait_and_reap_supervisor(&mut self.supervisor)
|
||||||
|
}
|
||||||
|
|
||||||
/// Waits for an exact attached-runsc loss report and reaps the retained RS.
|
/// Waits for an exact attached-runsc loss report and reaps the retained RS.
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,8 @@ use lazaret_core::records::{
|
||||||
SessionRegistrationResponse, TlsCredential,
|
SessionRegistrationResponse, TlsCredential,
|
||||||
};
|
};
|
||||||
use lazaret_host_coordinator::control::{
|
use lazaret_host_coordinator::control::{
|
||||||
CONTROL_ALPN, ControlError, PendingClientEnrollment, PublishedControlFace, TrustAnchor,
|
CONTROL_ALPN, ControlError, PendingClientEnrollment, PublishedControlFace, SessionRegistry,
|
||||||
|
StagedControlFace, TrustAnchor,
|
||||||
};
|
};
|
||||||
use lazaret_host_coordinator::domain_store::{DomainStoreMutation, HostCoordinatorDomainStore};
|
use lazaret_host_coordinator::domain_store::{DomainStoreMutation, HostCoordinatorDomainStore};
|
||||||
use lazaret_linux::filesystem::try_exclusive_flock;
|
use lazaret_linux::filesystem::try_exclusive_flock;
|
||||||
|
|
@ -687,26 +688,15 @@ fn prepared_control_face_admits_only_after_generation_publication() {
|
||||||
let directory = TestDirectory::new("publication-gate");
|
let directory = TestDirectory::new("publication-gate");
|
||||||
let identity = generation_identity(7);
|
let identity = generation_identity(7);
|
||||||
let store = open_preparing_store(&directory, &identity);
|
let store = open_preparing_store(&directory, &identity);
|
||||||
let face = Arc::new(
|
let registry = Arc::new(SessionRegistry::new(Arc::clone(&store), identity.clone()));
|
||||||
PublishedControlFace::publish(
|
let staged = StagedControlFace::stage(
|
||||||
&directory.0,
|
&directory.0,
|
||||||
getuid().as_raw(),
|
getuid().as_raw(),
|
||||||
TrustAnchor::create(identity.clone()).unwrap(),
|
TrustAnchor::create(identity.clone()).unwrap(),
|
||||||
Arc::clone(&store),
|
registry,
|
||||||
)
|
)
|
||||||
.unwrap(),
|
.unwrap();
|
||||||
);
|
assert!(std::os::unix::net::UnixStream::connect(directory.path("control.sock")).is_err());
|
||||||
|
|
||||||
let serving = server(&face);
|
|
||||||
let config = PublishedControlFace::client_config(face.publication_identity(), None).unwrap();
|
|
||||||
assert!(
|
|
||||||
PublishedControlFace::connect(face.socket_path(), config, face.publication_identity())
|
|
||||||
.is_err()
|
|
||||||
);
|
|
||||||
assert!(matches!(
|
|
||||||
serving.join().unwrap().unwrap_err(),
|
|
||||||
ControlError::StaleGeneration
|
|
||||||
));
|
|
||||||
|
|
||||||
store
|
store
|
||||||
.apply(&[DomainStoreMutation::PutGeneration(generation(
|
.apply(&[DomainStoreMutation::PutGeneration(generation(
|
||||||
|
|
@ -714,6 +704,7 @@ fn prepared_control_face_admits_only_after_generation_publication() {
|
||||||
DurableDecision::Published,
|
DurableDecision::Published,
|
||||||
))])
|
))])
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
let face = Arc::new(staged.expose().unwrap());
|
||||||
let client = enroll(&face, ClientId::new(bytes(90)));
|
let client = enroll(&face, ClientId::new(bytes(90)));
|
||||||
assert_eq!(client.credential.generation_identity, identity);
|
assert_eq!(client.credential.generation_identity, identity);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,32 @@ impl LifecycleAdapter for RecordingAdapter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct PublicationFailureAdapter;
|
||||||
|
|
||||||
|
impl LifecycleAdapter for PublicationFailureAdapter {
|
||||||
|
type Error = &'static str;
|
||||||
|
|
||||||
|
fn execute(
|
||||||
|
&mut self,
|
||||||
|
_action: LifecycleAction,
|
||||||
|
_generation: &Generation,
|
||||||
|
) -> Result<(), Self::Error> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn after_commit(
|
||||||
|
&mut self,
|
||||||
|
action: LifecycleAction,
|
||||||
|
_generation: &Generation,
|
||||||
|
) -> Result<(), Self::Error> {
|
||||||
|
if action == LifecycleAction::PublishDomain {
|
||||||
|
Err("exposure failed")
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct TestAdmission {
|
struct TestAdmission {
|
||||||
session: Session,
|
session: Session,
|
||||||
|
|
@ -272,6 +298,58 @@ fn exact_start_join_disconnect_and_teardown_actions_are_ordered() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cold_start_entry_reaches_publication_in_exact_modeled_order() {
|
||||||
|
let directory = tempfile::tempdir().unwrap();
|
||||||
|
let store = open_store(&directory);
|
||||||
|
let (domain_adapter, session_adapter) = adapters();
|
||||||
|
let mut lifecycle = HostCoordinatorLifecycle::new(&store, domain_adapter, session_adapter);
|
||||||
|
|
||||||
|
lifecycle
|
||||||
|
.bootstrap_new_domain(generation(1), session(10, 1))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(lifecycle.phase(), DomainPhase::Ready);
|
||||||
|
assert!(lifecycle.is_published());
|
||||||
|
assert_eq!(
|
||||||
|
lifecycle.into_adapters().0.actions.lock().unwrap().clone(),
|
||||||
|
vec![
|
||||||
|
LifecycleAction::StartNewDomain,
|
||||||
|
LifecycleAction::CreateTrustAnchor,
|
||||||
|
LifecycleAction::StartRuntimeSupervisor,
|
||||||
|
LifecycleAction::LeasePreparedLower,
|
||||||
|
LifecycleAction::MountStateAnchor,
|
||||||
|
LifecycleAction::StartSidecar,
|
||||||
|
LifecycleAction::StartConnectivityHelper,
|
||||||
|
LifecycleAction::MarkNetworkReady,
|
||||||
|
LifecycleAction::ArmRuntimeCausalEdge,
|
||||||
|
LifecycleAction::StartSandbox,
|
||||||
|
LifecycleAction::PublishDomain,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn publication_exposure_failure_retains_recoverable_durable_decision() {
|
||||||
|
let directory = tempfile::tempdir().unwrap();
|
||||||
|
let store = open_store(&directory);
|
||||||
|
let (_, session_adapter) = adapters();
|
||||||
|
let mut lifecycle =
|
||||||
|
HostCoordinatorLifecycle::new(&store, PublicationFailureAdapter, session_adapter);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
lifecycle
|
||||||
|
.bootstrap_new_domain(generation(1), session(10, 1))
|
||||||
|
.is_err()
|
||||||
|
);
|
||||||
|
assert_eq!(lifecycle.phase(), DomainPhase::Failed);
|
||||||
|
assert!(!lifecycle.is_published());
|
||||||
|
assert_eq!(
|
||||||
|
store.snapshot().unwrap().generations[0].durable_decision,
|
||||||
|
DurableDecision::Published
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn publication_and_invalid_transitions_fail_before_adapter_execution() {
|
fn publication_and_invalid_transitions_fail_before_adapter_execution() {
|
||||||
let directory = tempfile::tempdir().unwrap();
|
let directory = tempfile::tempdir().unwrap();
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ path = "src/main.rs"
|
||||||
[dependencies]
|
[dependencies]
|
||||||
lazaret-core = { version = "=0.0.0", path = "../lazaret-core" }
|
lazaret-core = { version = "=0.0.0", path = "../lazaret-core" }
|
||||||
lazaret-linux = { version = "=0.0.0", path = "../lazaret-linux" }
|
lazaret-linux = { version = "=0.0.0", path = "../lazaret-linux" }
|
||||||
|
lazaret-state-anchor = { version = "=0.0.0", path = "../lazaret-state-anchor" }
|
||||||
oci-spec.workspace = true
|
oci-spec.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
rustix.workspace = true
|
rustix.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,9 @@ use lazaret_linux::process::{
|
||||||
ChildStandardIo, ParentEdge, ProcessOwnershipContext, RetainedProcess, TargetCommand,
|
ChildStandardIo, ParentEdge, ProcessOwnershipContext, RetainedProcess, TargetCommand,
|
||||||
child_broker, current_process_identity,
|
child_broker, current_process_identity,
|
||||||
};
|
};
|
||||||
|
use lazaret_state_anchor::{
|
||||||
|
RuntimeStateAnchorAuthority, RuntimeStateAnchorMount, StateAnchorLaunchAuthority,
|
||||||
|
};
|
||||||
use rustix::event::{PollFd, PollFlags, Timespec, poll};
|
use rustix::event::{PollFd, PollFlags, Timespec, poll};
|
||||||
use rustix::fs::{MemfdFlags, memfd_create};
|
use rustix::fs::{MemfdFlags, memfd_create};
|
||||||
use rustix::io::{FdFlags, fcntl_setfd, read, write};
|
use rustix::io::{FdFlags, fcntl_setfd, read, write};
|
||||||
|
|
@ -58,6 +61,12 @@ impl AttachedRunscSignal {
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||||
pub enum SupervisorEvent {
|
pub enum SupervisorEvent {
|
||||||
|
StateAnchorStaged(StagedProcessOwnership),
|
||||||
|
StateAnchorReady {
|
||||||
|
staged: StagedProcessOwnership,
|
||||||
|
mount: lazaret_core::identity::MountIdentity,
|
||||||
|
},
|
||||||
|
SidecarStarted,
|
||||||
HelperStaged(StagedProcessOwnership),
|
HelperStaged(StagedProcessOwnership),
|
||||||
HelperReady(StagedProcessOwnership),
|
HelperReady(StagedProcessOwnership),
|
||||||
NetworkAttestation(NetworkReadinessAttestation),
|
NetworkAttestation(NetworkReadinessAttestation),
|
||||||
|
|
@ -69,6 +78,10 @@ pub enum SupervisorEvent {
|
||||||
sentry_pid_inventory: u32,
|
sentry_pid_inventory: u32,
|
||||||
},
|
},
|
||||||
SignalApplied(AttachedRunscSignal),
|
SignalApplied(AttachedRunscSignal),
|
||||||
|
SandboxStopped,
|
||||||
|
SidecarStopped,
|
||||||
|
StateAnchorUnmounted,
|
||||||
|
Exited,
|
||||||
AttachedRunscExited,
|
AttachedRunscExited,
|
||||||
Stopped,
|
Stopped,
|
||||||
Refused(String),
|
Refused(String),
|
||||||
|
|
@ -76,12 +89,19 @@ pub enum SupervisorEvent {
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||||
enum CoordinatorMessage {
|
enum CoordinatorMessage {
|
||||||
|
MountStateAnchor,
|
||||||
|
StateAnchorStagedAck(ProcessOwnershipCommitAck),
|
||||||
|
StartSidecar,
|
||||||
HelperStagedAck(ProcessOwnershipCommitAck),
|
HelperStagedAck(ProcessOwnershipCommitAck),
|
||||||
NetworkAttestationAccepted(NetworkProbeRequest),
|
NetworkAttestationAccepted(NetworkProbeRequest),
|
||||||
NetworkReadinessCommitted,
|
NetworkReadinessCommitted,
|
||||||
StagedAck(ProcessOwnershipCommitAck),
|
StagedAck(ProcessOwnershipCommitAck),
|
||||||
SandboxCommitted,
|
SandboxCommitted,
|
||||||
SignalAttachedRunsc(AttachedRunscSignal),
|
SignalAttachedRunsc(AttachedRunscSignal),
|
||||||
|
StopSandbox,
|
||||||
|
StopSidecar,
|
||||||
|
UnmountStateAnchor,
|
||||||
|
Exit,
|
||||||
Stop,
|
Stop,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -101,6 +121,30 @@ impl CoordinatorChannel {
|
||||||
receive_message(&self.descriptor)
|
receive_message(&self.descriptor)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// # Errors
|
||||||
|
/// Returns an error when exact State Anchor acquisition cannot be requested.
|
||||||
|
pub fn mount_state_anchor(&self) -> io::Result<()> {
|
||||||
|
send_message(&self.descriptor, &CoordinatorMessage::MountStateAnchor)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # Errors
|
||||||
|
/// Returns an error when the exact daemon acknowledgment cannot be delivered.
|
||||||
|
pub fn acknowledge_state_anchor(
|
||||||
|
&self,
|
||||||
|
acknowledgment: ProcessOwnershipCommitAck,
|
||||||
|
) -> io::Result<()> {
|
||||||
|
send_message(
|
||||||
|
&self.descriptor,
|
||||||
|
&CoordinatorMessage::StateAnchorStagedAck(acknowledgment),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # Errors
|
||||||
|
/// Returns an error when the RS-owned sidecar install path cannot be released.
|
||||||
|
pub fn start_sidecar(&self) -> io::Result<()> {
|
||||||
|
send_message(&self.descriptor, &CoordinatorMessage::StartSidecar)
|
||||||
|
}
|
||||||
|
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// Returns an error when the exact typed acknowledgment cannot be delivered.
|
/// Returns an error when the exact typed acknowledgment cannot be delivered.
|
||||||
pub fn acknowledge_staged(&self, acknowledgment: ProcessOwnershipCommitAck) -> io::Result<()> {
|
pub fn acknowledge_staged(&self, acknowledgment: ProcessOwnershipCommitAck) -> io::Result<()> {
|
||||||
|
|
@ -157,14 +201,41 @@ impl CoordinatorChannel {
|
||||||
pub fn stop(&self) -> io::Result<()> {
|
pub fn stop(&self) -> io::Result<()> {
|
||||||
send_message(&self.descriptor, &CoordinatorMessage::Stop)
|
send_message(&self.descriptor, &CoordinatorMessage::Stop)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// # Errors
|
||||||
|
/// Returns an error when ordered sandbox stop cannot be requested.
|
||||||
|
pub fn stop_sandbox(&self) -> io::Result<()> {
|
||||||
|
send_message(&self.descriptor, &CoordinatorMessage::StopSandbox)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # Errors
|
||||||
|
/// Returns an error when ordered sidecar stop cannot be requested.
|
||||||
|
pub fn stop_sidecar(&self) -> io::Result<()> {
|
||||||
|
send_message(&self.descriptor, &CoordinatorMessage::StopSidecar)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # Errors
|
||||||
|
/// Returns an error when exact State Anchor unmount cannot be requested.
|
||||||
|
pub fn unmount_state_anchor(&self) -> io::Result<()> {
|
||||||
|
send_message(&self.descriptor, &CoordinatorMessage::UnmountStateAnchor)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # Errors
|
||||||
|
/// Returns an error when graceful Runtime Supervisor exit cannot be requested.
|
||||||
|
pub fn exit(&self) -> io::Result<()> {
|
||||||
|
send_message(&self.descriptor, &CoordinatorMessage::Exit)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct RuntimeSupervisorLaunch {
|
pub struct RuntimeSupervisorLaunch {
|
||||||
target: TargetCommand,
|
target: TargetCommand,
|
||||||
ownership: ProcessOwnershipContext,
|
ownership: ProcessOwnershipContext,
|
||||||
policy_file: Option<File>,
|
policy_file: Option<File>,
|
||||||
|
policy_digest: Option<DerivedPolicyDigest>,
|
||||||
|
configuration_fingerprint: Option<ConfigurationFingerprint>,
|
||||||
network_sidecar_id: Option<NetworkSidecarId>,
|
network_sidecar_id: Option<NetworkSidecarId>,
|
||||||
runsc_command: RunscCommand,
|
runsc_command: RunscCommand,
|
||||||
|
state_anchor: Option<StateAnchorLaunchAuthority>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
|
|
@ -213,8 +284,11 @@ impl RuntimeSupervisorLaunch {
|
||||||
target: target.with_lifetime_pipe_parent_edge(),
|
target: target.with_lifetime_pipe_parent_edge(),
|
||||||
ownership,
|
ownership,
|
||||||
policy_file: None,
|
policy_file: None,
|
||||||
|
policy_digest: None,
|
||||||
|
configuration_fingerprint: None,
|
||||||
network_sidecar_id: None,
|
network_sidecar_id: None,
|
||||||
runsc_command: command.clone(),
|
runsc_command: command.clone(),
|
||||||
|
state_anchor: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -260,10 +334,68 @@ impl RuntimeSupervisorLaunch {
|
||||||
launch.target = launch.target.with_lifetime_pipe_parent_edge();
|
launch.target = launch.target.with_lifetime_pipe_parent_edge();
|
||||||
drop(target);
|
drop(target);
|
||||||
launch.policy_file = Some(policy_file);
|
launch.policy_file = Some(policy_file);
|
||||||
|
launch.policy_digest = Some(network.policy.digest());
|
||||||
|
launch.configuration_fingerprint = Some(network.configuration_fingerprint);
|
||||||
launch.network_sidecar_id = Some(network.network_sidecar_id);
|
launch.network_sidecar_id = Some(network.network_sidecar_id);
|
||||||
Ok(launch)
|
Ok(launch)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Adds exact inherited State Anchor authorities without beginning physical acquisition.
|
||||||
|
#[must_use]
|
||||||
|
pub fn with_state_anchor(mut self, authority: StateAnchorLaunchAuthority) -> Self {
|
||||||
|
self.state_anchor = Some(authority);
|
||||||
|
self.rebuild_target();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rebuild_target(&mut self) {
|
||||||
|
let mut target = TargetCommand::new("/proc/self/exe")
|
||||||
|
.argument(PRIVATE_ROLE_ARGUMENT)
|
||||||
|
.argument("--domain")
|
||||||
|
.argument(hex(self.ownership.domain_id.as_bytes()))
|
||||||
|
.argument("--generation")
|
||||||
|
.argument(self.ownership.generation_id.get().to_string())
|
||||||
|
.argument("--coordinator")
|
||||||
|
.argument(hex(self.ownership.coordinator_id.as_bytes()));
|
||||||
|
if let Some(authority) = &self.state_anchor {
|
||||||
|
let (device, inode) = authority.expected_lower();
|
||||||
|
target = target
|
||||||
|
.argument("--state-lower-fd")
|
||||||
|
.argument(authority.lower_fd().to_string())
|
||||||
|
.argument("--state-persistent-fd")
|
||||||
|
.argument(authority.persistent_anchor_fd().to_string())
|
||||||
|
.argument("--state-runtime-fd")
|
||||||
|
.argument(authority.runtime_anchor_fd().to_string())
|
||||||
|
.argument("--state-lower-device")
|
||||||
|
.argument(device.to_string())
|
||||||
|
.argument("--state-lower-inode")
|
||||||
|
.argument(inode.to_string());
|
||||||
|
}
|
||||||
|
if let (Some(policy_file), Some(digest), Some(fingerprint), Some(sidecar)) = (
|
||||||
|
&self.policy_file,
|
||||||
|
self.policy_digest,
|
||||||
|
self.configuration_fingerprint,
|
||||||
|
self.network_sidecar_id,
|
||||||
|
) {
|
||||||
|
target = target
|
||||||
|
.argument("--network-policy-fd")
|
||||||
|
.argument(policy_file.as_raw_fd().to_string())
|
||||||
|
.argument("--network-policy-digest")
|
||||||
|
.argument(hex(digest.as_bytes()))
|
||||||
|
.argument("--configuration-fingerprint")
|
||||||
|
.argument(hex(fingerprint.as_bytes()))
|
||||||
|
.argument("--network-sidecar")
|
||||||
|
.argument(hex(sidecar.as_bytes()));
|
||||||
|
}
|
||||||
|
target = target
|
||||||
|
.argument("--runsc")
|
||||||
|
.argument(self.runsc_command.program());
|
||||||
|
for argument in self.runsc_command.arguments() {
|
||||||
|
target = target.argument(argument);
|
||||||
|
}
|
||||||
|
self.target = target.with_lifetime_pipe_parent_edge();
|
||||||
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub const fn target(&self) -> &TargetCommand {
|
pub const fn target(&self) -> &TargetCommand {
|
||||||
&self.target
|
&self.target
|
||||||
|
|
@ -328,11 +460,19 @@ pub fn dispatch_private_runtime_role(arguments: &[OsString]) -> Option<io::Resul
|
||||||
|
|
||||||
struct RuntimeArguments {
|
struct RuntimeArguments {
|
||||||
ownership: ProcessOwnershipContext,
|
ownership: ProcessOwnershipContext,
|
||||||
|
state_anchor: Option<RuntimeStateAnchorArguments>,
|
||||||
network: Option<RuntimeNetworkArguments>,
|
network: Option<RuntimeNetworkArguments>,
|
||||||
runsc_program: OsString,
|
runsc_program: OsString,
|
||||||
runsc_arguments: Vec<OsString>,
|
runsc_arguments: Vec<OsString>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct RuntimeStateAnchorArguments {
|
||||||
|
lower_fd: i32,
|
||||||
|
persistent_anchor_fd: i32,
|
||||||
|
runtime_anchor_fd: i32,
|
||||||
|
expected_lower: (u64, u64),
|
||||||
|
}
|
||||||
|
|
||||||
struct RuntimeNetworkArguments {
|
struct RuntimeNetworkArguments {
|
||||||
policy_fd: i32,
|
policy_fd: i32,
|
||||||
policy_digest: DerivedPolicyDigest,
|
policy_digest: DerivedPolicyDigest,
|
||||||
|
|
@ -348,6 +488,22 @@ fn parse_runtime_arguments(arguments: &[OsString]) -> io::Result<RuntimeArgument
|
||||||
.and_then(|value| value.parse().ok())
|
.and_then(|value| value.parse().ok())
|
||||||
.ok_or_else(|| io::Error::other("Runtime Supervisor generation is invalid"))?;
|
.ok_or_else(|| io::Error::other("Runtime Supervisor generation is invalid"))?;
|
||||||
let coordinator = parse_fixed_hex(named(&mut arguments, "--coordinator")?)?;
|
let coordinator = parse_fixed_hex(named(&mut arguments, "--coordinator")?)?;
|
||||||
|
let state_anchor = if arguments
|
||||||
|
.peek()
|
||||||
|
.is_some_and(|value| *value == "--state-lower-fd")
|
||||||
|
{
|
||||||
|
Some(RuntimeStateAnchorArguments {
|
||||||
|
lower_fd: parse_number(named(&mut arguments, "--state-lower-fd")?)?,
|
||||||
|
persistent_anchor_fd: parse_number(named(&mut arguments, "--state-persistent-fd")?)?,
|
||||||
|
runtime_anchor_fd: parse_number(named(&mut arguments, "--state-runtime-fd")?)?,
|
||||||
|
expected_lower: (
|
||||||
|
parse_number(named(&mut arguments, "--state-lower-device")?)?,
|
||||||
|
parse_number(named(&mut arguments, "--state-lower-inode")?)?,
|
||||||
|
),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
let network = if arguments
|
let network = if arguments
|
||||||
.peek()
|
.peek()
|
||||||
.is_some_and(|value| *value == "--network-policy-fd")
|
.is_some_and(|value| *value == "--network-policy-fd")
|
||||||
|
|
@ -374,12 +530,23 @@ fn parse_runtime_arguments(arguments: &[OsString]) -> io::Result<RuntimeArgument
|
||||||
coordinator_id: CoordinatorId::new(coordinator),
|
coordinator_id: CoordinatorId::new(coordinator),
|
||||||
slot: HostProcessOwnershipSlot::SandboxAttachedRunsc,
|
slot: HostProcessOwnershipSlot::SandboxAttachedRunsc,
|
||||||
},
|
},
|
||||||
|
state_anchor,
|
||||||
network,
|
network,
|
||||||
runsc_program: (*program).clone(),
|
runsc_program: (*program).clone(),
|
||||||
runsc_arguments: arguments.cloned().collect(),
|
runsc_arguments: arguments.cloned().collect(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_number<T>(value: &OsStr) -> io::Result<T>
|
||||||
|
where
|
||||||
|
T: std::str::FromStr,
|
||||||
|
{
|
||||||
|
value
|
||||||
|
.to_str()
|
||||||
|
.and_then(|value| value.parse().ok())
|
||||||
|
.ok_or_else(|| io::Error::other("Runtime Supervisor numeric argument is invalid"))
|
||||||
|
}
|
||||||
|
|
||||||
fn parse_network_arguments<'a>(
|
fn parse_network_arguments<'a>(
|
||||||
arguments: &mut impl Iterator<Item = &'a OsString>,
|
arguments: &mut impl Iterator<Item = &'a OsString>,
|
||||||
) -> io::Result<RuntimeNetworkArguments> {
|
) -> io::Result<RuntimeNetworkArguments> {
|
||||||
|
|
@ -439,9 +606,92 @@ fn run_supervisor(arguments: &RuntimeArguments) -> io::Result<()> {
|
||||||
require_lifetime_held()?;
|
require_lifetime_held()?;
|
||||||
let control = std::io::stdout();
|
let control = std::io::stdout();
|
||||||
let parent = current_process_identity()?;
|
let parent = current_process_identity()?;
|
||||||
|
let mut state_anchor = prepare_state_anchor(arguments, parent, &control)?;
|
||||||
|
let result = run_supervisor_with_state(arguments, parent, &control, &mut state_anchor);
|
||||||
|
let cleanup = state_anchor.take().map(RuntimeStateAnchorMount::unmount);
|
||||||
|
match (result, cleanup) {
|
||||||
|
(Err(error), Some(Err(cleanup))) => Err(io::Error::new(
|
||||||
|
error.kind(),
|
||||||
|
format!("{error}; exact State Anchor cleanup failed: {cleanup}"),
|
||||||
|
)),
|
||||||
|
(Err(error), _) => Err(error),
|
||||||
|
(Ok(()), Some(Err(error))) => Err(io::Error::other(error)),
|
||||||
|
(Ok(()), _) => Ok(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepare_state_anchor(
|
||||||
|
arguments: &RuntimeArguments,
|
||||||
|
parent: lazaret_core::identity::PidfsProcessIdentityV1,
|
||||||
|
control: &std::io::Stdout,
|
||||||
|
) -> io::Result<Option<RuntimeStateAnchorMount>> {
|
||||||
|
let Some(state) = &arguments.state_anchor else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
let command: CoordinatorMessage = receive_message(control)?;
|
||||||
|
if command != CoordinatorMessage::MountStateAnchor {
|
||||||
|
return Err(io::Error::other(
|
||||||
|
"Runtime Supervisor received no State Anchor mount command",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let authority = RuntimeStateAnchorAuthority::from_inherited(
|
||||||
|
state.lower_fd,
|
||||||
|
state.persistent_anchor_fd,
|
||||||
|
state.runtime_anchor_fd,
|
||||||
|
state.expected_lower,
|
||||||
|
)
|
||||||
|
.map_err(io::Error::other)?;
|
||||||
|
let ownership = ProcessOwnershipContext {
|
||||||
|
slot: HostProcessOwnershipSlot::StateAnchorDaemon,
|
||||||
|
..arguments.ownership
|
||||||
|
};
|
||||||
|
authority
|
||||||
|
.acquire(
|
||||||
|
parent,
|
||||||
|
ownership,
|
||||||
|
|staged| send_message(control, &SupervisorEvent::StateAnchorStaged(staged)),
|
||||||
|
|staged| match receive_message(control)? {
|
||||||
|
CoordinatorMessage::StateAnchorStagedAck(acknowledgment)
|
||||||
|
if acknowledgment.acknowledges(&staged) =>
|
||||||
|
{
|
||||||
|
Ok(acknowledgment)
|
||||||
|
}
|
||||||
|
_ => Err(io::Error::other(
|
||||||
|
"Runtime Supervisor received a mismatched State Anchor acknowledgment",
|
||||||
|
)),
|
||||||
|
},
|
||||||
|
|readiness| {
|
||||||
|
send_message(
|
||||||
|
control,
|
||||||
|
&SupervisorEvent::StateAnchorReady {
|
||||||
|
staged: readiness.staged(),
|
||||||
|
mount: readiness.mount(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.map(Some)
|
||||||
|
.map_err(io::Error::other)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_supervisor_with_state(
|
||||||
|
arguments: &RuntimeArguments,
|
||||||
|
parent: lazaret_core::identity::PidfsProcessIdentityV1,
|
||||||
|
control: &std::io::Stdout,
|
||||||
|
state_anchor: &mut Option<RuntimeStateAnchorMount>,
|
||||||
|
) -> io::Result<()> {
|
||||||
|
if arguments.state_anchor.is_some() {
|
||||||
|
let message: CoordinatorMessage = receive_message(control)?;
|
||||||
|
if message != CoordinatorMessage::StartSidecar {
|
||||||
|
return Err(io::Error::other(
|
||||||
|
"Runtime Supervisor received no sidecar start command",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
send_message(control, &SupervisorEvent::SidecarStarted)?;
|
||||||
|
}
|
||||||
let network = match &arguments.network {
|
let network = match &arguments.network {
|
||||||
Some(network) => Some(prepare_runtime_network(
|
Some(network) => Some(prepare_runtime_network(
|
||||||
arguments, network, parent, &control,
|
arguments, network, parent, control,
|
||||||
)?),
|
)?),
|
||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
|
|
@ -456,9 +706,9 @@ fn run_supervisor(arguments: &RuntimeArguments) -> io::Result<()> {
|
||||||
attached_runsc_standard_io(&runsc)?,
|
attached_runsc_standard_io(&runsc)?,
|
||||||
|ownership| {
|
|ownership| {
|
||||||
staged.set(Some(ownership));
|
staged.set(Some(ownership));
|
||||||
send_message(&control, &SupervisorEvent::Staged(ownership))
|
send_message(control, &SupervisorEvent::Staged(ownership))
|
||||||
},
|
},
|
||||||
|ownership| match receive_message(&control)? {
|
|ownership| match receive_message(control)? {
|
||||||
CoordinatorMessage::StagedAck(acknowledgment)
|
CoordinatorMessage::StagedAck(acknowledgment)
|
||||||
if acknowledgment.acknowledges(&ownership) =>
|
if acknowledgment.acknowledges(&ownership) =>
|
||||||
{
|
{
|
||||||
|
|
@ -493,7 +743,7 @@ fn run_supervisor(arguments: &RuntimeArguments) -> io::Result<()> {
|
||||||
return fail_with_network_cleanup(process, network, error);
|
return fail_with_network_cleanup(process, network, error);
|
||||||
}
|
}
|
||||||
if let Err(error) = send_message(
|
if let Err(error) = send_message(
|
||||||
&control,
|
control,
|
||||||
&SupervisorEvent::Ready {
|
&SupervisorEvent::Ready {
|
||||||
staged,
|
staged,
|
||||||
sentry_pid_inventory,
|
sentry_pid_inventory,
|
||||||
|
|
@ -501,7 +751,7 @@ fn run_supervisor(arguments: &RuntimeArguments) -> io::Result<()> {
|
||||||
) {
|
) {
|
||||||
return fail_with_network_cleanup(process, network, error);
|
return fail_with_network_cleanup(process, network, error);
|
||||||
}
|
}
|
||||||
let committed: CoordinatorMessage = match receive_message(&control) {
|
let committed: CoordinatorMessage = match receive_message(control) {
|
||||||
Ok(committed) => committed,
|
Ok(committed) => committed,
|
||||||
Err(error) => return fail_with_network_cleanup(process, network, error),
|
Err(error) => return fail_with_network_cleanup(process, network, error),
|
||||||
};
|
};
|
||||||
|
|
@ -512,7 +762,7 @@ fn run_supervisor(arguments: &RuntimeArguments) -> io::Result<()> {
|
||||||
io::Error::other("sandbox commit acknowledgment was not received"),
|
io::Error::other("sandbox commit acknowledgment was not received"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
supervise(&control, process, network)
|
supervise(control, process, network, state_anchor)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prepare_runtime_network(
|
fn prepare_runtime_network(
|
||||||
|
|
@ -645,14 +895,23 @@ fn supervise(
|
||||||
control: &std::io::Stdout,
|
control: &std::io::Stdout,
|
||||||
process: RetainedProcess,
|
process: RetainedProcess,
|
||||||
mut network: Option<PreparedNetwork>,
|
mut network: Option<PreparedNetwork>,
|
||||||
|
state_anchor: &mut Option<RuntimeStateAnchorMount>,
|
||||||
) -> io::Result<()> {
|
) -> io::Result<()> {
|
||||||
|
let mut process = Some(process);
|
||||||
loop {
|
loop {
|
||||||
let exited = match process.has_exited() {
|
let exited = match process
|
||||||
|
.as_ref()
|
||||||
|
.map_or(Ok(false), RetainedProcess::has_exited)
|
||||||
|
{
|
||||||
Ok(exited) => exited,
|
Ok(exited) => exited,
|
||||||
Err(error) => return fail_with_network_cleanup(process, network, error),
|
Err(error) => return fail_active_runtime(process, network, error),
|
||||||
};
|
};
|
||||||
if exited {
|
if exited {
|
||||||
child_broker().reap(process)?;
|
child_broker().reap(
|
||||||
|
process
|
||||||
|
.take()
|
||||||
|
.ok_or_else(|| io::Error::other("attached runsc handle is absent"))?,
|
||||||
|
)?;
|
||||||
stop_network(network)?;
|
stop_network(network)?;
|
||||||
send_message(control, &SupervisorEvent::AttachedRunscExited)?;
|
send_message(control, &SupervisorEvent::AttachedRunscExited)?;
|
||||||
return Ok(());
|
return Ok(());
|
||||||
|
|
@ -669,10 +928,12 @@ fn supervise(
|
||||||
}
|
}
|
||||||
let lease_lost = match lifetime_lost() {
|
let lease_lost = match lifetime_lost() {
|
||||||
Ok(lost) => lost,
|
Ok(lost) => lost,
|
||||||
Err(error) => return fail_with_network_cleanup(process, network, error),
|
Err(error) => return fail_active_runtime(process, network, error),
|
||||||
};
|
};
|
||||||
if lease_lost {
|
if lease_lost {
|
||||||
|
if let Some(process) = process.take() {
|
||||||
terminate_and_reap(process)?;
|
terminate_and_reap(process)?;
|
||||||
|
}
|
||||||
stop_network(network)?;
|
stop_network(network)?;
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
@ -684,11 +945,11 @@ fn supervise(
|
||||||
tv_nsec: 10_000_000,
|
tv_nsec: 10_000_000,
|
||||||
}),
|
}),
|
||||||
) {
|
) {
|
||||||
return fail_with_network_cleanup(process, network, error.into());
|
return fail_active_runtime(process, network, error.into());
|
||||||
}
|
}
|
||||||
let events = descriptors[0].revents();
|
let events = descriptors[0].revents();
|
||||||
if events.intersects(PollFlags::HUP | PollFlags::ERR) {
|
if events.intersects(PollFlags::HUP | PollFlags::ERR) {
|
||||||
return fail_with_network_cleanup(
|
return fail_active_runtime(
|
||||||
process,
|
process,
|
||||||
network,
|
network,
|
||||||
io::Error::new(
|
io::Error::new(
|
||||||
|
|
@ -700,38 +961,107 @@ fn supervise(
|
||||||
if events.intersects(PollFlags::IN) {
|
if events.intersects(PollFlags::IN) {
|
||||||
let message = match receive_message(control) {
|
let message = match receive_message(control) {
|
||||||
Ok(message) => message,
|
Ok(message) => message,
|
||||||
Err(error) => return fail_with_network_cleanup(process, network, error),
|
Err(error) => return fail_active_runtime(process, network, error),
|
||||||
};
|
};
|
||||||
|
match handle_supervisor_message(
|
||||||
|
control,
|
||||||
|
&message,
|
||||||
|
&mut process,
|
||||||
|
&mut network,
|
||||||
|
state_anchor,
|
||||||
|
) {
|
||||||
|
Ok(true) => return Ok(()),
|
||||||
|
Ok(false) => {}
|
||||||
|
Err(error) => return fail_active_runtime(process, network, error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_supervisor_message(
|
||||||
|
control: &std::io::Stdout,
|
||||||
|
message: &CoordinatorMessage,
|
||||||
|
process: &mut Option<RetainedProcess>,
|
||||||
|
network: &mut Option<PreparedNetwork>,
|
||||||
|
state_anchor: &mut Option<RuntimeStateAnchorMount>,
|
||||||
|
) -> io::Result<bool> {
|
||||||
match message {
|
match message {
|
||||||
CoordinatorMessage::SignalAttachedRunsc(signal) => {
|
CoordinatorMessage::SignalAttachedRunsc(signal) => {
|
||||||
if let Err(error) = process.send_signal(signal.signal()) {
|
process
|
||||||
return fail_with_network_cleanup(process, network, error);
|
.as_ref()
|
||||||
|
.ok_or_else(|| io::Error::other("attached runsc is already stopped"))?
|
||||||
|
.send_signal(signal.signal())?;
|
||||||
|
send_message(control, &SupervisorEvent::SignalApplied(*signal))?;
|
||||||
}
|
}
|
||||||
if let Err(error) =
|
CoordinatorMessage::StopSandbox => {
|
||||||
send_message(control, &SupervisorEvent::SignalApplied(signal))
|
terminate_and_reap(
|
||||||
{
|
process
|
||||||
return fail_with_network_cleanup(process, network, error);
|
.take()
|
||||||
|
.ok_or_else(|| io::Error::other("attached runsc is already stopped"))?,
|
||||||
|
)?;
|
||||||
|
send_message(control, &SupervisorEvent::SandboxStopped)?;
|
||||||
}
|
}
|
||||||
|
CoordinatorMessage::StopSidecar => {
|
||||||
|
if process.is_some() {
|
||||||
|
return Err(io::Error::other("sidecar stop preceded sandbox stop"));
|
||||||
|
}
|
||||||
|
stop_network(network.take())?;
|
||||||
|
send_message(control, &SupervisorEvent::SidecarStopped)?;
|
||||||
|
}
|
||||||
|
CoordinatorMessage::UnmountStateAnchor => {
|
||||||
|
if process.is_some() || network.is_some() {
|
||||||
|
return Err(io::Error::other(
|
||||||
|
"State Anchor unmount preceded runtime resource stop",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
state_anchor
|
||||||
|
.take()
|
||||||
|
.ok_or_else(|| io::Error::other("State Anchor is absent"))?
|
||||||
|
.unmount()
|
||||||
|
.map_err(io::Error::other)?;
|
||||||
|
send_message(control, &SupervisorEvent::StateAnchorUnmounted)?;
|
||||||
|
}
|
||||||
|
CoordinatorMessage::Exit => {
|
||||||
|
if process.is_some() || network.is_some() || state_anchor.is_some() {
|
||||||
|
return Err(io::Error::other(
|
||||||
|
"Runtime Supervisor exit preceded resource cleanup",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
send_message(control, &SupervisorEvent::Exited)?;
|
||||||
|
return Ok(true);
|
||||||
}
|
}
|
||||||
CoordinatorMessage::Stop => {
|
CoordinatorMessage::Stop => {
|
||||||
|
if let Some(process) = process.take() {
|
||||||
terminate_and_reap(process)?;
|
terminate_and_reap(process)?;
|
||||||
stop_network(network)?;
|
}
|
||||||
|
stop_network(network.take())?;
|
||||||
send_message(control, &SupervisorEvent::Stopped)?;
|
send_message(control, &SupervisorEvent::Stopped)?;
|
||||||
return Ok(());
|
return Ok(true);
|
||||||
}
|
}
|
||||||
CoordinatorMessage::HelperStagedAck(_)
|
CoordinatorMessage::HelperStagedAck(_)
|
||||||
|
| CoordinatorMessage::MountStateAnchor
|
||||||
|
| CoordinatorMessage::StateAnchorStagedAck(_)
|
||||||
|
| CoordinatorMessage::StartSidecar
|
||||||
| CoordinatorMessage::NetworkAttestationAccepted(_)
|
| CoordinatorMessage::NetworkAttestationAccepted(_)
|
||||||
| CoordinatorMessage::NetworkReadinessCommitted
|
| CoordinatorMessage::NetworkReadinessCommitted
|
||||||
| CoordinatorMessage::StagedAck(_)
|
| CoordinatorMessage::StagedAck(_)
|
||||||
| CoordinatorMessage::SandboxCommitted => {
|
| CoordinatorMessage::SandboxCommitted => {
|
||||||
return fail_with_network_cleanup(
|
return Err(io::Error::other("unexpected duplicate coordinator message"));
|
||||||
process,
|
|
||||||
network,
|
|
||||||
io::Error::other("unexpected duplicate coordinator message"),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fail_active_runtime(
|
||||||
|
process: Option<RetainedProcess>,
|
||||||
|
network: Option<PreparedNetwork>,
|
||||||
|
error: io::Error,
|
||||||
|
) -> io::Result<()> {
|
||||||
|
if let Some(process) = process {
|
||||||
|
fail_with_network_cleanup(process, network, error)
|
||||||
|
} else {
|
||||||
|
let _ = stop_network(network);
|
||||||
|
Err(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -890,6 +1220,42 @@ mod tests {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn coordinator_channel_preserves_phased_resource_commands() {
|
||||||
|
let (coordinator, supervisor) = private_seqpacket_pair().unwrap();
|
||||||
|
let channel = CoordinatorChannel::new(coordinator);
|
||||||
|
channel.mount_state_anchor().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
receive_message::<CoordinatorMessage>(&supervisor).unwrap(),
|
||||||
|
CoordinatorMessage::MountStateAnchor
|
||||||
|
);
|
||||||
|
channel.start_sidecar().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
receive_message::<CoordinatorMessage>(&supervisor).unwrap(),
|
||||||
|
CoordinatorMessage::StartSidecar
|
||||||
|
);
|
||||||
|
channel.stop_sandbox().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
receive_message::<CoordinatorMessage>(&supervisor).unwrap(),
|
||||||
|
CoordinatorMessage::StopSandbox
|
||||||
|
);
|
||||||
|
channel.stop_sidecar().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
receive_message::<CoordinatorMessage>(&supervisor).unwrap(),
|
||||||
|
CoordinatorMessage::StopSidecar
|
||||||
|
);
|
||||||
|
channel.unmount_state_anchor().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
receive_message::<CoordinatorMessage>(&supervisor).unwrap(),
|
||||||
|
CoordinatorMessage::UnmountStateAnchor
|
||||||
|
);
|
||||||
|
channel.exit().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
receive_message::<CoordinatorMessage>(&supervisor).unwrap(),
|
||||||
|
CoordinatorMessage::Exit
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn runtime_supervisor_and_runsc_keep_distinct_edge_and_stdio_policies() {
|
fn runtime_supervisor_and_runsc_keep_distinct_edge_and_stdio_policies() {
|
||||||
let ownership = ProcessOwnershipContext {
|
let ownership = ProcessOwnershipContext {
|
||||||
|
|
@ -902,12 +1268,15 @@ mod tests {
|
||||||
target: TargetCommand::new("/proc/self/exe").with_lifetime_pipe_parent_edge(),
|
target: TargetCommand::new("/proc/self/exe").with_lifetime_pipe_parent_edge(),
|
||||||
ownership,
|
ownership,
|
||||||
policy_file: None,
|
policy_file: None,
|
||||||
|
policy_digest: None,
|
||||||
|
configuration_fingerprint: None,
|
||||||
runsc_command: RunscCommand::new(
|
runsc_command: RunscCommand::new(
|
||||||
"runsc",
|
"runsc",
|
||||||
[OsString::from("run"), OsString::from("container")],
|
[OsString::from("run"), OsString::from("container")],
|
||||||
[0; 32],
|
[0; 32],
|
||||||
),
|
),
|
||||||
network_sidecar_id: None,
|
network_sidecar_id: None,
|
||||||
|
state_anchor: None,
|
||||||
};
|
};
|
||||||
let (_coordinator_control, supervisor_control) = private_seqpacket_pair().unwrap();
|
let (_coordinator_control, supervisor_control) = private_seqpacket_pair().unwrap();
|
||||||
let (supervisor_lifetime, _coordinator_lifetime) = pipe_with(PipeFlags::CLOEXEC).unwrap();
|
let (supervisor_lifetime, _coordinator_lifetime) = pipe_with(PipeFlags::CLOEXEC).unwrap();
|
||||||
|
|
@ -925,6 +1294,7 @@ mod tests {
|
||||||
slot: HostProcessOwnershipSlot::SandboxAttachedRunsc,
|
slot: HostProcessOwnershipSlot::SandboxAttachedRunsc,
|
||||||
..ownership
|
..ownership
|
||||||
},
|
},
|
||||||
|
state_anchor: None,
|
||||||
network: None,
|
network: None,
|
||||||
runsc_program: "runsc".into(),
|
runsc_program: "runsc".into(),
|
||||||
runsc_arguments: ["-root=/runtime", "run", "container"]
|
runsc_arguments: ["-root=/runtime", "run", "container"]
|
||||||
|
|
|
||||||
|
|
@ -388,7 +388,223 @@ pub struct StateAnchorAcquisition<'path> {
|
||||||
pub ownership: ProcessOwnershipContext,
|
pub ownership: ProcessOwnershipContext,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct StateAnchorLaunchAuthority {
|
||||||
|
lower: OwnedFd,
|
||||||
|
persistent_anchor: OwnedFd,
|
||||||
|
runtime_anchor: OwnedFd,
|
||||||
|
expected_lower: (u64, u64),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StateAnchorLaunchAuthority {
|
||||||
|
/// Captures exact directory authorities for inheritance by the Runtime Supervisor.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Refuses a changed layout or prepared lower.
|
||||||
|
pub fn capture(
|
||||||
|
layout: &StateAnchorLayout<'_>,
|
||||||
|
artifact: &PreparedArtifact,
|
||||||
|
) -> Result<Self, StateAnchorError> {
|
||||||
|
layout.revalidate()?;
|
||||||
|
let expected_lower = (artifact.device.get(), artifact.inode.get());
|
||||||
|
let lower = open_exact_directory_path(&path_from_host(&artifact.path), expected_lower)?;
|
||||||
|
Ok(Self {
|
||||||
|
lower: inherit_for_exec(lower)?,
|
||||||
|
persistent_anchor: inherit_for_exec(&layout.persistent_anchor)?,
|
||||||
|
runtime_anchor: inherit_for_exec(&layout.runtime_anchor)?,
|
||||||
|
expected_lower,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn lower_fd(&self) -> i32 {
|
||||||
|
self.lower.as_raw_fd()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn persistent_anchor_fd(&self) -> i32 {
|
||||||
|
self.persistent_anchor.as_raw_fd()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn runtime_anchor_fd(&self) -> i32 {
|
||||||
|
self.runtime_anchor.as_raw_fd()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub const fn expected_lower(&self) -> (u64, u64) {
|
||||||
|
self.expected_lower
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct RuntimeStateAnchorAuthority {
|
||||||
|
lower: OwnedFd,
|
||||||
|
persistent_anchor: OwnedFd,
|
||||||
|
runtime_anchor: OwnedFd,
|
||||||
|
merged_descriptor: OwnedFd,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RuntimeStateAnchorAuthority {
|
||||||
|
/// Reopens inherited authorities without falling back to ambient configured paths.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Refuses missing, replaced, or incorrectly typed descriptors and fixed children.
|
||||||
|
pub fn from_inherited(
|
||||||
|
lower_fd: i32,
|
||||||
|
persistent_anchor_fd: i32,
|
||||||
|
runtime_anchor_fd: i32,
|
||||||
|
expected_lower: (u64, u64),
|
||||||
|
) -> Result<Self, StateAnchorError> {
|
||||||
|
let lower = reopen_inherited_directory(lower_fd)?;
|
||||||
|
require_directory_descriptor_identity(&lower, expected_lower)?;
|
||||||
|
let persistent_anchor = reopen_inherited_directory(persistent_anchor_fd)?;
|
||||||
|
let runtime_anchor = reopen_inherited_directory(runtime_anchor_fd)?;
|
||||||
|
for name in ["upper", "work"] {
|
||||||
|
drop(open_directory(&persistent_anchor, Path::new(name))?);
|
||||||
|
}
|
||||||
|
let merged_descriptor = open_directory(&runtime_anchor, Path::new("merged"))?;
|
||||||
|
Ok(Self {
|
||||||
|
lower,
|
||||||
|
persistent_anchor,
|
||||||
|
runtime_anchor,
|
||||||
|
merged_descriptor,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mounts the exact inherited prepared lower and anchors under RS child ownership.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Refuses acquisition, acknowledgment, readiness, or authority failures.
|
||||||
|
pub fn acquire<Stage, Acknowledge, Ready>(
|
||||||
|
self,
|
||||||
|
expected_parent: lazaret_core::identity::PidfsProcessIdentityV1,
|
||||||
|
ownership: ProcessOwnershipContext,
|
||||||
|
stage: Stage,
|
||||||
|
acknowledge: Acknowledge,
|
||||||
|
ready: Ready,
|
||||||
|
) -> Result<RuntimeStateAnchorMount, StateAnchorError>
|
||||||
|
where
|
||||||
|
Stage: FnOnce(StagedProcessOwnership) -> io::Result<()>,
|
||||||
|
Acknowledge: FnOnce(StagedProcessOwnership) -> io::Result<ProcessOwnershipCommitAck>,
|
||||||
|
Ready: FnOnce(VerifiedStateAnchorReadiness) -> io::Result<()>,
|
||||||
|
{
|
||||||
|
if ownership.slot != HostProcessOwnershipSlot::StateAnchorDaemon {
|
||||||
|
return Err(StateAnchorError::InvalidLayout(
|
||||||
|
"process slot is not State Anchor daemon",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let baseline = mount_identity(&self.merged_descriptor)?;
|
||||||
|
let lower_launch = inherit_for_exec(&self.lower)?;
|
||||||
|
let persistent_launch = inherit_for_exec(&self.persistent_anchor)?;
|
||||||
|
let runtime_launch = inherit_for_exec(&self.runtime_anchor)?;
|
||||||
|
let target = TargetCommand::new("fuse-overlayfs")
|
||||||
|
.argument("-f")
|
||||||
|
.argument("-o")
|
||||||
|
.argument(overlay_options(
|
||||||
|
&descriptor_path(&lower_launch),
|
||||||
|
&descriptor_child_path(&persistent_launch, "upper"),
|
||||||
|
&descriptor_child_path(&persistent_launch, "work"),
|
||||||
|
))
|
||||||
|
.argument(descriptor_child_path(&runtime_launch, "merged"));
|
||||||
|
let process =
|
||||||
|
child_broker().acquire(expected_parent, ownership, &target, stage, acknowledge)?;
|
||||||
|
let staged = StagedProcessOwnership::v1(
|
||||||
|
ownership.domain_id,
|
||||||
|
ownership.generation_id,
|
||||||
|
ownership.coordinator_id,
|
||||||
|
ownership.slot,
|
||||||
|
process.identity(),
|
||||||
|
);
|
||||||
|
let deadline = Instant::now() + MOUNT_TIMEOUT;
|
||||||
|
loop {
|
||||||
|
if process.has_exited()? {
|
||||||
|
child_broker().reap(process)?;
|
||||||
|
return Err(StateAnchorError::DaemonExited);
|
||||||
|
}
|
||||||
|
let observed = mount_identity(&open_mounted_directory(
|
||||||
|
&self.runtime_anchor,
|
||||||
|
Path::new("merged"),
|
||||||
|
)?)?;
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
let _ = rootless_unmount_anchor(&self.runtime_anchor);
|
||||||
|
child_broker().reap(process)?;
|
||||||
|
return Err(error.into());
|
||||||
|
}
|
||||||
|
return Ok(RuntimeStateAnchorMount {
|
||||||
|
authority: self,
|
||||||
|
process,
|
||||||
|
mount: observed,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if Instant::now() >= deadline {
|
||||||
|
process.send_signal(rustix::process::Signal::KILL)?;
|
||||||
|
while !process.has_exited()? {
|
||||||
|
thread::yield_now();
|
||||||
|
}
|
||||||
|
let merged = open_mounted_directory(&self.runtime_anchor, Path::new("merged"))?;
|
||||||
|
let observed = mount_identity(&merged)?;
|
||||||
|
if observed.mount_id != baseline.mount_id {
|
||||||
|
let _ = rootless_unmount_anchor(&self.runtime_anchor);
|
||||||
|
}
|
||||||
|
child_broker().reap(process)?;
|
||||||
|
return Err(StateAnchorError::MountTimeout);
|
||||||
|
}
|
||||||
|
thread::sleep(MOUNT_POLL_INTERVAL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct RuntimeStateAnchorMount {
|
||||||
|
authority: RuntimeStateAnchorAuthority,
|
||||||
|
process: RetainedProcess,
|
||||||
|
mount: MountIdentity,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RuntimeStateAnchorMount {
|
||||||
|
#[must_use]
|
||||||
|
pub const fn mount_identity(&self) -> MountIdentity {
|
||||||
|
self.mount
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Unmounts and reaps only the retained exact RS-owned State Anchor.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Refuses mount replacement, unmount failure, or exact child cleanup failure.
|
||||||
|
pub fn unmount(self) -> Result<(), StateAnchorError> {
|
||||||
|
let observed = mount_identity(&open_mounted_directory(
|
||||||
|
&self.authority.runtime_anchor,
|
||||||
|
Path::new("merged"),
|
||||||
|
)?)?;
|
||||||
|
if observed != self.mount {
|
||||||
|
return Err(StateAnchorError::OwnershipMismatch);
|
||||||
|
}
|
||||||
|
rootless_unmount_anchor(&self.authority.runtime_anchor)?;
|
||||||
|
let after = mount_identity(&open_directory(
|
||||||
|
&self.authority.runtime_anchor,
|
||||||
|
Path::new("merged"),
|
||||||
|
)?)?;
|
||||||
|
if after == self.mount {
|
||||||
|
return Err(StateAnchorError::OwnershipMismatch);
|
||||||
|
}
|
||||||
|
child_broker().reap(self.process)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl VerifiedStateAnchorReadiness {
|
impl VerifiedStateAnchorReadiness {
|
||||||
|
#[must_use]
|
||||||
|
pub const fn from_parts(staged: StagedProcessOwnership, mount: MountIdentity) -> Self {
|
||||||
|
Self { staged, mount }
|
||||||
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub const fn staged(self) -> StagedProcessOwnership {
|
pub const fn staged(self) -> StagedProcessOwnership {
|
||||||
self.staged
|
self.staged
|
||||||
|
|
@ -757,6 +973,24 @@ fn inherit_for_exec(descriptor: impl AsFd) -> Result<OwnedFd, StateAnchorError>
|
||||||
dup(descriptor).map_err(io::Error::from).map_err(Into::into)
|
dup(descriptor).map_err(io::Error::from).map_err(Into::into)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn reopen_inherited_directory(descriptor: i32) -> Result<OwnedFd, StateAnchorError> {
|
||||||
|
if descriptor < 0 {
|
||||||
|
return Err(StateAnchorError::InvalidLayout(
|
||||||
|
"inherited directory descriptor is invalid",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let file = File::open(format!("/proc/self/fd/{descriptor}"))?;
|
||||||
|
let descriptor: OwnedFd = file.into();
|
||||||
|
let metadata = rustix::fs::fstat(&descriptor).map_err(io::Error::from)?;
|
||||||
|
if FileType::from_raw_mode(metadata.st_mode).is_dir() {
|
||||||
|
Ok(descriptor)
|
||||||
|
} else {
|
||||||
|
Err(StateAnchorError::InvalidLayout(
|
||||||
|
"inherited authority is not a directory",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn overlay_options(lower: &Path, upper: &Path, work: &Path) -> OsString {
|
fn overlay_options(lower: &Path, upper: &Path, work: &Path) -> OsString {
|
||||||
let mut bytes = b"lowerdir=".to_vec();
|
let mut bytes = b"lowerdir=".to_vec();
|
||||||
append_fuse_option_path(&mut bytes, lower);
|
append_fuse_option_path(&mut bytes, lower);
|
||||||
|
|
@ -781,7 +1015,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(layout: &StateAnchorLayout<'_>) -> Result<(), StateAnchorError> {
|
fn rootless_unmount(layout: &StateAnchorLayout<'_>) -> Result<(), StateAnchorError> {
|
||||||
let inherited_anchor = inherit_for_exec(&layout.runtime_anchor)?;
|
rootless_unmount_anchor(&layout.runtime_anchor)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::disallowed_methods, clippy::disallowed_types)]
|
||||||
|
fn rootless_unmount_anchor(runtime_anchor: impl AsFd) -> Result<(), StateAnchorError> {
|
||||||
|
let inherited_anchor = inherit_for_exec(runtime_anchor)?;
|
||||||
let path = descriptor_child_path(&inherited_anchor, "merged");
|
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")
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,22 @@ use std::collections::BTreeMap;
|
||||||
use std::ffi::OsString;
|
use std::ffi::OsString;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::os::unix::ffi::OsStringExt;
|
use std::os::unix::ffi::OsStringExt;
|
||||||
|
use std::os::unix::fs::MetadataExt;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::atomic::{AtomicU64, Ordering};
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
|
|
||||||
use lazaret_core::configuration::{
|
use lazaret_core::configuration::{
|
||||||
ConfigurationLoadRequest, SandboxLayoutV1, load_trusted_configuration,
|
ConfigurationLoadRequest, SandboxLayoutV1, load_trusted_configuration,
|
||||||
};
|
};
|
||||||
use lazaret_state_anchor::{ProtectedRuntimeFiles, StateAnchorLayout};
|
use lazaret_core::identity::{
|
||||||
|
DeviceId, HostPath, IdMapDigest, InodeId, PolicyId, PreparedArtifactId, SelectedManifestDigest,
|
||||||
|
SourceIdentity,
|
||||||
|
};
|
||||||
|
use lazaret_core::records::{ArtifactState, PreparedArtifact};
|
||||||
|
use lazaret_state_anchor::{
|
||||||
|
ProtectedRuntimeFiles, RuntimeStateAnchorAuthority, StateAnchorLaunchAuthority,
|
||||||
|
StateAnchorLayout,
|
||||||
|
};
|
||||||
|
|
||||||
static SEQUENCE: AtomicU64 = AtomicU64::new(0);
|
static SEQUENCE: AtomicU64 = AtomicU64::new(0);
|
||||||
|
|
||||||
|
|
@ -48,6 +57,45 @@ fn replaced_runtime_root_is_refused_before_runtime_file_write() {
|
||||||
assert!(!replacement_files.join("hosts").exists());
|
assert!(!replacement_files.join("hosts").exists());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn inherited_runtime_authority_accepts_only_the_exact_prepared_lower() {
|
||||||
|
let tree = TestTree::new();
|
||||||
|
let configuration = configuration(&tree);
|
||||||
|
let layout = StateAnchorLayout::create(&configuration).unwrap();
|
||||||
|
let lower = tree.path.join("prepared-lower");
|
||||||
|
fs::create_dir(&lower).unwrap();
|
||||||
|
let metadata = lower.metadata().unwrap();
|
||||||
|
let artifact = PreparedArtifact {
|
||||||
|
prepared_artifact_id: PreparedArtifactId::new([8; 32]),
|
||||||
|
source_identity: SourceIdentity::new(b"fixture".to_vec()),
|
||||||
|
selected_manifest_digest: SelectedManifestDigest::new([9; 32]),
|
||||||
|
policy_id: PolicyId::new([10; 32]),
|
||||||
|
id_map_digest: IdMapDigest::new([11; 32]),
|
||||||
|
path: HostPath::new(lower.as_os_str().as_encoded_bytes().to_vec()),
|
||||||
|
device: DeviceId::new(metadata.dev()),
|
||||||
|
inode: InodeId::new(metadata.ino()),
|
||||||
|
state: ArtifactState::Available,
|
||||||
|
};
|
||||||
|
let launch = StateAnchorLaunchAuthority::capture(&layout, &artifact).unwrap();
|
||||||
|
let expected = launch.expected_lower();
|
||||||
|
RuntimeStateAnchorAuthority::from_inherited(
|
||||||
|
launch.lower_fd(),
|
||||||
|
launch.persistent_anchor_fd(),
|
||||||
|
launch.runtime_anchor_fd(),
|
||||||
|
expected,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!(
|
||||||
|
RuntimeStateAnchorAuthority::from_inherited(
|
||||||
|
launch.lower_fd(),
|
||||||
|
launch.persistent_anchor_fd(),
|
||||||
|
launch.runtime_anchor_fd(),
|
||||||
|
(expected.0, expected.1 + 1),
|
||||||
|
)
|
||||||
|
.is_err()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
struct TestTree {
|
struct TestTree {
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,9 @@ use lazaret_linux::process::{
|
||||||
ProcessOwnershipContext, current_process_identity, dispatch_private_process_role,
|
ProcessOwnershipContext, current_process_identity, dispatch_private_process_role,
|
||||||
};
|
};
|
||||||
use lazaret_state_anchor::{
|
use lazaret_state_anchor::{
|
||||||
FuseOverlayfsMount, ProtectedRuntimeFiles, StateAnchorAcquisition, StateAnchorLayout,
|
FuseOverlayfsMount, ProtectedRuntimeFiles, RuntimeStateAnchorAuthority, StateAnchorAcquisition,
|
||||||
StateAnchorSandboxView, unmount_after_coordinator_loss,
|
StateAnchorLaunchAuthority, StateAnchorLayout, StateAnchorSandboxView,
|
||||||
|
unmount_after_coordinator_loss,
|
||||||
};
|
};
|
||||||
use rustix::process::Signal;
|
use rustix::process::Signal;
|
||||||
|
|
||||||
|
|
@ -77,6 +78,45 @@ fn run() -> io::Result<()> {
|
||||||
};
|
};
|
||||||
let layout = StateAnchorLayout::create(&configuration).map_err(io::Error::other)?;
|
let layout = StateAnchorLayout::create(&configuration).map_err(io::Error::other)?;
|
||||||
|
|
||||||
|
let launch =
|
||||||
|
StateAnchorLaunchAuthority::capture(&layout, &artifact).map_err(io::Error::other)?;
|
||||||
|
let runtime_authority = RuntimeStateAnchorAuthority::from_inherited(
|
||||||
|
launch.lower_fd(),
|
||||||
|
launch.persistent_anchor_fd(),
|
||||||
|
launch.runtime_anchor_fd(),
|
||||||
|
launch.expected_lower(),
|
||||||
|
)
|
||||||
|
.map_err(io::Error::other)?;
|
||||||
|
let expected = std::cell::Cell::new(None);
|
||||||
|
let runtime_mount = runtime_authority
|
||||||
|
.acquire(
|
||||||
|
current_process_identity()?,
|
||||||
|
ProcessOwnershipContext {
|
||||||
|
domain_id: DomainId::new([1; 32]),
|
||||||
|
generation_id: GenerationId::new(1),
|
||||||
|
coordinator_id: CoordinatorId::new([3; 32]),
|
||||||
|
slot: HostProcessOwnershipSlot::StateAnchorDaemon,
|
||||||
|
},
|
||||||
|
|staged| {
|
||||||
|
expected.set(Some(staged));
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
|staged| Ok(ProcessOwnershipCommitAck::v1(staged)),
|
||||||
|
|readiness| {
|
||||||
|
if expected.get() != Some(readiness.staged())
|
||||||
|
|| lazaret_linux::filesystem::current_mount_identity(layout.merged())?
|
||||||
|
!= readiness.mount()
|
||||||
|
{
|
||||||
|
return Err(io::Error::other(
|
||||||
|
"inherited State Anchor readiness changed authority",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.map_err(io::Error::other)?;
|
||||||
|
runtime_mount.unmount().map_err(io::Error::other)?;
|
||||||
|
|
||||||
let first = acquire(&layout, &artifact)?;
|
let first = acquire(&layout, &artifact)?;
|
||||||
fs::write(
|
fs::write(
|
||||||
layout.merged().join("usr/bin/installed-tool"),
|
layout.merged().join("usr/bin/installed-tool"),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue