use std::cell::RefCell; use std::ffi::{OsStr, OsString}; use std::fmt; use std::fs::File; use std::io::{Read, Write}; use std::os::fd::{AsFd, OwnedFd}; use std::os::unix::ffi::OsStringExt; use std::os::unix::fs::{FileExt, PermissionsExt}; use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; use lazaret_core::configuration::{ConfinedPathPlan, ResolvedDomainConfiguration}; use lazaret_core::identity::{BundleDigest, ContainerId, HostPath, MountIdentity}; use lazaret_core::network::{ReachabilityExpectation, derive_ipv4_policy}; use lazaret_core::records::{ Generation, GenerationIdentity, HostProcessOwnershipSlot, IpAddress, PreparedArtifact, Session, }; use lazaret_ignored_workspace::git_common_directory; use lazaret_linux::filesystem::{RegularFileMailbox, mount_identity, namespace_identity}; use lazaret_linux::process::{ ProcessOwnershipContext, TargetCommand, child_broker, current_process_identity, }; use lazaret_runtime_supervisor::network::NetworkProbeRequest; use lazaret_runtime_supervisor::sandbox::{ RootProcess, SandboxBuildRequest, SandboxBundle, build_sandbox_bundle, build_sandbox_bundle_with_workspace, }; use lazaret_runtime_supervisor::supervision::{ RuntimeGitRoutingAuthority, RuntimeGitRoutingLaunch, RuntimeNetworkLaunch, RuntimeSupervisorLaunch, }; use lazaret_state_anchor::{StateAnchorLaunchAuthority, StateAnchorLayout}; use lazaret_storage_catalog::Catalog; use rustix::event::{PollFd, PollFlags, Timespec, poll}; use rustix::fs::{ AtFlags, FileType, Mode, OFlags, RawDir, ResolveFlags, fchmod, fstat, mkdirat, openat2, statat, unlinkat, }; use rustix::io::{FdFlags, dup, fcntl_setfd}; use rustix::pipe::{PipeFlags, pipe_with}; use sha2::{Digest, Sha256}; use crate::bootstrap::{BootstrapPlan, CoordinatorLifecycle, coordinator_lifecycle}; use crate::control::RunscNonInteractiveExecutor; use crate::domain_store::HostCoordinatorDomainStore; use crate::terminal::RunscInteractiveSessionExecutor; use crate::waiter::{BootstrapWaitOutcome, BootstrapWaiterFace}; const NETWORK_NAMESPACE_PATH: &str = "/proc/self/ns/net"; #[derive(Debug)] pub struct ColdStartError(String); impl fmt::Display for ColdStartError { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter.write_str(&self.0) } } impl std::error::Error for ColdStartError {} impl ColdStartError { pub(crate) fn service(error: impl fmt::Display) -> Self { Self(error.to_string()) } } fn failure(error: impl fmt::Display) -> ColdStartError { ColdStartError(error.to_string()) } pub struct ColdStartInputs<'a> { pub store: Arc, pub catalog: &'a Catalog, pub configuration: &'a ResolvedDomainConfiguration, pub generation: Generation, pub prepared_artifact: &'a PreparedArtifact, pub prepared_artifact_authority: &'a File, pub prepared_artifact_label: &'a Path, pub root_process: RootProcess, pub resolver_addresses: Vec, pub network_probe: NetworkProbeRequest, pub reachability: ReachabilityExpectation, pub container_id: ContainerId, pub invoking_uid: u32, pub escalation_client: &'a File, pub sandbox_init: &'a File, } pub struct BootstrapOwnerGuard { descriptor: OwnedFd, } impl BootstrapOwnerGuard { /// Adopts the private bootstrap entry's pipe standard input. /// /// # Errors /// Refuses a standard input that cannot be duplicated or is not a pipe. pub fn from_stdin() -> Result { let stdin = std::io::stdin(); Self::from_inherited(dup(&stdin).map_err(failure)?) } /// Adopts an inherited bootstrap-owner lifetime pipe without PID sampling. /// /// # Errors /// Refuses a descriptor that is not a pipe or cannot be confined to this process. pub fn from_inherited(descriptor: OwnedFd) -> Result { let metadata = fstat(&descriptor).map_err(failure)?; if !FileType::from_raw_mode(metadata.st_mode).is_fifo() { return Err(ColdStartError( "bootstrap-owner lifetime descriptor is not a pipe".into(), )); } fcntl_setfd(descriptor.as_fd(), FdFlags::CLOEXEC).map_err(failure)?; Ok(Self { descriptor }) } /// Observes whether the exact retained bootstrap-owner pipe still has a writer. /// /// # Errors /// Returns a polling failure for the retained descriptor. pub fn is_live(&self) -> std::io::Result { let mut descriptors = [PollFd::new(&self.descriptor, PollFlags::IN)]; poll(&mut descriptors, Some(&Timespec::default()))?; Ok(!descriptors[0] .revents() .intersects(PollFlags::IN | PollFlags::HUP | PollFlags::ERR)) } } pub struct ColdStart<'a> { store: Arc, catalog: &'a Catalog, configuration: &'a ResolvedDomainConfiguration, generation: Generation, generation_identity: GenerationIdentity, prepared_artifact: &'a PreparedArtifact, state_anchor_layout: StateAnchorLayout<'a>, runtime_launch: Option, sandbox_bundle: SandboxBundle, network_policy: lazaret_core::network::CanonicalDerivedNetworkPolicy, network_probe: NetworkProbeRequest, reachability: ReachabilityExpectation, container_id: ContainerId, runtime_root: PathBuf, bundle_digest: BundleDigest, invoking_uid: u32, staging: RuntimeStaging, git_routing: Option, } impl<'a> ColdStart<'a> { /// Stages one generation from validated component authorities. /// /// # Errors /// Refuses mismatched generation/configuration/probe inputs, changed host authority, unsafe /// staging paths, or component construction failure. #[allow(clippy::too_many_lines)] pub fn stage(inputs: ColdStartInputs<'a>) -> Result { require_inputs(&inputs)?; let runtime_domain = host_path( inputs .configuration .runtime_domain_root() .canonical_label() .as_bytes(), ); let state_anchor_layout = StateAnchorLayout::create(inputs.configuration) .map_err(|error| failure(format!("State Anchor layout: {error}")))?; let staging = RuntimeStaging::create( inputs.configuration.runtime_domain_root(), inputs.container_id, inputs.escalation_client, inputs.sandbox_init, inputs.invoking_uid, ) .map_err(|error| failure(format!("runtime staging: {error}")))?; let git_routing = GitRoutingLayout::create( inputs.configuration, inputs.container_id, inputs.invoking_uid, ) .map_err(|error| failure(format!("Git-routing staging: {error}")))?; let expected_network_namespace = namespace_identity(Path::new(NETWORK_NAMESPACE_PATH)) .map_err(|error| failure(format!("network namespace: {error}")))?; let request = SandboxBuildRequest { configuration: inputs.configuration, container_id: inputs.container_id, expected_network_namespace, root_process: inputs.root_process, terminal: None, prepared_root: inputs.prepared_artifact_label.to_owned(), }; let sandbox_bundle = match &git_routing { Some(git_routing) => { build_sandbox_bundle_with_workspace(request, git_routing.mountpoint_label.clone()) } None => build_sandbox_bundle(request), } .map_err(|error| failure(format!("sandbox bundle: {error}")))?; staging .write_bundle(sandbox_bundle.specification_json()) .map_err(|error| failure(format!("bundle staging: {error}")))?; let network_policy = derive_ipv4_policy(inputs.configuration, inputs.resolver_addresses) .map_err(|error| failure(format!("network policy: {error}")))?; let ownership = ProcessOwnershipContext { domain_id: inputs.generation.domain_id, generation_id: inputs.generation.generation_id, coordinator_id: inputs.generation.coordinator_id, slot: HostProcessOwnershipSlot::SandboxRuntimeSupervisor, }; let state_anchor = StateAnchorLaunchAuthority::capture_retained( &state_anchor_layout, inputs.prepared_artifact, inputs.prepared_artifact_label, inputs.prepared_artifact_authority, ) .map_err(|error| failure(format!("State Anchor authority: {error}")))?; let mut runtime_launch = RuntimeSupervisorLaunch::from_networked_runsc_command( ownership, sandbox_bundle.command(), RuntimeNetworkLaunch { policy: &network_policy, configuration_fingerprint: inputs.generation.configuration_fingerprint, network_sidecar_id: inputs.generation.network_sidecar_id, }, ) .map_err(|error| failure(format!("Runtime Supervisor launch: {error}")))? .with_state_anchor(state_anchor) .map_err(|error| failure(format!("Runtime Supervisor handoff: {error}")))?; if let Some(git_routing) = &git_routing { runtime_launch = runtime_launch .with_git_routing( git_routing .launch_authority(inputs.configuration) .map_err(|error| failure(format!("Git-routing authority: {error}")))?, ) .map_err(|error| failure(format!("Runtime Supervisor handoff: {error}")))?; } let bundle_digest = BundleDigest::new(Sha256::digest(sandbox_bundle.specification_json()).into()); let generation_identity = GenerationIdentity { agent_profile: inputs.configuration.selected_profile().to_owned(), domain_id: inputs.generation.domain_id, generation_id: inputs.generation.generation_id, configuration_fingerprint: inputs.generation.configuration_fingerprint, coordinator_id: inputs.generation.coordinator_id, sandbox_id: inputs.generation.sandbox_id, }; Ok(Self { store: inputs.store, catalog: inputs.catalog, configuration: inputs.configuration, generation: inputs.generation, generation_identity, prepared_artifact: inputs.prepared_artifact, state_anchor_layout, runtime_launch: Some(runtime_launch), sandbox_bundle, network_policy, network_probe: inputs.network_probe, reachability: inputs.reachability, container_id: inputs.container_id, runtime_root: runtime_domain.join("runsc"), bundle_digest, invoking_uid: inputs.invoking_uid, staging, git_routing, }) } /// Runs cold start and exposes the exact retained lifecycle to one service loop. /// /// # Errors /// Returns staging, lifecycle, or service-loop failure. The service is single-use. pub fn run( mut self, bootstrap_owner: Session, owner_guard: &BootstrapOwnerGuard, service: Service, ) -> Result<(), ColdStartError> where Service: FnOnce( &Mutex>, &RefCell, &BootstrapOwnerGuard, lazaret_core::identity::SessionId, ) -> Result<(), ColdStartError>, { let bootstrap_owner_session_id = bootstrap_owner.session_id; let waiters = RefCell::new( BootstrapWaiterFace::bind( self.configuration, self.generation.generation_id, self.invoking_uid, ) .map_err(failure)?, ); let store = Arc::clone(&self.store); let generation = self.generation.clone(); let plan = self.take_plan()?; let mut lifecycle = coordinator_lifecycle(&store, plan).map_err(failure)?; let bootstrap = lifecycle.bootstrap_new_domain_while_owner_live_and_service( generation, bootstrap_owner, || owner_guard.is_live().unwrap_or(false), |lifecycle| { waiters .borrow_mut() .admit_pending(lifecycle) .map_err(crate::bootstrap::LifecycleServiceError::service) }, |lifecycle| { let mut waiters = waiters.borrow_mut(); waiters .admit_pending(lifecycle) .map_err(crate::bootstrap::LifecycleServiceError::service)?; waiters .withdraw() .map_err(crate::bootstrap::LifecycleServiceError::service) }, ); match bootstrap { Ok(true) => { for session_id in waiters.borrow_mut().notify(BootstrapWaitOutcome::Published) { lifecycle.disconnect_session(session_id).map_err(failure)?; } } Ok(false) => { let mut waiters = waiters.borrow_mut(); let withdrawal = waiters.withdraw(); let disconnected = waiters.notify(BootstrapWaitOutcome::Failed); withdrawal.map_err(failure)?; drop(waiters); for session_id in disconnected { lifecycle.disconnect_session(session_id).map_err(failure)?; } return Err(ColdStartError( "bootstrap owner disconnected before publication".into(), )); } Err(error) => { let mut waiters = waiters.borrow_mut(); let _ = waiters.withdraw(); let _ = waiters.notify(BootstrapWaitOutcome::Failed); return Err(failure(error)); } } service( &Mutex::new(lifecycle), &waiters, owner_guard, bootstrap_owner_session_id, ) } fn take_plan(&mut self) -> Result, ColdStartError> { let interactive_executor = RunscInteractiveSessionExecutor::new( Arc::clone(&self.store), &self.generation_identity, self.container_id, current_process_identity().map_err(failure)?, self.sandbox_bundle.command(), self.runtime_root.clone(), ) .map_err(failure)?; let prompt_transport = interactive_executor.prompt_transport(); Ok(BootstrapPlan { store: Arc::clone(&self.store), catalog: self.catalog, configuration: self.configuration, generation_identity: self.generation_identity.clone(), prepared_artifact: self.prepared_artifact, state_anchor_layout: &self.state_anchor_layout, runtime_launch: self .runtime_launch .take() .ok_or_else(|| ColdStartError("cold start was already consumed".into()))?, sandbox_bundle: &self.sandbox_bundle, network_policy: &self.network_policy, network_probe: self.network_probe.clone(), reachability: &self.reachability, container_id: self.container_id, runtime_root: self.runtime_root.clone(), bundle_digest: self.bundle_digest, invoking_uid: self.invoking_uid, non_interactive_executor: Arc::new( RunscNonInteractiveExecutor::from_run_command( self.sandbox_bundle.command(), self.container_id, ) .map_err(failure)?, ), interactive_executor: Arc::new(interactive_executor), prompt_transport, runtime_staging: &self.staging, git_routing: self.git_routing.as_ref().map(|routing| { crate::bootstrap::BootstrapGitRouting { mountpoint: routing.mountpoint(), host_root: routing.host_root(), isolated_root: routing.isolated_root(), } }), }) } } fn require_inputs(inputs: &ColdStartInputs<'_>) -> Result<(), ColdStartError> { if inputs.generation.domain_id != inputs.configuration.domain_id() || inputs.generation.configuration_fingerprint != inputs.configuration.configuration_fingerprint() || inputs.network_probe.domain_id != inputs.generation.domain_id || inputs.network_probe.generation_id != inputs.generation.generation_id || inputs.resolver_addresses.is_empty() || inputs.invoking_uid != rustix::process::getuid().as_raw() { return Err(ColdStartError("cold-start inputs disagree".into())); } Ok(()) } mod git_routing; mod runtime_staging; use git_routing::GitRoutingLayout; #[cfg(test)] use git_routing::parse_global_excludes_path; pub(crate) use runtime_staging::RuntimeStaging; use runtime_staging::{ FileIdentity, confined_resolve_flags, hex, host_path, open_or_create_private_directory, remove_staging_directory, require_private_file, }; #[cfg(test)] mod tests;