Compose selected State Anchor paths over prepared roots

This commit is contained in:
ms2k 2026-07-17 03:58:27 +09:00
commit 4dd2b150d2
24 changed files with 1575 additions and 163 deletions

View file

@ -1,4 +1,12 @@
fn main() { fn main() {
let arguments = std::env::args_os().skip(1).collect::<Vec<_>>();
if let Some(result) = lazaret_linux::process::dispatch_private_process_role(&arguments) {
if let Err(error) = result {
eprintln!("private process role failed: {error}");
std::process::exit(1);
}
return;
}
let current_workspace = match std::env::current_dir() { let current_workspace = match std::env::current_dir() {
Ok(path) => path, Ok(path) => path,
Err(error) => { Err(error) => {
@ -6,8 +14,7 @@ fn main() {
std::process::exit(1); std::process::exit(1);
} }
}; };
let command = let command = match lazaret_client::parse_command_line(arguments, current_workspace) {
match lazaret_client::parse_command_line(std::env::args_os().skip(1), current_workspace) {
Ok(command) => command, Ok(command) => command,
Err(error) => { Err(error) => {
eprintln!("invalid command line: {error}"); eprintln!("invalid command line: {error}");

View file

@ -988,6 +988,8 @@ pub struct StateAnchorOwnership {
pub work_path: Option<HostPath>, pub work_path: Option<HostPath>,
pub merged_path: Option<HostPath>, pub merged_path: Option<HostPath>,
pub daemon: PersistedHostProcessOwnership, pub daemon: PersistedHostProcessOwnership,
pub user_namespace: Option<NamespaceIdentity>,
pub mount_namespace: Option<NamespaceIdentity>,
pub mount: Option<MountIdentity>, pub mount: Option<MountIdentity>,
} }
@ -1102,6 +1104,8 @@ impl StateAnchorOwnership {
work_path: None, work_path: None,
merged_path: None, merged_path: None,
daemon: PersistedHostProcessOwnership::unassigned(), daemon: PersistedHostProcessOwnership::unassigned(),
user_namespace: None,
mount_namespace: None,
mount: None, mount: None,
} }
} }
@ -1112,6 +1116,8 @@ impl StateAnchorOwnership {
&& self.work_path.is_none() && self.work_path.is_none()
&& self.merged_path.is_none() && self.merged_path.is_none()
&& self.daemon.is_unassigned() && self.daemon.is_unassigned()
&& self.user_namespace.is_none()
&& self.mount_namespace.is_none()
&& self.mount.is_none() && self.mount.is_none()
} }
@ -1121,6 +1127,8 @@ impl StateAnchorOwnership {
&& self.work_path.is_some() && self.work_path.is_some()
&& self.merged_path.is_some() && self.merged_path.is_some()
&& self.daemon.is_current_committed() && self.daemon.is_current_committed()
&& self.user_namespace.is_some()
&& self.mount_namespace.is_some()
&& self.mount.is_some() && self.mount.is_some()
} }
@ -1133,6 +1141,8 @@ impl StateAnchorOwnership {
validate_option_progress(self.work_path.as_ref(), next.work_path.as_ref())?; validate_option_progress(self.work_path.as_ref(), next.work_path.as_ref())?;
validate_option_progress(self.merged_path.as_ref(), next.merged_path.as_ref())?; validate_option_progress(self.merged_path.as_ref(), next.merged_path.as_ref())?;
validate_process_progress(&self.daemon, &next.daemon)?; validate_process_progress(&self.daemon, &next.daemon)?;
validate_option_progress(self.user_namespace.as_ref(), next.user_namespace.as_ref())?;
validate_option_progress(self.mount_namespace.as_ref(), next.mount_namespace.as_ref())?;
validate_option_progress(self.mount.as_ref(), next.mount.as_ref()) validate_option_progress(self.mount.as_ref(), next.mount.as_ref())
} }
} }

View file

@ -396,6 +396,8 @@ fn state_anchor_ownership(
work_path: Some(HostPath::new(b"work".to_vec())), work_path: Some(HostPath::new(b"work".to_vec())),
merged_path: Some(HostPath::new(b"merged".to_vec())), merged_path: Some(HostPath::new(b"merged".to_vec())),
daemon, daemon,
user_namespace: Some(namespace()),
mount_namespace: Some(namespace()),
mount: mount_identity, mount: mount_identity,
} }
} }

View file

@ -334,6 +334,8 @@ fn every_schema_shape_round_trips_and_rejects_malformed_or_unknown_versions() {
work_path: Some(HostPath::new(b"work".to_vec())), work_path: Some(HostPath::new(b"work".to_vec())),
merged_path: Some(HostPath::new(b"merged".to_vec())), merged_path: Some(HostPath::new(b"merged".to_vec())),
daemon: owned_process(63), daemon: owned_process(63),
user_namespace: Some(namespace()),
mount_namespace: Some(namespace()),
mount: Some(mount()), mount: Some(mount()),
}, },
&mut seen, &mut seen,

View file

