Compose cold-start resources under Runtime Supervisor ownership

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

1
Cargo.lock generated
View file

@ -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",

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 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(&current) != expected || FileIdentity::from_metadata(&current) != 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]

View file

@ -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;

View file

@ -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;

View file

@ -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,

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 { fn policy() -> CanonicalDerivedNetworkPolicy {
derive_policy(DerivedNetworkPolicyFields { derive_policy(DerivedNetworkPolicyFields {
address_families: vec![ address_families: vec![

View file

@ -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

View file

@ -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);
} }

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)] #[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();

View file

@ -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

View file

@ -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"]

View file

@ -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")

View file

@ -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,
} }

View file

@ -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"),