Compose selected State Anchor paths over prepared roots
This commit is contained in:
parent
71b0266cf2
commit
4dd2b150d2
24 changed files with 1575 additions and 163 deletions
|
|
@ -1,4 +1,12 @@
|
|||
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() {
|
||||
Ok(path) => path,
|
||||
Err(error) => {
|
||||
|
|
@ -6,8 +14,7 @@ fn main() {
|
|||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
let command =
|
||||
match lazaret_client::parse_command_line(std::env::args_os().skip(1), current_workspace) {
|
||||
let command = match lazaret_client::parse_command_line(arguments, current_workspace) {
|
||||
Ok(command) => command,
|
||||
Err(error) => {
|
||||
eprintln!("invalid command line: {error}");
|
||||
|
|
|
|||
|
|
@ -988,6 +988,8 @@ pub struct StateAnchorOwnership {
|
|||
pub work_path: Option<HostPath>,
|
||||
pub merged_path: Option<HostPath>,
|
||||
pub daemon: PersistedHostProcessOwnership,
|
||||
pub user_namespace: Option<NamespaceIdentity>,
|
||||
pub mount_namespace: Option<NamespaceIdentity>,
|
||||
pub mount: Option<MountIdentity>,
|
||||
}
|
||||
|
||||
|
|
@ -1102,6 +1104,8 @@ impl StateAnchorOwnership {
|
|||
work_path: None,
|
||||
merged_path: None,
|
||||
daemon: PersistedHostProcessOwnership::unassigned(),
|
||||
user_namespace: None,
|
||||
mount_namespace: None,
|
||||
mount: None,
|
||||
}
|
||||
}
|
||||
|
|
@ -1112,6 +1116,8 @@ impl StateAnchorOwnership {
|
|||
&& self.work_path.is_none()
|
||||
&& self.merged_path.is_none()
|
||||
&& self.daemon.is_unassigned()
|
||||
&& self.user_namespace.is_none()
|
||||
&& self.mount_namespace.is_none()
|
||||
&& self.mount.is_none()
|
||||
}
|
||||
|
||||
|
|
@ -1121,6 +1127,8 @@ impl StateAnchorOwnership {
|
|||
&& self.work_path.is_some()
|
||||
&& self.merged_path.is_some()
|
||||
&& self.daemon.is_current_committed()
|
||||
&& self.user_namespace.is_some()
|
||||
&& self.mount_namespace.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.merged_path.as_ref(), next.merged_path.as_ref())?;
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -396,6 +396,8 @@ fn state_anchor_ownership(
|
|||
work_path: Some(HostPath::new(b"work".to_vec())),
|
||||
merged_path: Some(HostPath::new(b"merged".to_vec())),
|
||||
daemon,
|
||||
user_namespace: Some(namespace()),
|
||||
mount_namespace: Some(namespace()),
|
||||
mount: mount_identity,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())),
|
||||
merged_path: Some(HostPath::new(b"merged".to_vec())),
|
||||
daemon: owned_process(63),
|
||||
user_namespace: Some(namespace()),
|
||||
mount_namespace: Some(namespace()),
|
||||
mount: Some(mount()),
|
||||
},
|
||||
&mut seen,
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@ pub struct ProductionBootstrapPlan<'a> {
|
|||
pub bundle_digest: BundleDigest,
|
||||
pub invoking_uid: u32,
|
||||
pub non_interactive_executor: Arc<dyn NonInteractiveSessionExecutor>,
|
||||
pub(crate) runtime_staging: &'a crate::production::RuntimeStaging,
|
||||
}
|
||||
|
||||
pub struct ProductionLifecycleAdapter<'a> {
|
||||
|
|
@ -268,7 +269,7 @@ impl<'a> ProductionLifecycleAdapter<'a> {
|
|||
&coordinator,
|
||||
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);
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -281,7 +282,7 @@ impl<'a> ProductionLifecycleAdapter<'a> {
|
|||
.ok_or_else(|| ProductionLifecycleError("Runtime Supervisor is absent".into()))?
|
||||
.receive_helper_staged(&network)
|
||||
.map(|_| ())
|
||||
.map_err(failure)
|
||||
.map_err(|error| failure(format!("starting connectivity helper: {error}")))
|
||||
}
|
||||
|
||||
fn start_sidecar(&mut self) -> Result<(), ProductionLifecycleError> {
|
||||
|
|
@ -289,7 +290,7 @@ impl<'a> ProductionLifecycleAdapter<'a> {
|
|||
.as_mut()
|
||||
.ok_or_else(|| ProductionLifecycleError("Runtime Supervisor is absent".into()))?
|
||||
.start_sidecar()
|
||||
.map_err(failure)
|
||||
.map_err(|error| failure(format!("starting sidecar: {error}")))
|
||||
}
|
||||
|
||||
fn mark_network_ready(&mut self) -> Result<(), ProductionLifecycleError> {
|
||||
|
|
@ -309,7 +310,7 @@ impl<'a> ProductionLifecycleAdapter<'a> {
|
|||
self.plan.reachability,
|
||||
)
|
||||
.map(|_| ())
|
||||
.map_err(failure)
|
||||
.map_err(|error| failure(format!("marking network ready: {error}")))
|
||||
}
|
||||
|
||||
fn start_sandbox(&mut self) -> Result<(), ProductionLifecycleError> {
|
||||
|
|
@ -319,17 +320,19 @@ impl<'a> ProductionLifecycleAdapter<'a> {
|
|||
.awaiting_runtime
|
||||
.take()
|
||||
.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
|
||||
.plan
|
||||
.sandbox_bundle
|
||||
.capture_handoff_authority(self.plan.configuration)
|
||||
.map_err(failure)?;
|
||||
.map_err(|error| failure(format!("capturing sandbox handoff: {error}")))?;
|
||||
let (runtime, _) = pending
|
||||
.acknowledge(staged, &handoff)
|
||||
.map_err(failure)?
|
||||
.map_err(|error| failure(format!("releasing sandbox handoff: {error}")))?
|
||||
.commit_networked(&sandbox)
|
||||
.map_err(failure)?;
|
||||
.map_err(|error| failure(format!("committing sandbox readiness: {error}")))?;
|
||||
self.runtime = Some(runtime);
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -376,7 +379,8 @@ impl<'a> ProductionLifecycleAdapter<'a> {
|
|||
.as_mut()
|
||||
.ok_or_else(|| ProductionLifecycleError("committed runtime is absent".into()))?
|
||||
.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> {
|
||||
|
|
@ -408,7 +412,14 @@ impl<'a> ProductionLifecycleAdapter<'a> {
|
|||
self.runtime
|
||||
.as_mut()
|
||||
.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)?;
|
||||
self.state_anchor_mount = None;
|
||||
Ok(())
|
||||
|
|
|
|||
|
|
@ -68,6 +68,8 @@ pub struct ProductionColdStartInputs<'a> {
|
|||
pub configuration: &'a ResolvedDomainConfiguration,
|
||||
pub generation: Generation,
|
||||
pub prepared_artifact: &'a PreparedArtifact,
|
||||
pub prepared_artifact_authority: &'a File,
|
||||
pub prepared_artifact_label: &'a Path,
|
||||
pub root_process: RootProcess,
|
||||
pub resolver_addresses: Vec<IpAddress>,
|
||||
pub network_probe: NetworkProbeRequest,
|
||||
|
|
@ -137,7 +139,7 @@ pub struct ProductionColdStart<'a> {
|
|||
runtime_root: PathBuf,
|
||||
bundle_digest: BundleDigest,
|
||||
invoking_uid: u32,
|
||||
_staging: RuntimeStaging,
|
||||
staging: RuntimeStaging,
|
||||
}
|
||||
|
||||
impl<'a> ProductionColdStart<'a> {
|
||||
|
|
@ -173,6 +175,7 @@ impl<'a> ProductionColdStart<'a> {
|
|||
expected_network_namespace,
|
||||
root_process: inputs.root_process,
|
||||
terminal: None,
|
||||
prepared_root: inputs.prepared_artifact_label.to_owned(),
|
||||
})
|
||||
.map_err(|error| failure(format!("sandbox bundle: {error}")))?;
|
||||
staging
|
||||
|
|
@ -186,8 +189,12 @@ impl<'a> ProductionColdStart<'a> {
|
|||
coordinator_id: inputs.generation.coordinator_id,
|
||||
slot: HostProcessOwnershipSlot::SandboxRuntimeSupervisor,
|
||||
};
|
||||
let state_anchor =
|
||||
StateAnchorLaunchAuthority::capture(&state_anchor_layout, inputs.prepared_artifact)
|
||||
let state_anchor = StateAnchorLaunchAuthority::capture_retained(
|
||||
&state_anchor_layout,
|
||||
inputs.prepared_artifact,
|
||||
inputs.prepared_artifact_label,
|
||||
inputs.prepared_artifact_authority,
|
||||
)
|
||||
.map_err(|error| failure(format!("State Anchor authority: {error}")))?;
|
||||
let runtime_launch = RuntimeSupervisorLaunch::from_networked_runsc_command(
|
||||
ownership,
|
||||
|
|
@ -227,7 +234,7 @@ impl<'a> ProductionColdStart<'a> {
|
|||
runtime_root: runtime_domain.join("runsc"),
|
||||
bundle_digest,
|
||||
invoking_uid: inputs.invoking_uid,
|
||||
_staging: staging,
|
||||
staging,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -342,6 +349,7 @@ impl<'a> ProductionColdStart<'a> {
|
|||
)
|
||||
.map_err(failure)?,
|
||||
),
|
||||
runtime_staging: &self.staging,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -362,17 +370,41 @@ fn require_inputs(inputs: &ProductionColdStartInputs<'_>) -> Result<(), Producti
|
|||
Ok(())
|
||||
}
|
||||
|
||||
struct RuntimeStaging {
|
||||
pub(crate) struct RuntimeStaging {
|
||||
bundles: OwnedFd,
|
||||
bundle: OwnedFd,
|
||||
controls: OwnedFd,
|
||||
control: OwnedFd,
|
||||
overlays: OwnedFd,
|
||||
overlay: OwnedFd,
|
||||
name: OsString,
|
||||
bundle_identity: FileIdentity,
|
||||
control_identity: FileIdentity,
|
||||
overlay_identity: FileIdentity,
|
||||
}
|
||||
|
||||
impl RuntimeStaging {
|
||||
pub(crate) fn 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(¤t)
|
||||
|| !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(
|
||||
runtime_domain: &ConfinedPathPlan,
|
||||
container_id: ContainerId,
|
||||
|
|
@ -384,6 +416,8 @@ impl RuntimeStaging {
|
|||
let bundles = open_or_create_private_directory(&root, "bundles", invoking_uid, false)?;
|
||||
let controls =
|
||||
open_or_create_private_directory(&root, "sandbox-control", invoking_uid, false)?;
|
||||
let overlays =
|
||||
open_or_create_private_directory(&root, "runsc-overlay", invoking_uid, false)?;
|
||||
drop(open_or_create_private_directory(
|
||||
&root,
|
||||
"runsc",
|
||||
|
|
@ -401,14 +435,26 @@ impl RuntimeStaging {
|
|||
}
|
||||
};
|
||||
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 {
|
||||
bundles,
|
||||
bundle,
|
||||
controls,
|
||||
control,
|
||||
overlays,
|
||||
overlay,
|
||||
name,
|
||||
bundle_identity,
|
||||
control_identity,
|
||||
overlay_identity,
|
||||
};
|
||||
staging.populate_control(escalation_client, sandbox_init)?;
|
||||
Ok(staging)
|
||||
|
|
@ -535,6 +581,12 @@ impl FileIdentity {
|
|||
|
||||
impl Drop for RuntimeStaging {
|
||||
fn drop(&mut self) {
|
||||
remove_staging_directory(
|
||||
&self.overlays,
|
||||
&self.name,
|
||||
&self.overlay,
|
||||
self.overlay_identity,
|
||||
);
|
||||
remove_staging_directory(
|
||||
&self.controls,
|
||||
&self.name,
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
use std::collections::BTreeSet;
|
||||
use std::fmt;
|
||||
use std::fs::File;
|
||||
use std::os::unix::ffi::OsStringExt;
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::path::Path;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
use lazaret_core::configuration::ResolvedDomainConfiguration;
|
||||
|
|
@ -11,14 +12,19 @@ use lazaret_core::identity::{
|
|||
PrepareOperationId, SandboxId,
|
||||
};
|
||||
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::platform::{PlatformObservation, check_platform};
|
||||
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::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 rustix::fs::{Mode, OFlags, ResolveFlags, openat2};
|
||||
|
||||
|
|
@ -54,6 +60,8 @@ pub struct ProductionSelection {
|
|||
configuration_fingerprint: ConfigurationFingerprint,
|
||||
catalog: Catalog,
|
||||
artifact: PreparedArtifact,
|
||||
artifact_authority: File,
|
||||
artifact_label: PathBuf,
|
||||
generation: Generation,
|
||||
resolver_addresses: Vec<IpAddress>,
|
||||
network_probe: NetworkProbeRequest,
|
||||
|
|
@ -83,6 +91,8 @@ impl ProductionSelection {
|
|||
configuration,
|
||||
generation: self.generation.clone(),
|
||||
prepared_artifact: &self.artifact,
|
||||
prepared_artifact_authority: &self.artifact_authority,
|
||||
prepared_artifact_label: &self.artifact_label,
|
||||
root_process: RootProcess::new(
|
||||
SANDBOX_INIT_DESTINATION,
|
||||
[PRIVATE_SANDBOX_INIT_ROLE.to_owned()],
|
||||
|
|
@ -149,14 +159,29 @@ async fn select_production_cold_start_for_platform(
|
|||
random_id()?,
|
||||
)
|
||||
.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
|
||||
.prepare_transported(
|
||||
PrepareOperationId::new(random_id()?),
|
||||
&image,
|
||||
rootless_id_map(),
|
||||
prepared_destinations(configuration),
|
||||
)
|
||||
.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)?;
|
||||
|
||||
let next_generation = next_generation(&store)?;
|
||||
|
|
@ -179,6 +204,8 @@ async fn select_production_cold_start_for_platform(
|
|||
configuration_fingerprint: configuration.configuration_fingerprint(),
|
||||
catalog,
|
||||
artifact,
|
||||
artifact_authority,
|
||||
artifact_label,
|
||||
generation,
|
||||
resolver_addresses,
|
||||
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> {
|
||||
store
|
||||
.snapshot()
|
||||
|
|
@ -380,6 +467,15 @@ mod tests {
|
|||
id_map.gid[0].namespace_id,
|
||||
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]
|
||||
|
|
|
|||
|
|
@ -411,8 +411,13 @@ impl AwaitingAttachedRunsc {
|
|||
.acknowledge_staged_daemon(staged)
|
||||
.map_err(|error| SandboxCoordinatorError::Io(io::Error::other(error)))?;
|
||||
control.acknowledge_state_anchor(acknowledgment)?;
|
||||
let (ready_staged, mount) = match control.receive()? {
|
||||
SupervisorEvent::StateAnchorReady { staged, mount } => (staged, mount),
|
||||
let (ready_staged, user_namespace, mount_namespace, mount) = match control.receive()? {
|
||||
SupervisorEvent::StateAnchorReady {
|
||||
staged,
|
||||
user_namespace,
|
||||
mount_namespace,
|
||||
mount,
|
||||
} => (staged, user_namespace, mount_namespace, mount),
|
||||
SupervisorEvent::Refused(detail) => {
|
||||
return Err(SandboxCoordinatorError::Io(io::Error::other(detail)));
|
||||
}
|
||||
|
|
@ -430,7 +435,7 @@ impl AwaitingAttachedRunsc {
|
|||
let readiness =
|
||||
lazaret_state_anchor::VerifiedStateAnchorReadiness::from_parts(ready_staged, mount);
|
||||
coordinator
|
||||
.commit_daemon_ready(readiness)
|
||||
.commit_daemon_ready(readiness, user_namespace, mount_namespace)
|
||||
.and_then(|()| {
|
||||
coordinator.commit_prepared(staged.generation_id(), prepared_artifact_id, mount)
|
||||
})
|
||||
|
|
@ -949,14 +954,16 @@ impl CommittedSandboxRuntime {
|
|||
///
|
||||
/// # Errors
|
||||
/// 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()?;
|
||||
if self.control.receive()? == SupervisorEvent::StateAnchorUnmounted {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(SandboxCoordinatorError::InvalidState(
|
||||
"Runtime Supervisor did not unmount the State Anchor",
|
||||
))
|
||||
match self.control.receive()? {
|
||||
SupervisorEvent::StateAnchorUnmounted { mount } if mount == expected_mount => Ok(()),
|
||||
_ => Err(SandboxCoordinatorError::InvalidState(
|
||||
"Runtime Supervisor did not confirm the exact State Anchor unmount",
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,15 +3,15 @@ use std::io;
|
|||
use std::os::unix::ffi::OsStrExt;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use lazaret_core::identity::{GenerationId, MountIdentity, PidfsProcessIdentityV1};
|
||||
use lazaret_core::identity::{
|
||||
GenerationId, MountIdentity, NamespaceIdentity, PidfsProcessIdentityV1,
|
||||
};
|
||||
use lazaret_core::records::{
|
||||
ArtifactLease, HostProcessOwnershipSlot, Participant, ParticipantOwnership, ParticipantRole,
|
||||
ParticipantState, PersistedHostProcessOwnership, PreparedArtifact, PreparedLowerOwnership,
|
||||
ProcessOwnershipCommitAck, StagedProcessOwnership, StateAnchorOwnership,
|
||||
};
|
||||
use lazaret_state_anchor::{
|
||||
StateAnchorError, StateAnchorLayout, VerifiedStateAnchorReadiness, require_mount_absent,
|
||||
};
|
||||
use lazaret_state_anchor::{StateAnchorError, StateAnchorLayout, VerifiedStateAnchorReadiness};
|
||||
use lazaret_storage_catalog::{Catalog, CatalogError};
|
||||
|
||||
use crate::domain_store::{DomainStoreError, DomainStoreMutation, HostCoordinatorDomainStore};
|
||||
|
|
@ -71,6 +71,8 @@ pub struct StateAnchorReleaseAuthority {
|
|||
merged_label: PathBuf,
|
||||
mount: MountIdentity,
|
||||
daemon: PidfsProcessIdentityV1,
|
||||
user_namespace: NamespaceIdentity,
|
||||
mount_namespace: NamespaceIdentity,
|
||||
}
|
||||
|
||||
impl StateAnchorReleaseAuthority {
|
||||
|
|
@ -88,6 +90,16 @@ impl StateAnchorReleaseAuthority {
|
|||
pub const fn daemon(&self) -> PidfsProcessIdentityV1 {
|
||||
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> {
|
||||
|
|
@ -252,6 +264,8 @@ impl<'a> StateAnchorCoordinator<'a> {
|
|||
pub fn commit_daemon_ready(
|
||||
&self,
|
||||
readiness: VerifiedStateAnchorReadiness,
|
||||
user_namespace: NamespaceIdentity,
|
||||
mount_namespace: NamespaceIdentity,
|
||||
) -> Result<(), StateAnchorCoordinatorError> {
|
||||
let staged = readiness.staged();
|
||||
self.require_staged_identity(staged)?;
|
||||
|
|
@ -262,6 +276,8 @@ impl<'a> StateAnchorCoordinator<'a> {
|
|||
self.update_daemon_and_mount(
|
||||
staged.generation_id(),
|
||||
PersistedHostProcessOwnership::committed(staged.identity()),
|
||||
Some(user_namespace),
|
||||
Some(mount_namespace),
|
||||
Some(readiness.mount()),
|
||||
)
|
||||
}
|
||||
|
|
@ -365,7 +381,6 @@ impl<'a> StateAnchorCoordinator<'a> {
|
|||
"release authority layout changed",
|
||||
));
|
||||
}
|
||||
require_mount_absent(layout)?;
|
||||
let anchor = self.participant(authority.generation_id, ParticipantRole::StateAnchor)?;
|
||||
let prepared = self.participant(authority.generation_id, ParticipantRole::PreparedLower)?;
|
||||
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
|
||||
.apply(&[DomainStoreMutation::CleanParticipantAfterAbsence(
|
||||
clean_participant(authority.generation_id, ParticipantRole::PreparedLower)?,
|
||||
|
|
@ -426,13 +441,15 @@ impl<'a> StateAnchorCoordinator<'a> {
|
|||
generation_id: GenerationId,
|
||||
daemon: PersistedHostProcessOwnership,
|
||||
) -> 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(
|
||||
&self,
|
||||
generation_id: GenerationId,
|
||||
daemon: PersistedHostProcessOwnership,
|
||||
user_namespace: Option<NamespaceIdentity>,
|
||||
mount_namespace: Option<NamespaceIdentity>,
|
||||
mount: Option<MountIdentity>,
|
||||
) -> Result<(), StateAnchorCoordinatorError> {
|
||||
let current = self.participant(generation_id, ParticipantRole::StateAnchor)?;
|
||||
|
|
@ -448,6 +465,8 @@ impl<'a> StateAnchorCoordinator<'a> {
|
|||
}
|
||||
let next = StateAnchorOwnership {
|
||||
daemon,
|
||||
user_namespace,
|
||||
mount_namespace,
|
||||
mount,
|
||||
..ownership.clone()
|
||||
};
|
||||
|
|
@ -512,7 +531,6 @@ impl<'a> StateAnchorCoordinator<'a> {
|
|||
fn release_catalog_lease_if_present(
|
||||
&self,
|
||||
authority: &StateAnchorReleaseAuthority,
|
||||
layout: &StateAnchorLayout<'_>,
|
||||
) -> Result<(), StateAnchorCoordinatorError> {
|
||||
let snapshot = self.catalog.snapshot()?;
|
||||
let generation_lease = snapshot.artifact_leases.iter().find(|lease| {
|
||||
|
|
@ -552,7 +570,7 @@ impl<'a> StateAnchorCoordinator<'a> {
|
|||
"Domain release evidence is not committed",
|
||||
)));
|
||||
}
|
||||
require_mount_absent(layout).map_err(|error| CatalogError::Io(io::Error::other(error)))
|
||||
Ok(())
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -610,6 +628,12 @@ fn release_authority(
|
|||
.daemon
|
||||
.serviceable_pidfs()
|
||||
.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"),
|
||||
)?,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,9 +9,10 @@ use lazaret_core::configuration::{
|
|||
};
|
||||
use lazaret_core::identity::{
|
||||
BootId, ConfigurationFingerprint, CoordinatorId, DeviceId, GenerationId, HostPath,
|
||||
HostPidLocator, IdMapDigest, InodeId, MountId, MountIdentity, NetworkSidecarId, PidfsDeviceId,
|
||||
PidfsInodeId, PidfsProcessAuthorityV1, PidfsProcessIdentityV1, PolicyId, PrepareOperationId,
|
||||
PreparedArtifactId, ProcessStartTimeTicks, SandboxId, SelectedManifestDigest, SourceIdentity,
|
||||
HostPidLocator, IdMapDigest, InodeId, MountId, MountIdentity, NamespaceIdentity,
|
||||
NetworkSidecarId, PidfsDeviceId, PidfsInodeId, PidfsProcessAuthorityV1, PidfsProcessIdentityV1,
|
||||
PolicyId, PrepareOperationId, PreparedArtifactId, ProcessStartTimeTicks, SandboxId,
|
||||
SelectedManifestDigest, SourceIdentity,
|
||||
};
|
||||
use lazaret_core::records::{
|
||||
ArtifactState, Domain, DurableDecision, Generation, ParticipantRole, ParticipantState,
|
||||
|
|
@ -138,6 +139,14 @@ fn durable_phases_resume_release_without_collapsing_lease_order() {
|
|||
ParticipantState::Intent,
|
||||
ParticipantOwnership::StateAnchor(StateAnchorOwnership {
|
||||
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_id: MountId::new(90),
|
||||
device: DeviceId::new(91),
|
||||
|
|
|
|||
|
|
@ -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 bundle = fixture.bundle();
|
||||
let control = fixture.control();
|
||||
let overlay = fixture.overlay();
|
||||
assert_private_artifacts(&bundle, &control);
|
||||
assert!(overlay.is_dir());
|
||||
drop(staged);
|
||||
assert!(!bundle.exists());
|
||||
assert!(!control.exists());
|
||||
assert!(!overlay.exists());
|
||||
assert!(
|
||||
ProductionColdStart::stage(fixture.inputs(&fixture.invalid_escalation_client)).is_err()
|
||||
);
|
||||
|
|
@ -90,6 +93,7 @@ struct ProductionFixture {
|
|||
catalog: Catalog,
|
||||
store: Arc<HostCoordinatorDomainStore>,
|
||||
artifact: PreparedArtifact,
|
||||
artifact_authority: File,
|
||||
generation: Generation,
|
||||
container_id: ContainerId,
|
||||
escalation_client: File,
|
||||
|
|
@ -139,6 +143,7 @@ impl ProductionFixture {
|
|||
let lower = tree.path().join("prepared-lower");
|
||||
fs::create_dir(&lower).unwrap();
|
||||
let metadata = lower.metadata().unwrap();
|
||||
let artifact_authority = File::open(&lower).unwrap();
|
||||
let artifact = PreparedArtifact {
|
||||
prepared_artifact_id: PreparedArtifactId::new([8; 32]),
|
||||
source_identity: SourceIdentity::new(b"synthetic-spike".to_vec()),
|
||||
|
|
@ -166,6 +171,7 @@ impl ProductionFixture {
|
|||
catalog,
|
||||
store,
|
||||
artifact,
|
||||
artifact_authority,
|
||||
generation,
|
||||
container_id: ContainerId::new([5; 32]),
|
||||
escalation_client: File::open("/bin/true").unwrap(),
|
||||
|
|
@ -180,6 +186,10 @@ impl ProductionFixture {
|
|||
configuration: &self.configuration,
|
||||
generation: self.generation.clone(),
|
||||
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()])
|
||||
.unwrap(),
|
||||
resolver_addresses: vec![IpAddress::V4([10, 0, 2, 3])],
|
||||
|
|
@ -214,6 +224,12 @@ impl ProductionFixture {
|
|||
.join("sandbox-control")
|
||||
.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) {
|
||||
|
|
|
|||
|
|
@ -686,6 +686,7 @@ impl Fixture {
|
|||
expected_network_namespace: namespace,
|
||||
root_process,
|
||||
terminal: None,
|
||||
prepared_root: runtime_domain.join("prepared-root"),
|
||||
})
|
||||
.map_err(failure)?;
|
||||
Self::finish_build(
|
||||
|
|
@ -743,6 +744,7 @@ impl Fixture {
|
|||
expected_network_namespace: namespace,
|
||||
root_process,
|
||||
terminal: None,
|
||||
prepared_root: runtime_domain.join("prepared-root"),
|
||||
})
|
||||
.map_err(failure)?;
|
||||
Self::finish_build(
|
||||
|
|
@ -846,6 +848,8 @@ impl Fixture {
|
|||
)
|
||||
.map_err(failure)?,
|
||||
terminal: None,
|
||||
prepared_root: host_path(self.configuration.runtime_domain_root().canonical_label())
|
||||
.join("prepared-root"),
|
||||
})
|
||||
.map_err(failure)
|
||||
}
|
||||
|
|
@ -888,7 +892,7 @@ impl NetworkFixture {
|
|||
let runtime_domain = host_path(configuration.runtime_domain_root().canonical_label());
|
||||
let container_id = ContainerId::new([16; 32]);
|
||||
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(
|
||||
std::env::current_exe()?,
|
||||
rootfs.join("bin/lazaret-network-probe"),
|
||||
|
|
@ -917,6 +921,7 @@ impl NetworkFixture {
|
|||
expected_network_namespace: namespace,
|
||||
root_process,
|
||||
terminal: None,
|
||||
prepared_root: runtime_domain.join("prepared-root"),
|
||||
})
|
||||
.map_err(failure)?;
|
||||
let bundle_path = runtime_domain
|
||||
|
|
@ -1177,10 +1182,44 @@ fn stage_rootfs_and_control(
|
|||
runtime_domain: &Path,
|
||||
container_id: ContainerId,
|
||||
) -> Result<(), MatrixError> {
|
||||
let rootfs = runtime_domain.join("state-anchor/merged");
|
||||
for directory in ["bin", "lib64"] {
|
||||
let rootfs = runtime_domain.join("prepared-root");
|
||||
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))?;
|
||||
}
|
||||
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 [
|
||||
("/bin/sh", "bin/sh"),
|
||||
("/bin/sleep", "bin/sleep"),
|
||||
|
|
@ -1199,11 +1238,12 @@ fn stage_rootfs_and_control(
|
|||
fs::create_dir_all(control.join(directory))?;
|
||||
}
|
||||
fs::copy("/bin/true", control.join("bin/lazaret-escalate"))?;
|
||||
fs::copy("/bin/true", control.join("bin/lazaret-sandbox-init"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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 [
|
||||
"bin",
|
||||
"tooling/usr",
|
||||
|
|
@ -1213,6 +1253,17 @@ fn stage_benchmark_rootfs_and_control(runtime_domain: &Path) -> Result<(), Matri
|
|||
"fixtures/host",
|
||||
"fixtures/output",
|
||||
"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))?;
|
||||
}
|
||||
|
|
@ -1224,12 +1275,31 @@ fn stage_benchmark_rootfs_and_control(runtime_domain: &Path) -> Result<(), Matri
|
|||
symlink("usr/lib", rootfs.join("lib"))?;
|
||||
symlink("usr/lib64", rootfs.join("lib64"))?;
|
||||
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]));
|
||||
for directory in ["credentials", "mailbox/requests", "mailbox/results", "bin"] {
|
||||
fs::create_dir_all(control.join(directory))?;
|
||||
}
|
||||
fs::copy("/bin/true", control.join("bin/lazaret-escalate"))?;
|
||||
fs::copy("/bin/true", control.join("bin/lazaret-sandbox-init"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -32,6 +32,18 @@ pub struct IdMap {
|
|||
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)]
|
||||
pub struct PreparationLimits {
|
||||
pub max_layers: usize,
|
||||
|
|
@ -189,11 +201,91 @@ pub(crate) fn apply_layout(
|
|||
bail!("layer {index} diff-ID mismatch");
|
||||
}
|
||||
validate_tar(&spool, id_map, limits, &mut usage)?;
|
||||
state.apply(&spool, rootfs)?;
|
||||
state.apply(&spool, rootfs, id_map)?;
|
||||
}
|
||||
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(¤t, name, Mode::from_raw_mode(0o755)) {
|
||||
Ok(()) => set_materialized_metadata(open_component(¤t, name)?, uid, gid)?,
|
||||
Err(Errno::EXIST) => {}
|
||||
Err(error) => return Err(error.into()),
|
||||
}
|
||||
current = open_component(¤t, 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, ×tamps(0))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_json<T: serde::de::DeserializeOwned>(
|
||||
layout: &LayoutSource<'_>,
|
||||
path: &Path,
|
||||
|
|
@ -315,7 +407,8 @@ fn validate_tar(
|
|||
let mut paths = HashSet::new();
|
||||
for entry in archive.entries()? {
|
||||
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()) {
|
||||
bail!("policy {POLICY_NAME} rejects duplicate layer path");
|
||||
}
|
||||
|
|
@ -353,9 +446,8 @@ fn validate_tar(
|
|||
bail!("policy {POLICY_NAME} rejects PAX metadata");
|
||||
}
|
||||
}
|
||||
validate_owner(entry.header().uid()?, &id_map.uid, "uid")?;
|
||||
validate_owner(entry.header().gid()?, &id_map.gid, "gid")?;
|
||||
let entry_type = entry.header().entry_type();
|
||||
map_owner(entry.header().uid()?, &id_map.uid, "uid")?;
|
||||
map_owner(entry.header().gid()?, &id_map.gid, "gid")?;
|
||||
if !matches!(
|
||||
entry_type,
|
||||
EntryType::Regular
|
||||
|
|
@ -407,17 +499,35 @@ fn normalize_relative(path: &Path) -> Result<PathBuf> {
|
|||
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 {
|
||||
bail!("{name} {value} is outside the target ID map");
|
||||
};
|
||||
if !ranges
|
||||
.iter()
|
||||
.any(|range| value >= range.container_id && value - range.container_id < range.size)
|
||||
{
|
||||
bail!("{name} {value} is outside the target ID map");
|
||||
for range in ranges {
|
||||
if value >= range.container_id && value - range.container_id < range.size {
|
||||
return range
|
||||
.namespace_id
|
||||
.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<()> {
|
||||
|
|
@ -483,7 +593,7 @@ struct 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)?;
|
||||
let mut archive = tar::Archive::new(rewound(tar_source)?);
|
||||
let mut hardlinks = Vec::new();
|
||||
|
|
@ -497,7 +607,7 @@ impl LayerState {
|
|||
if name == ".wh..wh..opq" || name.starts_with(".wh.") {
|
||||
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() {
|
||||
EntryType::Regular | EntryType::Continuous => {
|
||||
forget_directory_tree(&mut self.directories, &path);
|
||||
|
|
@ -557,11 +667,11 @@ struct 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 {
|
||||
path,
|
||||
uid: u32::try_from(header.uid()?).context("entry uid exceeds u32")?,
|
||||
gid: u32::try_from(header.gid()?).context("entry gid exceeds u32")?,
|
||||
uid: map_owner(header.uid()?, &id_map.uid, "uid")?,
|
||||
gid: map_owner(header.gid()?, &id_map.gid, "gid")?,
|
||||
mode: header.mode()?,
|
||||
mtime: header.mtime()?,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ mod layer;
|
|||
mod store;
|
||||
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 transport::{OciRegistryTransport, Platform, RegistryCredentials, TransportedImage};
|
||||
|
|
|
|||
|
|
@ -23,7 +23,8 @@ use rustix::fs::{
|
|||
use sha2::{Digest, Sha256};
|
||||
|
||||
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,
|
||||
};
|
||||
use crate::transport::TransportedImage;
|
||||
|
|
@ -38,6 +39,7 @@ pub struct PrepareRequest {
|
|||
pub source_identity: Vec<u8>,
|
||||
pub selected_manifest_digest: [u8; 32],
|
||||
pub id_map: IdMap,
|
||||
pub destinations: Vec<PreparedDestination>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
|
|
@ -115,6 +117,22 @@ impl PreparedLowerStore {
|
|||
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.
|
||||
///
|
||||
/// # Errors
|
||||
|
|
@ -134,6 +152,7 @@ impl PreparedLowerStore {
|
|||
operation_id: PrepareOperationId,
|
||||
image: &TransportedImage,
|
||||
id_map: IdMap,
|
||||
destinations: Vec<PreparedDestination>,
|
||||
) -> Result<PreparedArtifact> {
|
||||
let layout = image
|
||||
.retained_layout()
|
||||
|
|
@ -144,6 +163,7 @@ impl PreparedLowerStore {
|
|||
source_identity: image.source_identity.clone(),
|
||||
selected_manifest_digest: image.selected_manifest_digest,
|
||||
id_map,
|
||||
destinations,
|
||||
};
|
||||
self.prepare_from_source(&request, &LayoutSource::Retained(layout.root()))
|
||||
}
|
||||
|
|
@ -194,6 +214,7 @@ impl PreparedLowerStore {
|
|||
return Err(error.into());
|
||||
}
|
||||
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))
|
||||
{
|
||||
if self
|
||||
|
|
@ -494,7 +515,13 @@ impl Identities {
|
|||
fn new(request: &PrepareRequest) -> Result<Self> {
|
||||
let source = SourceIdentity::new(request.source_identity.clone());
|
||||
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 = IdMapDigest::new(hash_parts(&[b"lazaret-id-map-v1", &id_map_bytes]));
|
||||
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(
|
||||
operation: &PrepareOperation,
|
||||
path: &Path,
|
||||
|
|
|
|||
|
|
@ -223,8 +223,10 @@ impl OciRegistryTransport {
|
|||
}
|
||||
verify_platform(&config_json, platform)?;
|
||||
validate_manifest(&manifest)?;
|
||||
let layout = LayoutWriter::create(destination)?;
|
||||
layout.write_verified_blob(&manifest.config.digest, config_json.as_bytes())?;
|
||||
let layout = LayoutWriter::create(destination).context("creating private OCI layout")?;
|
||||
layout
|
||||
.write_verified_blob(&manifest.config.digest, config_json.as_bytes())
|
||||
.context("writing verified OCI config")?;
|
||||
for descriptor in &manifest.layers {
|
||||
let size = u64::try_from(descriptor.size).context("negative OCI layer size")?;
|
||||
if size > MAX_LAYER_BYTES {
|
||||
|
|
@ -235,23 +237,33 @@ impl OciRegistryTransport {
|
|||
layout.require_existing_blob(&descriptor.digest, size)?;
|
||||
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);
|
||||
if let Err(error) = client.pull_blob(&reference, descriptor, file).await {
|
||||
layout.remove_temporary_blob(&temporary);
|
||||
return Err(error)
|
||||
.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 {
|
||||
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_digest = format!("sha256:{}", hex(&Sha256::digest(&local_manifest)));
|
||||
layout.write_verified_blob(&local_digest, &local_manifest)?;
|
||||
layout.write_layout_index(&local_digest, local_manifest.len())?;
|
||||
layout
|
||||
.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 source_identity = format!(
|
||||
"oci-v1\0{}\0{}\0{manifest_digest}",
|
||||
|
|
@ -406,13 +418,19 @@ impl LayoutWriter {
|
|||
bail!("OCI descriptor digest mismatch for {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);
|
||||
}
|
||||
let (mut file, temporary) = self.create_blob(component)?;
|
||||
file.write_all(bytes)?;
|
||||
file.sync_all()?;
|
||||
let (mut file, temporary) = self
|
||||
.create_blob(component)
|
||||
.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)
|
||||
.context("publishing verified OCI blob")
|
||||
}
|
||||
|
||||
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> {
|
||||
let mode = if flags.contains(OFlags::CREATE) {
|
||||
Mode::from_raw_mode(0o600)
|
||||
} else {
|
||||
Mode::empty()
|
||||
};
|
||||
openat2(
|
||||
parent,
|
||||
path,
|
||||
flags | OFlags::NOFOLLOW | OFlags::CLOEXEC,
|
||||
Mode::from_raw_mode(0o600),
|
||||
mode,
|
||||
confined_resolve(),
|
||||
)
|
||||
.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<()> {
|
||||
let name = path.file_name().context("layout file has no name")?;
|
||||
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 mut file = open_relative_file(
|
||||
&parent,
|
||||
|
|
@ -709,6 +736,25 @@ mod tests {
|
|||
|
||||
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]
|
||||
fn preexisting_transport_parents_require_exact_private_mode() {
|
||||
for name in ["image-transports", "blobs", "sha256"] {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,8 @@ use lazaret_core::identity::{
|
|||
};
|
||||
use lazaret_core::records::{PrepareOperation, PrepareStageState};
|
||||
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 serde_json::json;
|
||||
|
|
@ -59,6 +60,7 @@ impl Fixture {
|
|||
source_identity: b"oci-v1\0registry.invalid\0fixture\0sha256:source".to_vec(),
|
||||
selected_manifest_digest: manifest,
|
||||
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]
|
||||
#[allow(clippy::disallowed_methods, clippy::disallowed_types)]
|
||||
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> {
|
||||
let uid = rustix::process::geteuid().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 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]
|
||||
fn root_opaque_whiteout_removes_only_lower_layer_entries() {
|
||||
let fixture = Fixture::new();
|
||||
|
|
@ -971,6 +1135,98 @@ fn reclaim_refuses_forged_identity_and_leases() {
|
|||
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 {
|
||||
use std::fmt::Write as _;
|
||||
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ use std::time::{Duration, Instant};
|
|||
|
||||
use lazaret_core::codec::decode;
|
||||
use lazaret_core::identity::{
|
||||
ConfigurationFingerprint, DerivedPolicyDigest, DomainId, GenerationId, NetworkSidecarId,
|
||||
PidfsProcessIdentityV1,
|
||||
ConfigurationFingerprint, DerivedPolicyDigest, DomainId, GenerationId, NamespaceIdentity,
|
||||
NetworkSidecarId, PidfsProcessIdentityV1,
|
||||
};
|
||||
use lazaret_core::network::{CanonicalDerivedNetworkPolicy, derive_policy};
|
||||
use lazaret_core::records::{
|
||||
|
|
@ -175,7 +175,7 @@ where
|
|||
},
|
||||
stage,
|
||||
acknowledge,
|
||||
enter_domain_namespaces,
|
||||
enter_generation_network_namespace,
|
||||
)?;
|
||||
drop(helper_ready_signal);
|
||||
drop(helper_exit);
|
||||
|
|
@ -214,18 +214,28 @@ where
|
|||
}
|
||||
|
||||
#[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 gid = getgid().as_raw();
|
||||
rustix::thread::unshare(UnshareFlags::NEWUSER)?;
|
||||
std::fs::write("/proc/self/setgroups", "deny")?;
|
||||
std::fs::write("/proc/self/uid_map", format!("0 {uid} 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(
|
||||
"/",
|
||||
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(())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -219,6 +219,8 @@ pub struct SandboxHandoffAuthority<'configuration> {
|
|||
configuration_authority: ConfigurationAuthorityIdentity,
|
||||
command: RunscCommand,
|
||||
derived: Vec<DerivedHandoffAuthority>,
|
||||
prepared_root: PathBuf,
|
||||
prepared_root_identity: FileIdentity,
|
||||
}
|
||||
|
||||
impl SandboxHandoffAuthority<'_> {
|
||||
|
|
@ -241,17 +243,30 @@ impl SandboxHandoffAuthority<'_> {
|
|||
self.configuration
|
||||
.workspace()
|
||||
.open_authority()
|
||||
.map_err(changed_configuration_authority)?;
|
||||
.map_err(|error| changed_authority_at("workspace", error))?;
|
||||
for volume in self.configuration.volumes() {
|
||||
volume
|
||||
.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
|
||||
.configuration
|
||||
.runtime_domain_root()
|
||||
.open_authority()
|
||||
.map_err(changed_configuration_authority)?;
|
||||
.map_err(|error| changed_authority_at("Runtime Domain", error))?;
|
||||
for authority in &self.derived {
|
||||
let descriptor = open_runtime_source(
|
||||
&runtime_root,
|
||||
|
|
@ -259,7 +274,16 @@ impl SandboxHandoffAuthority<'_> {
|
|||
authority.expected_type,
|
||||
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(
|
||||
SandboxBuildErrorKind::ChangedAuthority,
|
||||
"runtime handoff source changed identity",
|
||||
|
|
@ -416,6 +440,7 @@ pub struct SandboxBuildRequest<'a> {
|
|||
pub expected_network_namespace: NamespaceIdentity,
|
||||
pub root_process: RootProcess,
|
||||
pub terminal: Option<StagedHostTerminal>,
|
||||
pub prepared_root: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
|
|
@ -458,6 +483,8 @@ pub struct SandboxBundle {
|
|||
configuration_fingerprint: ConfigurationFingerprint,
|
||||
configuration_authority: ConfigurationAuthorityIdentity,
|
||||
runtime_sources: Vec<RuntimeHandoffSource>,
|
||||
prepared_root: PathBuf,
|
||||
prepared_root_identity: FileIdentity,
|
||||
}
|
||||
|
||||
impl SandboxBundle {
|
||||
|
|
@ -497,16 +524,29 @@ impl SandboxBundle {
|
|||
configuration
|
||||
.workspace()
|
||||
.open_authority()
|
||||
.map_err(changed_configuration_authority)?;
|
||||
.map_err(|error| changed_authority_at("workspace", error))?;
|
||||
for volume in configuration.volumes() {
|
||||
volume
|
||||
.open_source()
|
||||
.map_err(changed_configuration_authority)?;
|
||||
.map_err(|error| changed_authority_at("volume", error))?;
|
||||
}
|
||||
let runtime_root = configuration
|
||||
.runtime_domain_root()
|
||||
.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
|
||||
.runtime_sources
|
||||
.iter()
|
||||
|
|
@ -517,7 +557,15 @@ impl SandboxBundle {
|
|||
source.expected_type,
|
||||
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
|
||||
.build_identity
|
||||
.is_some_and(|expected| expected != identity)
|
||||
|
|
@ -545,6 +593,8 @@ impl SandboxBundle {
|
|||
configuration_authority: self.configuration_authority.clone(),
|
||||
command: self.command.clone(),
|
||||
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 {
|
||||
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 mounts = build_mounts(
|
||||
|
|
@ -604,7 +656,7 @@ pub fn build_sandbox_bundle(
|
|||
)?;
|
||||
let linux = build_linux()?;
|
||||
let root = RootBuilder::default()
|
||||
.path(paths.rootfs.clone())
|
||||
.path(request.prepared_root.clone())
|
||||
.readonly(false)
|
||||
.build()
|
||||
.map_err(oci_error)?;
|
||||
|
|
@ -632,6 +684,8 @@ pub fn build_sandbox_bundle(
|
|||
configuration_fingerprint: request.configuration.configuration_fingerprint(),
|
||||
configuration_authority: ConfigurationAuthorityIdentity::capture(request.configuration),
|
||||
runtime_sources,
|
||||
prepared_root: request.prepared_root,
|
||||
prepared_root_identity,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -639,7 +693,7 @@ struct RuntimePaths {
|
|||
container_name: String,
|
||||
bundle: PathBuf,
|
||||
runsc_root: PathBuf,
|
||||
rootfs: PathBuf,
|
||||
overlay: PathBuf,
|
||||
control_sources: PathBuf,
|
||||
}
|
||||
|
||||
|
|
@ -662,7 +716,7 @@ fn handoff_runtime_sources(
|
|||
),
|
||||
(PathBuf::from("runsc"), HandoffPathType::Directory),
|
||||
(
|
||||
PathBuf::from("state-anchor/merged"),
|
||||
PathBuf::from("runsc-overlay").join(&paths.container_name),
|
||||
HandoffPathType::Directory,
|
||||
),
|
||||
(container.clone(), HandoffPathType::Directory),
|
||||
|
|
@ -699,6 +753,14 @@ fn handoff_runtime_sources(
|
|||
}
|
||||
})
|
||||
.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 {
|
||||
sources.push(RuntimeHandoffSource {
|
||||
relative_path: terminal.relative_source.clone(),
|
||||
|
|
@ -727,7 +789,15 @@ fn open_runtime_source(
|
|||
Mode::empty(),
|
||||
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 actual = FileType::from_raw_mode(stat.st_mode);
|
||||
let matches = match expected_type {
|
||||
|
|
@ -793,7 +863,7 @@ fn runtime_paths(
|
|||
Ok(RuntimePaths {
|
||||
bundle: runtime_domain.join("bundles").join(&container_name),
|
||||
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),
|
||||
container_name,
|
||||
})
|
||||
|
|
@ -811,6 +881,17 @@ fn build_mounts(
|
|||
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() {
|
||||
let record = volume.record();
|
||||
mounts.push(bind_mount(
|
||||
|
|
@ -821,6 +902,14 @@ fn build_mounts(
|
|||
)?);
|
||||
}
|
||||
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 {
|
||||
mounts.push(bind_mount(
|
||||
host_path(configuration.runtime_domain_root().canonical_label())?
|
||||
|
|
@ -858,13 +947,13 @@ fn build_linux() -> Result<oci_spec::runtime::Linux, SandboxBuildError> {
|
|||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let uid_mapping = LinuxIdMappingBuilder::default()
|
||||
.container_id(0_u32)
|
||||
.host_id(rustix::process::getuid().as_raw())
|
||||
.host_id(0_u32)
|
||||
.size(1_u32)
|
||||
.build()
|
||||
.map_err(oci_error)?;
|
||||
let gid_mapping = LinuxIdMappingBuilder::default()
|
||||
.container_id(0_u32)
|
||||
.host_id(rustix::process::getgid().as_raw())
|
||||
.host_id(0_u32)
|
||||
.size(1_u32)
|
||||
.build()
|
||||
.map_err(oci_error)?;
|
||||
|
|
@ -891,6 +980,13 @@ fn build_runsc_command(paths: &RuntimePaths, bundle_authorization: [u8; 32]) ->
|
|||
OsString::from("-rootless"),
|
||||
OsString::from("-network=host"),
|
||||
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(format!(
|
||||
"-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(
|
||||
configuration: &ResolvedDomainConfiguration,
|
||||
root_process: RootProcess,
|
||||
|
|
@ -1094,6 +1207,13 @@ fn changed_configuration_authority(error: impl fmt::Display) -> SandboxBuildErro
|
|||
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 {
|
||||
SandboxBuildError::new(SandboxBuildErrorKind::OciSpecification, error.to_string())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ use std::fs::File;
|
|||
use std::io;
|
||||
use std::os::fd::{AsFd, AsRawFd, OwnedFd};
|
||||
use std::os::unix::fs::FileExt;
|
||||
use std::path::PathBuf;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use lazaret_core::codec::{decode, encode};
|
||||
|
|
@ -32,8 +33,8 @@ use rustix::thread::CapabilitySet;
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::network::{
|
||||
NetworkAttestationContext, NetworkProbeRequest, PreparedNetwork, prepare_network,
|
||||
probe_current_network,
|
||||
NetworkAttestationContext, NetworkProbeRequest, PreparedNetwork,
|
||||
enter_generation_filesystem_namespaces, prepare_network, probe_current_network,
|
||||
};
|
||||
use crate::sandbox::RunscCommand;
|
||||
|
||||
|
|
@ -64,6 +65,8 @@ pub enum SupervisorEvent {
|
|||
StateAnchorStaged(StagedProcessOwnership),
|
||||
StateAnchorReady {
|
||||
staged: StagedProcessOwnership,
|
||||
user_namespace: lazaret_core::identity::NamespaceIdentity,
|
||||
mount_namespace: lazaret_core::identity::NamespaceIdentity,
|
||||
mount: lazaret_core::identity::MountIdentity,
|
||||
},
|
||||
SidecarStarted,
|
||||
|
|
@ -80,7 +83,9 @@ pub enum SupervisorEvent {
|
|||
SignalApplied(AttachedRunscSignal),
|
||||
SandboxStopped,
|
||||
SidecarStopped,
|
||||
StateAnchorUnmounted,
|
||||
StateAnchorUnmounted {
|
||||
mount: lazaret_core::identity::MountIdentity,
|
||||
},
|
||||
Exited,
|
||||
AttachedRunscExited,
|
||||
Stopped,
|
||||
|
|
@ -362,14 +367,31 @@ impl RuntimeSupervisorLaunch {
|
|||
target = target
|
||||
.argument("--state-lower-fd")
|
||||
.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(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(authority.runtime_anchor_fd().to_string())
|
||||
.argument("--state-runtime-label")
|
||||
.argument(authority.runtime_anchor_label())
|
||||
.argument("--state-lower-device")
|
||||
.argument(device.to_string())
|
||||
.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)) = (
|
||||
&self.policy_file,
|
||||
|
|
@ -468,9 +490,15 @@ struct RuntimeArguments {
|
|||
|
||||
struct RuntimeStateAnchorArguments {
|
||||
lower_fd: i32,
|
||||
lower_label: PathBuf,
|
||||
persistent_root_fd: i32,
|
||||
persistent_anchor_fd: i32,
|
||||
persistent_anchor_label: PathBuf,
|
||||
runtime_root_fd: i32,
|
||||
runtime_anchor_fd: i32,
|
||||
runtime_anchor_label: PathBuf,
|
||||
expected_lower: (u64, u64),
|
||||
persistent_destinations: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
struct RuntimeNetworkArguments {
|
||||
|
|
@ -492,14 +520,35 @@ fn parse_runtime_arguments(arguments: &[OsString]) -> io::Result<RuntimeArgument
|
|||
.peek()
|
||||
.is_some_and(|value| *value == "--state-lower-fd")
|
||||
{
|
||||
Some(RuntimeStateAnchorArguments {
|
||||
lower_fd: parse_number(named(&mut arguments, "--state-lower-fd")?)?,
|
||||
persistent_anchor_fd: parse_number(named(&mut arguments, "--state-persistent-fd")?)?,
|
||||
runtime_anchor_fd: parse_number(named(&mut arguments, "--state-runtime-fd")?)?,
|
||||
expected_lower: (
|
||||
let lower_fd = parse_number(named(&mut arguments, "--state-lower-fd")?)?;
|
||||
let lower_label = PathBuf::from(named(&mut arguments, "--state-lower-label")?);
|
||||
let persistent_root_fd =
|
||||
parse_number(named(&mut arguments, "--state-persistent-root-fd")?)?;
|
||||
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-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 {
|
||||
None
|
||||
|
|
@ -604,9 +653,10 @@ fn parse_fixed_hex(value: &OsStr) -> io::Result<[u8; 32]> {
|
|||
|
||||
fn run_supervisor(arguments: &RuntimeArguments) -> io::Result<()> {
|
||||
require_lifetime_held()?;
|
||||
let namespaces = enter_generation_filesystem_namespaces()?;
|
||||
let control = std::io::stdout();
|
||||
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 cleanup = state_anchor.take().map(RuntimeStateAnchorMount::unmount);
|
||||
match (result, cleanup) {
|
||||
|
|
@ -623,6 +673,10 @@ fn run_supervisor(arguments: &RuntimeArguments) -> io::Result<()> {
|
|||
fn prepare_state_anchor(
|
||||
arguments: &RuntimeArguments,
|
||||
parent: lazaret_core::identity::PidfsProcessIdentityV1,
|
||||
namespaces: (
|
||||
lazaret_core::identity::NamespaceIdentity,
|
||||
lazaret_core::identity::NamespaceIdentity,
|
||||
),
|
||||
control: &std::io::Stdout,
|
||||
) -> io::Result<Option<RuntimeStateAnchorMount>> {
|
||||
let Some(state) = &arguments.state_anchor else {
|
||||
|
|
@ -636,8 +690,13 @@ fn prepare_state_anchor(
|
|||
}
|
||||
let authority = RuntimeStateAnchorAuthority::from_inherited(
|
||||
state.lower_fd,
|
||||
&state.lower_label,
|
||||
state.persistent_root_fd,
|
||||
state.persistent_anchor_fd,
|
||||
&state.persistent_anchor_label,
|
||||
state.runtime_root_fd,
|
||||
state.runtime_anchor_fd,
|
||||
&state.runtime_anchor_label,
|
||||
state.expected_lower,
|
||||
)
|
||||
.map_err(io::Error::other)?;
|
||||
|
|
@ -645,7 +704,7 @@ fn prepare_state_anchor(
|
|||
slot: HostProcessOwnershipSlot::StateAnchorDaemon,
|
||||
..arguments.ownership
|
||||
};
|
||||
authority
|
||||
let mount = authority
|
||||
.acquire(
|
||||
parent,
|
||||
ownership,
|
||||
|
|
@ -665,13 +724,18 @@ fn prepare_state_anchor(
|
|||
control,
|
||||
&SupervisorEvent::StateAnchorReady {
|
||||
staged: readiness.staged(),
|
||||
user_namespace: namespaces.0,
|
||||
mount_namespace: namespaces.1,
|
||||
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(
|
||||
|
|
@ -698,6 +762,7 @@ fn run_supervisor_with_state(
|
|||
let pid_file = File::from(memfd_create("lazaret-runsc-pid", MemfdFlags::empty())?);
|
||||
fcntl_setfd(pid_file.as_fd(), FdFlags::empty())?;
|
||||
let runsc = runsc_target(arguments, pid_file.as_raw_fd())?;
|
||||
let delete = runsc_delete_target(arguments)?;
|
||||
let staged = std::cell::Cell::new(None);
|
||||
let process = match child_broker().acquire_with_stdio(
|
||||
parent,
|
||||
|
|
@ -762,7 +827,7 @@ fn run_supervisor_with_state(
|
|||
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(
|
||||
|
|
@ -858,6 +923,29 @@ fn runsc_target(arguments: &RuntimeArguments, pid_file: i32) -> io::Result<Targe
|
|||
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> {
|
||||
if target.parent_edge() != ParentEdge::ParentDeathKill {
|
||||
return Err(io::Error::other(
|
||||
|
|
@ -896,6 +984,7 @@ fn supervise(
|
|||
process: RetainedProcess,
|
||||
mut network: Option<PreparedNetwork>,
|
||||
state_anchor: &mut Option<RuntimeStateAnchorMount>,
|
||||
delete: &TargetCommand,
|
||||
) -> io::Result<()> {
|
||||
let mut process = Some(process);
|
||||
loop {
|
||||
|
|
@ -912,6 +1001,7 @@ fn supervise(
|
|||
.take()
|
||||
.ok_or_else(|| io::Error::other("attached runsc handle is absent"))?,
|
||||
)?;
|
||||
child_broker().run_transient(delete)?;
|
||||
stop_network(network)?;
|
||||
send_message(control, &SupervisorEvent::AttachedRunscExited)?;
|
||||
return Ok(());
|
||||
|
|
@ -933,6 +1023,7 @@ fn supervise(
|
|||
if lease_lost {
|
||||
if let Some(process) = process.take() {
|
||||
terminate_and_reap(process)?;
|
||||
child_broker().run_transient(delete)?;
|
||||
}
|
||||
stop_network(network)?;
|
||||
return Ok(());
|
||||
|
|
@ -969,6 +1060,7 @@ fn supervise(
|
|||
&mut process,
|
||||
&mut network,
|
||||
state_anchor,
|
||||
delete,
|
||||
) {
|
||||
Ok(true) => return Ok(()),
|
||||
Ok(false) => {}
|
||||
|
|
@ -984,6 +1076,7 @@ fn handle_supervisor_message(
|
|||
process: &mut Option<RetainedProcess>,
|
||||
network: &mut Option<PreparedNetwork>,
|
||||
state_anchor: &mut Option<RuntimeStateAnchorMount>,
|
||||
delete: &TargetCommand,
|
||||
) -> io::Result<bool> {
|
||||
match message {
|
||||
CoordinatorMessage::SignalAttachedRunsc(signal) => {
|
||||
|
|
@ -999,6 +1092,7 @@ fn handle_supervisor_message(
|
|||
.take()
|
||||
.ok_or_else(|| io::Error::other("attached runsc is already stopped"))?,
|
||||
)?;
|
||||
child_broker().run_transient(delete)?;
|
||||
send_message(control, &SupervisorEvent::SandboxStopped)?;
|
||||
}
|
||||
CoordinatorMessage::StopSidecar => {
|
||||
|
|
@ -1014,12 +1108,16 @@ fn handle_supervisor_message(
|
|||
"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
|
||||
.take()
|
||||
.ok_or_else(|| io::Error::other("State Anchor is absent"))?
|
||||
.unmount()
|
||||
.map_err(io::Error::other)?;
|
||||
send_message(control, &SupervisorEvent::StateAnchorUnmounted)?;
|
||||
send_message(control, &SupervisorEvent::StateAnchorUnmounted { mount })?;
|
||||
}
|
||||
CoordinatorMessage::Exit => {
|
||||
if process.is_some() || network.is_some() || state_anchor.is_some() {
|
||||
|
|
@ -1307,6 +1405,14 @@ mod tests {
|
|||
attached_runsc_standard_io(&runsc).unwrap().policy(),
|
||||
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]
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue