2026-07-16 16:28:18 +09:00
|
|
|
use std::cell::RefCell;
|
2026-07-18 04:03:39 +09:00
|
|
|
use std::ffi::{OsStr, OsString};
|
2026-07-16 18:39:03 +09:00
|
|
|
use std::fmt;
|
2026-07-16 18:53:20 +09:00
|
|
|
use std::fs::File;
|
2026-07-18 02:08:14 +09:00
|
|
|
use std::io::{Read, Write};
|
2026-07-16 18:39:03 +09:00
|
|
|
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};
|
|
|
|
|
|
2026-07-16 18:53:20 +09:00
|
|
|
use lazaret_core::configuration::{ConfinedPathPlan, ResolvedDomainConfiguration};
|
2026-07-18 02:08:14 +09:00
|
|
|
use lazaret_core::identity::{BundleDigest, ContainerId, HostPath, MountIdentity};
|
2026-07-16 18:39:03 +09:00
|
|
|
use lazaret_core::network::{ReachabilityExpectation, derive_ipv4_policy};
|
|
|
|
|
use lazaret_core::records::{
|
|
|
|
|
Generation, GenerationIdentity, HostProcessOwnershipSlot, IpAddress, PreparedArtifact, Session,
|
|
|
|
|
};
|
2026-07-18 02:08:14 +09:00
|
|
|
use lazaret_ignored_workspace::git_common_directory;
|
2026-07-18 04:03:39 +09:00
|
|
|
use lazaret_linux::filesystem::{RegularFileMailbox, mount_identity, namespace_identity};
|
2026-07-18 02:08:14 +09:00
|
|
|
use lazaret_linux::process::{
|
|
|
|
|
ProcessOwnershipContext, TargetCommand, child_broker, current_process_identity,
|
|
|
|
|
};
|
2026-07-16 18:39:03 +09:00
|
|
|
use lazaret_runtime_supervisor::network::NetworkProbeRequest;
|
|
|
|
|
use lazaret_runtime_supervisor::sandbox::{
|
|
|
|
|
RootProcess, SandboxBuildRequest, SandboxBundle, build_sandbox_bundle,
|
2026-07-18 02:08:14 +09:00
|
|
|
build_sandbox_bundle_with_workspace,
|
|
|
|
|
};
|
|
|
|
|
use lazaret_runtime_supervisor::supervision::{
|
|
|
|
|
RuntimeGitRoutingAuthority, RuntimeGitRoutingLaunch, RuntimeNetworkLaunch,
|
|
|
|
|
RuntimeSupervisorLaunch,
|
2026-07-16 18:39:03 +09:00
|
|
|
};
|
|
|
|
|
use lazaret_state_anchor::{StateAnchorLaunchAuthority, StateAnchorLayout};
|
|
|
|
|
use lazaret_storage_catalog::Catalog;
|
|
|
|
|
use rustix::event::{PollFd, PollFlags, Timespec, poll};
|
2026-07-16 18:53:20 +09:00
|
|
|
use rustix::fs::{
|
|
|
|
|
AtFlags, FileType, Mode, OFlags, RawDir, ResolveFlags, fchmod, fstat, mkdirat, openat2, statat,
|
|
|
|
|
unlinkat,
|
|
|
|
|
};
|
2026-07-16 16:28:18 +09:00
|
|
|
use rustix::io::{FdFlags, dup, fcntl_setfd};
|
2026-07-18 02:08:14 +09:00
|
|
|
use rustix::pipe::{PipeFlags, pipe_with};
|
2026-07-16 18:39:03 +09:00
|
|
|
use sha2::{Digest, Sha256};
|
|
|
|
|
|
2026-07-18 22:35:48 +09:00
|
|
|
use crate::bootstrap::{BootstrapPlan, CoordinatorLifecycle, coordinator_lifecycle};
|
2026-07-16 16:28:18 +09:00
|
|
|
use crate::control::RunscNonInteractiveExecutor;
|
2026-07-16 18:39:03 +09:00
|
|
|
use crate::domain_store::HostCoordinatorDomainStore;
|
2026-07-17 06:59:39 +09:00
|
|
|
use crate::terminal::RunscInteractiveSessionExecutor;
|
2026-07-16 16:28:18 +09:00
|
|
|
use crate::waiter::{BootstrapWaitOutcome, BootstrapWaiterFace};
|
2026-07-16 18:39:03 +09:00
|
|
|
|
|
|
|
|
const NETWORK_NAMESPACE_PATH: &str = "/proc/self/ns/net";
|
|
|
|
|
|
|
|
|
|
#[derive(Debug)]
|
2026-07-18 22:35:48 +09:00
|
|
|
pub struct ColdStartError(String);
|
2026-07-16 18:39:03 +09:00
|
|
|
|
2026-07-18 22:35:48 +09:00
|
|
|
impl fmt::Display for ColdStartError {
|
2026-07-16 18:39:03 +09:00
|
|
|
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
|
|
|
formatter.write_str(&self.0)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-18 22:35:48 +09:00
|
|
|
impl std::error::Error for ColdStartError {}
|
2026-07-16 18:39:03 +09:00
|
|
|
|
2026-07-18 22:35:48 +09:00
|
|
|
impl ColdStartError {
|
2026-07-16 16:28:18 +09:00
|
|
|
pub(crate) fn service(error: impl fmt::Display) -> Self {
|
|
|
|
|
Self(error.to_string())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-18 22:35:48 +09:00
|
|
|
fn failure(error: impl fmt::Display) -> ColdStartError {
|
|
|
|
|
ColdStartError(error.to_string())
|
2026-07-16 18:39:03 +09:00
|
|
|
}
|
|
|
|
|
|
2026-07-18 22:35:48 +09:00
|
|
|
pub struct ColdStartInputs<'a> {
|
2026-07-16 18:39:03 +09:00
|
|
|
pub store: Arc<HostCoordinatorDomainStore>,
|
|
|
|
|
pub catalog: &'a Catalog,
|
|
|
|
|
pub configuration: &'a ResolvedDomainConfiguration,
|
|
|
|
|
pub generation: Generation,
|
|
|
|
|
pub prepared_artifact: &'a PreparedArtifact,
|
2026-07-17 03:58:27 +09:00
|
|
|
pub prepared_artifact_authority: &'a File,
|
|
|
|
|
pub prepared_artifact_label: &'a Path,
|
2026-07-16 18:39:03 +09:00
|
|
|
pub root_process: RootProcess,
|
|
|
|
|
pub resolver_addresses: Vec<IpAddress>,
|
|
|
|
|
pub network_probe: NetworkProbeRequest,
|
|
|
|
|
pub reachability: ReachabilityExpectation,
|
|
|
|
|
pub container_id: ContainerId,
|
|
|
|
|
pub invoking_uid: u32,
|
|
|
|
|
pub escalation_client: &'a File,
|
2026-07-16 20:41:40 +09:00
|
|
|
pub sandbox_init: &'a File,
|
2026-07-16 18:39:03 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub struct BootstrapOwnerGuard {
|
|
|
|
|
descriptor: OwnedFd,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl BootstrapOwnerGuard {
|
2026-07-16 16:28:18 +09:00
|
|
|
/// Adopts the private bootstrap entry's pipe standard input.
|
|
|
|
|
///
|
|
|
|
|
/// # Errors
|
|
|
|
|
/// Refuses a standard input that cannot be duplicated or is not a pipe.
|
2026-07-18 22:35:48 +09:00
|
|
|
pub fn from_stdin() -> Result<Self, ColdStartError> {
|
2026-07-16 16:28:18 +09:00
|
|
|
let stdin = std::io::stdin();
|
|
|
|
|
Self::from_inherited(dup(&stdin).map_err(failure)?)
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-16 18:39:03 +09:00
|
|
|
/// 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.
|
2026-07-18 22:35:48 +09:00
|
|
|
pub fn from_inherited(descriptor: OwnedFd) -> Result<Self, ColdStartError> {
|
2026-07-16 18:39:03 +09:00
|
|
|
let metadata = fstat(&descriptor).map_err(failure)?;
|
|
|
|
|
if !FileType::from_raw_mode(metadata.st_mode).is_fifo() {
|
2026-07-18 22:35:48 +09:00
|
|
|
return Err(ColdStartError(
|
2026-07-16 18:39:03 +09:00
|
|
|
"bootstrap-owner lifetime descriptor is not a pipe".into(),
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
fcntl_setfd(descriptor.as_fd(), FdFlags::CLOEXEC).map_err(failure)?;
|
|
|
|
|
Ok(Self { descriptor })
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-16 16:28:18 +09:00
|
|
|
/// 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> {
|
2026-07-16 18:39:03 +09:00
|
|
|
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))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-18 22:35:48 +09:00
|
|
|
pub struct ColdStart<'a> {
|
2026-07-16 18:39:03 +09:00
|
|
|
store: Arc<HostCoordinatorDomainStore>,
|
|
|
|
|
catalog: &'a Catalog,
|
|
|
|
|
configuration: &'a ResolvedDomainConfiguration,
|
|
|
|
|
generation: Generation,
|
|
|
|
|
generation_identity: GenerationIdentity,
|
|
|
|
|
prepared_artifact: &'a PreparedArtifact,
|
|
|
|
|
state_anchor_layout: StateAnchorLayout<'a>,
|
|
|
|
|
runtime_launch: Option<RuntimeSupervisorLaunch>,
|
|
|
|
|
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,
|
2026-07-17 03:58:27 +09:00
|
|
|
staging: RuntimeStaging,
|
2026-07-18 02:08:14 +09:00
|
|
|
git_routing: Option<GitRoutingLayout>,
|
2026-07-16 18:39:03 +09:00
|
|
|
}
|
|
|
|
|
|
2026-07-18 22:35:48 +09:00
|
|
|
impl<'a> ColdStart<'a> {
|
|
|
|
|
/// Stages one generation from validated component authorities.
|
2026-07-16 18:39:03 +09:00
|
|
|
///
|
|
|
|
|
/// # Errors
|
|
|
|
|
/// Refuses mismatched generation/configuration/probe inputs, changed host authority, unsafe
|
|
|
|
|
/// staging paths, or component construction failure.
|
2026-07-18 02:08:14 +09:00
|
|
|
#[allow(clippy::too_many_lines)]
|
2026-07-18 22:35:48 +09:00
|
|
|
pub fn stage(inputs: ColdStartInputs<'a>) -> Result<Self, ColdStartError> {
|
2026-07-16 18:39:03 +09:00
|
|
|
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(
|
2026-07-16 18:53:20 +09:00
|
|
|
inputs.configuration.runtime_domain_root(),
|
2026-07-16 18:39:03 +09:00
|
|
|
inputs.container_id,
|
|
|
|
|
inputs.escalation_client,
|
2026-07-16 20:41:40 +09:00
|
|
|
inputs.sandbox_init,
|
2026-07-16 18:53:20 +09:00
|
|
|
inputs.invoking_uid,
|
2026-07-16 18:39:03 +09:00
|
|
|
)
|
|
|
|
|
.map_err(|error| failure(format!("runtime staging: {error}")))?;
|
2026-07-18 02:08:14 +09:00
|
|
|
let git_routing = GitRoutingLayout::create(
|
|
|
|
|
inputs.configuration,
|
|
|
|
|
inputs.container_id,
|
|
|
|
|
inputs.invoking_uid,
|
|
|
|
|
)
|
|
|
|
|
.map_err(|error| failure(format!("Git-routing staging: {error}")))?;
|
2026-07-16 18:39:03 +09:00
|
|
|
let expected_network_namespace = namespace_identity(Path::new(NETWORK_NAMESPACE_PATH))
|
|
|
|
|
.map_err(|error| failure(format!("network namespace: {error}")))?;
|
2026-07-18 02:08:14 +09:00
|
|
|
let request = SandboxBuildRequest {
|
2026-07-16 18:39:03 +09:00
|
|
|
configuration: inputs.configuration,
|
|
|
|
|
container_id: inputs.container_id,
|
|
|
|
|
expected_network_namespace,
|
|
|
|
|
root_process: inputs.root_process,
|
|
|
|
|
terminal: None,
|
2026-07-17 03:58:27 +09:00
|
|
|
prepared_root: inputs.prepared_artifact_label.to_owned(),
|
2026-07-18 02:08:14 +09:00
|
|
|
};
|
|
|
|
|
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),
|
|
|
|
|
}
|
2026-07-16 18:39:03 +09:00
|
|
|
.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,
|
|
|
|
|
};
|
2026-07-17 03:58:27 +09:00
|
|
|
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}")))?;
|
2026-07-18 02:08:14 +09:00
|
|
|
let mut runtime_launch = RuntimeSupervisorLaunch::from_networked_runsc_command(
|
2026-07-16 18:39:03 +09:00
|
|
|
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);
|
2026-07-18 02:08:14 +09:00
|
|
|
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}")))?,
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-07-16 18:39:03 +09:00
|
|
|
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,
|
2026-07-17 03:58:27 +09:00
|
|
|
staging,
|
2026-07-18 02:08:14 +09:00
|
|
|
git_routing,
|
2026-07-16 18:39:03 +09:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-18 22:35:48 +09:00
|
|
|
/// Runs cold start and exposes the exact retained lifecycle to one service loop.
|
2026-07-16 18:39:03 +09:00
|
|
|
///
|
|
|
|
|
/// # 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,
|
2026-07-18 22:35:48 +09:00
|
|
|
) -> Result<(), ColdStartError>
|
2026-07-16 18:39:03 +09:00
|
|
|
where
|
|
|
|
|
Service: FnOnce(
|
2026-07-18 22:35:48 +09:00
|
|
|
&Mutex<CoordinatorLifecycle<'_, '_>>,
|
2026-07-16 16:28:18 +09:00
|
|
|
&RefCell<BootstrapWaiterFace>,
|
|
|
|
|
&BootstrapOwnerGuard,
|
|
|
|
|
lazaret_core::identity::SessionId,
|
2026-07-18 22:35:48 +09:00
|
|
|
) -> Result<(), ColdStartError>,
|
2026-07-16 18:39:03 +09:00
|
|
|
{
|
2026-07-16 16:28:18 +09:00
|
|
|
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)?,
|
|
|
|
|
);
|
2026-07-16 18:39:03 +09:00
|
|
|
let store = Arc::clone(&self.store);
|
|
|
|
|
let generation = self.generation.clone();
|
|
|
|
|
let plan = self.take_plan()?;
|
2026-07-18 22:35:48 +09:00
|
|
|
let mut lifecycle = coordinator_lifecycle(&store, plan).map_err(failure)?;
|
2026-07-16 16:28:18 +09:00
|
|
|
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)
|
2026-07-18 22:35:48 +09:00
|
|
|
.map_err(crate::bootstrap::LifecycleServiceError::service)
|
2026-07-16 16:28:18 +09:00
|
|
|
},
|
|
|
|
|
|lifecycle| {
|
|
|
|
|
let mut waiters = waiters.borrow_mut();
|
|
|
|
|
waiters
|
|
|
|
|
.admit_pending(lifecycle)
|
2026-07-18 22:35:48 +09:00
|
|
|
.map_err(crate::bootstrap::LifecycleServiceError::service)?;
|
2026-07-16 16:28:18 +09:00
|
|
|
waiters
|
|
|
|
|
.withdraw()
|
2026-07-18 22:35:48 +09:00
|
|
|
.map_err(crate::bootstrap::LifecycleServiceError::service)
|
2026-07-16 16:28:18 +09:00
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
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)?;
|
|
|
|
|
}
|
2026-07-18 22:35:48 +09:00
|
|
|
return Err(ColdStartError(
|
2026-07-16 16:28:18 +09:00
|
|
|
"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));
|
|
|
|
|
}
|
2026-07-16 18:39:03 +09:00
|
|
|
}
|
2026-07-16 16:28:18 +09:00
|
|
|
service(
|
|
|
|
|
&Mutex::new(lifecycle),
|
|
|
|
|
&waiters,
|
|
|
|
|
owner_guard,
|
|
|
|
|
bootstrap_owner_session_id,
|
|
|
|
|
)
|
2026-07-16 18:39:03 +09:00
|
|
|
}
|
|
|
|
|
|
2026-07-18 22:35:48 +09:00
|
|
|
fn take_plan(&mut self) -> Result<BootstrapPlan<'_>, ColdStartError> {
|
2026-07-17 07:20:21 +09:00
|
|
|
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();
|
2026-07-18 22:35:48 +09:00
|
|
|
Ok(BootstrapPlan {
|
2026-07-16 18:39:03 +09:00
|
|
|
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,
|
2026-07-18 22:35:48 +09:00
|
|
|
runtime_launch: self
|
|
|
|
|
.runtime_launch
|
|
|
|
|
.take()
|
|
|
|
|
.ok_or_else(|| ColdStartError("cold start was already consumed".into()))?,
|
2026-07-16 18:39:03 +09:00
|
|
|
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,
|
2026-07-16 16:28:18 +09:00
|
|
|
non_interactive_executor: Arc::new(
|
|
|
|
|
RunscNonInteractiveExecutor::from_run_command(
|
|
|
|
|
self.sandbox_bundle.command(),
|
|
|
|
|
self.container_id,
|
|
|
|
|
)
|
|
|
|
|
.map_err(failure)?,
|
|
|
|
|
),
|
2026-07-17 07:20:21 +09:00
|
|
|
interactive_executor: Arc::new(interactive_executor),
|
|
|
|
|
prompt_transport,
|
2026-07-17 03:58:27 +09:00
|
|
|
runtime_staging: &self.staging,
|
2026-07-18 02:08:14 +09:00
|
|
|
git_routing: self.git_routing.as_ref().map(|routing| {
|
2026-07-18 22:35:48 +09:00
|
|
|
crate::bootstrap::BootstrapGitRouting {
|
2026-07-18 02:08:14 +09:00
|
|
|
mountpoint: routing.mountpoint(),
|
|
|
|
|
host_root: routing.host_root(),
|
|
|
|
|
isolated_root: routing.isolated_root(),
|
|
|
|
|
}
|
|
|
|
|
}),
|
2026-07-16 18:39:03 +09:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-18 22:35:48 +09:00
|
|
|
fn require_inputs(inputs: &ColdStartInputs<'_>) -> Result<(), ColdStartError> {
|
2026-07-16 18:39:03 +09:00
|
|
|
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()
|
2026-07-16 18:53:20 +09:00
|
|
|
|| inputs.invoking_uid != rustix::process::getuid().as_raw()
|
2026-07-16 18:39:03 +09:00
|
|
|
{
|
2026-07-18 22:35:48 +09:00
|
|
|
return Err(ColdStartError("cold-start inputs disagree".into()));
|
2026-07-16 18:39:03 +09:00
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-18 02:08:14 +09:00
|
|
|
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<PathBuf>,
|
|
|
|
|
isolate_ignored: bool,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl GitRoutingLayout {
|
|
|
|
|
fn create(
|
|
|
|
|
configuration: &ResolvedDomainConfiguration,
|
|
|
|
|
container_id: ContainerId,
|
|
|
|
|
invoking_uid: u32,
|
2026-07-18 22:35:48 +09:00
|
|
|
) -> Result<Option<Self>, ColdStartError> {
|
2026-07-18 02:08:14 +09:00
|
|
|
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,
|
2026-07-18 22:35:48 +09:00
|
|
|
) -> Result<RuntimeGitRoutingLaunch, ColdStartError> {
|
2026-07-18 02:08:14 +09:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-18 22:35:48 +09:00
|
|
|
fn open_absolute_directory(path: &Path) -> Result<File, ColdStartError> {
|
2026-07-18 02:08:14 +09:00
|
|
|
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))
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-18 22:35:48 +09:00
|
|
|
fn global_excludes_path(workspace: &Path) -> Result<Option<PathBuf>, ColdStartError> {
|
2026-07-18 02:08:14 +09:00
|
|
|
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],
|
2026-07-18 22:35:48 +09:00
|
|
|
) -> Result<Option<PathBuf>, ColdStartError> {
|
2026-07-18 02:08:14 +09:00
|
|
|
let value = output.strip_suffix(b"\n").unwrap_or(output);
|
|
|
|
|
if value.contains(&0) {
|
2026-07-18 22:35:48 +09:00
|
|
|
return Err(ColdStartError("Git global-excludes path is invalid".into()));
|
2026-07-18 02:08:14 +09:00
|
|
|
}
|
|
|
|
|
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<PathBuf> {
|
|
|
|
|
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"))
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-17 03:58:27 +09:00
|
|
|
pub(crate) struct RuntimeStaging {
|
2026-07-17 06:48:49 +09:00
|
|
|
runsc_root: OwnedFd,
|
2026-07-16 18:53:20 +09:00
|
|
|
bundles: OwnedFd,
|
|
|
|
|
bundle: OwnedFd,
|
|
|
|
|
controls: OwnedFd,
|
|
|
|
|
control: OwnedFd,
|
2026-07-17 03:58:27 +09:00
|
|
|
overlays: OwnedFd,
|
|
|
|
|
overlay: OwnedFd,
|
2026-07-16 18:53:20 +09:00
|
|
|
name: OsString,
|
|
|
|
|
bundle_identity: FileIdentity,
|
|
|
|
|
control_identity: FileIdentity,
|
2026-07-17 03:58:27 +09:00
|
|
|
overlay_identity: FileIdentity,
|
2026-07-16 18:39:03 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl RuntimeStaging {
|
2026-07-18 22:35:48 +09:00
|
|
|
pub(crate) fn runsc_root_identity(&self) -> Result<MountIdentity, ColdStartError> {
|
2026-07-17 06:48:49 +09:00
|
|
|
mount_identity(&self.runsc_root).map_err(failure)
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-18 22:35:48 +09:00
|
|
|
pub(crate) fn cleanup_overlay(&self) -> Result<(), ColdStartError> {
|
2026-07-17 03:58:27 +09:00
|
|
|
let retained = fstat(&self.overlay).map_err(failure)?;
|
|
|
|
|
if !self.overlay_identity.matches(&retained) {
|
2026-07-18 22:35:48 +09:00
|
|
|
return Err(ColdStartError(
|
2026-07-17 03:58:27 +09:00
|
|
|
"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()
|
|
|
|
|
{
|
2026-07-18 22:35:48 +09:00
|
|
|
return Err(ColdStartError(
|
2026-07-17 03:58:27 +09:00
|
|
|
"runsc overlay path changed before cleanup".into(),
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
unlinkat(&self.overlays, &self.name, AtFlags::REMOVEDIR).map_err(failure)?;
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-18 04:03:39 +09:00
|
|
|
pub(crate) fn escalation_mailboxes(
|
|
|
|
|
&self,
|
|
|
|
|
invoking_uid: u32,
|
2026-07-18 22:35:48 +09:00
|
|
|
) -> Result<(RegularFileMailbox, RegularFileMailbox, RegularFileMailbox), ColdStartError> {
|
2026-07-18 04:03:39 +09:00
|
|
|
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))
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-16 18:39:03 +09:00
|
|
|
fn create(
|
2026-07-16 18:53:20 +09:00
|
|
|
runtime_domain: &ConfinedPathPlan,
|
2026-07-16 18:39:03 +09:00
|
|
|
container_id: ContainerId,
|
|
|
|
|
escalation_client: &File,
|
2026-07-16 20:41:40 +09:00
|
|
|
sandbox_init: &File,
|
2026-07-16 18:53:20 +09:00
|
|
|
invoking_uid: u32,
|
2026-07-18 22:35:48 +09:00
|
|
|
) -> Result<Self, ColdStartError> {
|
2026-07-16 18:53:20 +09:00
|
|
|
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)?;
|
2026-07-17 03:58:27 +09:00
|
|
|
let overlays =
|
|
|
|
|
open_or_create_private_directory(&root, "runsc-overlay", invoking_uid, false)?;
|
2026-07-17 06:48:49 +09:00
|
|
|
let runsc_root = open_or_create_private_directory(&root, "runsc", invoking_uid, false)?;
|
2026-07-16 18:53:20 +09:00
|
|
|
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)?;
|
2026-07-17 03:58:27 +09:00
|
|
|
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)?;
|
2026-07-16 18:53:20 +09:00
|
|
|
let staging = Self {
|
2026-07-17 06:48:49 +09:00
|
|
|
runsc_root,
|
2026-07-16 18:53:20 +09:00
|
|
|
bundles,
|
|
|
|
|
bundle,
|
|
|
|
|
controls,
|
|
|
|
|
control,
|
2026-07-17 03:58:27 +09:00
|
|
|
overlays,
|
|
|
|
|
overlay,
|
2026-07-16 18:53:20 +09:00
|
|
|
name,
|
|
|
|
|
bundle_identity,
|
|
|
|
|
control_identity,
|
2026-07-17 03:58:27 +09:00
|
|
|
overlay_identity,
|
2026-07-16 18:53:20 +09:00
|
|
|
};
|
2026-07-16 20:41:40 +09:00
|
|
|
staging.populate_control(escalation_client, sandbox_init)?;
|
2026-07-16 18:39:03 +09:00
|
|
|
Ok(staging)
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-16 20:41:40 +09:00
|
|
|
fn populate_control(
|
|
|
|
|
&self,
|
|
|
|
|
escalation_client: &File,
|
|
|
|
|
sandbox_init: &File,
|
2026-07-18 22:35:48 +09:00
|
|
|
) -> Result<(), ColdStartError> {
|
2026-07-16 18:53:20 +09:00
|
|
|
let invoking_uid = fstat(&self.control).map_err(failure)?.st_uid;
|
|
|
|
|
drop(open_or_create_private_directory(
|
|
|
|
|
&self.control,
|
2026-07-16 18:39:03 +09:00
|
|
|
"credentials",
|
2026-07-16 18:53:20 +09:00
|
|
|
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)?;
|
2026-07-16 20:41:40 +09:00
|
|
|
require_executable_regular_file(escalation_client, "escalation client")?;
|
2026-07-16 18:53:20 +09:00
|
|
|
copy_descriptor(escalation_client, &bin, "lazaret-escalate", invoking_uid)?;
|
2026-07-16 20:41:40 +09:00
|
|
|
require_executable_regular_file(sandbox_init, "sandbox init")?;
|
|
|
|
|
copy_descriptor(sandbox_init, &bin, "lazaret-sandbox-init", invoking_uid)?;
|
2026-07-16 18:39:03 +09:00
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-18 22:35:48 +09:00
|
|
|
fn write_bundle(&self, bytes: &[u8]) -> Result<(), ColdStartError> {
|
2026-07-16 18:53:20 +09:00
|
|
|
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);
|
2026-07-16 18:39:03 +09:00
|
|
|
file.write_all(bytes).map_err(failure)?;
|
2026-07-16 18:53:20 +09:00
|
|
|
fchmod(file.as_fd(), Mode::RUSR | Mode::WUSR).map_err(failure)?;
|
2026-07-16 18:39:03 +09:00
|
|
|
file.sync_all().map_err(failure)?;
|
2026-07-16 18:53:20 +09:00
|
|
|
require_private_file(&file, fstat(&self.bundle).map_err(failure)?.st_uid, 0o600)?;
|
2026-07-16 18:39:03 +09:00
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-16 18:53:20 +09:00
|
|
|
fn copy_descriptor(
|
|
|
|
|
source: &File,
|
|
|
|
|
parent: &OwnedFd,
|
|
|
|
|
name: &str,
|
|
|
|
|
invoking_uid: u32,
|
2026-07-18 22:35:48 +09:00
|
|
|
) -> Result<(), ColdStartError> {
|
2026-07-16 18:53:20 +09:00
|
|
|
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)?;
|
2026-07-16 18:39:03 +09:00
|
|
|
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)
|
2026-07-18 22:35:48 +09:00
|
|
|
.ok_or_else(|| ColdStartError("escalation client is too large".into()))?;
|
2026-07-16 18:39:03 +09:00
|
|
|
}
|
2026-07-16 18:53:20 +09:00
|
|
|
destination.sync_all().map_err(failure)?;
|
|
|
|
|
require_private_file(&destination, invoking_uid, 0o700)
|
2026-07-16 18:39:03 +09:00
|
|
|
}
|
|
|
|
|
|
2026-07-18 22:35:48 +09:00
|
|
|
fn require_executable_regular_file(source: &File, label: &str) -> Result<(), ColdStartError> {
|
2026-07-16 20:41:40 +09:00
|
|
|
let metadata = source
|
|
|
|
|
.metadata()
|
|
|
|
|
.map_err(|error| failure(format!("inspecting {label}: {error}")))?;
|
|
|
|
|
if !metadata.file_type().is_file() || metadata.permissions().mode() & 0o111 == 0 {
|
2026-07-18 22:35:48 +09:00
|
|
|
return Err(ColdStartError(format!(
|
2026-07-16 20:41:40 +09:00
|
|
|
"{label} is not an executable regular file"
|
|
|
|
|
)));
|
|
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-16 18:53:20 +09:00
|
|
|
#[derive(Clone, Copy)]
|
|
|
|
|
struct FileIdentity {
|
|
|
|
|
device: u64,
|
|
|
|
|
inode: u64,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl FileIdentity {
|
2026-07-18 22:35:48 +09:00
|
|
|
fn capture(descriptor: impl AsFd) -> Result<Self, ColdStartError> {
|
2026-07-16 18:53:20 +09:00
|
|
|
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
|
|
|
|
|
}
|
2026-07-16 18:39:03 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Drop for RuntimeStaging {
|
|
|
|
|
fn drop(&mut self) {
|
2026-07-17 03:58:27 +09:00
|
|
|
remove_staging_directory(
|
|
|
|
|
&self.overlays,
|
|
|
|
|
&self.name,
|
|
|
|
|
&self.overlay,
|
|
|
|
|
self.overlay_identity,
|
|
|
|
|
);
|
2026-07-16 18:53:20 +09:00
|
|
|
remove_staging_directory(
|
|
|
|
|
&self.controls,
|
|
|
|
|
&self.name,
|
|
|
|
|
&self.control,
|
|
|
|
|
self.control_identity,
|
|
|
|
|
);
|
|
|
|
|
remove_staging_directory(
|
|
|
|
|
&self.bundles,
|
|
|
|
|
&self.name,
|
|
|
|
|
&self.bundle,
|
|
|
|
|
self.bundle_identity,
|
|
|
|
|
);
|
2026-07-16 18:39:03 +09:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-16 18:53:20 +09:00
|
|
|
fn open_or_create_private_directory(
|
|
|
|
|
parent: impl AsFd,
|
|
|
|
|
name: impl AsRef<std::ffi::OsStr>,
|
|
|
|
|
invoking_uid: u32,
|
|
|
|
|
require_new: bool,
|
2026-07-18 22:35:48 +09:00
|
|
|
) -> Result<OwnedFd, ColdStartError> {
|
2026-07-16 18:53:20 +09:00
|
|
|
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,
|
2026-07-18 22:35:48 +09:00
|
|
|
) -> Result<(), ColdStartError> {
|
2026-07-16 18:53:20 +09:00
|
|
|
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
|
|
|
|
|
{
|
2026-07-18 22:35:48 +09:00
|
|
|
return Err(ColdStartError(
|
2026-07-16 18:53:20 +09:00
|
|
|
"runtime staging directory is not invoking-user-owned mode 0700".into(),
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn require_private_file(
|
|
|
|
|
descriptor: impl AsFd,
|
|
|
|
|
invoking_uid: u32,
|
|
|
|
|
mode: u32,
|
2026-07-18 22:35:48 +09:00
|
|
|
) -> Result<(), ColdStartError> {
|
2026-07-16 18:53:20 +09:00
|
|
|
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
|
|
|
|
|
{
|
2026-07-18 22:35:48 +09:00
|
|
|
return Err(ColdStartError(
|
2026-07-16 18:53:20 +09:00
|
|
|
"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()));
|
2026-07-16 18:39:03 +09:00
|
|
|
}
|
|
|
|
|
}
|
2026-07-16 18:53:20 +09:00
|
|
|
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())?;
|
|
|
|
|
}
|
2026-07-16 18:39:03 +09:00
|
|
|
}
|
2026-07-16 18:53:20 +09:00
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn confined_resolve_flags() -> ResolveFlags {
|
|
|
|
|
ResolveFlags::BENEATH
|
|
|
|
|
| ResolveFlags::NO_MAGICLINKS
|
|
|
|
|
| ResolveFlags::NO_SYMLINKS
|
|
|
|
|
| ResolveFlags::NO_XDEV
|
2026-07-16 18:39:03 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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());
|
|
|
|
|
}
|
2026-07-18 02:08:14 +09:00
|
|
|
|
|
|
|
|
#[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());
|
|
|
|
|
}
|
2026-07-16 18:39:03 +09:00
|
|
|
}
|