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); 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}")))?, ); } 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(()) } struct GitRoutingLayout { _runtime_directory: RuntimeGitRoutingDirectory, workspace: File, workspace_label: PathBuf, isolated: File, isolated_label: PathBuf, mountpoint: File, mountpoint_label: PathBuf, git_common: File, git_common_label: PathBuf, transient_hooks: File, transient_hooks_label: PathBuf, transient_config: File, transient_config_label: PathBuf, global_excludes: Option, isolate_ignored: bool, } impl GitRoutingLayout { fn create( configuration: &ResolvedDomainConfiguration, container_id: ContainerId, invoking_uid: u32, ) -> Result, ColdStartError> { let workspace_label = host_path(configuration.workspace().canonical_label().as_bytes()); let workspace = File::from( configuration .workspace() .open_authority() .map_err(failure)?, ); match statat(&workspace, ".git", AtFlags::SYMLINK_NOFOLLOW) { Ok(_) => {} Err(rustix::io::Errno::NOENT) => return Ok(None), Err(error) => return Err(failure(error)), } let persistent_root = configuration .persistent_domain_root() .open_authority() .map_err(failure)?; let isolated = open_or_create_private_directory( &persistent_root, "ignored-workspace", invoking_uid, false, )?; let runtime_root = configuration .runtime_domain_root() .open_authority() .map_err(failure)?; let runtime_parent = open_or_create_private_directory(&runtime_root, "git-routing", invoking_uid, false)?; let runtime_name = OsString::from(hex(container_id.as_bytes())); let directory = open_or_create_private_directory(&runtime_parent, &runtime_name, invoking_uid, true)?; let identity = FileIdentity::capture(&directory)?; let runtime_directory = RuntimeGitRoutingDirectory { parent: runtime_parent, directory, name: runtime_name.clone(), identity, }; let mountpoint = open_or_create_private_directory( &runtime_directory.directory, "workspace", invoking_uid, true, )?; let transient_hooks = open_or_create_private_directory( &runtime_directory.directory, "hooks", invoking_uid, true, )?; let transient_config = openat2( &runtime_directory.directory, "config", OFlags::RDWR | OFlags::CREATE | OFlags::EXCL | OFlags::CLOEXEC | OFlags::NOFOLLOW, Mode::RUSR | Mode::WUSR, confined_resolve_flags(), ) .map_err(failure)?; fchmod(&transient_config, Mode::RUSR | Mode::WUSR).map_err(failure)?; require_private_file(&transient_config, invoking_uid, 0o600)?; let isolated_label = host_path( configuration .persistent_domain_root() .canonical_label() .as_bytes(), ) .join("ignored-workspace"); let runtime_label = host_path( configuration .runtime_domain_root() .canonical_label() .as_bytes(), ) .join("git-routing") .join(&runtime_name); let mountpoint_label = runtime_label.join("workspace"); let transient_hooks_label = runtime_label.join("hooks"); let transient_config_label = runtime_label.join("config"); let git_common_path = git_common_directory(&workspace_label).map_err(failure)?; let git_common = open_absolute_directory(&git_common_path)?; let global_excludes = global_excludes_path(&workspace_label)?; Ok(Some(Self { _runtime_directory: runtime_directory, workspace, workspace_label, isolated: File::from(isolated), isolated_label, mountpoint: File::from(mountpoint), mountpoint_label, git_common, git_common_label: git_common_path, transient_hooks: File::from(transient_hooks), transient_hooks_label, transient_config: File::from(transient_config), transient_config_label, global_excludes, isolate_ignored: configuration.fingerprint_record().isolate_gitignored, })) } fn launch_authority( &self, configuration: &ResolvedDomainConfiguration, ) -> Result { configuration .workspace() .open_authority() .map_err(failure)?; RuntimeGitRoutingLaunch::new(RuntimeGitRoutingAuthority { workspace: self.workspace.try_clone().map_err(failure)?, workspace_label: self.workspace_label.clone(), isolated: self.isolated.try_clone().map_err(failure)?, mountpoint: self.mountpoint.try_clone().map_err(failure)?, mountpoint_label: self.mountpoint_label.clone(), git_common: self.git_common.try_clone().map_err(failure)?, git_common_label: self.git_common_label.clone(), transient_hooks: self.transient_hooks.try_clone().map_err(failure)?, transient_hooks_label: self.transient_hooks_label.clone(), transient_config: self.transient_config.try_clone().map_err(failure)?, transient_config_label: self.transient_config_label.clone(), global_excludes: self.global_excludes.clone(), isolate_ignored: self.isolate_ignored, }) .map_err(failure) } fn mountpoint(&self) -> HostPath { HostPath::new( self.mountpoint_label .as_os_str() .as_encoded_bytes() .to_vec(), ) } fn host_root(&self) -> HostPath { HostPath::new(self.workspace_label.as_os_str().as_encoded_bytes().to_vec()) } fn isolated_root(&self) -> HostPath { HostPath::new(self.isolated_label.as_os_str().as_encoded_bytes().to_vec()) } } struct RuntimeGitRoutingDirectory { parent: OwnedFd, directory: OwnedFd, name: OsString, identity: FileIdentity, } impl Drop for RuntimeGitRoutingDirectory { fn drop(&mut self) { remove_staging_directory(&self.parent, &self.name, &self.directory, self.identity); } } fn open_absolute_directory(path: &Path) -> Result { let descriptor = openat2( rustix::fs::CWD, path, OFlags::RDONLY | OFlags::DIRECTORY | OFlags::CLOEXEC | OFlags::NOFOLLOW, Mode::empty(), ResolveFlags::NO_MAGICLINKS | ResolveFlags::NO_SYMLINKS, ) .map_err(failure)?; Ok(File::from(descriptor)) } fn global_excludes_path(workspace: &Path) -> Result, ColdStartError> { let (reader, writer) = pipe_with(PipeFlags::CLOEXEC).map_err(failure)?; let target = TargetCommand::new("git") .argument("-C") .argument(workspace) .argument("config") .argument("--path") .argument("--get") .argument("--default") .argument("") .argument("core.excludesFile"); child_broker() .run_transient_with_private_stdout(&target, writer) .map_err(|error| failure(format!("Git global-excludes discovery: {error}")))?; let mut output = Vec::new(); File::from(reader) .read_to_end(&mut output) .map_err(failure)?; parse_global_excludes_path(workspace, &output) } fn parse_global_excludes_path( workspace: &Path, output: &[u8], ) -> Result, ColdStartError> { let value = output.strip_suffix(b"\n").unwrap_or(output); if value.contains(&0) { return Err(ColdStartError("Git global-excludes path is invalid".into())); } if value.is_empty() { return Ok(default_global_excludes_path()); } let path = PathBuf::from(OsString::from_vec(value.to_vec())); Ok(Some(if path.is_absolute() { path } else { workspace.join(path) })) } fn default_global_excludes_path() -> Option { std::env::var_os("XDG_CONFIG_HOME") .filter(|path| !path.is_empty()) .map(PathBuf::from) .or_else(|| { std::env::var_os("HOME") .filter(|path| !path.is_empty()) .map(|home| PathBuf::from(home).join(".config")) }) .map(|home| home.join("git/ignore")) } pub(crate) struct RuntimeStaging { runsc_root: OwnedFd, bundles: OwnedFd, bundle: OwnedFd, controls: OwnedFd, control: OwnedFd, overlays: OwnedFd, overlay: OwnedFd, name: OsString, bundle_identity: FileIdentity, control_identity: FileIdentity, overlay_identity: FileIdentity, } impl RuntimeStaging { pub(crate) fn runsc_root_identity(&self) -> Result { mount_identity(&self.runsc_root).map_err(failure) } pub(crate) fn cleanup_overlay(&self) -> Result<(), ColdStartError> { let retained = fstat(&self.overlay).map_err(failure)?; if !self.overlay_identity.matches(&retained) { return Err(ColdStartError( "runsc overlay retained authority changed".into(), )); } remove_children(&self.overlay).map_err(failure)?; let current = statat(&self.overlays, &self.name, AtFlags::SYMLINK_NOFOLLOW).map_err(failure)?; if !self.overlay_identity.matches(¤t) || !FileType::from_raw_mode(current.st_mode).is_dir() { return Err(ColdStartError( "runsc overlay path changed before cleanup".into(), )); } unlinkat(&self.overlays, &self.name, AtFlags::REMOVEDIR).map_err(failure)?; Ok(()) } pub(crate) fn escalation_mailboxes( &self, invoking_uid: u32, ) -> Result<(RegularFileMailbox, RegularFileMailbox, RegularFileMailbox), ColdStartError> { let credentials = RegularFileMailbox::openat_private( &self.control, OsStr::new("credentials"), invoking_uid, ) .map_err(failure)?; let mailbox = RegularFileMailbox::openat_private(&self.control, OsStr::new("mailbox"), invoking_uid) .map_err(failure)?; let requests = RegularFileMailbox::openat_private(&mailbox, OsStr::new("requests"), invoking_uid) .map_err(failure)?; let results = RegularFileMailbox::openat_private(&mailbox, OsStr::new("results"), invoking_uid) .map_err(failure)?; Ok((credentials, requests, results)) } fn create( runtime_domain: &ConfinedPathPlan, container_id: ContainerId, escalation_client: &File, sandbox_init: &File, invoking_uid: u32, ) -> Result { let root = runtime_domain.open_authority().map_err(failure)?; let bundles = open_or_create_private_directory(&root, "bundles", invoking_uid, false)?; let controls = open_or_create_private_directory(&root, "sandbox-control", invoking_uid, false)?; let overlays = open_or_create_private_directory(&root, "runsc-overlay", invoking_uid, false)?; let runsc_root = open_or_create_private_directory(&root, "runsc", invoking_uid, false)?; let name = OsString::from(hex(container_id.as_bytes())); let bundle = open_or_create_private_directory(&bundles, &name, invoking_uid, true)?; let bundle_identity = FileIdentity::capture(&bundle)?; let control = match open_or_create_private_directory(&controls, &name, invoking_uid, true) { Ok(control) => control, Err(error) => { remove_staging_directory(&bundles, &name, &bundle, bundle_identity); return Err(error); } }; let control_identity = FileIdentity::capture(&control)?; let overlay = match open_or_create_private_directory(&overlays, &name, invoking_uid, true) { Ok(overlay) => overlay, Err(error) => { remove_staging_directory(&controls, &name, &control, control_identity); remove_staging_directory(&bundles, &name, &bundle, bundle_identity); return Err(error); } }; let overlay_identity = FileIdentity::capture(&overlay)?; let staging = Self { runsc_root, bundles, bundle, controls, control, overlays, overlay, name, bundle_identity, control_identity, overlay_identity, }; staging.populate_control(escalation_client, sandbox_init)?; Ok(staging) } fn populate_control( &self, escalation_client: &File, sandbox_init: &File, ) -> Result<(), ColdStartError> { let invoking_uid = fstat(&self.control).map_err(failure)?.st_uid; drop(open_or_create_private_directory( &self.control, "credentials", invoking_uid, true, )?); let mailbox = open_or_create_private_directory(&self.control, "mailbox", invoking_uid, true)?; drop(open_or_create_private_directory( &mailbox, "requests", invoking_uid, true, )?); drop(open_or_create_private_directory( &mailbox, "results", invoking_uid, true, )?); let bin = open_or_create_private_directory(&self.control, "bin", invoking_uid, true)?; require_executable_regular_file(escalation_client, "escalation client")?; copy_descriptor(escalation_client, &bin, "lazaret-escalate", invoking_uid)?; require_executable_regular_file(sandbox_init, "sandbox init")?; copy_descriptor(sandbox_init, &bin, "lazaret-sandbox-init", invoking_uid)?; Ok(()) } fn write_bundle(&self, bytes: &[u8]) -> Result<(), ColdStartError> { let descriptor = openat2( &self.bundle, "config.json", OFlags::WRONLY | OFlags::CREATE | OFlags::EXCL | OFlags::CLOEXEC | OFlags::NOFOLLOW, Mode::RUSR | Mode::WUSR, confined_resolve_flags(), ) .map_err(failure)?; let mut file = File::from(descriptor); file.write_all(bytes).map_err(failure)?; fchmod(file.as_fd(), Mode::RUSR | Mode::WUSR).map_err(failure)?; file.sync_all().map_err(failure)?; require_private_file(&file, fstat(&self.bundle).map_err(failure)?.st_uid, 0o600)?; Ok(()) } } fn copy_descriptor( source: &File, parent: &OwnedFd, name: &str, invoking_uid: u32, ) -> Result<(), ColdStartError> { let descriptor = openat2( parent, name, OFlags::WRONLY | OFlags::CREATE | OFlags::EXCL | OFlags::CLOEXEC | OFlags::NOFOLLOW, Mode::RWXU, confined_resolve_flags(), ) .map_err(failure)?; let mut destination = File::from(descriptor); fchmod(destination.as_fd(), Mode::RWXU).map_err(failure)?; let mut offset = 0_u64; let mut buffer = [0_u8; 16 * 1024]; loop { let read = source.read_at(&mut buffer, offset).map_err(failure)?; if read == 0 { break; } destination.write_all(&buffer[..read]).map_err(failure)?; offset = offset .checked_add(read as u64) .ok_or_else(|| ColdStartError("escalation client is too large".into()))?; } destination.sync_all().map_err(failure)?; require_private_file(&destination, invoking_uid, 0o700) } fn require_executable_regular_file(source: &File, label: &str) -> Result<(), ColdStartError> { let metadata = source .metadata() .map_err(|error| failure(format!("inspecting {label}: {error}")))?; if !metadata.file_type().is_file() || metadata.permissions().mode() & 0o111 == 0 { return Err(ColdStartError(format!( "{label} is not an executable regular file" ))); } Ok(()) } #[derive(Clone, Copy)] struct FileIdentity { device: u64, inode: u64, } impl FileIdentity { fn capture(descriptor: impl AsFd) -> Result { let metadata = fstat(descriptor).map_err(failure)?; Ok(Self { device: metadata.st_dev, inode: metadata.st_ino, }) } fn matches(self, metadata: &rustix::fs::Stat) -> bool { metadata.st_dev == self.device && metadata.st_ino == self.inode } } impl Drop for RuntimeStaging { fn drop(&mut self) { remove_staging_directory( &self.overlays, &self.name, &self.overlay, self.overlay_identity, ); remove_staging_directory( &self.controls, &self.name, &self.control, self.control_identity, ); remove_staging_directory( &self.bundles, &self.name, &self.bundle, self.bundle_identity, ); } } fn open_or_create_private_directory( parent: impl AsFd, name: impl AsRef, invoking_uid: u32, require_new: bool, ) -> Result { let name = name.as_ref(); let created = match mkdirat(parent.as_fd(), name, Mode::RWXU) { Ok(()) => true, Err(rustix::io::Errno::EXIST) if !require_new => false, Err(error) => return Err(failure(error)), }; let directory = openat2( parent.as_fd(), name, OFlags::RDONLY | OFlags::DIRECTORY | OFlags::CLOEXEC | OFlags::NOFOLLOW, Mode::empty(), confined_resolve_flags(), ) .map_err(failure)?; if created { fchmod(&directory, Mode::RWXU).map_err(failure)?; } require_private_directory(&directory, invoking_uid)?; Ok(directory) } fn require_private_directory( descriptor: impl AsFd, invoking_uid: u32, ) -> Result<(), ColdStartError> { let metadata = fstat(descriptor).map_err(failure)?; if !FileType::from_raw_mode(metadata.st_mode).is_dir() || metadata.st_uid != invoking_uid || metadata.st_mode & 0o7777 != 0o700 { return Err(ColdStartError( "runtime staging directory is not invoking-user-owned mode 0700".into(), )); } Ok(()) } fn require_private_file( descriptor: impl AsFd, invoking_uid: u32, mode: u32, ) -> Result<(), ColdStartError> { let metadata = fstat(descriptor).map_err(failure)?; if !FileType::from_raw_mode(metadata.st_mode).is_file() || metadata.st_uid != invoking_uid || metadata.st_mode & 0o7777 != mode || metadata.st_nlink != 1 { return Err(ColdStartError( "runtime staging file has unsafe type, ownership, mode, or link count".into(), )); } Ok(()) } fn remove_staging_directory( parent: impl AsFd, name: &std::ffi::OsStr, directory: impl AsFd, expected: FileIdentity, ) { let retained = fstat(directory.as_fd()); if !retained .as_ref() .is_ok_and(|metadata| expected.matches(metadata)) { return; } if remove_children(directory.as_fd()).is_err() { return; } let Ok(current) = statat(parent.as_fd(), name, AtFlags::SYMLINK_NOFOLLOW) else { return; }; if expected.matches(¤t) && FileType::from_raw_mode(current.st_mode).is_dir() { let _ = unlinkat(parent.as_fd(), name, AtFlags::REMOVEDIR); } } fn remove_children(directory: impl AsFd) -> std::io::Result<()> { let duplicate = rustix::io::dup(directory.as_fd())?; let mut buffer = [std::mem::MaybeUninit::uninit(); 8192]; let names = { let mut names = Vec::new(); let mut entries = RawDir::new(duplicate, &mut buffer); while let Some(entry) = entries.next() { let entry = entry?; let bytes = entry.file_name().to_bytes(); if bytes != b"." && bytes != b".." { names.push(OsString::from_vec(bytes.to_vec())); } } names }; for name in names { let metadata = statat(directory.as_fd(), &name, AtFlags::SYMLINK_NOFOLLOW)?; if FileType::from_raw_mode(metadata.st_mode).is_dir() { let child = openat2( directory.as_fd(), &name, OFlags::RDONLY | OFlags::DIRECTORY | OFlags::CLOEXEC | OFlags::NOFOLLOW, Mode::empty(), confined_resolve_flags(), )?; remove_children(&child)?; unlinkat(directory.as_fd(), &name, AtFlags::REMOVEDIR)?; } else { unlinkat(directory.as_fd(), &name, AtFlags::empty())?; } } Ok(()) } fn confined_resolve_flags() -> ResolveFlags { ResolveFlags::BENEATH | ResolveFlags::NO_MAGICLINKS | ResolveFlags::NO_SYMLINKS | ResolveFlags::NO_XDEV } fn host_path(bytes: &[u8]) -> PathBuf { PathBuf::from(OsString::from_vec(bytes.to_vec())) } fn hex(bytes: &[u8]) -> String { use std::fmt::Write as _; bytes.iter().fold(String::new(), |mut output, byte| { write!(output, "{byte:02x}").expect("writing to a String cannot fail"); output }) } #[cfg(test)] mod tests { use std::fs::File; use std::os::fd::OwnedFd; use rustix::pipe::{PipeFlags, pipe_with}; use super::BootstrapOwnerGuard; #[test] fn bootstrap_owner_guard_tracks_the_exact_inherited_pipe() { let (reader, writer) = pipe_with(PipeFlags::CLOEXEC).unwrap(); let guard = BootstrapOwnerGuard::from_inherited(reader).unwrap(); assert!(guard.is_live().unwrap()); drop(writer); assert!(!guard.is_live().unwrap()); } #[test] fn bootstrap_owner_guard_refuses_non_pipe_authority() { let descriptor: OwnedFd = File::open("/dev/null").unwrap().into(); assert!(BootstrapOwnerGuard::from_inherited(descriptor).is_err()); } #[test] fn repository_relative_global_excludes_are_workspace_relative() { let root = tempfile::tempdir().unwrap(); let workspace = root.path().join("workspace"); assert_eq!( super::parse_global_excludes_path(&workspace, b"policy/ignore\n").unwrap(), Some(workspace.join("policy/ignore")) ); assert!(super::parse_global_excludes_path(&workspace, b"bad\0path\n").is_err()); } }