Compose cold-start resources under Runtime Supervisor ownership

This commit is contained in:
ms2k 2026-07-16 17:29:23 +09:00
commit 33ac0ba4ee
9 changed files with 1827 additions and 139 deletions

View 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(&registry),
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))
}

View file

@ -49,6 +49,8 @@ pub const MAX_ENROLLMENT_BYTES: usize = 8_192;
const MAX_CONTROL_BYTES: usize = 65_536;
const CONTROL_SOCKET_NAME: &str = "control.sock";
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)]
pub enum ControlError {
@ -207,6 +209,11 @@ pub struct 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.
///
/// # Errors
@ -412,7 +419,8 @@ pub struct SessionRegistry {
}
impl SessionRegistry {
fn new(
#[must_use]
pub fn new(
store: Arc<HostCoordinatorDomainStore>,
generation_identity: GenerationIdentity,
) -> Self {
@ -676,6 +684,8 @@ struct ControlPaths {
root_identity: FileIdentity,
socket: PathBuf,
identity: PathBuf,
staged_socket: PathBuf,
staged_identity: PathBuf,
}
impl ControlPaths {
@ -708,6 +718,8 @@ impl ControlPaths {
root_identity,
socket: root.join(CONTROL_SOCKET_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(())
}
fn create_identity(
fn create_named_identity(
&self,
name: &str,
bytes: &[u8],
uid: u32,
) -> Result<(File, FileIdentity), ControlError> {
self.validate_root(uid)?;
let fd = openat(
self.root_file.as_fd(),
PUBLICATION_IDENTITY_NAME,
name,
OFlags::CREATE | OFlags::EXCL | OFlags::RDWR | OFlags::CLOEXEC | OFlags::NOFOLLOW,
Mode::RUSR | Mode::WUSR,
)
@ -749,10 +761,10 @@ impl ControlPaths {
self.root_file
.sync_all()
.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 {
remove_if_identity_matches(&self.identity, identity);
remove_if_identity_matches(&self.root.join(name), identity);
return Err(error);
}
Ok((file, identity))
@ -764,10 +776,21 @@ impl ControlPaths {
expected: FileIdentity,
uid: u32,
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> {
self.validate_root(uid)?;
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
|| FileIdentity::from_metadata(&current) != expected
|| !descriptor.file_type().is_file()
@ -801,6 +824,150 @@ pub struct PublishedControlFace {
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 {
/// Publishes one generation control face in the fixed host-private runtime path.
///
@ -813,83 +980,11 @@ impl PublishedControlFace {
anchor: TrustAnchor,
store: Arc<HostCoordinatorDomainStore>,
) -> 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(
store,
anchor.generation_identity.clone(),
));
registry.require_generation_authority()?;
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,
})
StagedControlFace::stage(runtime_domain_root, uid, anchor, registry)?.expose()
}
#[must_use]

View file

@ -1,5 +1,6 @@
#![doc = "Host-side coordination authority for one Lazaret Domain."]
pub mod bootstrap;
pub mod control;
pub mod domain_store;
pub mod escalation;

View file

@ -45,6 +45,18 @@ pub trait LifecycleAdapter {
action: LifecycleAction,
generation: &Generation,
) -> 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)]
@ -209,6 +221,16 @@ where
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
/// Refuses a non-absent Domain, invalid initial records, or adapter/store failure.
pub fn start_new_domain(
@ -251,6 +273,28 @@ where
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
/// Refuses an unpublished, stale, mismatched, duplicate, or unauthenticated session.
pub fn admit_session(
@ -529,7 +573,16 @@ where
generation.durable_decision = DurableDecision::Published;
self.store
.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.bootstrap_owner = None;
self.published = true;

View file

@ -368,6 +368,92 @@ impl<'store> NetworkCoordinator<'store> {
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(
&self,
staged: StagedProcessOwnership,

View file

@ -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 {
derive_policy(DerivedNetworkPolicyFields {
address_families: vec![

View file

@ -4,8 +4,10 @@ use std::os::unix::ffi::OsStrExt;
use std::path::Path;
use std::time::{Duration, Instant};
use crate::state_anchor::StateAnchorCoordinator;
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::records::{
@ -375,6 +377,88 @@ impl AwaitingAttachedRunsc {
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.
///
/// # 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.
///
/// # Errors

View file

@ -17,7 +17,8 @@ use lazaret_core::records::{
SessionRegistrationResponse, TlsCredential,
};
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_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 identity = generation_identity(7);
let store = open_preparing_store(&directory, &identity);
let face = Arc::new(
PublishedControlFace::publish(
&directory.0,
getuid().as_raw(),
TrustAnchor::create(identity.clone()).unwrap(),
Arc::clone(&store),
)
.unwrap(),
);
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
));
let registry = Arc::new(SessionRegistry::new(Arc::clone(&store), identity.clone()));
let staged = StagedControlFace::stage(
&directory.0,
getuid().as_raw(),
TrustAnchor::create(identity.clone()).unwrap(),
registry,
)
.unwrap();
assert!(std::os::unix::net::UnixStream::connect(directory.path("control.sock")).is_err());
store
.apply(&[DomainStoreMutation::PutGeneration(generation(
@ -714,6 +704,7 @@ fn prepared_control_face_admits_only_after_generation_publication() {
DurableDecision::Published,
))])
.unwrap();
let face = Arc::new(staged.expose().unwrap());
let client = enroll(&face, ClientId::new(bytes(90)));
assert_eq!(client.credential.generation_identity, identity);
}

View file

@ -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)]
struct TestAdmission {
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]
fn publication_and_invalid_transitions_fail_before_adapter_execution() {
let directory = tempfile::tempdir().unwrap();