Implement supervised rootless network readiness

This commit is contained in:
ms2k 2026-07-15 23:26:37 +09:00
commit ba6a075839

View file

@ -5,15 +5,17 @@ use std::path::Path;
use std::time::{Duration, Instant};
use lazaret_core::identity::{
BundleDigest, ContainerId, GenerationId, HostPath, PidfsProcessIdentityV1,
BundleDigest, ContainerId, GenerationId, HostPath, NamespaceIdentity, PidfsProcessIdentityV1,
};
use lazaret_core::network::{CanonicalDerivedNetworkPolicy, ReachabilityExpectation};
use lazaret_core::records::{
HostProcessOwnershipSlot, Participant, ParticipantOwnership, ParticipantRole, ParticipantState,
PersistedHostProcessOwnership, ProcessOwnershipCommitAck, SandboxOwnership,
StagedProcessOwnership,
HostProcessOwnershipSlot, NetworkReadinessEvidence, Participant, ParticipantOwnership,
ParticipantRole, ParticipantState, PersistedHostProcessOwnership, ProcessOwnershipCommitAck,
SandboxOwnership, StagedProcessOwnership,
};
use lazaret_linux::ipc::private_stream_pair;
use lazaret_linux::ipc::private_seqpacket_pair;
use lazaret_linux::process::{RetainedProcess, child_broker, current_process_identity};
use lazaret_runtime_supervisor::network::NetworkProbeRequest;
use lazaret_runtime_supervisor::supervision::{
AttachedRunscSignal, CoordinatorChannel, RuntimeSupervisorLaunch, SupervisorEvent,
};
@ -21,12 +23,14 @@ use rustix::pipe::{PipeFlags, pipe_with};
use rustix::process::Signal;
use crate::domain_store::{DomainStoreError, DomainStoreMutation, HostCoordinatorDomainStore};
use crate::network::{NetworkCoordinator, NetworkCoordinatorError};
const SUPERVISOR_EXIT_TIMEOUT: Duration = Duration::from_secs(5);
#[derive(Debug)]
pub enum SandboxCoordinatorError {
Store(DomainStoreError),
Network(NetworkCoordinatorError),
Io(io::Error),
InvalidState(&'static str),
}
@ -37,6 +41,7 @@ impl fmt::Display for SandboxCoordinatorError {
Self::Store(error) => {
write!(formatter, "Sandbox Domain Store operation failed: {error}")
}
Self::Network(error) => write!(formatter, "Sandbox network operation failed: {error}"),
Self::Io(error) => write!(formatter, "Sandbox process operation failed: {error}"),
Self::InvalidState(detail) => write!(formatter, "Sandbox state refused: {detail}"),
}
@ -47,6 +52,7 @@ impl std::error::Error for SandboxCoordinatorError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Store(error) => Some(error),
Self::Network(error) => Some(error),
Self::Io(error) => Some(error),
Self::InvalidState(_) => None,
}
@ -65,6 +71,12 @@ impl From<io::Error> for SandboxCoordinatorError {
}
}
impl From<NetworkCoordinatorError> for SandboxCoordinatorError {
fn from(error: NetworkCoordinatorError) -> Self {
Self::Network(error)
}
}
pub struct SandboxCoordinator<'store> {
domain_store: &'store HostCoordinatorDomainStore,
}
@ -351,9 +363,16 @@ pub struct AwaitingAttachedRunsc {
control: Option<CoordinatorChannel>,
lifetime: Option<std::os::fd::OwnedFd>,
staged: Option<StagedProcessOwnership>,
helper_staged: Option<StagedProcessOwnership>,
network_evidence: Option<NetworkReadinessEvidence>,
}
impl AwaitingAttachedRunsc {
#[must_use]
pub fn runtime_supervisor_identity(&self) -> Option<PidfsProcessIdentityV1> {
self.supervisor.as_ref().map(RetainedProcess::identity)
}
/// Launches the RS through the HC broker and commits its staged ownership before execution.
///
/// # Errors
@ -361,9 +380,39 @@ impl AwaitingAttachedRunsc {
pub fn launch(
coordinator: &SandboxCoordinator<'_>,
launch: &RuntimeSupervisorLaunch,
) -> Result<Self, SandboxCoordinatorError> {
if launch.network_sidecar_id().is_some() {
return Err(SandboxCoordinatorError::InvalidState(
"networked Runtime Supervisor requires helper intent",
));
}
Self::launch_inner(coordinator, None, launch)
}
/// Launches the RS only after independent durable helper intent is recorded.
///
/// # Errors
/// Refuses a non-network launch or any sandbox/helper staging failure.
pub fn launch_networked(
coordinator: &SandboxCoordinator<'_>,
network: &NetworkCoordinator<'_>,
launch: &RuntimeSupervisorLaunch,
) -> Result<Self, SandboxCoordinatorError> {
if launch.network_sidecar_id().is_none() {
return Err(SandboxCoordinatorError::InvalidState(
"Runtime Supervisor launch has no network policy",
));
}
Self::launch_inner(coordinator, Some(network), launch)
}
fn launch_inner(
coordinator: &SandboxCoordinator<'_>,
network: Option<&NetworkCoordinator<'_>>,
launch: &RuntimeSupervisorLaunch,
) -> Result<Self, SandboxCoordinatorError> {
let parent = current_process_identity()?;
let (coordinator_control, supervisor_control) = private_stream_pair()?;
let (coordinator_control, supervisor_control) = private_seqpacket_pair()?;
let (supervisor_lifetime, coordinator_lifetime) =
pipe_with(PipeFlags::CLOEXEC).map_err(io::Error::from)?;
let process = child_broker().acquire_with_stdio(
@ -374,7 +423,19 @@ impl AwaitingAttachedRunsc {
|staged| {
coordinator
.stage_runtime_supervisor(staged)
.map_err(io::Error::other)
.map_err(io::Error::other)?;
if let Some(network) = network {
network
.begin_helper_intent(
staged.generation_id(),
launch.network_sidecar_id().ok_or_else(|| {
io::Error::other("network sidecar identity is absent")
})?,
staged.identity(),
)
.map_err(io::Error::other)?;
}
Ok(())
},
|staged| {
coordinator
@ -387,9 +448,131 @@ impl AwaitingAttachedRunsc {
control: Some(CoordinatorChannel::new(coordinator_control)),
lifetime: Some(coordinator_lifetime),
staged: None,
helper_staged: None,
network_evidence: None,
})
}
/// Receives, persists, and acknowledges the exact helper child acquisition.
///
/// # Errors
/// Refuses unexpected events, changed ownership, or a missing network launch.
pub fn receive_helper_staged(
&mut self,
network: &NetworkCoordinator<'_>,
) -> Result<StagedProcessOwnership, SandboxCoordinatorError> {
if self.helper_staged.is_some() || self.network_evidence.is_some() {
return Err(SandboxCoordinatorError::InvalidState(
"helper stage was already received",
));
}
let control = self
.control
.as_ref()
.ok_or(SandboxCoordinatorError::InvalidState(
"Runtime Supervisor control channel is absent",
))?;
let staged = match control.receive()? {
SupervisorEvent::HelperStaged(staged) => staged,
SupervisorEvent::Refused(detail) => {
return Err(SandboxCoordinatorError::Io(io::Error::other(detail)));
}
_ => {
return Err(SandboxCoordinatorError::InvalidState(
"Runtime Supervisor did not stage the helper first",
));
}
};
let runtime_supervisor = self
.supervisor
.as_ref()
.ok_or(SandboxCoordinatorError::InvalidState(
"Runtime Supervisor handle is absent",
))?
.identity();
network.stage_helper(staged, runtime_supervisor)?;
let acknowledgment = network.acknowledge_helper(staged)?;
control.acknowledge_helper(acknowledgment)?;
self.helper_staged = Some(staged);
Ok(staged)
}
/// Commits helper readiness, independently observes the namespace, and validates attestation.
///
/// # Errors
/// Refuses event ordering, namespace mutation, or any attestation mismatch.
pub fn receive_network_readiness<BuildProbeRequest>(
&mut self,
network: &NetworkCoordinator<'_>,
policy: &CanonicalDerivedNetworkPolicy,
build_probe_request: BuildProbeRequest,
expected_reachability: &ReachabilityExpectation,
) -> Result<NetworkReadinessEvidence, SandboxCoordinatorError>
where
BuildProbeRequest: FnOnce(NamespaceIdentity) -> NetworkProbeRequest,
{
let staged = self
.helper_staged
.ok_or(SandboxCoordinatorError::InvalidState(
"helper ownership was not staged",
))?;
let control = self
.control
.as_ref()
.ok_or(SandboxCoordinatorError::InvalidState(
"Runtime Supervisor control channel is absent",
))?;
if control.receive()? != SupervisorEvent::HelperReady(staged) {
return Err(SandboxCoordinatorError::InvalidState(
"Runtime Supervisor helper readiness mismatched ownership",
));
}
network.commit_helper(staged)?;
let attestation = match control.receive()? {
SupervisorEvent::NetworkAttestation(attestation) => attestation,
SupervisorEvent::Refused(detail) => {
return Err(SandboxCoordinatorError::Io(io::Error::other(detail)));
}
_ => {
return Err(SandboxCoordinatorError::InvalidState(
"Runtime Supervisor did not provide network attestation",
));
}
};
let namespace = network.begin_network_intent(
attestation.generation_id,
policy.digest(),
attestation.runtime_supervisor,
)?;
network.validate_attestation(policy, &attestation)?;
let probe_request = build_probe_request(namespace);
if probe_request.domain_id != attestation.domain_id
|| probe_request.generation_id != attestation.generation_id
|| probe_request.enforcement_namespace != attestation.enforcement_namespace
{
return Err(SandboxCoordinatorError::InvalidState(
"network probe request mismatches attestation",
));
}
control.acknowledge_network_attestation(probe_request)?;
let probe = match control.receive()? {
SupervisorEvent::NetworkProbe(probe) => probe,
SupervisorEvent::Refused(detail) => {
return Err(SandboxCoordinatorError::Io(io::Error::other(detail)));
}
_ => {
return Err(SandboxCoordinatorError::InvalidState(
"Runtime Supervisor did not return the network probe",
));
}
};
let evidence =
network.commit_readiness(policy, attestation, probe, expected_reachability)?;
control.acknowledge_network_readiness()?;
self.network_evidence = Some(evidence.clone());
Ok(evidence)
}
/// Receives and durably stages the exact RS child without releasing its barrier.
///
/// # Errors
@ -403,6 +586,11 @@ impl AwaitingAttachedRunsc {
"attached runsc stage was already received",
));
}
if self.helper_staged.is_some() && self.network_evidence.is_none() {
return Err(SandboxCoordinatorError::InvalidState(
"attached runsc cannot start before network attestation",
));
}
let event = self
.control
.as_ref()
@ -450,6 +638,7 @@ impl AwaitingAttachedRunsc {
control: self.control.take(),
lifetime: self.lifetime.take(),
staged,
network_evidence: self.network_evidence.take(),
})
}
@ -474,6 +663,7 @@ pub struct AwaitingSandboxReadiness {
control: Option<CoordinatorChannel>,
lifetime: Option<std::os::fd::OwnedFd>,
staged: StagedProcessOwnership,
network_evidence: Option<NetworkReadinessEvidence>,
}
impl AwaitingSandboxReadiness {
@ -482,8 +672,39 @@ impl AwaitingSandboxReadiness {
/// # Errors
/// Refuses a mismatched readiness tuple, durable commit failure, or channel failure.
pub fn commit(
self,
coordinator: &SandboxCoordinator<'_>,
) -> Result<CommittedSandboxRuntime, SandboxCoordinatorError> {
if self.network_evidence.is_some() {
return Err(SandboxCoordinatorError::InvalidState(
"networked sandbox requires a successful sandbox-view probe",
));
}
self.commit_after_probe(coordinator, None)
}
/// Commits the Sandbox only after the enforcement-namespace probe is durably accepted.
///
/// # Errors
/// Refuses missing attestation, failed/mutated probe evidence, or any runtime mismatch.
pub fn commit_networked(
mut self,
coordinator: &SandboxCoordinator<'_>,
) -> Result<(CommittedSandboxRuntime, NetworkReadinessEvidence), SandboxCoordinatorError> {
let evidence =
self.network_evidence
.take()
.ok_or(SandboxCoordinatorError::InvalidState(
"network readiness evidence is absent",
))?;
let runtime = self.commit_after_probe(coordinator, Some(evidence.clone()))?;
Ok((runtime, evidence))
}
fn commit_after_probe(
mut self,
coordinator: &SandboxCoordinator<'_>,
network_evidence: Option<NetworkReadinessEvidence>,
) -> Result<CommittedSandboxRuntime, SandboxCoordinatorError> {
let control = self
.control
@ -520,6 +741,7 @@ impl AwaitingSandboxReadiness {
"Runtime Supervisor control channel is absent",
))?,
lifetime: self.lifetime.take(),
network_evidence,
})
}
}
@ -536,9 +758,28 @@ pub struct CommittedSandboxRuntime {
supervisor: Option<RetainedProcess>,
control: CoordinatorChannel,
lifetime: Option<std::os::fd::OwnedFd>,
network_evidence: Option<NetworkReadinessEvidence>,
}
impl CommittedSandboxRuntime {
#[must_use]
pub const fn network_evidence(&self) -> Option<&NetworkReadinessEvidence> {
self.network_evidence.as_ref()
}
/// Withdraws live readiness after the RS reports exact helper-only loss.
///
/// # Errors
/// Refuses any event other than connectivity-helper loss.
pub fn wait_for_helper_exit(&mut self) -> Result<(), SandboxCoordinatorError> {
if self.control.receive()? != SupervisorEvent::HelperExited {
return Err(SandboxCoordinatorError::InvalidState(
"Runtime Supervisor did not report helper-only loss",
));
}
self.network_evidence = None;
Ok(())
}
#[must_use]
pub fn runtime_supervisor_identity(&self) -> Option<PidfsProcessIdentityV1> {
self.supervisor.as_ref().map(RetainedProcess::identity)