@ -67,6 +67,7 @@ pub struct ProductionBootstrapPlan<'a> {
pub bundle_digest: BundleDigest, pub bundle_digest: BundleDigest,
pub invoking_uid: u32, pub invoking_uid: u32,
pub non_interactive_executor: Arc<dyn NonInteractiveSessionExecutor>, pub non_interactive_executor: Arc<dyn NonInteractiveSessionExecutor>,
pub(crate) runtime_staging: &'a crate::production::RuntimeStaging,
} }
pub struct ProductionLifecycleAdapter<'a> { pub struct ProductionLifecycleAdapter<'a> {
@ -268,7 +269,7 @@ impl<'a> ProductionLifecycleAdapter<'a> {
&coordinator, &coordinator,
self.plan.prepared_artifact.prepared_artifact_id, self.plan.prepared_artifact.prepared_artifact_id,
) )
.map_err(failure)?; .map_err(|error| failure(format!("mounting State Anchor: {error}")))?;
self.state_anchor_mount = Some(mount); self.state_anchor_mount = Some(mount);
Ok(()) Ok(())
} }
@ -281,7 +282,7 @@ impl<'a> ProductionLifecycleAdapter<'a> {
.ok_or_else(|| ProductionLifecycleError("Runtime Supervisor is absent".into()))? .ok_or_else(|| ProductionLifecycleError("Runtime Supervisor is absent".into()))?
.receive_helper_staged(&network) .receive_helper_staged(&network)
.map(|_| ()) .map(|_| ())
.map_err(failure) .map_err(|error| failure(format!("starting connectivity helper: {error}")))
} }
fn start_sidecar(&mut self) -> Result<(), ProductionLifecycleError> { fn start_sidecar(&mut self) -> Result<(), ProductionLifecycleError> {
@ -289,7 +290,7 @@ impl<'a> ProductionLifecycleAdapter<'a> {
.as_mut() .as_mut()
.ok_or_else(|| ProductionLifecycleError("Runtime Supervisor is absent".into()))? .ok_or_else(|| ProductionLifecycleError("Runtime Supervisor is absent".into()))?
.start_sidecar() .start_sidecar()
.map_err(failure) .map_err(|error| failure(format!("starting sidecar: {error}")))
} }
fn mark_network_ready(&mut self) -> Result<(), ProductionLifecycleError> { fn mark_network_ready(&mut self) -> Result<(), ProductionLifecycleError> {
@ -309,7 +310,7 @@ impl<'a> ProductionLifecycleAdapter<'a> {
self.plan.reachability, self.plan.reachability,
) )
.map(|_| ()) .map(|_| ())
.map_err(failure) .map_err(|error| failure(format!("marking network ready: {error}")))
} }
fn start_sandbox(&mut self) -> Result<(), ProductionLifecycleError> { fn start_sandbox(&mut self) -> Result<(), ProductionLifecycleError> {
@ -319,17 +320,19 @@ impl<'a> ProductionLifecycleAdapter<'a> {
.awaiting_runtime .awaiting_runtime
.take() .take()
.ok_or_else(|| ProductionLifecycleError("Runtime Supervisor is absent".into()))?; .ok_or_else(|| ProductionLifecycleError("Runtime Supervisor is absent".into()))?;
let staged = pending.receive_staged(&sandbox).map_err(failure)?; let staged = pending
.receive_staged(&sandbox)
.map_err(|error| failure(format!("receiving attached runsc staging: {error}")))?;
let handoff = self let handoff = self
.plan .plan
.sandbox_bundle .sandbox_bundle
.capture_handoff_authority(self.plan.configuration) .capture_handoff_authority(self.plan.configuration)
.map_err(failure)?; .map_err(|error| failure(format!("capturing sandbox handoff: {error}")))?;
let (runtime, _) = pending let (runtime, _) = pending
.acknowledge(staged, &handoff) .acknowledge(staged, &handoff)
.map_err(failure)? .map_err(|error| failure(format!("releasing sandbox handoff: {error}")))?
.commit_networked(&sandbox) .commit_networked(&sandbox)
.map_err(failure)?; .map_err(|error| failure(format!("committing sandbox readiness: {error}")))?;
self.runtime = Some(runtime); self.runtime = Some(runtime);
Ok(()) Ok(())
} }
@ -376,7 +379,8 @@ impl<'a> ProductionLifecycleAdapter<'a> {
.as_mut() .as_mut()
.ok_or_else(|| ProductionLifecycleError("committed runtime is absent".into()))? .ok_or_else(|| ProductionLifecycleError("committed runtime is absent".into()))?
.stop_sandbox(&sandbox) .stop_sandbox(&sandbox)
.map_err(failure) .map_err(failure)?;
self.plan.runtime_staging.cleanup_overlay().map_err(failure)
} }
fn stop_sidecar(&mut self) -> Result<(), ProductionLifecycleError> { fn stop_sidecar(&mut self) -> Result<(), ProductionLifecycleError> {
@ -408,7 +412,14 @@ impl<'a> ProductionLifecycleAdapter<'a> {
self.runtime self.runtime
.as_mut() .as_mut()
.ok_or_else(|| ProductionLifecycleError("committed runtime is absent".into()))? .ok_or_else(|| ProductionLifecycleError("committed runtime is absent".into()))?
.unmount_state_anchor() .unmount_state_anchor(
self.state_anchor_release
.as_ref()
.ok_or_else(|| {
ProductionLifecycleError("State Anchor release is absent".into())
})?
.mount(),
)
.map_err(failure)?; .map_err(failure)?;
self.state_anchor_mount = None; self.state_anchor_mount = None;
Ok(()) Ok(())

View file

@ -68,6 +68,8 @@ pub struct ProductionColdStartInputs<'a> {
pub configuration: &'a ResolvedDomainConfiguration, pub configuration: &'a ResolvedDomainConfiguration,
pub generation: Generation, pub generation: Generation,
pub prepared_artifact: &'a PreparedArtifact, pub prepared_artifact: &'a PreparedArtifact,
pub prepared_artifact_authority: &'a File,
pub prepared_artifact_label: &'a Path,
pub root_process: RootProcess, pub root_process: RootProcess,
pub resolver_addresses: Vec<IpAddress>, pub resolver_addresses: Vec<IpAddress>,
pub network_probe: NetworkProbeRequest, pub network_probe: NetworkProbeRequest,
@ -137,7 +139,7 @@ pub struct ProductionColdStart<'a> {
runtime_root: PathBuf, runtime_root: PathBuf,
bundle_digest: BundleDigest, bundle_digest: BundleDigest,
invoking_uid: u32, invoking_uid: u32,
_staging: RuntimeStaging, staging: RuntimeStaging,
} }
impl<'a> ProductionColdStart<'a> { impl<'a> ProductionColdStart<'a> {
@ -173,6 +175,7 @@ impl<'a> ProductionColdStart<'a> {
expected_network_namespace, expected_network_namespace,
root_process: inputs.root_process, root_process: inputs.root_process,
terminal: None, terminal: None,
prepared_root: inputs.prepared_artifact_label.to_owned(),
}) })
.map_err(|error| failure(format!("sandbox bundle: {error}")))?; .map_err(|error| failure(format!("sandbox bundle: {error}")))?;
staging staging
@ -186,8 +189,12 @@ impl<'a> ProductionColdStart<'a> {
coordinator_id: inputs.generation.coordinator_id, coordinator_id: inputs.generation.coordinator_id,
slot: HostProcessOwnershipSlot::SandboxRuntimeSupervisor, slot: HostProcessOwnershipSlot::SandboxRuntimeSupervisor,
}; };
let state_anchor = let state_anchor = StateAnchorLaunchAuthority::capture_retained(
StateAnchorLaunchAuthority::capture(&state_anchor_layout, inputs.prepared_artifact) &state_anchor_layout,
inputs.prepared_artifact,
inputs.prepared_artifact_label,
inputs.prepared_artifact_authority,
)
.map_err(|error| failure(format!("State Anchor authority: {error}")))?; .map_err(|error| failure(format!("State Anchor authority: {error}")))?;
let runtime_launch = RuntimeSupervisorLaunch::from_networked_runsc_command( let runtime_launch = RuntimeSupervisorLaunch::from_networked_runsc_command(
ownership, ownership,
@ -227,7 +234,7 @@ impl<'a> ProductionColdStart<'a> {
runtime_root: runtime_domain.join("runsc"), runtime_root: runtime_domain.join("runsc"),
bundle_digest, bundle_digest,
invoking_uid: inputs.invoking_uid, invoking_uid: inputs.invoking_uid,
_staging: staging, staging,
}) })
} }
@ -342,6 +349,7 @@ impl<'a> ProductionColdStart<'a> {
) )
.map_err(failure)?, .map_err(failure)?,
), ),
runtime_staging: &self.staging,
}) })
} }
} }
@ -362,17 +370,41 @@ fn require_inputs(inputs: &ProductionColdStartInputs<'_>) -> Result<(), Producti
Ok(()) Ok(())
} }
struct RuntimeStaging { pub(crate) struct RuntimeStaging {
bundles: OwnedFd, bundles: OwnedFd,
bundle: OwnedFd, bundle: OwnedFd,
controls: OwnedFd, controls: OwnedFd,
control: OwnedFd, control: OwnedFd,
overlays: OwnedFd,
overlay: OwnedFd,
name: OsString, name: OsString,
bundle_identity: FileIdentity, bundle_identity: FileIdentity,
control_identity: FileIdentity, control_identity: FileIdentity,
overlay_identity: FileIdentity,
} }
impl RuntimeStaging { impl RuntimeStaging {
pub(crate) fn cleanup_overlay(&self) -> Result<(), ProductionColdStartError> {
let retained = fstat(&self.overlay).map_err(failure)?;
if !self.overlay_identity.matches(&retained) {
return Err(ProductionColdStartError(
"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(&current)
|| !FileType::from_raw_mode(current.st_mode).is_dir()
{
return Err(ProductionColdStartError(
"runsc overlay path changed before cleanup".into(),
));
}
unlinkat(&self.overlays, &self.name, AtFlags::REMOVEDIR).map_err(failure)?;
Ok(())
}
fn create( fn create(
runtime_domain: &ConfinedPathPlan, runtime_domain: &ConfinedPathPlan,
container_id: ContainerId, container_id: ContainerId,
@ -384,6 +416,8 @@ impl RuntimeStaging {
let bundles = open_or_create_private_directory(&root, "bundles", invoking_uid, false)?; let bundles = open_or_create_private_directory(&root, "bundles", invoking_uid, false)?;
let controls = let controls =
open_or_create_private_directory(&root, "sandbox-control", invoking_uid, false)?; open_or_create_private_directory(&root, "sandbox-control", invoking_uid, false)?;
let overlays =
open_or_create_private_directory(&root, "runsc-overlay", invoking_uid, false)?;
drop(open_or_create_private_directory( drop(open_or_create_private_directory(
&root, &root,
"runsc", "runsc",
@ -401,14 +435,26 @@ impl RuntimeStaging {
} }
}; };
let control_identity = FileIdentity::capture(&control)?; 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 { let staging = Self {
bundles, bundles,
bundle, bundle,
controls, controls,
control, control,
overlays,
overlay,
name, name,
bundle_identity, bundle_identity,
control_identity, control_identity,
overlay_identity,
}; };
staging.populate_control(escalation_client, sandbox_init)?; staging.populate_control(escalation_client, sandbox_init)?;
Ok(staging) Ok(staging)
@ -535,6 +581,12 @@ impl FileIdentity {
impl Drop for RuntimeStaging { impl Drop for RuntimeStaging {
fn drop(&mut self) { fn drop(&mut self) {
remove_staging_directory(
&self.overlays,
&self.name,
&self.overlay,
self.overlay_identity,
);
remove_staging_directory( remove_staging_directory(
&self.controls, &self.controls,
&self.name, &self.name,

View file

@ -1,8 +1,9 @@
use std::collections::BTreeSet; use std::collections::BTreeSet;
use std::fmt; use std::fmt;
use std::fs::File; use std::fs::File;
use std::os::unix::ffi::OsStringExt;
use std::os::unix::fs::PermissionsExt; use std::os::unix::fs::PermissionsExt;
use std::path::Path; use std::path::{Path, PathBuf};
use std::sync::Arc; use std::sync::Arc;
use lazaret_core::configuration::ResolvedDomainConfiguration; use lazaret_core::configuration::ResolvedDomainConfiguration;
@ -11,14 +12,19 @@ use lazaret_core::identity::{
PrepareOperationId, SandboxId, PrepareOperationId, SandboxId,
}; };
use lazaret_core::network::{DnsQueryExpectation, ReachabilityExpectation}; use lazaret_core::network::{DnsQueryExpectation, ReachabilityExpectation};
use lazaret_core::records::{DurableDecision, Generation, IpAddress, PreparedArtifact}; use lazaret_core::records::{DurableDecision, Generation, IpAddress, PreparedArtifact, VolumeKind};
use lazaret_linux::filesystem::namespace_identity; use lazaret_linux::filesystem::namespace_identity;
use lazaret_linux::platform::{PlatformObservation, check_platform}; use lazaret_linux::platform::{PlatformObservation, check_platform};
use lazaret_prepared_lower::{ use lazaret_prepared_lower::{
IdMap, IdMapRange, OciRegistryTransport, Platform, PreparedLowerStore, RegistryCredentials, IdMap, IdMapRange, OciRegistryTransport, Platform, PreparedDestination,
PreparedDestinationKind, PreparedLowerStore, RegistryCredentials,
}; };
use lazaret_runtime_supervisor::network::{DnsProbeTarget, NetworkProbeRequest, TcpProbeTarget}; use lazaret_runtime_supervisor::network::{DnsProbeTarget, NetworkProbeRequest, TcpProbeTarget};
use lazaret_runtime_supervisor::sandbox::{RootProcess, SANDBOX_INIT_DESTINATION}; use lazaret_runtime_supervisor::sandbox::{
CREDENTIALS_DESTINATION, ESCALATION_CLIENT_DESTINATION, MAILBOX_REQUESTS_DESTINATION,
MAILBOX_RESULTS_DESTINATION, RootProcess, SANDBOX_INIT_DESTINATION, TERMINFO_ROOT_DESTINATION,
WORKSPACE_DESTINATION,
};
use lazaret_storage_catalog::{Catalog, CatalogError}; use lazaret_storage_catalog::{Catalog, CatalogError};
use rustix::fs::{Mode, OFlags, ResolveFlags, openat2}; use rustix::fs::{Mode, OFlags, ResolveFlags, openat2};
@ -54,6 +60,8 @@ pub struct ProductionSelection {
configuration_fingerprint: ConfigurationFingerprint, configuration_fingerprint: ConfigurationFingerprint,
catalog: Catalog, catalog: Catalog,
artifact: PreparedArtifact, artifact: PreparedArtifact,
artifact_authority: File,
artifact_label: PathBuf,
generation: Generation, generation: Generation,
resolver_addresses: Vec<IpAddress>, resolver_addresses: Vec<IpAddress>,
network_probe: NetworkProbeRequest, network_probe: NetworkProbeRequest,
@ -83,6 +91,8 @@ impl ProductionSelection {
configuration, configuration,
generation: self.generation.clone(), generation: self.generation.clone(),
prepared_artifact: &self.artifact, prepared_artifact: &self.artifact,
prepared_artifact_authority: &self.artifact_authority,
prepared_artifact_label: &self.artifact_label,
root_process: RootProcess::new( root_process: RootProcess::new(
SANDBOX_INIT_DESTINATION, SANDBOX_INIT_DESTINATION,
[PRIVATE_SANDBOX_INIT_ROLE.to_owned()], [PRIVATE_SANDBOX_INIT_ROLE.to_owned()],
@ -149,14 +159,29 @@ async fn select_production_cold_start_for_platform(
random_id()?, random_id()?,
) )
.await .await
.map_err(|error| failure(format!("pulling configured anonymous OCI image: {error}")))?; .map_err(|error| failure(format!("pulling configured anonymous OCI image: {error:#}")))?;
let artifact = prepared let artifact = prepared
.prepare_transported( .prepare_transported(
PrepareOperationId::new(random_id()?), PrepareOperationId::new(random_id()?),
&image, &image,
rootless_id_map(), rootless_id_map(),
prepared_destinations(configuration),
) )
.map_err(failure)?; .map_err(failure)?;
let artifact_authority = prepared
.open_artifact_authority(&artifact)
.map_err(failure)?;
let artifact_label = PathBuf::from(std::ffi::OsString::from_vec(
configuration
.storage_root()
.canonical_label()
.as_bytes()
.to_vec(),
))
.join("prepared-lower")
.join(PathBuf::from(std::ffi::OsString::from_vec(
artifact.path.as_bytes().to_vec(),
)));
image.cleanup().map_err(failure)?; image.cleanup().map_err(failure)?;
let next_generation = next_generation(&store)?; let next_generation = next_generation(&store)?;
@ -179,6 +204,8 @@ async fn select_production_cold_start_for_platform(
configuration_fingerprint: configuration.configuration_fingerprint(), configuration_fingerprint: configuration.configuration_fingerprint(),
catalog, catalog,
artifact, artifact,
artifact_authority,
artifact_label,
generation, generation,
resolver_addresses, resolver_addresses,
network_probe, network_probe,
@ -189,6 +216,66 @@ async fn select_production_cold_start_for_platform(
}) })
} }
fn prepared_destinations(configuration: &ResolvedDomainConfiguration) -> Vec<PreparedDestination> {
let mut destinations = [
"/dev",
"/proc",
"/sys",
"/tmp",
"/etc",
"/home",
"/run",
WORKSPACE_DESTINATION,
CREDENTIALS_DESTINATION,
MAILBOX_REQUESTS_DESTINATION,
MAILBOX_RESULTS_DESTINATION,
TERMINFO_ROOT_DESTINATION,
]
.into_iter()
.map(|path| PreparedDestination {
path: path.into(),
kind: PreparedDestinationKind::Directory,
})
.chain(
[
"/etc/resolv.conf",
"/etc/hosts",
"/etc/hostname",
ESCALATION_CLIENT_DESTINATION,
SANDBOX_INIT_DESTINATION,
]
.into_iter()
.map(|path| PreparedDestination {
path: path.into(),
kind: PreparedDestinationKind::RegularFile,
}),
)
.collect::<Vec<_>>();
destinations.extend(
configuration
.fingerprint_record()
.persistent_dirs
.iter()
.map(|path| PreparedDestination {
path: path.into(),
kind: PreparedDestinationKind::Directory,
}),
);
destinations.extend(
configuration
.volumes()
.iter()
.map(|volume| PreparedDestination {
path: volume.record().container_path.as_str().into(),
kind: match volume.record().kind {
VolumeKind::Directory => PreparedDestinationKind::Directory,
VolumeKind::File => PreparedDestinationKind::RegularFile,
},
}),
);
destinations
}
fn next_generation(store: &HostCoordinatorDomainStore) -> Result<u64, ProductionSelectionError> { fn next_generation(store: &HostCoordinatorDomainStore) -> Result<u64, ProductionSelectionError> {
store store
.snapshot() .snapshot()
@ -380,6 +467,15 @@ mod tests {
id_map.gid[0].namespace_id, id_map.gid[0].namespace_id,
rustix::process::getgid().as_raw() rustix::process::getgid().as_raw()
); );
let destinations = prepared_destinations(&configuration);
assert!(destinations.iter().any(|destination| {
destination.path == Path::new(WORKSPACE_DESTINATION)
&& destination.kind == PreparedDestinationKind::Directory
}));
assert!(destinations.iter().any(|destination| {
destination.path == Path::new(SANDBOX_INIT_DESTINATION)
&& destination.kind == PreparedDestinationKind::RegularFile
}));
} }
#[test] #[test]

View file

@ -411,8 +411,13 @@ impl AwaitingAttachedRunsc {
.acknowledge_staged_daemon(staged) .acknowledge_staged_daemon(staged)
.map_err(|error| SandboxCoordinatorError::Io(io::Error::other(error)))?; .map_err(|error| SandboxCoordinatorError::Io(io::Error::other(error)))?;
control.acknowledge_state_anchor(acknowledgment)?; control.acknowledge_state_anchor(acknowledgment)?;
let (ready_staged, mount) = match control.receive()? { let (ready_staged, user_namespace, mount_namespace, mount) = match control.receive()? {
SupervisorEvent::StateAnchorReady { staged, mount } => (staged, mount), SupervisorEvent::StateAnchorReady {
staged,
user_namespace,
mount_namespace,
mount,
} => (staged, user_namespace, mount_namespace, mount),
SupervisorEvent::Refused(detail) => { SupervisorEvent::Refused(detail) => {
return Err(SandboxCoordinatorError::Io(io::Error::other(detail))); return Err(SandboxCoordinatorError::Io(io::Error::other(detail)));
} }
@ -430,7 +435,7 @@ impl AwaitingAttachedRunsc {
let readiness = let readiness =
lazaret_state_anchor::VerifiedStateAnchorReadiness::from_parts(ready_staged, mount); lazaret_state_anchor::VerifiedStateAnchorReadiness::from_parts(ready_staged, mount);
coordinator coordinator
.commit_daemon_ready(readiness) .commit_daemon_ready(readiness, user_namespace, mount_namespace)
.and_then(|()| { .and_then(|()| {
coordinator.commit_prepared(staged.generation_id(), prepared_artifact_id, mount) coordinator.commit_prepared(staged.generation_id(), prepared_artifact_id, mount)
}) })
@ -949,14 +954,16 @@ impl CommittedSandboxRuntime {
/// ///
/// # Errors /// # Errors
/// Refuses an unexpected RS response or channel failure. /// Refuses an unexpected RS response or channel failure.
pub fn unmount_state_anchor(&mut self) -> Result<(), SandboxCoordinatorError> { pub fn unmount_state_anchor(
&mut self,
expected_mount: MountIdentity,
) -> Result<(), SandboxCoordinatorError> {
self.control.unmount_state_anchor()?; self.control.unmount_state_anchor()?;
if self.control.receive()? == SupervisorEvent::StateAnchorUnmounted { match self.control.receive()? {
Ok(()) SupervisorEvent::StateAnchorUnmounted { mount } if mount == expected_mount => Ok(()),
} else { _ => Err(SandboxCoordinatorError::InvalidState(
Err(SandboxCoordinatorError::InvalidState( "Runtime Supervisor did not confirm the exact State Anchor unmount",
"Runtime Supervisor did not unmount the State Anchor", )),
))
} }
} }

View file

@ -3,15 +3,15 @@ use std::io;
use std::os::unix::ffi::OsStrExt; use std::os::unix::ffi::OsStrExt;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use lazaret_core::identity::{GenerationId, MountIdentity, PidfsProcessIdentityV1}; use lazaret_core::identity::{
GenerationId, MountIdentity, NamespaceIdentity, PidfsProcessIdentityV1,
};
use lazaret_core::records::{ use lazaret_core::records::{
ArtifactLease, HostProcessOwnershipSlot, Participant, ParticipantOwnership, ParticipantRole, ArtifactLease, HostProcessOwnershipSlot, Participant, ParticipantOwnership, ParticipantRole,
ParticipantState, PersistedHostProcessOwnership, PreparedArtifact, PreparedLowerOwnership, ParticipantState, PersistedHostProcessOwnership, PreparedArtifact, PreparedLowerOwnership,
ProcessOwnershipCommitAck, StagedProcessOwnership, StateAnchorOwnership, ProcessOwnershipCommitAck, StagedProcessOwnership, StateAnchorOwnership,
}; };
use lazaret_state_anchor::{ use lazaret_state_anchor::{StateAnchorError, StateAnchorLayout, VerifiedStateAnchorReadiness};
StateAnchorError, StateAnchorLayout, VerifiedStateAnchorReadiness, require_mount_absent,
};
use lazaret_storage_catalog::{Catalog, CatalogError}; use lazaret_storage_catalog::{Catalog, CatalogError};
use crate::domain_store::{DomainStoreError, DomainStoreMutation, HostCoordinatorDomainStore}; use crate::domain_store::{DomainStoreError, DomainStoreMutation, HostCoordinatorDomainStore};
@ -71,6 +71,8 @@ pub struct StateAnchorReleaseAuthority {
merged_label: PathBuf, merged_label: PathBuf,
mount: MountIdentity, mount: MountIdentity,
daemon: PidfsProcessIdentityV1, daemon: PidfsProcessIdentityV1,
user_namespace: NamespaceIdentity,
mount_namespace: NamespaceIdentity,
} }
impl StateAnchorReleaseAuthority { impl StateAnchorReleaseAuthority {
@ -88,6 +90,16 @@ impl StateAnchorReleaseAuthority {
pub const fn daemon(&self) -> PidfsProcessIdentityV1 { pub const fn daemon(&self) -> PidfsProcessIdentityV1 {
self.daemon self.daemon
} }
#[must_use]
pub const fn user_namespace(&self) -> NamespaceIdentity {
self.user_namespace
}
#[must_use]
pub const fn mount_namespace(&self) -> NamespaceIdentity {
self.mount_namespace
}
} }
pub struct StateAnchorCoordinator<'a> { pub struct StateAnchorCoordinator<'a> {
@ -252,6 +264,8 @@ impl<'a> StateAnchorCoordinator<'a> {
pub fn commit_daemon_ready( pub fn commit_daemon_ready(
&self, &self,
readiness: VerifiedStateAnchorReadiness, readiness: VerifiedStateAnchorReadiness,
user_namespace: NamespaceIdentity,
mount_namespace: NamespaceIdentity,
) -> Result<(), StateAnchorCoordinatorError> { ) -> Result<(), StateAnchorCoordinatorError> {
let staged = readiness.staged(); let staged = readiness.staged();
self.require_staged_identity(staged)?; self.require_staged_identity(staged)?;
@ -262,6 +276,8 @@ impl<'a> StateAnchorCoordinator<'a> {
self.update_daemon_and_mount( self.update_daemon_and_mount(
staged.generation_id(), staged.generation_id(),
PersistedHostProcessOwnership::committed(staged.identity()), PersistedHostProcessOwnership::committed(staged.identity()),
Some(user_namespace),
Some(mount_namespace),
Some(readiness.mount()), Some(readiness.mount()),
) )
} }
@ -365,7 +381,6 @@ impl<'a> StateAnchorCoordinator<'a> {
"release authority layout changed", "release authority layout changed",
)); ));
} }
require_mount_absent(layout)?;
let anchor = self.participant(authority.generation_id, ParticipantRole::StateAnchor)?; let anchor = self.participant(authority.generation_id, ParticipantRole::StateAnchor)?;
let prepared = self.participant(authority.generation_id, ParticipantRole::PreparedLower)?; let prepared = self.participant(authority.generation_id, ParticipantRole::PreparedLower)?;
match (anchor.state(), prepared.state()) { match (anchor.state(), prepared.state()) {
@ -392,7 +407,7 @@ impl<'a> StateAnchorCoordinator<'a> {
)); ));
} }
} }
self.release_catalog_lease_if_present(authority, layout)?; self.release_catalog_lease_if_present(authority)?;
self.domain_store self.domain_store
.apply(&[DomainStoreMutation::CleanParticipantAfterAbsence( .apply(&[DomainStoreMutation::CleanParticipantAfterAbsence(
clean_participant(authority.generation_id, ParticipantRole::PreparedLower)?, clean_participant(authority.generation_id, ParticipantRole::PreparedLower)?,
@ -426,13 +441,15 @@ impl<'a> StateAnchorCoordinator<'a> {
generation_id: GenerationId, generation_id: GenerationId,
daemon: PersistedHostProcessOwnership, daemon: PersistedHostProcessOwnership,
) -> Result<(), StateAnchorCoordinatorError> { ) -> Result<(), StateAnchorCoordinatorError> {
self.update_daemon_and_mount(generation_id, daemon, None) self.update_daemon_and_mount(generation_id, daemon, None, None, None)
} }
fn update_daemon_and_mount( fn update_daemon_and_mount(
&self, &self,
generation_id: GenerationId, generation_id: GenerationId,
daemon: PersistedHostProcessOwnership, daemon: PersistedHostProcessOwnership,
user_namespace: Option<NamespaceIdentity>,
mount_namespace: Option<NamespaceIdentity>,
mount: Option<MountIdentity>, mount: Option<MountIdentity>,
) -> Result<(), StateAnchorCoordinatorError> { ) -> Result<(), StateAnchorCoordinatorError> {
let current = self.participant(generation_id, ParticipantRole::StateAnchor)?; let current = self.participant(generation_id, ParticipantRole::StateAnchor)?;
@ -448,6 +465,8 @@ impl<'a> StateAnchorCoordinator<'a> {
} }
let next = StateAnchorOwnership { let next = StateAnchorOwnership {
daemon, daemon,
user_namespace,
mount_namespace,
mount, mount,
..ownership.clone() ..ownership.clone()
}; };
@ -512,7 +531,6 @@ impl<'a> StateAnchorCoordinator<'a> {
fn release_catalog_lease_if_present( fn release_catalog_lease_if_present(
&self, &self,
authority: &StateAnchorReleaseAuthority, authority: &StateAnchorReleaseAuthority,
layout: &StateAnchorLayout<'_>,
) -> Result<(), StateAnchorCoordinatorError> { ) -> Result<(), StateAnchorCoordinatorError> {
let snapshot = self.catalog.snapshot()?; let snapshot = self.catalog.snapshot()?;
let generation_lease = snapshot.artifact_leases.iter().find(|lease| { let generation_lease = snapshot.artifact_leases.iter().find(|lease| {
@ -552,7 +570,7 @@ impl<'a> StateAnchorCoordinator<'a> {
"Domain release evidence is not committed", "Domain release evidence is not committed",
))); )));
} }
require_mount_absent(layout).map_err(|error| CatalogError::Io(io::Error::other(error))) Ok(())
})?; })?;
Ok(()) Ok(())
} }
@ -610,6 +628,12 @@ fn release_authority(
.daemon .daemon
.serviceable_pidfs() .serviceable_pidfs()
.map_err(|_| StateAnchorCoordinatorError::InvalidState("daemon is not committed"))?, .map_err(|_| StateAnchorCoordinatorError::InvalidState("daemon is not committed"))?,
user_namespace: anchor_ownership.user_namespace.ok_or(
StateAnchorCoordinatorError::InvalidState("user namespace identity is absent"),
)?,
mount_namespace: anchor_ownership.mount_namespace.ok_or(
StateAnchorCoordinatorError::InvalidState("mount namespace identity is absent"),
)?,
}) })
} }

View file

@ -9,9 +9,10 @@ use lazaret_core::configuration::{
}; };
use lazaret_core::identity::{ use lazaret_core::identity::{
BootId, ConfigurationFingerprint, CoordinatorId, DeviceId, GenerationId, HostPath, BootId, ConfigurationFingerprint, CoordinatorId, DeviceId, GenerationId, HostPath,
HostPidLocator, IdMapDigest, InodeId, MountId, MountIdentity, NetworkSidecarId, PidfsDeviceId, HostPidLocator, IdMapDigest, InodeId, MountId, MountIdentity, NamespaceIdentity,
PidfsInodeId, PidfsProcessAuthorityV1, PidfsProcessIdentityV1, PolicyId, PrepareOperationId, NetworkSidecarId, PidfsDeviceId, PidfsInodeId, PidfsProcessAuthorityV1, PidfsProcessIdentityV1,
PreparedArtifactId, ProcessStartTimeTicks, SandboxId, SelectedManifestDigest, SourceIdentity, PolicyId, PrepareOperationId, PreparedArtifactId, ProcessStartTimeTicks, SandboxId,
SelectedManifestDigest, SourceIdentity,
}; };
use lazaret_core::records::{ use lazaret_core::records::{
ArtifactState, Domain, DurableDecision, Generation, ParticipantRole, ParticipantState, ArtifactState, Domain, DurableDecision, Generation, ParticipantRole, ParticipantState,
@ -138,6 +139,14 @@ fn durable_phases_resume_release_without_collapsing_lease_order() {
ParticipantState::Intent, ParticipantState::Intent,
ParticipantOwnership::StateAnchor(StateAnchorOwnership { ParticipantOwnership::StateAnchor(StateAnchorOwnership {
daemon: PersistedHostProcessOwnership::committed(staged.identity()), daemon: PersistedHostProcessOwnership::committed(staged.identity()),
user_namespace: Some(NamespaceIdentity {
device: DeviceId::new(93),
inode: InodeId::new(94),
}),
mount_namespace: Some(NamespaceIdentity {
device: DeviceId::new(95),
inode: InodeId::new(96),
}),
mount: Some(MountIdentity { mount: Some(MountIdentity {
mount_id: MountId::new(90), mount_id: MountId::new(90),
device: DeviceId::new(91), device: DeviceId::new(91),

View file

@ -27,10 +27,13 @@ fn validated_inputs_stage_one_complete_plan_and_drop_exact_runtime_artifacts() {
let staged = ProductionColdStart::stage(fixture.inputs(&fixture.escalation_client)).unwrap(); let staged = ProductionColdStart::stage(fixture.inputs(&fixture.escalation_client)).unwrap();
let bundle = fixture.bundle(); let bundle = fixture.bundle();
let control = fixture.control(); let control = fixture.control();
let overlay = fixture.overlay();
assert_private_artifacts(&bundle, &control); assert_private_artifacts(&bundle, &control);
assert!(overlay.is_dir());
drop(staged); drop(staged);
assert!(!bundle.exists()); assert!(!bundle.exists());
assert!(!control.exists()); assert!(!control.exists());
assert!(!overlay.exists());
assert!( assert!(
ProductionColdStart::stage(fixture.inputs(&fixture.invalid_escalation_client)).is_err() ProductionColdStart::stage(fixture.inputs(&fixture.invalid_escalation_client)).is_err()
); );
@ -90,6 +93,7 @@ struct ProductionFixture {
catalog: Catalog, catalog: Catalog,
store: Arc<HostCoordinatorDomainStore>, store: Arc<HostCoordinatorDomainStore>,
artifact: PreparedArtifact, artifact: PreparedArtifact,
artifact_authority: File,
generation: Generation, generation: Generation,
container_id: ContainerId, container_id: ContainerId,
escalation_client: File, escalation_client: File,
@ -139,6 +143,7 @@ impl ProductionFixture {
let lower = tree.path().join("prepared-lower"); let lower = tree.path().join("prepared-lower");
fs::create_dir(&lower).unwrap(); fs::create_dir(&lower).unwrap();
let metadata = lower.metadata().unwrap(); let metadata = lower.metadata().unwrap();
let artifact_authority = File::open(&lower).unwrap();
let artifact = PreparedArtifact { let artifact = PreparedArtifact {
prepared_artifact_id: PreparedArtifactId::new([8; 32]), prepared_artifact_id: PreparedArtifactId::new([8; 32]),
source_identity: SourceIdentity::new(b"synthetic-spike".to_vec()), source_identity: SourceIdentity::new(b"synthetic-spike".to_vec()),
@ -166,6 +171,7 @@ impl ProductionFixture {
catalog, catalog,
store, store,
artifact, artifact,
artifact_authority,
generation, generation,
container_id: ContainerId::new([5; 32]), container_id: ContainerId::new([5; 32]),
escalation_client: File::open("/bin/true").unwrap(), escalation_client: File::open("/bin/true").unwrap(),
@ -180,6 +186,10 @@ impl ProductionFixture {
configuration: &self.configuration, configuration: &self.configuration,
generation: self.generation.clone(), generation: self.generation.clone(),
prepared_artifact: &self.artifact, prepared_artifact: &self.artifact,
prepared_artifact_authority: &self.artifact_authority,
prepared_artifact_label: Path::new(
std::str::from_utf8(self.artifact.path.as_bytes()).unwrap(),
),
root_process: RootProcess::new("/bin/sh", ["-c".into(), "sleep infinity".into()]) root_process: RootProcess::new("/bin/sh", ["-c".into(), "sleep infinity".into()])
.unwrap(), .unwrap(),
resolver_addresses: vec![IpAddress::V4([10, 0, 2, 3])], resolver_addresses: vec![IpAddress::V4([10, 0, 2, 3])],
@ -214,6 +224,12 @@ impl ProductionFixture {
.join("sandbox-control") .join("sandbox-control")
.join(hex(self.container_id.as_bytes())) .join(hex(self.container_id.as_bytes()))
} }
fn overlay(&self) -> PathBuf {
self.runtime_domain
.join("runsc-overlay")
.join(hex(self.container_id.as_bytes()))
}
} }
fn assert_private_artifacts(bundle: &Path, control: &Path) { fn assert_private_artifacts(bundle: &Path, control: &Path) {

View file

@ -686,6 +686,7 @@ impl Fixture {
expected_network_namespace: namespace, expected_network_namespace: namespace,
root_process, root_process,
terminal: None, terminal: None,
prepared_root: runtime_domain.join("prepared-root"),
}) })
.map_err(failure)?; .map_err(failure)?;
Self::finish_build( Self::finish_build(
@ -743,6 +744,7 @@ impl Fixture {
expected_network_namespace: namespace, expected_network_namespace: namespace,
root_process, root_process,
terminal: None, terminal: None,
prepared_root: runtime_domain.join("prepared-root"),
}) })
.map_err(failure)?; .map_err(failure)?;
Self::finish_build( Self::finish_build(
@ -846,6 +848,8 @@ impl Fixture {
) )
.map_err(failure)?, .map_err(failure)?,
terminal: None, terminal: None,
prepared_root: host_path(self.configuration.runtime_domain_root().canonical_label())
.join("prepared-root"),
}) })
.map_err(failure) .map_err(failure)
} }
@ -888,7 +892,7 @@ impl NetworkFixture {
let runtime_domain = host_path(configuration.runtime_domain_root().canonical_label()); let runtime_domain = host_path(configuration.runtime_domain_root().canonical_label());
let container_id = ContainerId::new([16; 32]); let container_id = ContainerId::new([16; 32]);
stage_rootfs_and_control(&runtime_domain, container_id)?; stage_rootfs_and_control(&runtime_domain, container_id)?;
let rootfs = runtime_domain.join("state-anchor/merged"); let rootfs = runtime_domain.join("prepared-root");
fs::copy( fs::copy(
std::env::current_exe()?, std::env::current_exe()?,
rootfs.join("bin/lazaret-network-probe"), rootfs.join("bin/lazaret-network-probe"),
@ -917,6 +921,7 @@ impl NetworkFixture {
expected_network_namespace: namespace, expected_network_namespace: namespace,
root_process, root_process,
terminal: None, terminal: None,
prepared_root: runtime_domain.join("prepared-root"),
}) })
.map_err(failure)?; .map_err(failure)?;
let bundle_path = runtime_domain let bundle_path = runtime_domain
@ -1177,10 +1182,44 @@ fn stage_rootfs_and_control(
runtime_domain: &Path, runtime_domain: &Path,
container_id: ContainerId, container_id: ContainerId,
) -> Result<(), MatrixError> { ) -> Result<(), MatrixError> {
let rootfs = runtime_domain.join("state-anchor/merged"); let rootfs = runtime_domain.join("prepared-root");
for directory in ["bin", "lib64"] { for directory in [
"bin",
"lib64",
"dev",
"proc",
"sys",
"tmp",
"etc",
"home",
"workspace",
"run/lazaret/credentials",
"run/lazaret/mailbox/requests",
"run/lazaret/mailbox/results",
"run/lazaret/bin",
] {
fs::create_dir_all(rootfs.join(directory))?; fs::create_dir_all(rootfs.join(directory))?;
} }
for file in [
"etc/resolv.conf",
"etc/hosts",
"etc/hostname",
"run/lazaret/bin/lazaret-escalate",
"run/lazaret/bin/lazaret-sandbox-init",
] {
fs::write(rootfs.join(file), b"")?;
}
fs::create_dir_all(runtime_domain.join("state-anchor/merged"))?;
let runtime_files = runtime_domain.join("state-anchor/runtime-files");
fs::create_dir_all(&runtime_files)?;
for file in ["resolv.conf", "hosts", "hostname"] {
fs::write(runtime_files.join(file), b"fixture")?;
}
fs::create_dir_all(
runtime_domain
.join("runsc-overlay")
.join(hex(container_id.as_bytes())),
)?;
for (source, destination) in [ for (source, destination) in [
("/bin/sh", "bin/sh"), ("/bin/sh", "bin/sh"),
("/bin/sleep", "bin/sleep"), ("/bin/sleep", "bin/sleep"),
@ -1199,11 +1238,12 @@ fn stage_rootfs_and_control(
fs::create_dir_all(control.join(directory))?; fs::create_dir_all(control.join(directory))?;
} }
fs::copy("/bin/true", control.join("bin/lazaret-escalate"))?; fs::copy("/bin/true", control.join("bin/lazaret-escalate"))?;
fs::copy("/bin/true", control.join("bin/lazaret-sandbox-init"))?;
Ok(()) Ok(())
} }
fn stage_benchmark_rootfs_and_control(runtime_domain: &Path) -> Result<(), MatrixError> { fn stage_benchmark_rootfs_and_control(runtime_domain: &Path) -> Result<(), MatrixError> {
let rootfs = runtime_domain.join("state-anchor/merged"); let rootfs = runtime_domain.join("prepared-root");
for directory in [ for directory in [
"bin", "bin",
"tooling/usr", "tooling/usr",
@ -1213,6 +1253,17 @@ fn stage_benchmark_rootfs_and_control(runtime_domain: &Path) -> Result<(), Matri
"fixtures/host", "fixtures/host",
"fixtures/output", "fixtures/output",
"fixtures/repository", "fixtures/repository",
"dev",
"proc",
"sys",
"tmp",
"etc",
"home",
"workspace",
"run/lazaret/credentials",
"run/lazaret/mailbox/requests",
"run/lazaret/mailbox/results",
"run/lazaret/bin",
] { ] {
fs::create_dir_all(rootfs.join(directory))?; fs::create_dir_all(rootfs.join(directory))?;
} }
@ -1224,12 +1275,31 @@ fn stage_benchmark_rootfs_and_control(runtime_domain: &Path) -> Result<(), Matri
symlink("usr/lib", rootfs.join("lib"))?; symlink("usr/lib", rootfs.join("lib"))?;
symlink("usr/lib64", rootfs.join("lib64"))?; symlink("usr/lib64", rootfs.join("lib64"))?;
symlink("usr/sbin", rootfs.join("sbin"))?; symlink("usr/sbin", rootfs.join("sbin"))?;
for file in [
"etc/resolv.conf",
"etc/hosts",
"etc/hostname",
"run/lazaret/bin/lazaret-escalate",
"run/lazaret/bin/lazaret-sandbox-init",
] {
fs::write(rootfs.join(file), b"")?;
}
for directory in ["tooling", "fixtures"] {
fs::create_dir_all(runtime_domain.join("state-anchor/merged").join(directory))?;
}
let runtime_files = runtime_domain.join("state-anchor/runtime-files");
fs::create_dir_all(&runtime_files)?;
for file in ["resolv.conf", "hosts", "hostname"] {
fs::write(runtime_files.join(file), b"fixture")?;
}
fs::create_dir_all(runtime_domain.join("runsc-overlay").join(hex(&[6; 32])))?;
let control = runtime_domain.join("sandbox-control").join(hex(&[6; 32])); let control = runtime_domain.join("sandbox-control").join(hex(&[6; 32]));
for directory in ["credentials", "mailbox/requests", "mailbox/results", "bin"] { for directory in ["credentials", "mailbox/requests", "mailbox/results", "bin"] {
fs::create_dir_all(control.join(directory))?; fs::create_dir_all(control.join(directory))?;
} }
fs::copy("/bin/true", control.join("bin/lazaret-escalate"))?; fs::copy("/bin/true", control.join("bin/lazaret-escalate"))?;
fs::copy("/bin/true", control.join("bin/lazaret-sandbox-init"))?;
Ok(()) Ok(())
} }

View file

@ -32,6 +32,18 @@ pub struct IdMap {
pub gid: Vec<IdMapRange>, pub gid: Vec<IdMapRange>,
} }
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub enum PreparedDestinationKind {
Directory,
RegularFile,
}
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub struct PreparedDestination {
pub path: PathBuf,
pub kind: PreparedDestinationKind,
}
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
pub struct PreparationLimits { pub struct PreparationLimits {
pub max_layers: usize, pub max_layers: usize,
@ -189,11 +201,91 @@ pub(crate) fn apply_layout(
bail!("layer {index} diff-ID mismatch"); bail!("layer {index} diff-ID mismatch");
} }
validate_tar(&spool, id_map, limits, &mut usage)?; validate_tar(&spool, id_map, limits, &mut usage)?;
state.apply(&spool, rootfs)?; state.apply(&spool, rootfs, id_map)?;
} }
state.finish(rootfs) state.finish(rootfs)
} }
pub(crate) fn materialize_destinations(
root: &File,
destinations: &[PreparedDestination],
id_map: &IdMap,
) -> Result<()> {
if destinations.is_empty() {
return Ok(());
}
let uid = map_owner(0, &id_map.uid, "uid")?;
let gid = map_owner(0, &id_map.gid, "gid")?;
for destination in destinations {
let relative = destination
.path
.strip_prefix("/")
.context("prepared destination is not absolute")?;
let relative = normalize_relative(relative)?;
let (parent, leaf) = materialize_parent(root, &relative, uid, gid)?;
let expected = match destination.kind {
PreparedDestinationKind::Directory => FileType::Directory,
PreparedDestinationKind::RegularFile => FileType::RegularFile,
};
match path_type(&parent, Path::new(&leaf))? {
Some(actual) if actual == expected => {}
Some(_) => bail!("prepared destination conflicts with the image layout"),
None => match destination.kind {
PreparedDestinationKind::Directory => {
mkdirat(&parent, &leaf, Mode::from_raw_mode(0o755))?;
set_materialized_metadata(open_component(&parent, &leaf)?, uid, gid)?;
}
PreparedDestinationKind::RegularFile => {
let file = openat2(
&parent,
&leaf,
OFlags::WRONLY
| OFlags::CREATE
| OFlags::EXCL
| OFlags::CLOEXEC
| OFlags::NOFOLLOW,
Mode::from_raw_mode(0o755),
secure_resolve(),
)?;
set_materialized_metadata(file, uid, gid)?;
}
},
}
}
Ok(())
}
fn materialize_parent(root: &File, path: &Path, uid: u32, gid: u32) -> Result<(OwnedFd, OsString)> {
let leaf = path
.file_name()
.context("prepared destination has no final component")?
.to_os_string();
let mut current = dup(root)?;
for component in path.parent().into_iter().flat_map(Path::components) {
let Component::Normal(name) = component else {
bail!("prepared destination is not normalized");
};
match mkdirat(&current, name, Mode::from_raw_mode(0o755)) {
Ok(()) => set_materialized_metadata(open_component(&current, name)?, uid, gid)?,
Err(Errno::EXIST) => {}
Err(error) => return Err(error.into()),
}
current = open_component(&current, name)?;
}
Ok((current, leaf))
}
fn set_materialized_metadata(descriptor: impl AsFd, uid: u32, gid: u32) -> Result<()> {
fchown(
&descriptor,
Some(Uid::from_raw(uid)),
Some(Gid::from_raw(gid)),
)?;
fchmod(&descriptor, Mode::from_raw_mode(0o755))?;
futimens(&descriptor, &timestamps(0))?;
Ok(())
}
fn read_json<T: serde::de::DeserializeOwned>( fn read_json<T: serde::de::DeserializeOwned>(
layout: &LayoutSource<'_>, layout: &LayoutSource<'_>,
path: &Path, path: &Path,
@ -315,7 +407,8 @@ fn validate_tar(
let mut paths = HashSet::new(); let mut paths = HashSet::new();
for entry in archive.entries()? { for entry in archive.entries()? {
let mut entry = entry?; let mut entry = entry?;
let path = normalize_relative(&entry.path()?)?; let entry_type = entry.header().entry_type();
let path = normalize_layer_path(&entry.path()?, entry_type)?;
if !paths.insert(path.clone()) { if !paths.insert(path.clone()) {
bail!("policy {POLICY_NAME} rejects duplicate layer path"); bail!("policy {POLICY_NAME} rejects duplicate layer path");
} }
@ -353,9 +446,8 @@ fn validate_tar(
bail!("policy {POLICY_NAME} rejects PAX metadata"); bail!("policy {POLICY_NAME} rejects PAX metadata");
} }
} }
validate_owner(entry.header().uid()?, &id_map.uid, "uid")?; map_owner(entry.header().uid()?, &id_map.uid, "uid")?;
validate_owner(entry.header().gid()?, &id_map.gid, "gid")?; map_owner(entry.header().gid()?, &id_map.gid, "gid")?;
let entry_type = entry.header().entry_type();
if !matches!( if !matches!(
entry_type, entry_type,
EntryType::Regular EntryType::Regular
@ -407,17 +499,35 @@ fn normalize_relative(path: &Path) -> Result<PathBuf> {
Ok(normalized) Ok(normalized)
} }
fn validate_owner(value: u64, ranges: &[IdMapRange], name: &str) -> Result<()> { fn normalize_layer_path(path: &Path, entry_type: EntryType) -> Result<PathBuf> {
match normalize_relative(path) {
Ok(path) => Ok(path),
Err(_)
if entry_type == EntryType::Directory
&& path.components().next().is_some()
&& path
.components()
.all(|component| component == Component::CurDir) =>
{
Ok(PathBuf::from("."))
}
Err(error) => Err(error),
}
}
fn map_owner(value: u64, ranges: &[IdMapRange], name: &str) -> Result<u32> {
let Ok(value) = u32::try_from(value) else { let Ok(value) = u32::try_from(value) else {
bail!("{name} {value} is outside the target ID map"); bail!("{name} {value} is outside the target ID map");
}; };
if !ranges for range in ranges {
.iter() if value >= range.container_id && value - range.container_id < range.size {
.any(|range| value >= range.container_id && value - range.container_id < range.size) return range
{ .namespace_id
bail!("{name} {value} is outside the target ID map"); .checked_add(value - range.container_id)
.context("mapped owner overflows");
} }
Ok(()) }
bail!("{name} {value} is outside the target ID map")
} }
fn validate_id_map(id_map: &IdMap) -> Result<()> { fn validate_id_map(id_map: &IdMap) -> Result<()> {
@ -483,7 +593,7 @@ struct LayerState {
} }
impl LayerState { impl LayerState {
fn apply(&mut self, tar_source: &File, root: &File) -> Result<()> { fn apply(&mut self, tar_source: &File, root: &File, id_map: &IdMap) -> Result<()> {
apply_whiteouts(tar_source, root, &mut self.directories)?; apply_whiteouts(tar_source, root, &mut self.directories)?;
let mut archive = tar::Archive::new(rewound(tar_source)?); let mut archive = tar::Archive::new(rewound(tar_source)?);
let mut hardlinks = Vec::new(); let mut hardlinks = Vec::new();
@ -497,7 +607,7 @@ impl LayerState {
if name == ".wh..wh..opq" || name.starts_with(".wh.") { if name == ".wh..wh..opq" || name.starts_with(".wh.") {
continue; continue;
} }
let metadata = EntryMetadata::from_header(path.clone(), entry.header())?; let metadata = EntryMetadata::from_header(path.clone(), entry.header(), id_map)?;
match entry.header().entry_type() { match entry.header().entry_type() {
EntryType::Regular | EntryType::Continuous => { EntryType::Regular | EntryType::Continuous => {
forget_directory_tree(&mut self.directories, &path); forget_directory_tree(&mut self.directories, &path);
@ -557,11 +667,11 @@ struct EntryMetadata {
} }
impl EntryMetadata { impl EntryMetadata {
fn from_header(path: PathBuf, header: &Header) -> Result<Self> { fn from_header(path: PathBuf, header: &Header, id_map: &IdMap) -> Result<Self> {
Ok(Self { Ok(Self {
path, path,
uid: u32::try_from(header.uid()?).context("entry uid exceeds u32")?, uid: map_owner(header.uid()?, &id_map.uid, "uid")?,
gid: u32::try_from(header.gid()?).context("entry gid exceeds u32")?, gid: map_owner(header.gid()?, &id_map.gid, "gid")?,
mode: header.mode()?, mode: header.mode()?,
mtime: header.mtime()?, mtime: header.mtime()?,
}) })

View file

@ -4,6 +4,8 @@ mod layer;
mod store; mod store;
mod transport; mod transport;
pub use layer::{IdMap, IdMapRange, PreparationLimits}; pub use layer::{
IdMap, IdMapRange, PreparationLimits, PreparedDestination, PreparedDestinationKind,
};
pub use store::{PrepareRecovery, PrepareRequest, PreparedLowerStore, ReclaimOutcome}; pub use store::{PrepareRecovery, PrepareRequest, PreparedLowerStore, ReclaimOutcome};
pub use transport::{OciRegistryTransport, Platform, RegistryCredentials, TransportedImage}; pub use transport::{OciRegistryTransport, Platform, RegistryCredentials, TransportedImage};

View file

@ -23,7 +23,8 @@ use rustix::fs::{
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
use crate::layer::{ use crate::layer::{
IdMap, LayoutSource, POLICY_NAME, PreparationLimits, apply_layout, remove_children, IdMap, LayoutSource, POLICY_NAME, PreparationLimits, PreparedDestination,
PreparedDestinationKind, apply_layout, materialize_destinations, remove_children,
verify_layout, verify_layout,
}; };
use crate::transport::TransportedImage; use crate::transport::TransportedImage;
@ -38,6 +39,7 @@ pub struct PrepareRequest {
pub source_identity: Vec<u8>, pub source_identity: Vec<u8>,
pub selected_manifest_digest: [u8; 32], pub selected_manifest_digest: [u8; 32],
pub id_map: IdMap, pub id_map: IdMap,
pub destinations: Vec<PreparedDestination>,
} }
#[derive(Clone, Copy, Debug, Eq, PartialEq)] #[derive(Clone, Copy, Debug, Eq, PartialEq)]
@ -115,6 +117,22 @@ impl PreparedLowerStore {
self self
} }
/// Opens the exact published artifact beneath the retained prepared-lower root.
///
/// # Errors
/// Refuses a non-canonical artifact record, catalog drift, or physical identity drift.
pub fn open_artifact_authority(&self, artifact: &PreparedArtifact) -> Result<File> {
let catalog_artifact = self
.catalog_artifact(artifact.prepared_artifact_id)?
.context("prepared artifact is absent from the catalog")?;
if catalog_artifact != *artifact {
bail!("prepared artifact does not match the catalog");
}
self.require_artifact_identity(artifact)?;
self.open_relative(&artifact_path(artifact.prepared_artifact_id))?
.context("prepared artifact path is absent")
}
/// Materializes and atomically publishes one verified immutable prepared lower. /// Materializes and atomically publishes one verified immutable prepared lower.
/// ///
/// # Errors /// # Errors
@ -134,6 +152,7 @@ impl PreparedLowerStore {
operation_id: PrepareOperationId, operation_id: PrepareOperationId,
image: &TransportedImage, image: &TransportedImage,
id_map: IdMap, id_map: IdMap,
destinations: Vec<PreparedDestination>,
) -> Result<PreparedArtifact> { ) -> Result<PreparedArtifact> {
let layout = image let layout = image
.retained_layout() .retained_layout()
@ -144,6 +163,7 @@ impl PreparedLowerStore {
source_identity: image.source_identity.clone(), source_identity: image.source_identity.clone(),
selected_manifest_digest: image.selected_manifest_digest, selected_manifest_digest: image.selected_manifest_digest,
id_map, id_map,
destinations,
}; };
self.prepare_from_source(&request, &LayoutSource::Retained(layout.root())) self.prepare_from_source(&request, &LayoutSource::Retained(layout.root()))
} }
@ -194,6 +214,7 @@ impl PreparedLowerStore {
return Err(error.into()); return Err(error.into());
} }
if let Err(error) = apply_layout(layout, &stage, &verified, &request.id_map, self.limits) if let Err(error) = apply_layout(layout, &stage, &verified, &request.id_map, self.limits)
.and_then(|()| materialize_destinations(&stage, &request.destinations, &request.id_map))
.and_then(|()| sync_tree(&stage)) .and_then(|()| sync_tree(&stage))
{ {
if self if self
@ -494,7 +515,13 @@ impl Identities {
fn new(request: &PrepareRequest) -> Result<Self> { fn new(request: &PrepareRequest) -> Result<Self> {
let source = SourceIdentity::new(request.source_identity.clone()); let source = SourceIdentity::new(request.source_identity.clone());
let manifest = SelectedManifestDigest::new(request.selected_manifest_digest); let manifest = SelectedManifestDigest::new(request.selected_manifest_digest);
let policy = PolicyId::new(hash_parts(&[b"lazaret-policy-v1", POLICY_NAME.as_bytes()])); let destinations = canonical_destinations(&request.destinations)?;
let destination_bytes = destination_policy_bytes(&destinations);
let policy = PolicyId::new(hash_parts(&[
b"lazaret-policy-v2",
POLICY_NAME.as_bytes(),
&destination_bytes,
]));
let id_map_bytes = postcard::to_stdvec(&request.id_map)?; let id_map_bytes = postcard::to_stdvec(&request.id_map)?;
let id_map = IdMapDigest::new(hash_parts(&[b"lazaret-id-map-v1", &id_map_bytes])); let id_map = IdMapDigest::new(hash_parts(&[b"lazaret-id-map-v1", &id_map_bytes]));
let artifact_id = PreparedArtifactId::new(hash_parts(&[ let artifact_id = PreparedArtifactId::new(hash_parts(&[
@ -555,6 +582,42 @@ impl Identities {
} }
} }
fn canonical_destinations(
destinations: &[PreparedDestination],
) -> Result<Vec<PreparedDestination>> {
let mut destinations = destinations.to_vec();
for destination in &destinations {
let mut components = destination.path.components();
if components.next() != Some(std::path::Component::RootDir)
|| components.any(|component| !matches!(component, std::path::Component::Normal(_)))
{
bail!("prepared destination is not an absolute normalized path");
}
}
destinations.sort();
destinations.dedup();
for pair in destinations.windows(2) {
if pair[0].path == pair[1].path && pair[0].kind != pair[1].kind {
bail!("prepared destination has conflicting types");
}
}
Ok(destinations)
}
fn destination_policy_bytes(destinations: &[PreparedDestination]) -> Vec<u8> {
let mut bytes = b"lazaret-prepared-layout-policy-v1\0".to_vec();
for destination in destinations {
bytes.extend_from_slice(destination.path.as_os_str().as_bytes());
bytes.push(0);
bytes.push(match destination.kind {
PreparedDestinationKind::Directory => b'd',
PreparedDestinationKind::RegularFile => b'f',
});
bytes.push(0);
}
bytes
}
fn artifact_from_operation( fn artifact_from_operation(
operation: &PrepareOperation, operation: &PrepareOperation,
path: &Path, path: &Path,

View file

@ -223,8 +223,10 @@ impl OciRegistryTransport {
} }
verify_platform(&config_json, platform)?; verify_platform(&config_json, platform)?;
validate_manifest(&manifest)?; validate_manifest(&manifest)?;
let layout = LayoutWriter::create(destination)?; let layout = LayoutWriter::create(destination).context("creating private OCI layout")?;
layout.write_verified_blob(&manifest.config.digest, config_json.as_bytes())?; layout
.write_verified_blob(&manifest.config.digest, config_json.as_bytes())
.context("writing verified OCI config")?;
for descriptor in &manifest.layers { for descriptor in &manifest.layers {
let size = u64::try_from(descriptor.size).context("negative OCI layer size")?; let size = u64::try_from(descriptor.size).context("negative OCI layer size")?;
if size > MAX_LAYER_BYTES { if size > MAX_LAYER_BYTES {
@ -235,23 +237,33 @@ impl OciRegistryTransport {
layout.require_existing_blob(&descriptor.digest, size)?; layout.require_existing_blob(&descriptor.digest, size)?;
continue; continue;
} }
let (file, temporary) = layout.create_blob(digest_component)?; let (file, temporary) = layout
.create_blob(digest_component)
.with_context(|| format!("creating OCI layer {}", descriptor.digest))?;
let file = tokio::fs::File::from_std(file); let file = tokio::fs::File::from_std(file);
if let Err(error) = client.pull_blob(&reference, descriptor, file).await { if let Err(error) = client.pull_blob(&reference, descriptor, file).await {
layout.remove_temporary_blob(&temporary); layout.remove_temporary_blob(&temporary);
return Err(error) return Err(error)
.with_context(|| format!("pulling OCI layer {}", descriptor.digest)); .with_context(|| format!("pulling OCI layer {}", descriptor.digest));
} }
let observed = layout.temporary_blob_size(&temporary)?; let observed = layout
.temporary_blob_size(&temporary)
.with_context(|| format!("measuring OCI layer {}", descriptor.digest))?;
if observed != size { if observed != size {
bail!("OCI layer {} size mismatch", descriptor.digest); bail!("OCI layer {} size mismatch", descriptor.digest);
} }
layout.publish_blob(&temporary, digest_component)?; layout
.publish_blob(&temporary, digest_component)
.with_context(|| format!("publishing OCI layer {}", descriptor.digest))?;
} }
let local_manifest = serde_json::to_vec(&manifest)?; let local_manifest = serde_json::to_vec(&manifest)?;
let local_digest = format!("sha256:{}", hex(&Sha256::digest(&local_manifest))); let local_digest = format!("sha256:{}", hex(&Sha256::digest(&local_manifest)));
layout.write_verified_blob(&local_digest, &local_manifest)?; layout
layout.write_layout_index(&local_digest, local_manifest.len())?; .write_verified_blob(&local_digest, &local_manifest)
.context("writing verified OCI manifest")?;
layout
.write_layout_index(&local_digest, local_manifest.len())
.context("writing OCI layout index")?;
let selected_manifest_digest = parse_sha256(&local_digest)?; let selected_manifest_digest = parse_sha256(&local_digest)?;
let source_identity = format!( let source_identity = format!(
"oci-v1\0{}\0{}\0{manifest_digest}", "oci-v1\0{}\0{}\0{manifest_digest}",
@ -406,13 +418,19 @@ impl LayoutWriter {
bail!("OCI descriptor digest mismatch for {digest}"); bail!("OCI descriptor digest mismatch for {digest}");
} }
let component = digest_component(digest)?; let component = digest_component(digest)?;
if self.blob_exists(component)? { if self
.blob_exists(component)
.context("checking verified OCI blob destination")?
{
return self.require_existing_blob(digest, bytes.len() as u64); return self.require_existing_blob(digest, bytes.len() as u64);
} }
let (mut file, temporary) = self.create_blob(component)?; let (mut file, temporary) = self
file.write_all(bytes)?; .create_blob(component)
file.sync_all()?; .context("creating verified OCI blob")?;
file.write_all(bytes).context("writing verified OCI blob")?;
file.sync_all().context("syncing verified OCI blob")?;
self.publish_blob(&temporary, component) self.publish_blob(&temporary, component)
.context("publishing verified OCI blob")
} }
fn blob_exists(&self, component: &str) -> Result<bool> { fn blob_exists(&self, component: &str) -> Result<bool> {
@ -552,11 +570,16 @@ fn open_directory(parent: &File, path: &Path) -> std::io::Result<File> {
} }
fn open_relative_file(parent: &File, path: &Path, flags: OFlags) -> std::io::Result<File> { fn open_relative_file(parent: &File, path: &Path, flags: OFlags) -> std::io::Result<File> {
let mode = if flags.contains(OFlags::CREATE) {
Mode::from_raw_mode(0o600)
} else {
Mode::empty()
};
openat2( openat2(
parent, parent,
path, path,
flags | OFlags::NOFOLLOW | OFlags::CLOEXEC, flags | OFlags::NOFOLLOW | OFlags::CLOEXEC,
Mode::from_raw_mode(0o600), mode,
confined_resolve(), confined_resolve(),
) )
.map(File::from) .map(File::from)
@ -566,7 +589,11 @@ fn open_relative_file(parent: &File, path: &Path, flags: OFlags) -> std::io::Res
fn write_relative_atomic(root: &File, path: &Path, bytes: &[u8]) -> Result<()> { fn write_relative_atomic(root: &File, path: &Path, bytes: &[u8]) -> Result<()> {
let name = path.file_name().context("layout file has no name")?; let name = path.file_name().context("layout file has no name")?;
let parent_path = path.parent().unwrap_or(Path::new(".")); let parent_path = path.parent().unwrap_or(Path::new("."));
let parent = open_directory(root, parent_path)?; let parent = if parent_path.as_os_str().is_empty() {
File::from(rustix::io::dup(root)?)
} else {
open_directory(root, parent_path)?
};
let temporary = format!(".{}.partial", name.to_string_lossy()); let temporary = format!(".{}.partial", name.to_string_lossy());
let mut file = open_relative_file( let mut file = open_relative_file(
&parent, &parent,
@ -709,6 +736,25 @@ mod tests {
use super::*; use super::*;
#[test]
fn atomic_layout_write_accepts_a_root_level_destination() {
let tree = tempfile::tempdir().unwrap();
let root = File::open(tree.path()).unwrap();
write_relative_atomic(&root, Path::new("oci-layout"), b"layout").unwrap();
assert_eq!(
std::fs::read(tree.path().join("oci-layout")).unwrap(),
b"layout"
);
}
#[test]
fn confined_read_open_uses_an_empty_creation_mode() {
let tree = tempfile::tempdir().unwrap();
let root = File::open(tree.path()).unwrap();
let error = open_relative_file(&root, Path::new("absent"), OFlags::RDONLY).unwrap_err();
assert_eq!(error.kind(), std::io::ErrorKind::NotFound);
}
#[test] #[test]
fn preexisting_transport_parents_require_exact_private_mode() { fn preexisting_transport_parents_require_exact_private_mode() {
for name in ["image-transports", "blobs", "sha256"] { for name in ["image-transports", "blobs", "sha256"] {

View file

@ -14,7 +14,8 @@ use lazaret_core::identity::{
}; };
use lazaret_core::records::{PrepareOperation, PrepareStageState}; use lazaret_core::records::{PrepareOperation, PrepareStageState};
use lazaret_prepared_lower::{ use lazaret_prepared_lower::{
IdMap, IdMapRange, PrepareRecovery, PrepareRequest, PreparedLowerStore, ReclaimOutcome, IdMap, IdMapRange, PrepareRecovery, PrepareRequest, PreparedDestination,
PreparedDestinationKind, PreparedLowerStore, ReclaimOutcome,
}; };
use lazaret_storage_catalog::{Catalog, CatalogError}; use lazaret_storage_catalog::{Catalog, CatalogError};
use serde_json::json; use serde_json::json;
@ -59,6 +60,7 @@ impl Fixture {
source_identity: b"oci-v1\0registry.invalid\0fixture\0sha256:source".to_vec(), source_identity: b"oci-v1\0registry.invalid\0fixture\0sha256:source".to_vec(),
selected_manifest_digest: manifest, selected_manifest_digest: manifest,
id_map: current_id_map(), id_map: current_id_map(),
destinations: Vec::new(),
} }
} }
@ -85,6 +87,55 @@ fn preexisting_fixed_children_require_exact_private_mode() {
} }
} }
#[test]
fn published_artifact_authority_is_revalidated_beneath_the_retained_root() {
let fixture = Fixture::new();
let request = fixture.request(
1,
&[(
layer(&[Entry {
path: "payload",
kind: EntryType::Regular,
data: b"content",
target: None,
mode: 0o644,
}]),
OCI_TAR,
)],
);
let artifact = fixture.store.prepare(&request).unwrap();
let authority = fixture.store.open_artifact_authority(&artifact).unwrap();
let metadata = authority.metadata().unwrap();
assert_eq!(metadata.dev(), artifact.device.get());
assert_eq!(metadata.ino(), artifact.inode.get());
}
#[test]
fn replaced_published_artifact_is_refused() {
let fixture = Fixture::new();
let request = fixture.request(
1,
&[(
layer(&[Entry {
path: "payload",
kind: EntryType::Regular,
data: b"content",
target: None,
mode: 0o644,
}]),
OCI_TAR,
)],
);
let artifact = fixture.store.prepare(&request).unwrap();
let artifact_root = fixture.artifact_root(&artifact);
let displaced = fixture.root.join("displaced-artifact");
std::fs::rename(&artifact_root, &displaced).unwrap();
std::fs::create_dir(&artifact_root).unwrap();
assert!(fixture.store.open_artifact_authority(&artifact).is_err());
}
#[test] #[test]
#[allow(clippy::disallowed_methods, clippy::disallowed_types)] #[allow(clippy::disallowed_methods, clippy::disallowed_types)]
fn retained_fixed_child_bind_mount_is_refused_when_mount_setup_is_available() { fn retained_fixed_child_bind_mount_is_refused_when_mount_setup_is_available() {
@ -155,6 +206,10 @@ struct Entry<'a> {
fn layer(entries: &[Entry<'_>]) -> Vec<u8> { fn layer(entries: &[Entry<'_>]) -> Vec<u8> {
let uid = rustix::process::geteuid().as_raw(); let uid = rustix::process::geteuid().as_raw();
let gid = rustix::process::getegid().as_raw(); let gid = rustix::process::getegid().as_raw();
layer_with_owner(entries, uid, gid)
}
fn layer_with_owner(entries: &[Entry<'_>], uid: u32, gid: u32) -> Vec<u8> {
let mut bytes = Vec::new(); let mut bytes = Vec::new();
{ {
let mut builder = Builder::new(&mut bytes); let mut builder = Builder::new(&mut bytes);
@ -514,6 +569,115 @@ fn layer_entry_matching_internal_spool_name_is_preserved() {
); );
} }
#[test]
fn root_directory_layer_entry_is_applied() {
let fixture = Fixture::new();
let bytes = layer(&[
Entry {
path: ".",
kind: EntryType::Directory,
data: b"",
target: None,
mode: 0o755,
},
Entry {
path: "payload",
kind: EntryType::Regular,
data: b"content",
target: None,
mode: 0o644,
},
]);
let request = fixture.request(5, &[(bytes, OCI_TAR)]);
let artifact = fixture.store.prepare(&request).unwrap();
let root = fixture.artifact_root(&artifact);
assert_eq!(std::fs::read(root.join("payload")).unwrap(), b"content");
assert_eq!(
root.metadata().unwrap().permissions().mode() & 0o7777,
0o755
);
}
#[test]
fn container_root_owner_maps_to_invoking_host_ids() {
let fixture = Fixture::new();
let bytes = layer_with_owner(
&[Entry {
path: "payload",
kind: EntryType::Regular,
data: b"content",
target: None,
mode: 0o640,
}],
0,
0,
);
let mut request = fixture.request(6, &[(bytes, OCI_TAR)]);
request.id_map = IdMap {
uid: vec![IdMapRange {
container_id: 0,
namespace_id: rustix::process::geteuid().as_raw(),
size: 1,
}],
gid: vec![IdMapRange {
container_id: 0,
namespace_id: rustix::process::getegid().as_raw(),
size: 1,
}],
};
let artifact = fixture.store.prepare(&request).unwrap();
let metadata = fixture
.artifact_root(&artifact)
.join("payload")
.metadata()
.unwrap();
assert_eq!(metadata.uid(), rustix::process::geteuid().as_raw());
assert_eq!(metadata.gid(), rustix::process::getegid().as_raw());
}
#[test]
fn unmapped_nonzero_owners_refuse_before_publication() {
for (operation, uid, gid) in [(7, 1, 0), (8, 0, 1)] {
let fixture = Fixture::new();
let bytes = layer_with_owner(
&[Entry {
path: "payload",
kind: EntryType::Regular,
data: b"content",
target: None,
mode: 0o640,
}],
uid,
gid,
);
let mut request = fixture.request(operation, &[(bytes, OCI_TAR)]);
request.id_map = IdMap {
uid: vec![IdMapRange {
container_id: 0,
namespace_id: rustix::process::geteuid().as_raw(),
size: 1,
}],
gid: vec![IdMapRange {
container_id: 0,
namespace_id: rustix::process::getegid().as_raw(),
size: 1,
}],
};
assert!(fixture.store.prepare(&request).is_err());
assert!(
fixture
.catalog
.snapshot()
.unwrap()
.prepared_artifacts
.is_empty()
);
}
}
#[test] #[test]
fn root_opaque_whiteout_removes_only_lower_layer_entries() { fn root_opaque_whiteout_removes_only_lower_layer_entries() {
let fixture = Fixture::new(); let fixture = Fixture::new();
@ -971,6 +1135,98 @@ fn reclaim_refuses_forged_identity_and_leases() {
assert!(fixture.artifact_root(&artifact).exists()); assert!(fixture.artifact_root(&artifact).exists());
} }
#[test]
fn destination_layout_is_materialized_and_canonical_in_artifact_identity() {
let fixture = Fixture::new();
let bytes = layer_with_owner(&[], 0, 0);
let mut first = fixture.request(72, &[(bytes.clone(), OCI_TAR)]);
first.id_map = id_map_with_container_root();
first.destinations = vec![
PreparedDestination {
path: "/workspace".into(),
kind: PreparedDestinationKind::Directory,
},
PreparedDestination {
path: "/run/lazaret/bin/entry".into(),
kind: PreparedDestinationKind::RegularFile,
},
PreparedDestination {
path: "/workspace".into(),
kind: PreparedDestinationKind::Directory,
},
];
let artifact = fixture.store.prepare(&first).unwrap();
let root = fixture.artifact_root(&artifact);
assert!(root.join("workspace").is_dir());
assert!(root.join("run/lazaret/bin/entry").is_file());
let mut reordered = fixture.request(73, &[(bytes.clone(), OCI_TAR)]);
reordered.id_map = id_map_with_container_root();
reordered.destinations = first.destinations.iter().rev().cloned().collect();
assert_eq!(fixture.store.prepare(&reordered).unwrap(), artifact);
let mut unequal = fixture.request(74, &[(bytes, OCI_TAR)]);
unequal.id_map = id_map_with_container_root();
unequal.destinations = vec![PreparedDestination {
path: "/opt/other".into(),
kind: PreparedDestinationKind::Directory,
}];
assert_ne!(
fixture
.store
.prepare(&unequal)
.unwrap()
.prepared_artifact_id,
artifact.prepared_artifact_id
);
}
#[test]
fn destination_type_conflict_refuses_before_publication() {
let fixture = Fixture::new();
let bytes = layer_with_owner(
&[Entry {
path: "opt/lazaret-state",
kind: EntryType::Regular,
data: b"conflict",
target: None,
mode: 0o644,
}],
0,
0,
);
let mut request = fixture.request(75, &[(bytes, OCI_TAR)]);
request.id_map = id_map_with_container_root();
request.destinations = vec![PreparedDestination {
path: "/opt/lazaret-state".into(),
kind: PreparedDestinationKind::Directory,
}];
assert!(fixture.store.prepare(&request).is_err());
assert!(
fixture
.catalog
.snapshot()
.unwrap()
.prepared_artifacts
.is_empty()
);
}
fn id_map_with_container_root() -> IdMap {
IdMap {
uid: vec![IdMapRange {
container_id: 0,
namespace_id: rustix::process::geteuid().as_raw(),
size: 1,
}],
gid: vec![IdMapRange {
container_id: 0,
namespace_id: rustix::process::getegid().as_raw(),
size: 1,
}],
}
}
fn hex(bytes: &[u8]) -> String { fn hex(bytes: &[u8]) -> String {
use std::fmt::Write as _; use std::fmt::Write as _;

View file

@ -7,8 +7,8 @@ use std::time::{Duration, Instant};
use lazaret_core::codec::decode; use lazaret_core::codec::decode;
use lazaret_core::identity::{ use lazaret_core::identity::{
ConfigurationFingerprint, DerivedPolicyDigest, DomainId, GenerationId, NetworkSidecarId, ConfigurationFingerprint, DerivedPolicyDigest, DomainId, GenerationId, NamespaceIdentity,
PidfsProcessIdentityV1, NetworkSidecarId, PidfsProcessIdentityV1,
}; };
use lazaret_core::network::{CanonicalDerivedNetworkPolicy, derive_policy}; use lazaret_core::network::{CanonicalDerivedNetworkPolicy, derive_policy};
use lazaret_core::records::{ use lazaret_core::records::{
@ -175,7 +175,7 @@ where
}, },
stage, stage,
acknowledge, acknowledge,
enter_domain_namespaces, enter_generation_network_namespace,
)?; )?;
drop(helper_ready_signal); drop(helper_ready_signal);
drop(helper_exit); drop(helper_exit);
@ -214,18 +214,28 @@ where
} }
#[allow(deprecated)] #[allow(deprecated)]
fn enter_domain_namespaces() -> io::Result<()> { pub(crate) fn enter_generation_filesystem_namespaces()
-> io::Result<(NamespaceIdentity, NamespaceIdentity)> {
let uid = getuid().as_raw(); let uid = getuid().as_raw();
let gid = getgid().as_raw(); let gid = getgid().as_raw();
rustix::thread::unshare(UnshareFlags::NEWUSER)?; rustix::thread::unshare(UnshareFlags::NEWUSER)?;
std::fs::write("/proc/self/setgroups", "deny")?; std::fs::write("/proc/self/setgroups", "deny")?;
std::fs::write("/proc/self/uid_map", format!("0 {uid} 1\n"))?; std::fs::write("/proc/self/uid_map", format!("0 {uid} 1\n"))?;
std::fs::write("/proc/self/gid_map", format!("0 {gid} 1\n"))?; std::fs::write("/proc/self/gid_map", format!("0 {gid} 1\n"))?;
rustix::thread::unshare(UnshareFlags::NEWNS | UnshareFlags::NEWNET)?; rustix::thread::unshare(UnshareFlags::NEWNS)?;
mount_change( mount_change(
"/", "/",
MountPropagationFlags::PRIVATE | MountPropagationFlags::REC, MountPropagationFlags::PRIVATE | MountPropagationFlags::REC,
)?; )?;
Ok((
namespace_identity(Path::new("/proc/self/ns/user"))?,
namespace_identity(Path::new("/proc/self/ns/mnt"))?,
))
}
#[allow(deprecated)]
fn enter_generation_network_namespace() -> io::Result<()> {
rustix::thread::unshare(UnshareFlags::NEWNET)?;
Ok(()) Ok(())
} }

View file

@ -219,6 +219,8 @@ pub struct SandboxHandoffAuthority<'configuration> {
configuration_authority: ConfigurationAuthorityIdentity, configuration_authority: ConfigurationAuthorityIdentity,
command: RunscCommand, command: RunscCommand,
derived: Vec<DerivedHandoffAuthority>, derived: Vec<DerivedHandoffAuthority>,
prepared_root: PathBuf,
prepared_root_identity: FileIdentity,
} }
impl SandboxHandoffAuthority<'_> { impl SandboxHandoffAuthority<'_> {
@ -241,17 +243,30 @@ impl SandboxHandoffAuthority<'_> {
self.configuration self.configuration
.workspace() .workspace()
.open_authority() .open_authority()
.map_err(changed_configuration_authority)?; .map_err(|error| changed_authority_at("workspace", error))?;
for volume in self.configuration.volumes() { for volume in self.configuration.volumes() {
volume volume
.open_source() .open_source()
.map_err(changed_configuration_authority)?; .map_err(|error| changed_authority_at("volume", error))?;
}
let prepared_root = open_absolute_directory(&self.prepared_root)?;
if descriptor_identity(&prepared_root).map_err(|error| {
SandboxBuildError::new(
SandboxBuildErrorKind::ChangedAuthority,
format!("prepared root identity: {error}"),
)
})? != self.prepared_root_identity
{
return Err(SandboxBuildError::new(
SandboxBuildErrorKind::ChangedAuthority,
"prepared root changed identity after bundle construction",
));
} }
let runtime_root = self let runtime_root = self
.configuration .configuration
.runtime_domain_root() .runtime_domain_root()
.open_authority() .open_authority()
.map_err(changed_configuration_authority)?; .map_err(|error| changed_authority_at("Runtime Domain", error))?;
for authority in &self.derived { for authority in &self.derived {
let descriptor = open_runtime_source( let descriptor = open_runtime_source(
&runtime_root, &runtime_root,
@ -259,7 +274,16 @@ impl SandboxHandoffAuthority<'_> {
authority.expected_type, authority.expected_type,
authority.expected_bytes.is_some(), authority.expected_bytes.is_some(),
)?; )?;
if descriptor_identity(&descriptor)? != authority.identity { if descriptor_identity(&descriptor).map_err(|error| {
SandboxBuildError::new(
SandboxBuildErrorKind::ChangedAuthority,
format!(
"runtime handoff source {} identity: {error}",
authority.relative_path.display()
),
)
})? != authority.identity
{
return Err(SandboxBuildError::new( return Err(SandboxBuildError::new(
SandboxBuildErrorKind::ChangedAuthority, SandboxBuildErrorKind::ChangedAuthority,
"runtime handoff source changed identity", "runtime handoff source changed identity",
@ -416,6 +440,7 @@ pub struct SandboxBuildRequest<'a> {
pub expected_network_namespace: NamespaceIdentity, pub expected_network_namespace: NamespaceIdentity,
pub root_process: RootProcess, pub root_process: RootProcess,
pub terminal: Option<StagedHostTerminal>, pub terminal: Option<StagedHostTerminal>,
pub prepared_root: PathBuf,
} }
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
@ -458,6 +483,8 @@ pub struct SandboxBundle {
configuration_fingerprint: ConfigurationFingerprint, configuration_fingerprint: ConfigurationFingerprint,
configuration_authority: ConfigurationAuthorityIdentity, configuration_authority: ConfigurationAuthorityIdentity,
runtime_sources: Vec<RuntimeHandoffSource>, runtime_sources: Vec<RuntimeHandoffSource>,
prepared_root: PathBuf,
prepared_root_identity: FileIdentity,
} }
impl SandboxBundle { impl SandboxBundle {
@ -497,16 +524,29 @@ impl SandboxBundle {
configuration configuration
.workspace() .workspace()
.open_authority() .open_authority()
.map_err(changed_configuration_authority)?; .map_err(|error| changed_authority_at("workspace", error))?;
for volume in configuration.volumes() { for volume in configuration.volumes() {
volume volume
.open_source() .open_source()
.map_err(changed_configuration_authority)?; .map_err(|error| changed_authority_at("volume", error))?;
} }
let runtime_root = configuration let runtime_root = configuration
.runtime_domain_root() .runtime_domain_root()
.open_authority() .open_authority()
.map_err(changed_configuration_authority)?; .map_err(|error| changed_authority_at("Runtime Domain", error))?;
let prepared_root = open_absolute_directory(&self.prepared_root)?;
if descriptor_identity(&prepared_root).map_err(|error| {
SandboxBuildError::new(
SandboxBuildErrorKind::ChangedAuthority,
format!("prepared root identity: {error}"),
)
})? != self.prepared_root_identity
{
return Err(SandboxBuildError::new(
SandboxBuildErrorKind::ChangedAuthority,
"prepared root changed identity after bundle construction",
));
}
let derived = self let derived = self
.runtime_sources .runtime_sources
.iter() .iter()
@ -517,7 +557,15 @@ impl SandboxBundle {
source.expected_type, source.expected_type,
source.expected_bytes.is_some(), source.expected_bytes.is_some(),
)?; )?;
let identity = descriptor_identity(&descriptor)?; let identity = descriptor_identity(&descriptor).map_err(|error| {
SandboxBuildError::new(
SandboxBuildErrorKind::ChangedAuthority,
format!(
"runtime handoff source {} identity: {error}",
source.relative_path.display()
),
)
})?;
if source if source
.build_identity .build_identity
.is_some_and(|expected| expected != identity) .is_some_and(|expected| expected != identity)
@ -545,6 +593,8 @@ impl SandboxBundle {
configuration_authority: self.configuration_authority.clone(), configuration_authority: self.configuration_authority.clone(),
command: self.command.clone(), command: self.command.clone(),
derived, derived,
prepared_root: self.prepared_root.clone(),
prepared_root_identity: self.prepared_root_identity,
}) })
} }
} }
@ -590,6 +640,8 @@ pub fn build_sandbox_bundle(
if let Some(terminal) = &request.terminal { if let Some(terminal) = &request.terminal {
terminal.revalidate(request.configuration)?; terminal.revalidate(request.configuration)?;
} }
let prepared_root = open_absolute_directory(&request.prepared_root)?;
let prepared_root_identity = descriptor_identity(&prepared_root)?;
let paths = runtime_paths(request.configuration, request.container_id)?; let paths = runtime_paths(request.configuration, request.container_id)?;
let mounts = build_mounts( let mounts = build_mounts(
@ -604,7 +656,7 @@ pub fn build_sandbox_bundle(
)?; )?;
let linux = build_linux()?; let linux = build_linux()?;
let root = RootBuilder::default() let root = RootBuilder::default()
.path(paths.rootfs.clone()) .path(request.prepared_root.clone())
.readonly(false) .readonly(false)
.build() .build()
.map_err(oci_error)?; .map_err(oci_error)?;
@ -632,6 +684,8 @@ pub fn build_sandbox_bundle(
configuration_fingerprint: request.configuration.configuration_fingerprint(), configuration_fingerprint: request.configuration.configuration_fingerprint(),
configuration_authority: ConfigurationAuthorityIdentity::capture(request.configuration), configuration_authority: ConfigurationAuthorityIdentity::capture(request.configuration),
runtime_sources, runtime_sources,
prepared_root: request.prepared_root,
prepared_root_identity,
}) })
} }
@ -639,7 +693,7 @@ struct RuntimePaths {
container_name: String, container_name: String,
bundle: PathBuf, bundle: PathBuf,
runsc_root: PathBuf, runsc_root: PathBuf,
rootfs: PathBuf, overlay: PathBuf,
control_sources: PathBuf, control_sources: PathBuf,
} }
@ -662,7 +716,7 @@ fn handoff_runtime_sources(
), ),
(PathBuf::from("runsc"), HandoffPathType::Directory), (PathBuf::from("runsc"), HandoffPathType::Directory),
( (
PathBuf::from("state-anchor/merged"), PathBuf::from("runsc-overlay").join(&paths.container_name),
HandoffPathType::Directory, HandoffPathType::Directory,
), ),
(container.clone(), HandoffPathType::Directory), (container.clone(), HandoffPathType::Directory),
@ -699,6 +753,14 @@ fn handoff_runtime_sources(
} }
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
for name in ["resolv.conf", "hosts", "hostname"] {
sources.push(RuntimeHandoffSource {
relative_path: Path::new("state-anchor/runtime-files").join(name),
expected_type: HandoffPathType::RegularFile,
build_identity: None,
expected_bytes: None,
});
}
if let Some(terminal) = terminal { if let Some(terminal) = terminal {
sources.push(RuntimeHandoffSource { sources.push(RuntimeHandoffSource {
relative_path: terminal.relative_source.clone(), relative_path: terminal.relative_source.clone(),
@ -727,7 +789,15 @@ fn open_runtime_source(
Mode::empty(), Mode::empty(),
ResolveFlags::BENEATH | ResolveFlags::NO_MAGICLINKS | ResolveFlags::NO_SYMLINKS, ResolveFlags::BENEATH | ResolveFlags::NO_MAGICLINKS | ResolveFlags::NO_SYMLINKS,
) )
.map_err(staging_error)?; .map_err(|error| {
SandboxBuildError::new(
SandboxBuildErrorKind::ChangedAuthority,
format!(
"runtime handoff source {}: {error}",
relative_path.display()
),
)
})?;
let stat = rustix::fs::fstat(&descriptor).map_err(staging_error)?; let stat = rustix::fs::fstat(&descriptor).map_err(staging_error)?;
let actual = FileType::from_raw_mode(stat.st_mode); let actual = FileType::from_raw_mode(stat.st_mode);
let matches = match expected_type { let matches = match expected_type {
@ -793,7 +863,7 @@ fn runtime_paths(
Ok(RuntimePaths { Ok(RuntimePaths {
bundle: runtime_domain.join("bundles").join(&container_name), bundle: runtime_domain.join("bundles").join(&container_name),
runsc_root: runtime_domain.join("runsc"), runsc_root: runtime_domain.join("runsc"),
rootfs: runtime_domain.join("state-anchor").join("merged"), overlay: runtime_domain.join("runsc-overlay").join(&container_name),
control_sources: runtime_domain.join("sandbox-control").join(&container_name), control_sources: runtime_domain.join("sandbox-control").join(&container_name),
container_name, container_name,
}) })
@ -811,6 +881,17 @@ fn build_mounts(
false, false,
false, false,
)?); )?);
let runtime_domain = host_path(configuration.runtime_domain_root().canonical_label())?;
for destination in &configuration.fingerprint_record().persistent_dirs {
mounts.push(bind_mount(
runtime_domain
.join("state-anchor/merged")
.join(destination.strip_prefix('/').unwrap_or(destination)),
PathBuf::from(destination),
false,
false,
)?);
}
for volume in configuration.volumes() { for volume in configuration.volumes() {
let record = volume.record(); let record = volume.record();
mounts.push(bind_mount( mounts.push(bind_mount(
@ -821,6 +902,14 @@ fn build_mounts(
)?); )?);
} }
mounts.extend(control_mounts(control_sources)?); mounts.extend(control_mounts(control_sources)?);
for name in ["resolv.conf", "hosts", "hostname"] {
mounts.push(bind_mount(
runtime_domain.join("state-anchor/runtime-files").join(name),
Path::new("/etc").join(name),
true,
true,
)?);
}
if let Some(terminal) = terminal { if let Some(terminal) = terminal {
mounts.push(bind_mount( mounts.push(bind_mount(
host_path(configuration.runtime_domain_root().canonical_label())? host_path(configuration.runtime_domain_root().canonical_label())?
@ -858,13 +947,13 @@ fn build_linux() -> Result<oci_spec::runtime::Linux, SandboxBuildError> {
.collect::<Result<Vec<_>, _>>()?; .collect::<Result<Vec<_>, _>>()?;
let uid_mapping = LinuxIdMappingBuilder::default() let uid_mapping = LinuxIdMappingBuilder::default()
.container_id(0_u32) .container_id(0_u32)
.host_id(rustix::process::getuid().as_raw()) .host_id(0_u32)
.size(1_u32) .size(1_u32)
.build() .build()
.map_err(oci_error)?; .map_err(oci_error)?;
let gid_mapping = LinuxIdMappingBuilder::default() let gid_mapping = LinuxIdMappingBuilder::default()
.container_id(0_u32) .container_id(0_u32)
.host_id(rustix::process::getgid().as_raw()) .host_id(0_u32)
.size(1_u32) .size(1_u32)
.build() .build()
.map_err(oci_error)?; .map_err(oci_error)?;
@ -891,6 +980,13 @@ fn build_runsc_command(paths: &RuntimePaths, bundle_authorization: [u8; 32]) ->
OsString::from("-rootless"), OsString::from("-rootless"),
OsString::from("-network=host"), OsString::from("-network=host"),
OsString::from("-ignore-cgroups"), OsString::from("-ignore-cgroups"),
OsString::from(format!(
"-overlay2=root:dir={}",
paths
.overlay
.to_str()
.expect("validated runtime path remains UTF-8")
)),
OsString::from("run"), OsString::from("run"),
OsString::from(format!( OsString::from(format!(
"-bundle={}", "-bundle={}",
@ -905,6 +1001,23 @@ fn build_runsc_command(paths: &RuntimePaths, bundle_authorization: [u8; 32]) ->
) )
} }
fn open_absolute_directory(path: &Path) -> Result<OwnedFd, SandboxBuildError> {
validate_absolute_normalized(path)?;
openat2(
rustix::fs::CWD,
path,
OFlags::PATH | OFlags::DIRECTORY | OFlags::CLOEXEC | OFlags::NOFOLLOW,
Mode::empty(),
ResolveFlags::NO_MAGICLINKS | ResolveFlags::NO_SYMLINKS,
)
.map_err(|error| {
SandboxBuildError::new(
SandboxBuildErrorKind::ChangedAuthority,
format!("prepared root {}: {error}", path.display()),
)
})
}
fn build_process( fn build_process(
configuration: &ResolvedDomainConfiguration, configuration: &ResolvedDomainConfiguration,
root_process: RootProcess, root_process: RootProcess,
@ -1094,6 +1207,13 @@ fn changed_configuration_authority(error: impl fmt::Display) -> SandboxBuildErro
SandboxBuildError::new(SandboxBuildErrorKind::ChangedAuthority, error.to_string()) SandboxBuildError::new(SandboxBuildErrorKind::ChangedAuthority, error.to_string())
} }
fn changed_authority_at(label: &str, error: impl fmt::Display) -> SandboxBuildError {
SandboxBuildError::new(
SandboxBuildErrorKind::ChangedAuthority,
format!("{label}: {error}"),
)
}
fn oci_error(error: impl fmt::Display) -> SandboxBuildError { fn oci_error(error: impl fmt::Display) -> SandboxBuildError {
SandboxBuildError::new(SandboxBuildErrorKind::OciSpecification, error.to_string()) SandboxBuildError::new(SandboxBuildErrorKind::OciSpecification, error.to_string())
} }

View file

@ -3,6 +3,7 @@ use std::fs::File;
use std::io; use std::io;
use std::os::fd::{AsFd, AsRawFd, OwnedFd}; use std::os::fd::{AsFd, AsRawFd, OwnedFd};
use std::os::unix::fs::FileExt; use std::os::unix::fs::FileExt;
use std::path::PathBuf;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use lazaret_core::codec::{decode, encode}; use lazaret_core::codec::{decode, encode};
@ -32,8 +33,8 @@ use rustix::thread::CapabilitySet;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::network::{ use crate::network::{
NetworkAttestationContext, NetworkProbeRequest, PreparedNetwork, prepare_network, NetworkAttestationContext, NetworkProbeRequest, PreparedNetwork,
probe_current_network, enter_generation_filesystem_namespaces, prepare_network, probe_current_network,
}; };
use crate::sandbox::RunscCommand; use crate::sandbox::RunscCommand;
@ -64,6 +65,8 @@ pub enum SupervisorEvent {
StateAnchorStaged(StagedProcessOwnership), StateAnchorStaged(StagedProcessOwnership),
StateAnchorReady { StateAnchorReady {
staged: StagedProcessOwnership, staged: StagedProcessOwnership,
user_namespace: lazaret_core::identity::NamespaceIdentity,
mount_namespace: lazaret_core::identity::NamespaceIdentity,
mount: lazaret_core::identity::MountIdentity, mount: lazaret_core::identity::MountIdentity,
}, },
SidecarStarted, SidecarStarted,
@ -80,7 +83,9 @@ pub enum SupervisorEvent {
SignalApplied(AttachedRunscSignal), SignalApplied(AttachedRunscSignal),
SandboxStopped, SandboxStopped,
SidecarStopped, SidecarStopped,
StateAnchorUnmounted, StateAnchorUnmounted {
mount: lazaret_core::identity::MountIdentity,
},
Exited, Exited,
AttachedRunscExited, AttachedRunscExited,
Stopped, Stopped,
@ -362,14 +367,31 @@ impl RuntimeSupervisorLaunch {
target = target target = target
.argument("--state-lower-fd") .argument("--state-lower-fd")
.argument(authority.lower_fd().to_string()) .argument(authority.lower_fd().to_string())
.argument("--state-lower-label")
.argument(authority.lower_label())
.argument("--state-persistent-root-fd")
.argument(authority.persistent_root_fd().to_string())
.argument("--state-persistent-fd") .argument("--state-persistent-fd")
.argument(authority.persistent_anchor_fd().to_string()) .argument(authority.persistent_anchor_fd().to_string())
.argument("--state-persistent-label")
.argument(authority.persistent_anchor_label())
.argument("--state-runtime-root-fd")
.argument(authority.runtime_root_fd().to_string())
.argument("--state-runtime-fd") .argument("--state-runtime-fd")
.argument(authority.runtime_anchor_fd().to_string()) .argument(authority.runtime_anchor_fd().to_string())
.argument("--state-runtime-label")
.argument(authority.runtime_anchor_label())
.argument("--state-lower-device") .argument("--state-lower-device")
.argument(device.to_string()) .argument(device.to_string())
.argument("--state-lower-inode") .argument("--state-lower-inode")
.argument(inode.to_string()); .argument(inode.to_string())
.argument("--state-persistent-count")
.argument(authority.persistent_destinations().len().to_string());
for destination in authority.persistent_destinations() {
target = target
.argument("--state-persistent-destination")
.argument(destination);
}
} }
if let (Some(policy_file), Some(digest), Some(fingerprint), Some(sidecar)) = ( if let (Some(policy_file), Some(digest), Some(fingerprint), Some(sidecar)) = (
&self.policy_file, &self.policy_file,
@ -468,9 +490,15 @@ struct RuntimeArguments {
struct RuntimeStateAnchorArguments { struct RuntimeStateAnchorArguments {
lower_fd: i32, lower_fd: i32,
lower_label: PathBuf,
persistent_root_fd: i32,
persistent_anchor_fd: i32, persistent_anchor_fd: i32,
persistent_anchor_label: PathBuf,
runtime_root_fd: i32,
runtime_anchor_fd: i32, runtime_anchor_fd: i32,
runtime_anchor_label: PathBuf,
expected_lower: (u64, u64), expected_lower: (u64, u64),
persistent_destinations: Vec<PathBuf>,
} }
struct RuntimeNetworkArguments { struct RuntimeNetworkArguments {
@ -492,14 +520,35 @@ fn parse_runtime_arguments(arguments: &[OsString]) -> io::Result<RuntimeArgument
.peek() .peek()
.is_some_and(|value| *value == "--state-lower-fd") .is_some_and(|value| *value == "--state-lower-fd")
{ {
Some(RuntimeStateAnchorArguments { let lower_fd = parse_number(named(&mut arguments, "--state-lower-fd")?)?;
lower_fd: parse_number(named(&mut arguments, "--state-lower-fd")?)?, let lower_label = PathBuf::from(named(&mut arguments, "--state-lower-label")?);
persistent_anchor_fd: parse_number(named(&mut arguments, "--state-persistent-fd")?)?, let persistent_root_fd =
runtime_anchor_fd: parse_number(named(&mut arguments, "--state-runtime-fd")?)?, parse_number(named(&mut arguments, "--state-persistent-root-fd")?)?;
expected_lower: ( let persistent_anchor_fd = parse_number(named(&mut arguments, "--state-persistent-fd")?)?;
let persistent_anchor_label =
PathBuf::from(named(&mut arguments, "--state-persistent-label")?);
let runtime_root_fd = parse_number(named(&mut arguments, "--state-runtime-root-fd")?)?;
let runtime_anchor_fd = parse_number(named(&mut arguments, "--state-runtime-fd")?)?;
let runtime_anchor_label = PathBuf::from(named(&mut arguments, "--state-runtime-label")?);
let expected_lower = (
parse_number(named(&mut arguments, "--state-lower-device")?)?, parse_number(named(&mut arguments, "--state-lower-device")?)?,
parse_number(named(&mut arguments, "--state-lower-inode")?)?, parse_number(named(&mut arguments, "--state-lower-inode")?)?,
), );
let count: usize = parse_number(named(&mut arguments, "--state-persistent-count")?)?;
let persistent_destinations = (0..count)
.map(|_| named(&mut arguments, "--state-persistent-destination").map(PathBuf::from))
.collect::<io::Result<Vec<_>>>()?;
Some(RuntimeStateAnchorArguments {
lower_fd,
lower_label,
persistent_root_fd,
persistent_anchor_fd,
persistent_anchor_label,
runtime_root_fd,
runtime_anchor_fd,
runtime_anchor_label,
expected_lower,
persistent_destinations,
}) })
} else { } else {
None None
@ -604,9 +653,10 @@ fn parse_fixed_hex(value: &OsStr) -> io::Result<[u8; 32]> {
fn run_supervisor(arguments: &RuntimeArguments) -> io::Result<()> { fn run_supervisor(arguments: &RuntimeArguments) -> io::Result<()> {
require_lifetime_held()?; require_lifetime_held()?;
let namespaces = enter_generation_filesystem_namespaces()?;
let control = std::io::stdout(); let control = std::io::stdout();
let parent = current_process_identity()?; let parent = current_process_identity()?;
let mut state_anchor = prepare_state_anchor(arguments, parent, &control)?; let mut state_anchor = prepare_state_anchor(arguments, parent, namespaces, &control)?;
let result = run_supervisor_with_state(arguments, parent, &control, &mut state_anchor); let result = run_supervisor_with_state(arguments, parent, &control, &mut state_anchor);
let cleanup = state_anchor.take().map(RuntimeStateAnchorMount::unmount); let cleanup = state_anchor.take().map(RuntimeStateAnchorMount::unmount);
match (result, cleanup) { match (result, cleanup) {
@ -623,6 +673,10 @@ fn run_supervisor(arguments: &RuntimeArguments) -> io::Result<()> {
fn prepare_state_anchor( fn prepare_state_anchor(
arguments: &RuntimeArguments, arguments: &RuntimeArguments,
parent: lazaret_core::identity::PidfsProcessIdentityV1, parent: lazaret_core::identity::PidfsProcessIdentityV1,
namespaces: (
lazaret_core::identity::NamespaceIdentity,
lazaret_core::identity::NamespaceIdentity,
),
control: &std::io::Stdout, control: &std::io::Stdout,
) -> io::Result<Option<RuntimeStateAnchorMount>> { ) -> io::Result<Option<RuntimeStateAnchorMount>> {
let Some(state) = &arguments.state_anchor else { let Some(state) = &arguments.state_anchor else {
@ -636,8 +690,13 @@ fn prepare_state_anchor(
} }
let authority = RuntimeStateAnchorAuthority::from_inherited( let authority = RuntimeStateAnchorAuthority::from_inherited(
state.lower_fd, state.lower_fd,
&state.lower_label,
state.persistent_root_fd,
state.persistent_anchor_fd, state.persistent_anchor_fd,
&state.persistent_anchor_label,
state.runtime_root_fd,
state.runtime_anchor_fd, state.runtime_anchor_fd,
&state.runtime_anchor_label,
state.expected_lower, state.expected_lower,
) )
.map_err(io::Error::other)?; .map_err(io::Error::other)?;
@ -645,7 +704,7 @@ fn prepare_state_anchor(
slot: HostProcessOwnershipSlot::StateAnchorDaemon, slot: HostProcessOwnershipSlot::StateAnchorDaemon,
..arguments.ownership ..arguments.ownership
}; };
authority let mount = authority
.acquire( .acquire(
parent, parent,
ownership, ownership,
@ -665,13 +724,18 @@ fn prepare_state_anchor(
control, control,
&SupervisorEvent::StateAnchorReady { &SupervisorEvent::StateAnchorReady {
staged: readiness.staged(), staged: readiness.staged(),
user_namespace: namespaces.0,
mount_namespace: namespaces.1,
mount: readiness.mount(), mount: readiness.mount(),
}, },
) )
}, },
) )
.map(Some) .map_err(io::Error::other)?;
.map_err(io::Error::other) mount
.require_persistent_destinations(&state.persistent_destinations)
.map_err(io::Error::other)?;
Ok(Some(mount))
} }
fn run_supervisor_with_state( fn run_supervisor_with_state(
@ -698,6 +762,7 @@ fn run_supervisor_with_state(
let pid_file = File::from(memfd_create("lazaret-runsc-pid", MemfdFlags::empty())?); let pid_file = File::from(memfd_create("lazaret-runsc-pid", MemfdFlags::empty())?);
fcntl_setfd(pid_file.as_fd(), FdFlags::empty())?; fcntl_setfd(pid_file.as_fd(), FdFlags::empty())?;
let runsc = runsc_target(arguments, pid_file.as_raw_fd())?; let runsc = runsc_target(arguments, pid_file.as_raw_fd())?;
let delete = runsc_delete_target(arguments)?;
let staged = std::cell::Cell::new(None); let staged = std::cell::Cell::new(None);
let process = match child_broker().acquire_with_stdio( let process = match child_broker().acquire_with_stdio(
parent, parent,
@ -762,7 +827,7 @@ fn run_supervisor_with_state(
io::Error::other("sandbox commit acknowledgment was not received"), io::Error::other("sandbox commit acknowledgment was not received"),
); );
} }
supervise(control, process, network, state_anchor) supervise(control, process, network, state_anchor, &delete)
} }
fn prepare_runtime_network( fn prepare_runtime_network(
@ -858,6 +923,29 @@ fn runsc_target(arguments: &RuntimeArguments, pid_file: i32) -> io::Result<Targe
Ok(target.with_parent_death_kill_edge()) Ok(target.with_parent_death_kill_edge())
} }
fn runsc_delete_target(arguments: &RuntimeArguments) -> io::Result<TargetCommand> {
let run = arguments
.runsc_arguments
.iter()
.position(|argument| argument == "run")
.ok_or_else(|| io::Error::other("attached runsc command has no run operation"))?;
let container = arguments
.runsc_arguments
.last()
.filter(|container| {
!container.is_empty() && !container.as_encoded_bytes().starts_with(b"-")
})
.ok_or_else(|| io::Error::other("attached runsc command has no container ID"))?;
let mut target = TargetCommand::new(arguments.runsc_program.clone());
for argument in &arguments.runsc_arguments[..run] {
target = target.argument(argument);
}
Ok(target
.argument("delete")
.argument("--force")
.argument(container))
}
fn attached_runsc_standard_io(target: &TargetCommand) -> io::Result<ChildStandardIo> { fn attached_runsc_standard_io(target: &TargetCommand) -> io::Result<ChildStandardIo> {
if target.parent_edge() != ParentEdge::ParentDeathKill { if target.parent_edge() != ParentEdge::ParentDeathKill {
return Err(io::Error::other( return Err(io::Error::other(
@ -896,6 +984,7 @@ fn supervise(
process: RetainedProcess, process: RetainedProcess,
mut network: Option<PreparedNetwork>, mut network: Option<PreparedNetwork>,
state_anchor: &mut Option<RuntimeStateAnchorMount>, state_anchor: &mut Option<RuntimeStateAnchorMount>,
delete: &TargetCommand,
) -> io::Result<()> { ) -> io::Result<()> {
let mut process = Some(process); let mut process = Some(process);
loop { loop {
@ -912,6 +1001,7 @@ fn supervise(
.take() .take()
.ok_or_else(|| io::Error::other("attached runsc handle is absent"))?, .ok_or_else(|| io::Error::other("attached runsc handle is absent"))?,
)?; )?;
child_broker().run_transient(delete)?;
stop_network(network)?; stop_network(network)?;
send_message(control, &SupervisorEvent::AttachedRunscExited)?; send_message(control, &SupervisorEvent::AttachedRunscExited)?;
return Ok(()); return Ok(());
@ -933,6 +1023,7 @@ fn supervise(
if lease_lost { if lease_lost {
if let Some(process) = process.take() { if let Some(process) = process.take() {
terminate_and_reap(process)?; terminate_and_reap(process)?;
child_broker().run_transient(delete)?;
} }
stop_network(network)?; stop_network(network)?;
return Ok(()); return Ok(());
@ -969,6 +1060,7 @@ fn supervise(
&mut process, &mut process,
&mut network, &mut network,
state_anchor, state_anchor,
delete,
) { ) {
Ok(true) => return Ok(()), Ok(true) => return Ok(()),
Ok(false) => {} Ok(false) => {}
@ -984,6 +1076,7 @@ fn handle_supervisor_message(
process: &mut Option<RetainedProcess>, process: &mut Option<RetainedProcess>,
network: &mut Option<PreparedNetwork>, network: &mut Option<PreparedNetwork>,
state_anchor: &mut Option<RuntimeStateAnchorMount>, state_anchor: &mut Option<RuntimeStateAnchorMount>,
delete: &TargetCommand,
) -> io::Result<bool> { ) -> io::Result<bool> {
match message { match message {
CoordinatorMessage::SignalAttachedRunsc(signal) => { CoordinatorMessage::SignalAttachedRunsc(signal) => {
@ -999,6 +1092,7 @@ fn handle_supervisor_message(
.take() .take()
.ok_or_else(|| io::Error::other("attached runsc is already stopped"))?, .ok_or_else(|| io::Error::other("attached runsc is already stopped"))?,
)?; )?;
child_broker().run_transient(delete)?;
send_message(control, &SupervisorEvent::SandboxStopped)?; send_message(control, &SupervisorEvent::SandboxStopped)?;
} }
CoordinatorMessage::StopSidecar => { CoordinatorMessage::StopSidecar => {
@ -1014,12 +1108,16 @@ fn handle_supervisor_message(
"State Anchor unmount preceded runtime resource stop", "State Anchor unmount preceded runtime resource stop",
)); ));
} }
let mount = state_anchor
.as_ref()
.ok_or_else(|| io::Error::other("State Anchor is absent"))?
.mount_identity();
state_anchor state_anchor
.take() .take()
.ok_or_else(|| io::Error::other("State Anchor is absent"))? .ok_or_else(|| io::Error::other("State Anchor is absent"))?
.unmount() .unmount()
.map_err(io::Error::other)?; .map_err(io::Error::other)?;
send_message(control, &SupervisorEvent::StateAnchorUnmounted)?; send_message(control, &SupervisorEvent::StateAnchorUnmounted { mount })?;
} }
CoordinatorMessage::Exit => { CoordinatorMessage::Exit => {
if process.is_some() || network.is_some() || state_anchor.is_some() { if process.is_some() || network.is_some() || state_anchor.is_some() {
@ -1307,6 +1405,14 @@ mod tests {
attached_runsc_standard_io(&runsc).unwrap().policy(), attached_runsc_standard_io(&runsc).unwrap().policy(),
ChildStandardIoPolicy::Silenced ChildStandardIoPolicy::Silenced
); );
let delete = runsc_delete_target(&runsc_arguments).unwrap();
assert_eq!(delete.program(), "runsc");
assert_eq!(
delete.arguments(),
["-root=/runtime", "delete", "--force", "container"]
.map(OsString::from)
.as_slice()
);
} }
#[test] #[test]

Some files were not shown because too many files have changed in this diff Show more