Compose cold-start resources under Runtime Supervisor ownership
This commit is contained in:
parent
510dfccf5a
commit
33ac0ba4ee
14 changed files with 1827 additions and 139 deletions
483
crates/lazaret-host-coordinator/src/bootstrap.rs
Normal file
483
crates/lazaret-host-coordinator/src/bootstrap.rs
Normal file
|
|
@ -0,0 +1,483 @@
|
|||
use std::fmt;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
use lazaret_core::configuration::ResolvedDomainConfiguration;
|
||||
use lazaret_core::identity::{BundleDigest, ContainerId, MountIdentity};
|
||||
use lazaret_core::network::{CanonicalDerivedNetworkPolicy, ReachabilityExpectation};
|
||||
use lazaret_core::records::Session;
|
||||
use lazaret_core::records::{Generation, GenerationIdentity, PreparedArtifact};
|
||||
use lazaret_linux::process::current_process_identity;
|
||||
use lazaret_runtime_supervisor::network::NetworkProbeRequest;
|
||||
use lazaret_runtime_supervisor::sandbox::SandboxBundle;
|
||||
use lazaret_runtime_supervisor::supervision::RuntimeSupervisorLaunch;
|
||||
use lazaret_state_anchor::StateAnchorLayout;
|
||||
use lazaret_storage_catalog::Catalog;
|
||||
|
||||
use crate::control::ControlSessionAdmission;
|
||||
use crate::control::{PublishedControlFace, SessionRegistry, StagedControlFace, TrustAnchor};
|
||||
use crate::domain_store::HostCoordinatorDomainStore;
|
||||
use crate::lifecycle::{
|
||||
HostCoordinatorLifecycle, LifecycleAction, LifecycleAdapter, SessionAdmissionIdentity,
|
||||
SessionLifecycleAdapter,
|
||||
};
|
||||
use crate::network::NetworkCoordinator;
|
||||
use crate::runtime::{AwaitingAttachedRunsc, CommittedSandboxRuntime, SandboxCoordinator};
|
||||
use crate::state_anchor::{StateAnchorCoordinator, StateAnchorReleaseAuthority};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ProductionLifecycleError(String);
|
||||
|
||||
impl fmt::Display for ProductionLifecycleError {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter.write_str(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for ProductionLifecycleError {}
|
||||
|
||||
fn failure(error: impl fmt::Display) -> ProductionLifecycleError {
|
||||
ProductionLifecycleError(error.to_string())
|
||||
}
|
||||
|
||||
pub struct ProductionBootstrapPlan<'a> {
|
||||
pub store: Arc<HostCoordinatorDomainStore>,
|
||||
pub catalog: &'a Catalog,
|
||||
pub configuration: &'a ResolvedDomainConfiguration,
|
||||
pub generation_identity: GenerationIdentity,
|
||||
pub prepared_artifact: &'a PreparedArtifact,
|
||||
pub state_anchor_layout: &'a StateAnchorLayout<'a>,
|
||||
pub runtime_launch: RuntimeSupervisorLaunch,
|
||||
pub sandbox_bundle: &'a SandboxBundle,
|
||||
pub network_policy: &'a CanonicalDerivedNetworkPolicy,
|
||||
pub network_probe: NetworkProbeRequest,
|
||||
pub reachability: &'a ReachabilityExpectation,
|
||||
pub container_id: ContainerId,
|
||||
pub runtime_root: PathBuf,
|
||||
pub bundle_digest: BundleDigest,
|
||||
pub invoking_uid: u32,
|
||||
}
|
||||
|
||||
pub struct ProductionLifecycleAdapter<'a> {
|
||||
plan: ProductionBootstrapPlan<'a>,
|
||||
registry: Arc<SessionRegistry>,
|
||||
trust_anchor: Option<TrustAnchor>,
|
||||
staged_control: Option<StagedControlFace>,
|
||||
published_control: Option<PublishedControlFace>,
|
||||
awaiting_runtime: Option<AwaitingAttachedRunsc>,
|
||||
runtime: Option<CommittedSandboxRuntime>,
|
||||
state_anchor_mount: Option<MountIdentity>,
|
||||
state_anchor_release: Option<StateAnchorReleaseAuthority>,
|
||||
}
|
||||
|
||||
pub type ProductionHostCoordinatorLifecycle<'store, 'resources> = HostCoordinatorLifecycle<
|
||||
'store,
|
||||
ProductionLifecycleAdapter<'resources>,
|
||||
ProductionSessionRegistry,
|
||||
>;
|
||||
|
||||
/// Composes the cold-start lifecycle with the exact shared production session registry.
|
||||
///
|
||||
/// # Errors
|
||||
/// Refuses a plan whose Domain Store is not the caller-retained lock-owning store.
|
||||
pub fn production_lifecycle<'store, 'resources>(
|
||||
store: &'store Arc<HostCoordinatorDomainStore>,
|
||||
plan: ProductionBootstrapPlan<'resources>,
|
||||
) -> Result<ProductionHostCoordinatorLifecycle<'store, 'resources>, ProductionLifecycleError> {
|
||||
if !Arc::ptr_eq(store, &plan.store) {
|
||||
return Err(ProductionLifecycleError(
|
||||
"production bootstrap Domain Store authority differs".into(),
|
||||
));
|
||||
}
|
||||
let (adapter, registry) = ProductionLifecycleAdapter::new(plan);
|
||||
Ok(HostCoordinatorLifecycle::new(store, adapter, registry))
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ProductionSessionRegistry {
|
||||
registry: Arc<SessionRegistry>,
|
||||
}
|
||||
|
||||
impl ProductionSessionRegistry {
|
||||
#[must_use]
|
||||
pub fn registry(&self) -> Arc<SessionRegistry> {
|
||||
Arc::clone(&self.registry)
|
||||
}
|
||||
}
|
||||
|
||||
impl SessionLifecycleAdapter for ProductionSessionRegistry {
|
||||
type Error = ProductionLifecycleError;
|
||||
type Admission = ControlSessionAdmission;
|
||||
|
||||
fn admission_identity(&self, admission: &Self::Admission) -> SessionAdmissionIdentity {
|
||||
<Arc<SessionRegistry> as SessionLifecycleAdapter>::admission_identity(
|
||||
&self.registry,
|
||||
admission,
|
||||
)
|
||||
}
|
||||
|
||||
fn register_session(
|
||||
&self,
|
||||
store: &HostCoordinatorDomainStore,
|
||||
entry_action: LifecycleAction,
|
||||
admission: Self::Admission,
|
||||
) -> Result<Session, Self::Error> {
|
||||
<Arc<SessionRegistry> as SessionLifecycleAdapter>::register_session(
|
||||
&self.registry,
|
||||
store,
|
||||
entry_action,
|
||||
admission,
|
||||
)
|
||||
.map_err(failure)
|
||||
}
|
||||
|
||||
fn disconnect_session(
|
||||
&self,
|
||||
store: &HostCoordinatorDomainStore,
|
||||
session: &Session,
|
||||
) -> Result<(), Self::Error> {
|
||||
<Arc<SessionRegistry> as SessionLifecycleAdapter>::disconnect_session(
|
||||
&self.registry,
|
||||
store,
|
||||
session,
|
||||
)
|
||||
.map_err(failure)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> ProductionLifecycleAdapter<'a> {
|
||||
#[must_use]
|
||||
pub fn new(plan: ProductionBootstrapPlan<'a>) -> (Self, ProductionSessionRegistry) {
|
||||
let registry = Arc::new(SessionRegistry::new(
|
||||
Arc::clone(&plan.store),
|
||||
plan.generation_identity.clone(),
|
||||
));
|
||||
(
|
||||
Self {
|
||||
plan,
|
||||
registry: Arc::clone(®istry),
|
||||
trust_anchor: None,
|
||||
staged_control: None,
|
||||
published_control: None,
|
||||
awaiting_runtime: None,
|
||||
runtime: None,
|
||||
state_anchor_mount: None,
|
||||
state_anchor_release: None,
|
||||
},
|
||||
ProductionSessionRegistry { registry },
|
||||
)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn published_control(&self) -> Option<&PublishedControlFace> {
|
||||
self.published_control.as_ref()
|
||||
}
|
||||
|
||||
fn require_generation(&self, generation: &Generation) -> Result<(), ProductionLifecycleError> {
|
||||
if generation.domain_id == self.plan.generation_identity.domain_id
|
||||
&& generation.generation_id == self.plan.generation_identity.generation_id
|
||||
&& generation.configuration_fingerprint
|
||||
== self.plan.generation_identity.configuration_fingerprint
|
||||
&& generation.coordinator_id == self.plan.generation_identity.coordinator_id
|
||||
&& generation.sandbox_id == self.plan.generation_identity.sandbox_id
|
||||
{
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ProductionLifecycleError(
|
||||
"production bootstrap generation identity changed".into(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn store(&self) -> &HostCoordinatorDomainStore {
|
||||
&self.plan.store
|
||||
}
|
||||
|
||||
fn sandbox(&self) -> SandboxCoordinator<'_> {
|
||||
SandboxCoordinator::new(self.store())
|
||||
}
|
||||
|
||||
fn network(&self) -> NetworkCoordinator<'_> {
|
||||
NetworkCoordinator::new(self.store())
|
||||
}
|
||||
|
||||
fn state_anchor(&self) -> StateAnchorCoordinator<'_> {
|
||||
StateAnchorCoordinator::new(self.store(), self.plan.catalog)
|
||||
}
|
||||
|
||||
fn create_trust_anchor(&mut self) -> Result<(), ProductionLifecycleError> {
|
||||
if self.trust_anchor.is_some() {
|
||||
return Err(ProductionLifecycleError(
|
||||
"Trust Anchor already exists".into(),
|
||||
));
|
||||
}
|
||||
self.trust_anchor =
|
||||
Some(TrustAnchor::create(self.plan.generation_identity.clone()).map_err(failure)?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn start_runtime_supervisor(&mut self) -> Result<(), ProductionLifecycleError> {
|
||||
let sandbox = self.sandbox();
|
||||
sandbox
|
||||
.begin_intent(
|
||||
self.plan.generation_identity.generation_id,
|
||||
self.plan.container_id,
|
||||
&self.plan.runtime_root,
|
||||
self.plan.bundle_digest,
|
||||
current_process_identity().map_err(failure)?,
|
||||
)
|
||||
.map_err(failure)?;
|
||||
let launch = &self.plan.runtime_launch;
|
||||
self.awaiting_runtime = Some(
|
||||
AwaitingAttachedRunsc::launch_networked(&sandbox, &self.network(), launch)
|
||||
.map_err(failure)?,
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn lease_prepared_lower(&self) -> Result<(), ProductionLifecycleError> {
|
||||
self.state_anchor()
|
||||
.begin_acquisition(
|
||||
self.plan.generation_identity.generation_id,
|
||||
self.plan.prepared_artifact,
|
||||
self.plan.state_anchor_layout,
|
||||
)
|
||||
.map(|_| ())
|
||||
.map_err(failure)
|
||||
}
|
||||
|
||||
fn mount_state_anchor(&mut self) -> Result<(), ProductionLifecycleError> {
|
||||
let store = Arc::clone(&self.plan.store);
|
||||
let coordinator = StateAnchorCoordinator::new(&store, self.plan.catalog);
|
||||
let mount = self
|
||||
.awaiting_runtime
|
||||
.as_mut()
|
||||
.ok_or_else(|| ProductionLifecycleError("Runtime Supervisor is absent".into()))?
|
||||
.mount_state_anchor(
|
||||
&coordinator,
|
||||
self.plan.prepared_artifact.prepared_artifact_id,
|
||||
)
|
||||
.map_err(failure)?;
|
||||
self.state_anchor_mount = Some(mount);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn start_connectivity_helper(&mut self) -> Result<(), ProductionLifecycleError> {
|
||||
let store = Arc::clone(&self.plan.store);
|
||||
let network = NetworkCoordinator::new(&store);
|
||||
self.awaiting_runtime
|
||||
.as_mut()
|
||||
.ok_or_else(|| ProductionLifecycleError("Runtime Supervisor is absent".into()))?
|
||||
.receive_helper_staged(&network)
|
||||
.map(|_| ())
|
||||
.map_err(failure)
|
||||
}
|
||||
|
||||
fn start_sidecar(&mut self) -> Result<(), ProductionLifecycleError> {
|
||||
self.awaiting_runtime
|
||||
.as_mut()
|
||||
.ok_or_else(|| ProductionLifecycleError("Runtime Supervisor is absent".into()))?
|
||||
.start_sidecar()
|
||||
.map_err(failure)
|
||||
}
|
||||
|
||||
fn mark_network_ready(&mut self) -> Result<(), ProductionLifecycleError> {
|
||||
let store = Arc::clone(&self.plan.store);
|
||||
let network = NetworkCoordinator::new(&store);
|
||||
let mut probe = self.plan.network_probe.clone();
|
||||
self.awaiting_runtime
|
||||
.as_mut()
|
||||
.ok_or_else(|| ProductionLifecycleError("Runtime Supervisor is absent".into()))?
|
||||
.receive_network_readiness(
|
||||
&network,
|
||||
self.plan.network_policy,
|
||||
|namespace| {
|
||||
probe.enforcement_namespace = namespace;
|
||||
probe
|
||||
},
|
||||
self.plan.reachability,
|
||||
)
|
||||
.map(|_| ())
|
||||
.map_err(failure)
|
||||
}
|
||||
|
||||
fn start_sandbox(&mut self) -> Result<(), ProductionLifecycleError> {
|
||||
let store = Arc::clone(&self.plan.store);
|
||||
let sandbox = SandboxCoordinator::new(&store);
|
||||
let mut pending = self
|
||||
.awaiting_runtime
|
||||
.take()
|
||||
.ok_or_else(|| ProductionLifecycleError("Runtime Supervisor is absent".into()))?;
|
||||
let staged = pending.receive_staged(&sandbox).map_err(failure)?;
|
||||
let handoff = self
|
||||
.plan
|
||||
.sandbox_bundle
|
||||
.capture_handoff_authority(self.plan.configuration)
|
||||
.map_err(failure)?;
|
||||
let (runtime, _) = pending
|
||||
.acknowledge(staged, &handoff)
|
||||
.map_err(failure)?
|
||||
.commit_networked(&sandbox)
|
||||
.map_err(failure)?;
|
||||
self.runtime = Some(runtime);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn stage_publication(&mut self) -> Result<(), ProductionLifecycleError> {
|
||||
let anchor = self
|
||||
.trust_anchor
|
||||
.take()
|
||||
.ok_or_else(|| ProductionLifecycleError("Trust Anchor is absent".into()))?;
|
||||
self.staged_control = Some(
|
||||
StagedControlFace::stage(
|
||||
path_from_host(
|
||||
self.plan
|
||||
.configuration
|
||||
.runtime_domain_root()
|
||||
.canonical_label()
|
||||
.as_bytes(),
|
||||
),
|
||||
self.plan.invoking_uid,
|
||||
anchor,
|
||||
Arc::clone(&self.registry),
|
||||
)
|
||||
.map_err(failure)?,
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn expose_publication(&mut self) -> Result<(), ProductionLifecycleError> {
|
||||
self.published_control = Some(
|
||||
self.staged_control
|
||||
.take()
|
||||
.ok_or_else(|| ProductionLifecycleError("staged control face is absent".into()))?
|
||||
.expose()
|
||||
.map_err(failure)?,
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn stop_sandbox(&mut self) -> Result<(), ProductionLifecycleError> {
|
||||
let store = Arc::clone(&self.plan.store);
|
||||
let sandbox = SandboxCoordinator::new(&store);
|
||||
self.runtime
|
||||
.as_mut()
|
||||
.ok_or_else(|| ProductionLifecycleError("committed runtime is absent".into()))?
|
||||
.stop_sandbox(&sandbox)
|
||||
.map_err(failure)
|
||||
}
|
||||
|
||||
fn stop_sidecar(&mut self) -> Result<(), ProductionLifecycleError> {
|
||||
let store = Arc::clone(&self.plan.store);
|
||||
let network = NetworkCoordinator::new(&store);
|
||||
network
|
||||
.begin_release(self.plan.generation_identity.generation_id)
|
||||
.map_err(failure)?;
|
||||
self.runtime
|
||||
.as_mut()
|
||||
.ok_or_else(|| ProductionLifecycleError("committed runtime is absent".into()))?
|
||||
.stop_sidecar()
|
||||
.map_err(failure)?;
|
||||
network
|
||||
.complete_clean_after_absence(self.plan.generation_identity.generation_id)
|
||||
.map_err(failure)
|
||||
}
|
||||
|
||||
fn unmount_state_anchor(&mut self) -> Result<(), ProductionLifecycleError> {
|
||||
let coordinator = self.state_anchor();
|
||||
self.state_anchor_release = Some(
|
||||
coordinator
|
||||
.begin_release(
|
||||
self.plan.generation_identity.generation_id,
|
||||
self.plan.state_anchor_layout,
|
||||
)
|
||||
.map_err(failure)?,
|
||||
);
|
||||
self.runtime
|
||||
.as_mut()
|
||||
.ok_or_else(|| ProductionLifecycleError("committed runtime is absent".into()))?
|
||||
.unmount_state_anchor()
|
||||
.map_err(failure)?;
|
||||
self.state_anchor_mount = None;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn release_prepared_lower(&mut self) -> Result<(), ProductionLifecycleError> {
|
||||
let release = self
|
||||
.state_anchor_release
|
||||
.take()
|
||||
.ok_or_else(|| ProductionLifecycleError("State Anchor release is absent".into()))?;
|
||||
self.state_anchor()
|
||||
.complete_release_after_unmount(&release, self.plan.state_anchor_layout)
|
||||
.map_err(failure)
|
||||
}
|
||||
|
||||
fn graceful_exit(&mut self) -> Result<(), ProductionLifecycleError> {
|
||||
self.runtime
|
||||
.take()
|
||||
.ok_or_else(|| ProductionLifecycleError("committed runtime is absent".into()))?
|
||||
.graceful_exit()
|
||||
.map_err(failure)
|
||||
}
|
||||
}
|
||||
|
||||
impl LifecycleAdapter for ProductionLifecycleAdapter<'_> {
|
||||
type Error = ProductionLifecycleError;
|
||||
|
||||
fn execute(
|
||||
&mut self,
|
||||
action: LifecycleAction,
|
||||
generation: &Generation,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.require_generation(generation)?;
|
||||
match action {
|
||||
LifecycleAction::CreateTrustAnchor => self.create_trust_anchor(),
|
||||
LifecycleAction::StartRuntimeSupervisor => self.start_runtime_supervisor(),
|
||||
LifecycleAction::LeasePreparedLower => self.lease_prepared_lower(),
|
||||
LifecycleAction::MountStateAnchor => self.mount_state_anchor(),
|
||||
LifecycleAction::StartSidecar => self.start_sidecar(),
|
||||
LifecycleAction::StartConnectivityHelper => self.start_connectivity_helper(),
|
||||
LifecycleAction::MarkNetworkReady => self.mark_network_ready(),
|
||||
LifecycleAction::StartSandbox => self.start_sandbox(),
|
||||
LifecycleAction::PublishDomain => self.stage_publication(),
|
||||
LifecycleAction::StopSandbox => self.stop_sandbox(),
|
||||
LifecycleAction::StopSidecar => self.stop_sidecar(),
|
||||
LifecycleAction::UnmountStateAnchor => self.unmount_state_anchor(),
|
||||
LifecycleAction::ReleasePreparedLowerLease => self.release_prepared_lower(),
|
||||
LifecycleAction::GracefulSupervisorExit => self.graceful_exit(),
|
||||
LifecycleAction::FinishTeardown => {
|
||||
self.published_control.take();
|
||||
Ok(())
|
||||
}
|
||||
LifecycleAction::StartNewDomain
|
||||
| LifecycleAction::StartReadySession
|
||||
| LifecycleAction::WaitForDomain
|
||||
| LifecycleAction::RestartWaitingDomain
|
||||
| LifecycleAction::ArmRuntimeCausalEdge
|
||||
| LifecycleAction::AdvanceWaitingSession
|
||||
| LifecycleAction::RejectConfigurationMismatch
|
||||
| LifecycleAction::RegisterSession
|
||||
| LifecycleAction::DisconnectSession
|
||||
| LifecycleAction::BeginTeardown => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
fn after_commit(
|
||||
&mut self,
|
||||
action: LifecycleAction,
|
||||
generation: &Generation,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.require_generation(generation)?;
|
||||
if action == LifecycleAction::PublishDomain {
|
||||
self.expose_publication()
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn path_from_host(bytes: &[u8]) -> &Path {
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
|
||||
Path::new(std::ffi::OsStr::from_bytes(bytes))
|
||||
}
|
||||
|
|
@ -49,6 +49,8 @@ pub const MAX_ENROLLMENT_BYTES: usize = 8_192;
|
|||
const MAX_CONTROL_BYTES: usize = 65_536;
|
||||
const 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(¤t) != 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]
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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![
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ path = "src/main.rs"
|
|||
[dependencies]
|
||||
lazaret-core = { version = "=0.0.0", path = "../lazaret-core" }
|
||||
lazaret-linux = { version = "=0.0.0", path = "../lazaret-linux" }
|
||||
lazaret-state-anchor = { version = "=0.0.0", path = "../lazaret-state-anchor" }
|
||||
oci-spec.workspace = true
|
||||
serde.workspace = true
|
||||
rustix.workspace = true
|
||||
|
|
|
|||
|
|
@ -21,6 +21,9 @@ use lazaret_linux::process::{
|
|||
ChildStandardIo, ParentEdge, ProcessOwnershipContext, RetainedProcess, TargetCommand,
|
||||
child_broker, current_process_identity,
|
||||
};
|
||||
use lazaret_state_anchor::{
|
||||
RuntimeStateAnchorAuthority, RuntimeStateAnchorMount, StateAnchorLaunchAuthority,
|
||||
};
|
||||
use rustix::event::{PollFd, PollFlags, Timespec, poll};
|
||||
use rustix::fs::{MemfdFlags, memfd_create};
|
||||
use rustix::io::{FdFlags, fcntl_setfd, read, write};
|
||||
|
|
@ -58,6 +61,12 @@ impl AttachedRunscSignal {
|
|||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub enum SupervisorEvent {
|
||||
StateAnchorStaged(StagedProcessOwnership),
|
||||
StateAnchorReady {
|
||||
staged: StagedProcessOwnership,
|
||||
mount: lazaret_core::identity::MountIdentity,
|
||||
},
|
||||
SidecarStarted,
|
||||
HelperStaged(StagedProcessOwnership),
|
||||
HelperReady(StagedProcessOwnership),
|
||||
NetworkAttestation(NetworkReadinessAttestation),
|
||||
|
|
@ -69,6 +78,10 @@ pub enum SupervisorEvent {
|
|||
sentry_pid_inventory: u32,
|
||||
},
|
||||
SignalApplied(AttachedRunscSignal),
|
||||
SandboxStopped,
|
||||
SidecarStopped,
|
||||
StateAnchorUnmounted,
|
||||
Exited,
|
||||
AttachedRunscExited,
|
||||
Stopped,
|
||||
Refused(String),
|
||||
|
|
@ -76,12 +89,19 @@ pub enum SupervisorEvent {
|
|||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
enum CoordinatorMessage {
|
||||
MountStateAnchor,
|
||||
StateAnchorStagedAck(ProcessOwnershipCommitAck),
|
||||
StartSidecar,
|
||||
HelperStagedAck(ProcessOwnershipCommitAck),
|
||||
NetworkAttestationAccepted(NetworkProbeRequest),
|
||||
NetworkReadinessCommitted,
|
||||
StagedAck(ProcessOwnershipCommitAck),
|
||||
SandboxCommitted,
|
||||
SignalAttachedRunsc(AttachedRunscSignal),
|
||||
StopSandbox,
|
||||
StopSidecar,
|
||||
UnmountStateAnchor,
|
||||
Exit,
|
||||
Stop,
|
||||
}
|
||||
|
||||
|
|
@ -101,6 +121,30 @@ impl CoordinatorChannel {
|
|||
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
|
||||
/// Returns an error when the exact typed acknowledgment cannot be delivered.
|
||||
pub fn acknowledge_staged(&self, acknowledgment: ProcessOwnershipCommitAck) -> io::Result<()> {
|
||||
|
|
@ -157,14 +201,41 @@ impl CoordinatorChannel {
|
|||
pub fn stop(&self) -> io::Result<()> {
|
||||
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 {
|
||||
target: TargetCommand,
|
||||
ownership: ProcessOwnershipContext,
|
||||
policy_file: Option<File>,
|
||||
policy_digest: Option<DerivedPolicyDigest>,
|
||||
configuration_fingerprint: Option<ConfigurationFingerprint>,
|
||||
network_sidecar_id: Option<NetworkSidecarId>,
|
||||
runsc_command: RunscCommand,
|
||||
state_anchor: Option<StateAnchorLaunchAuthority>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
|
|
@ -213,8 +284,11 @@ impl RuntimeSupervisorLaunch {
|
|||
target: target.with_lifetime_pipe_parent_edge(),
|
||||
ownership,
|
||||
policy_file: None,
|
||||
policy_digest: None,
|
||||
configuration_fingerprint: None,
|
||||
network_sidecar_id: None,
|
||||
runsc_command: command.clone(),
|
||||
state_anchor: None,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -260,10 +334,68 @@ impl RuntimeSupervisorLaunch {
|
|||
launch.target = launch.target.with_lifetime_pipe_parent_edge();
|
||||
drop(target);
|
||||
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);
|
||||
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]
|
||||
pub const fn target(&self) -> &TargetCommand {
|
||||
&self.target
|
||||
|
|
@ -328,11 +460,19 @@ pub fn dispatch_private_runtime_role(arguments: &[OsString]) -> Option<io::Resul
|
|||
|
||||
struct RuntimeArguments {
|
||||
ownership: ProcessOwnershipContext,
|
||||
state_anchor: Option<RuntimeStateAnchorArguments>,
|
||||
network: Option<RuntimeNetworkArguments>,
|
||||
runsc_program: OsString,
|
||||
runsc_arguments: Vec<OsString>,
|
||||
}
|
||||
|
||||
struct RuntimeStateAnchorArguments {
|
||||
lower_fd: i32,
|
||||
persistent_anchor_fd: i32,
|
||||
runtime_anchor_fd: i32,
|
||||
expected_lower: (u64, u64),
|
||||
}
|
||||
|
||||
struct RuntimeNetworkArguments {
|
||||
policy_fd: i32,
|
||||
policy_digest: DerivedPolicyDigest,
|
||||
|
|
@ -348,6 +488,22 @@ fn parse_runtime_arguments(arguments: &[OsString]) -> io::Result<RuntimeArgument
|
|||
.and_then(|value| value.parse().ok())
|
||||
.ok_or_else(|| io::Error::other("Runtime Supervisor generation is invalid"))?;
|
||||
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
|
||||
.peek()
|
||||
.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),
|
||||
slot: HostProcessOwnershipSlot::SandboxAttachedRunsc,
|
||||
},
|
||||
state_anchor,
|
||||
network,
|
||||
runsc_program: (*program).clone(),
|
||||
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>(
|
||||
arguments: &mut impl Iterator<Item = &'a OsString>,
|
||||
) -> io::Result<RuntimeNetworkArguments> {
|
||||
|
|
@ -439,9 +606,92 @@ fn run_supervisor(arguments: &RuntimeArguments) -> io::Result<()> {
|
|||
require_lifetime_held()?;
|
||||
let control = std::io::stdout();
|
||||
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 {
|
||||
Some(network) => Some(prepare_runtime_network(
|
||||
arguments, network, parent, &control,
|
||||
arguments, network, parent, control,
|
||||
)?),
|
||||
None => None,
|
||||
};
|
||||
|
|
@ -456,9 +706,9 @@ fn run_supervisor(arguments: &RuntimeArguments) -> io::Result<()> {
|
|||
attached_runsc_standard_io(&runsc)?,
|
||||
|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)
|
||||
if acknowledgment.acknowledges(&ownership) =>
|
||||
{
|
||||
|
|
@ -493,7 +743,7 @@ fn run_supervisor(arguments: &RuntimeArguments) -> io::Result<()> {
|
|||
return fail_with_network_cleanup(process, network, error);
|
||||
}
|
||||
if let Err(error) = send_message(
|
||||
&control,
|
||||
control,
|
||||
&SupervisorEvent::Ready {
|
||||
staged,
|
||||
sentry_pid_inventory,
|
||||
|
|
@ -501,7 +751,7 @@ fn run_supervisor(arguments: &RuntimeArguments) -> io::Result<()> {
|
|||
) {
|
||||
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,
|
||||
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"),
|
||||
);
|
||||
}
|
||||
supervise(&control, process, network)
|
||||
supervise(control, process, network, state_anchor)
|
||||
}
|
||||
|
||||
fn prepare_runtime_network(
|
||||
|
|
@ -645,14 +895,23 @@ fn supervise(
|
|||
control: &std::io::Stdout,
|
||||
process: RetainedProcess,
|
||||
mut network: Option<PreparedNetwork>,
|
||||
state_anchor: &mut Option<RuntimeStateAnchorMount>,
|
||||
) -> io::Result<()> {
|
||||
let mut process = Some(process);
|
||||
loop {
|
||||
let exited = match process.has_exited() {
|
||||
let exited = match process
|
||||
.as_ref()
|
||||
.map_or(Ok(false), RetainedProcess::has_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 {
|
||||
child_broker().reap(process)?;
|
||||
child_broker().reap(
|
||||
process
|
||||
.take()
|
||||
.ok_or_else(|| io::Error::other("attached runsc handle is absent"))?,
|
||||
)?;
|
||||
stop_network(network)?;
|
||||
send_message(control, &SupervisorEvent::AttachedRunscExited)?;
|
||||
return Ok(());
|
||||
|
|
@ -669,10 +928,12 @@ fn supervise(
|
|||
}
|
||||
let lease_lost = match lifetime_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 {
|
||||
terminate_and_reap(process)?;
|
||||
if let Some(process) = process.take() {
|
||||
terminate_and_reap(process)?;
|
||||
}
|
||||
stop_network(network)?;
|
||||
return Ok(());
|
||||
}
|
||||
|
|
@ -684,11 +945,11 @@ fn supervise(
|
|||
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();
|
||||
if events.intersects(PollFlags::HUP | PollFlags::ERR) {
|
||||
return fail_with_network_cleanup(
|
||||
return fail_active_runtime(
|
||||
process,
|
||||
network,
|
||||
io::Error::new(
|
||||
|
|
@ -700,41 +961,110 @@ fn supervise(
|
|||
if events.intersects(PollFlags::IN) {
|
||||
let message = match receive_message(control) {
|
||||
Ok(message) => message,
|
||||
Err(error) => return fail_with_network_cleanup(process, network, error),
|
||||
Err(error) => return fail_active_runtime(process, network, error),
|
||||
};
|
||||
match message {
|
||||
CoordinatorMessage::SignalAttachedRunsc(signal) => {
|
||||
if let Err(error) = process.send_signal(signal.signal()) {
|
||||
return fail_with_network_cleanup(process, network, error);
|
||||
}
|
||||
if let Err(error) =
|
||||
send_message(control, &SupervisorEvent::SignalApplied(signal))
|
||||
{
|
||||
return fail_with_network_cleanup(process, network, error);
|
||||
}
|
||||
}
|
||||
CoordinatorMessage::Stop => {
|
||||
terminate_and_reap(process)?;
|
||||
stop_network(network)?;
|
||||
send_message(control, &SupervisorEvent::Stopped)?;
|
||||
return Ok(());
|
||||
}
|
||||
CoordinatorMessage::HelperStagedAck(_)
|
||||
| CoordinatorMessage::NetworkAttestationAccepted(_)
|
||||
| CoordinatorMessage::NetworkReadinessCommitted
|
||||
| CoordinatorMessage::StagedAck(_)
|
||||
| CoordinatorMessage::SandboxCommitted => {
|
||||
return fail_with_network_cleanup(
|
||||
process,
|
||||
network,
|
||||
io::Error::other("unexpected duplicate coordinator message"),
|
||||
);
|
||||
}
|
||||
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 {
|
||||
CoordinatorMessage::SignalAttachedRunsc(signal) => {
|
||||
process
|
||||
.as_ref()
|
||||
.ok_or_else(|| io::Error::other("attached runsc is already stopped"))?
|
||||
.send_signal(signal.signal())?;
|
||||
send_message(control, &SupervisorEvent::SignalApplied(*signal))?;
|
||||
}
|
||||
CoordinatorMessage::StopSandbox => {
|
||||
terminate_and_reap(
|
||||
process
|
||||
.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 => {
|
||||
if let Some(process) = process.take() {
|
||||
terminate_and_reap(process)?;
|
||||
}
|
||||
stop_network(network.take())?;
|
||||
send_message(control, &SupervisorEvent::Stopped)?;
|
||||
return Ok(true);
|
||||
}
|
||||
CoordinatorMessage::HelperStagedAck(_)
|
||||
| CoordinatorMessage::MountStateAnchor
|
||||
| CoordinatorMessage::StateAnchorStagedAck(_)
|
||||
| CoordinatorMessage::StartSidecar
|
||||
| CoordinatorMessage::NetworkAttestationAccepted(_)
|
||||
| CoordinatorMessage::NetworkReadinessCommitted
|
||||
| CoordinatorMessage::StagedAck(_)
|
||||
| CoordinatorMessage::SandboxCommitted => {
|
||||
return Err(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)
|
||||
}
|
||||
}
|
||||
|
||||
fn terminate_and_reap(process: RetainedProcess) -> io::Result<()> {
|
||||
let term_error = process.send_signal(Signal::TERM).err();
|
||||
let deadline = Instant::now() + TERMINATION_GRACE;
|
||||
|
|
@ -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]
|
||||
fn runtime_supervisor_and_runsc_keep_distinct_edge_and_stdio_policies() {
|
||||
let ownership = ProcessOwnershipContext {
|
||||
|
|
@ -902,12 +1268,15 @@ mod tests {
|
|||
target: TargetCommand::new("/proc/self/exe").with_lifetime_pipe_parent_edge(),
|
||||
ownership,
|
||||
policy_file: None,
|
||||
policy_digest: None,
|
||||
configuration_fingerprint: None,
|
||||
runsc_command: RunscCommand::new(
|
||||
"runsc",
|
||||
[OsString::from("run"), OsString::from("container")],
|
||||
[0; 32],
|
||||
),
|
||||
network_sidecar_id: None,
|
||||
state_anchor: None,
|
||||
};
|
||||
let (_coordinator_control, supervisor_control) = private_seqpacket_pair().unwrap();
|
||||
let (supervisor_lifetime, _coordinator_lifetime) = pipe_with(PipeFlags::CLOEXEC).unwrap();
|
||||
|
|
@ -925,6 +1294,7 @@ mod tests {
|
|||
slot: HostProcessOwnershipSlot::SandboxAttachedRunsc,
|
||||
..ownership
|
||||
},
|
||||
state_anchor: None,
|
||||
network: None,
|
||||
runsc_program: "runsc".into(),
|
||||
runsc_arguments: ["-root=/runtime", "run", "container"]
|
||||
|
|
|
|||
|
|
@ -388,7 +388,223 @@ pub struct StateAnchorAcquisition<'path> {
|
|||
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 {
|
||||
#[must_use]
|
||||
pub const fn from_parts(staged: StagedProcessOwnership, mount: MountIdentity) -> Self {
|
||||
Self { staged, mount }
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn staged(self) -> StagedProcessOwnership {
|
||||
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)
|
||||
}
|
||||
|
||||
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 {
|
||||
let mut bytes = b"lowerdir=".to_vec();
|
||||
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)]
|
||||
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 status = std::process::Command::new("fusermount3")
|
||||
.arg("-u")
|
||||
|
|
|
|||
|
|
@ -2,13 +2,22 @@ use std::collections::BTreeMap;
|
|||
use std::ffi::OsString;
|
||||
use std::fs;
|
||||
use std::os::unix::ffi::OsStringExt;
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
|
||||
use lazaret_core::configuration::{
|
||||
ConfigurationLoadRequest, SandboxLayoutV1, load_trusted_configuration,
|
||||
};
|
||||
use lazaret_state_anchor::{ProtectedRuntimeFiles, StateAnchorLayout};
|
||||
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);
|
||||
|
||||
|
|
@ -48,6 +57,45 @@ fn replaced_runtime_root_is_refused_before_runtime_file_write() {
|
|||
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 {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,8 +19,9 @@ use lazaret_linux::process::{
|
|||
ProcessOwnershipContext, current_process_identity, dispatch_private_process_role,
|
||||
};
|
||||
use lazaret_state_anchor::{
|
||||
FuseOverlayfsMount, ProtectedRuntimeFiles, StateAnchorAcquisition, StateAnchorLayout,
|
||||
StateAnchorSandboxView, unmount_after_coordinator_loss,
|
||||
FuseOverlayfsMount, ProtectedRuntimeFiles, RuntimeStateAnchorAuthority, StateAnchorAcquisition,
|
||||
StateAnchorLaunchAuthority, StateAnchorLayout, StateAnchorSandboxView,
|
||||
unmount_after_coordinator_loss,
|
||||
};
|
||||
use rustix::process::Signal;
|
||||
|
||||
|
|
@ -77,6 +78,45 @@ fn run() -> io::Result<()> {
|
|||
};
|
||||
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)?;
|
||||
fs::write(
|
||||
layout.merged().join("usr/bin/installed-tool"),
|
||||
|
|
|
|||
Loading…
Reference in a new issue