lazaret/crates/lazaret-host-coordinator/src/cold_start.rs

437 lines
17 KiB
Rust
Raw Normal View History

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::derive_ipv4_policy;
use lazaret_core::records::{
Generation, GenerationIdentity, HostProcessOwnershipSlot, PreparedArtifact, Session,
};
use lazaret_ignored_workspace::git_common_directory;
use lazaret_linux::filesystem::SharedFileLock;
use lazaret_linux::filesystem::{RegularFileMailbox, mount_identity, namespace_identity};
use lazaret_linux::process::{
ProcessOwnershipContext, TargetCommand, child_broker, current_process_identity,
};
use lazaret_linux::user_namespace::UserNamespaceMap;
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<HostCoordinatorDomainStore>,
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_selection: SharedFileLock,
pub prepared_artifact_label: &'a Path,
pub root_process: RootProcess,
pub container_id: ContainerId,
pub invoking_uid: u32,
pub escalation_client: &'a File,
pub sandbox_init: &'a File,
pub user_namespace_map: UserNamespaceMap,
}
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<Self, ColdStartError> {
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<Self, ColdStartError> {
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<bool> {
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<HostCoordinatorDomainStore>,
catalog: &'a Catalog,
configuration: &'a ResolvedDomainConfiguration,
generation: Generation,
generation_identity: GenerationIdentity,
prepared_artifact: &'a PreparedArtifact,
state_anchor_layout: StateAnchorLayout<'a>,
prepared_artifact_selection: Option<SharedFileLock>,
runtime_launch: Option<RuntimeSupervisorLaunch>,
sandbox_bundle: SandboxBundle,
network_policy: lazaret_core::network::CanonicalDerivedNetworkPolicy,
container_id: ContainerId,
runtime_root: PathBuf,
bundle_digest: BundleDigest,
invoking_uid: u32,
staging: RuntimeStaging,
git_routing: Option<GitRoutingLayout>,
}
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<Self, ColdStartError> {
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 network_policy = derive_ipv4_policy(inputs.configuration)
.map_err(|error| failure(format!("network policy: {error}")))?;
runtime_files::replace_protected_runtime_files(&state_anchor_layout, &network_policy)
.map_err(|error| failure(format!("protected runtime files: {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 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_user_namespace_map(inputs.user_namespace_map)
.map_err(|error| failure(format!("Runtime Supervisor user namespace: {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,
prepared_artifact_selection: Some(inputs.prepared_artifact_selection),
runtime_launch: Some(runtime_launch),
sandbox_bundle,
network_policy,
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<Service>(
mut self,
bootstrap_owner: Session,
owner_guard: &BootstrapOwnerGuard,
service: Service,
) -> Result<(), ColdStartError>
where
Service: FnOnce(
&Mutex<CoordinatorLifecycle<'_, '_>>,
&RefCell<BootstrapWaiterFace>,
&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<BootstrapPlan<'_>, 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.configuration.ephemeral_environment().clone(),
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,
prepared_artifact_selection: self.prepared_artifact_selection.take(),
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,
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,
self.configuration.ephemeral_environment().clone(),
)
.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.invoking_uid != rustix::process::getuid().as_raw()
{
return Err(ColdStartError("cold-start inputs disagree".into()));
}
Ok(())
}
mod git_routing;
mod runtime_files;
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;