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() {
|
fn main() {
|
||||||
|
let arguments = std::env::args_os().skip(1).collect::<Vec<_>>();
|
||||||
|
if let Some(result) = lazaret_linux::process::dispatch_private_process_role(&arguments) {
|
||||||
|
if let Err(error) = result {
|
||||||
|
eprintln!("private process role failed: {error}");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
let current_workspace = match std::env::current_dir() {
|
let current_workspace = match std::env::current_dir() {
|
||||||
Ok(path) => path,
|
Ok(path) => path,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
|
|
@ -6,8 +14,7 @@ fn main() {
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let command =
|
let command = match lazaret_client::parse_command_line(arguments, current_workspace) {
|
||||||
match lazaret_client::parse_command_line(std::env::args_os().skip(1), current_workspace) {
|
|
||||||
Ok(command) => command,
|
Ok(command) => command,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
eprintln!("invalid command line: {error}");
|
eprintln!("invalid command line: {error}");
|
||||||
|
|
|
||||||
|
|
@ -988,6 +988,8 @@ pub struct StateAnchorOwnership {
|
||||||
pub work_path: Option<HostPath>,
|
pub work_path: Option<HostPath>,
|
||||||
pub merged_path: Option<HostPath>,
|
pub merged_path: Option<HostPath>,
|
||||||
pub daemon: PersistedHostProcessOwnership,
|
pub daemon: PersistedHostProcessOwnership,
|
||||||
|
pub user_namespace: Option<NamespaceIdentity>,
|
||||||
|
pub mount_namespace: Option<NamespaceIdentity>,
|
||||||
pub mount: Option<MountIdentity>,
|
pub mount: Option<MountIdentity>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1102,6 +1104,8 @@ impl StateAnchorOwnership {
|
||||||
work_path: None,
|
work_path: None,
|
||||||
merged_path: None,
|
merged_path: None,
|
||||||
daemon: PersistedHostProcessOwnership::unassigned(),
|
daemon: PersistedHostProcessOwnership::unassigned(),
|
||||||
|
user_namespace: None,
|
||||||
|
mount_namespace: None,
|
||||||
mount: None,
|
mount: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1112,6 +1116,8 @@ impl StateAnchorOwnership {
|
||||||
&& self.work_path.is_none()
|
&& self.work_path.is_none()
|
||||||
&& self.merged_path.is_none()
|
&& self.merged_path.is_none()
|
||||||
&& self.daemon.is_unassigned()
|
&& self.daemon.is_unassigned()
|
||||||
|
&& self.user_namespace.is_none()
|
||||||
|
&& self.mount_namespace.is_none()
|
||||||
&& self.mount.is_none()
|
&& self.mount.is_none()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1121,6 +1127,8 @@ impl StateAnchorOwnership {
|
||||||
&& self.work_path.is_some()
|
&& self.work_path.is_some()
|
||||||
&& self.merged_path.is_some()
|
&& self.merged_path.is_some()
|
||||||
&& self.daemon.is_current_committed()
|
&& self.daemon.is_current_committed()
|
||||||
|
&& self.user_namespace.is_some()
|
||||||
|
&& self.mount_namespace.is_some()
|
||||||
&& self.mount.is_some()
|
&& self.mount.is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1133,6 +1141,8 @@ impl StateAnchorOwnership {
|
||||||
validate_option_progress(self.work_path.as_ref(), next.work_path.as_ref())?;
|
validate_option_progress(self.work_path.as_ref(), next.work_path.as_ref())?;
|
||||||
validate_option_progress(self.merged_path.as_ref(), next.merged_path.as_ref())?;
|
validate_option_progress(self.merged_path.as_ref(), next.merged_path.as_ref())?;
|
||||||
validate_process_progress(&self.daemon, &next.daemon)?;
|
validate_process_progress(&self.daemon, &next.daemon)?;
|
||||||
|
validate_option_progress(self.user_namespace.as_ref(), next.user_namespace.as_ref())?;
|
||||||
|
validate_option_progress(self.mount_namespace.as_ref(), next.mount_namespace.as_ref())?;
|
||||||
validate_option_progress(self.mount.as_ref(), next.mount.as_ref())
|
validate_option_progress(self.mount.as_ref(), next.mount.as_ref())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -396,6 +396,8 @@ fn state_anchor_ownership(
|
||||||
work_path: Some(HostPath::new(b"work".to_vec())),
|
work_path: Some(HostPath::new(b"work".to_vec())),
|
||||||
merged_path: Some(HostPath::new(b"merged".to_vec())),
|
merged_path: Some(HostPath::new(b"merged".to_vec())),
|
||||||
daemon,
|
daemon,
|
||||||
|
user_namespace: Some(namespace()),
|
||||||
|
mount_namespace: Some(namespace()),
|
||||||
mount: mount_identity,
|
mount: mount_identity,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -334,6 +334,8 @@ fn every_schema_shape_round_trips_and_rejects_malformed_or_unknown_versions() {
|
||||||
work_path: Some(HostPath::new(b"work".to_vec())),
|
work_path: Some(HostPath::new(b"work".to_vec())),
|
||||||
merged_path: Some(HostPath::new(b"merged".to_vec())),
|
merged_path: Some(HostPath::new(b"merged".to_vec())),
|
||||||
daemon: owned_process(63),
|
daemon: owned_process(63),
|
||||||
|
user_namespace: Some(namespace()),
|
||||||
|
mount_namespace: Some(namespace()),
|
||||||
mount: Some(mount()),
|
mount: Some(mount()),
|
||||||
},
|
},
|
||||||
&mut seen,
|
&mut seen,
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,7 @@ pub struct ProductionBootstrapPlan<'a> {
|
||||||
pub bundle_digest: BundleDigest,
|
pub bundle_digest: BundleDigest,
|
||||||
pub invoking_uid: u32,
|
pub invoking_uid: u32,
|
||||||
pub non_interactive_executor: Arc<dyn NonInteractiveSessionExecutor>,
|
pub non_interactive_executor: Arc<dyn NonInteractiveSessionExecutor>,
|
||||||
|
pub(crate) runtime_staging: &'a crate::production::RuntimeStaging,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ProductionLifecycleAdapter<'a> {
|
pub struct ProductionLifecycleAdapter<'a> {
|
||||||
|
|
@ -268,7 +269,7 @@ impl<'a> ProductionLifecycleAdapter<'a> {
|
||||||
&coordinator,
|
&coordinator,
|
||||||
self.plan.prepared_artifact.prepared_artifact_id,
|
self.plan.prepared_artifact.prepared_artifact_id,
|
||||||
)
|
)
|
||||||
.map_err(failure)?;
|
.map_err(|error| failure(format!("mounting State Anchor: {error}")))?;
|
||||||
self.state_anchor_mount = Some(mount);
|
self.state_anchor_mount = Some(mount);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -281,7 +282,7 @@ impl<'a> ProductionLifecycleAdapter<'a> {
|
||||||
.ok_or_else(|| ProductionLifecycleError("Runtime Supervisor is absent".into()))?
|
.ok_or_else(|| ProductionLifecycleError("Runtime Supervisor is absent".into()))?
|
||||||
.receive_helper_staged(&network)
|
.receive_helper_staged(&network)
|
||||||
.map(|_| ())
|
.map(|_| ())
|
||||||
.map_err(failure)
|
.map_err(|error| failure(format!("starting connectivity helper: {error}")))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn start_sidecar(&mut self) -> Result<(), ProductionLifecycleError> {
|
fn start_sidecar(&mut self) -> Result<(), ProductionLifecycleError> {
|
||||||
|
|
@ -289,7 +290,7 @@ impl<'a> ProductionLifecycleAdapter<'a> {
|
||||||
.as_mut()
|
.as_mut()
|
||||||
.ok_or_else(|| ProductionLifecycleError("Runtime Supervisor is absent".into()))?
|
.ok_or_else(|| ProductionLifecycleError("Runtime Supervisor is absent".into()))?
|
||||||
.start_sidecar()
|
.start_sidecar()
|
||||||
.map_err(failure)
|
.map_err(|error| failure(format!("starting sidecar: {error}")))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn mark_network_ready(&mut self) -> Result<(), ProductionLifecycleError> {
|
fn mark_network_ready(&mut self) -> Result<(), ProductionLifecycleError> {
|
||||||
|
|
@ -309,7 +310,7 @@ impl<'a> ProductionLifecycleAdapter<'a> {
|
||||||
self.plan.reachability,
|
self.plan.reachability,
|
||||||
)
|
)
|
||||||
.map(|_| ())
|
.map(|_| ())
|
||||||
.map_err(failure)
|
.map_err(|error| failure(format!("marking network ready: {error}")))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn start_sandbox(&mut self) -> Result<(), ProductionLifecycleError> {
|
fn start_sandbox(&mut self) -> Result<(), ProductionLifecycleError> {
|
||||||
|
|
@ -319,17 +320,19 @@ impl<'a> ProductionLifecycleAdapter<'a> {
|
||||||
.awaiting_runtime
|
.awaiting_runtime
|
||||||
.take()
|
.take()
|
||||||
.ok_or_else(|| ProductionLifecycleError("Runtime Supervisor is absent".into()))?;
|
.ok_or_else(|| ProductionLifecycleError("Runtime Supervisor is absent".into()))?;
|
||||||
let staged = pending.receive_staged(&sandbox).map_err(failure)?;
|
let staged = pending
|
||||||
|
.receive_staged(&sandbox)
|
||||||
|
.map_err(|error| failure(format!("receiving attached runsc staging: {error}")))?;
|
||||||
let handoff = self
|
let handoff = self
|
||||||
.plan
|
.plan
|
||||||
.sandbox_bundle
|
.sandbox_bundle
|
||||||
.capture_handoff_authority(self.plan.configuration)
|
.capture_handoff_authority(self.plan.configuration)
|
||||||
.map_err(failure)?;
|
.map_err(|error| failure(format!("capturing sandbox handoff: {error}")))?;
|
||||||
let (runtime, _) = pending
|
let (runtime, _) = pending
|
||||||
.acknowledge(staged, &handoff)
|
.acknowledge(staged, &handoff)
|
||||||
.map_err(failure)?
|
.map_err(|error| failure(format!("releasing sandbox handoff: {error}")))?
|
||||||
.commit_networked(&sandbox)
|
.commit_networked(&sandbox)
|
||||||
.map_err(failure)?;
|
.map_err(|error| failure(format!("committing sandbox readiness: {error}")))?;
|
||||||
self.runtime = Some(runtime);
|
self.runtime = Some(runtime);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -376,7 +379,8 @@ impl<'a> ProductionLifecycleAdapter<'a> {
|
||||||
.as_mut()
|
.as_mut()
|
||||||
.ok_or_else(|| ProductionLifecycleError("committed runtime is absent".into()))?
|
.ok_or_else(|| ProductionLifecycleError("committed runtime is absent".into()))?
|
||||||
.stop_sandbox(&sandbox)
|
.stop_sandbox(&sandbox)
|
||||||
.map_err(failure)
|
.map_err(failure)?;
|
||||||
|
self.plan.runtime_staging.cleanup_overlay().map_err(failure)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn stop_sidecar(&mut self) -> Result<(), ProductionLifecycleError> {
|
fn stop_sidecar(&mut self) -> Result<(), ProductionLifecycleError> {
|
||||||
|
|
@ -408,7 +412,14 @@ impl<'a> ProductionLifecycleAdapter<'a> {
|
||||||
self.runtime
|
self.runtime
|
||||||
.as_mut()
|
.as_mut()
|
||||||
.ok_or_else(|| ProductionLifecycleError("committed runtime is absent".into()))?
|
.ok_or_else(|| ProductionLifecycleError("committed runtime is absent".into()))?
|
||||||
.unmount_state_anchor()
|
.unmount_state_anchor(
|
||||||
|
self.state_anchor_release
|
||||||
|
.as_ref()
|
||||||
|
.ok_or_else(|| {
|
||||||
|
ProductionLifecycleError("State Anchor release is absent".into())
|
||||||
|
})?
|
||||||
|
.mount(),
|
||||||
|
)
|
||||||
.map_err(failure)?;
|
.map_err(failure)?;
|
||||||
self.state_anchor_mount = None;
|
self.state_anchor_mount = None;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,8 @@ pub struct ProductionColdStartInputs<'a> {
|
||||||
pub configuration: &'a ResolvedDomainConfiguration,
|
pub configuration: &'a ResolvedDomainConfiguration,
|
||||||
pub generation: Generation,
|
pub generation: Generation,
|
||||||
pub prepared_artifact: &'a PreparedArtifact,
|
pub prepared_artifact: &'a PreparedArtifact,
|
||||||
|
pub prepared_artifact_authority: &'a File,
|
||||||
|
pub prepared_artifact_label: &'a Path,
|
||||||
pub root_process: RootProcess,
|
pub root_process: RootProcess,
|
||||||
pub resolver_addresses: Vec<IpAddress>,
|
pub resolver_addresses: Vec<IpAddress>,
|
||||||
pub network_probe: NetworkProbeRequest,
|
pub network_probe: NetworkProbeRequest,
|
||||||
|
|
@ -137,7 +139,7 @@ pub struct ProductionColdStart<'a> {
|
||||||
runtime_root: PathBuf,
|
runtime_root: PathBuf,
|
||||||
bundle_digest: BundleDigest,
|
bundle_digest: BundleDigest,
|
||||||
invoking_uid: u32,
|
invoking_uid: u32,
|
||||||
_staging: RuntimeStaging,
|
staging: RuntimeStaging,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> ProductionColdStart<'a> {
|
impl<'a> ProductionColdStart<'a> {
|
||||||
|
|
@ -173,6 +175,7 @@ impl<'a> ProductionColdStart<'a> {
|
||||||
expected_network_namespace,
|
expected_network_namespace,
|
||||||
root_process: inputs.root_process,
|
root_process: inputs.root_process,
|
||||||
terminal: None,
|
terminal: None,
|
||||||
|
prepared_root: inputs.prepared_artifact_label.to_owned(),
|
||||||
})
|
})
|
||||||
.map_err(|error| failure(format!("sandbox bundle: {error}")))?;
|
.map_err(|error| failure(format!("sandbox bundle: {error}")))?;
|
||||||
staging
|
staging
|
||||||
|
|
@ -186,8 +189,12 @@ impl<'a> ProductionColdStart<'a> {
|
||||||
coordinator_id: inputs.generation.coordinator_id,
|
coordinator_id: inputs.generation.coordinator_id,
|
||||||
slot: HostProcessOwnershipSlot::SandboxRuntimeSupervisor,
|
slot: HostProcessOwnershipSlot::SandboxRuntimeSupervisor,
|
||||||
};
|
};
|
||||||
let state_anchor =
|
let state_anchor = StateAnchorLaunchAuthority::capture_retained(
|
||||||
StateAnchorLaunchAuthority::capture(&state_anchor_layout, inputs.prepared_artifact)
|
&state_anchor_layout,
|
||||||
|
inputs.prepared_artifact,
|
||||||
|
inputs.prepared_artifact_label,
|
||||||
|
inputs.prepared_artifact_authority,
|
||||||
|
)
|
||||||
.map_err(|error| failure(format!("State Anchor authority: {error}")))?;
|
.map_err(|error| failure(format!("State Anchor authority: {error}")))?;
|
||||||
let runtime_launch = RuntimeSupervisorLaunch::from_networked_runsc_command(
|
let runtime_launch = RuntimeSupervisorLaunch::from_networked_runsc_command(
|
||||||
ownership,
|
ownership,
|
||||||
|
|
@ -227,7 +234,7 @@ impl<'a> ProductionColdStart<'a> {
|
||||||
runtime_root: runtime_domain.join("runsc"),
|
runtime_root: runtime_domain.join("runsc"),
|
||||||
bundle_digest,
|
bundle_digest,
|
||||||
invoking_uid: inputs.invoking_uid,
|
invoking_uid: inputs.invoking_uid,
|
||||||
_staging: staging,
|
staging,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -342,6 +349,7 @@ impl<'a> ProductionColdStart<'a> {
|
||||||
)
|
)
|
||||||
.map_err(failure)?,
|
.map_err(failure)?,
|
||||||
),
|
),
|
||||||
|
runtime_staging: &self.staging,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -362,17 +370,41 @@ fn require_inputs(inputs: &ProductionColdStartInputs<'_>) -> Result<(), Producti
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
struct RuntimeStaging {
|
pub(crate) struct RuntimeStaging {
|
||||||
bundles: OwnedFd,
|
bundles: OwnedFd,
|
||||||
bundle: OwnedFd,
|
bundle: OwnedFd,
|
||||||
controls: OwnedFd,
|
controls: OwnedFd,
|
||||||
control: OwnedFd,
|
control: OwnedFd,
|
||||||
|
overlays: OwnedFd,
|
||||||
|
overlay: OwnedFd,
|
||||||
name: OsString,
|
name: OsString,
|
||||||
bundle_identity: FileIdentity,
|
bundle_identity: FileIdentity,
|
||||||
control_identity: FileIdentity,
|
control_identity: FileIdentity,
|
||||||
|
overlay_identity: FileIdentity,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RuntimeStaging {
|
impl RuntimeStaging {
|
||||||
|
pub(crate) fn cleanup_overlay(&self) -> Result<(), ProductionColdStartError> {
|
||||||
|
let retained = fstat(&self.overlay).map_err(failure)?;
|
||||||
|
if !self.overlay_identity.matches(&retained) {
|
||||||
|
return Err(ProductionColdStartError(
|
||||||
|
"runsc overlay retained authority changed".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
remove_children(&self.overlay).map_err(failure)?;
|
||||||
|
let current =
|
||||||
|
statat(&self.overlays, &self.name, AtFlags::SYMLINK_NOFOLLOW).map_err(failure)?;
|
||||||
|
if !self.overlay_identity.matches(¤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(
|
fn create(
|
||||||
runtime_domain: &ConfinedPathPlan,
|
runtime_domain: &ConfinedPathPlan,
|
||||||
container_id: ContainerId,
|
container_id: ContainerId,
|
||||||
|
|
@ -384,6 +416,8 @@ impl RuntimeStaging {
|
||||||
let bundles = open_or_create_private_directory(&root, "bundles", invoking_uid, false)?;
|
let bundles = open_or_create_private_directory(&root, "bundles", invoking_uid, false)?;
|
||||||
let controls =
|
let controls =
|
||||||
open_or_create_private_directory(&root, "sandbox-control", invoking_uid, false)?;
|
open_or_create_private_directory(&root, "sandbox-control", invoking_uid, false)?;
|
||||||
|
let overlays =
|
||||||
|
open_or_create_private_directory(&root, "runsc-overlay", invoking_uid, false)?;
|
||||||
drop(open_or_create_private_directory(
|
drop(open_or_create_private_directory(
|
||||||
&root,
|
&root,
|
||||||
"runsc",
|
"runsc",
|
||||||
|
|
@ -401,14 +435,26 @@ impl RuntimeStaging {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let control_identity = FileIdentity::capture(&control)?;
|
let control_identity = FileIdentity::capture(&control)?;
|
||||||
|
let overlay = match open_or_create_private_directory(&overlays, &name, invoking_uid, true) {
|
||||||
|
Ok(overlay) => overlay,
|
||||||
|
Err(error) => {
|
||||||
|
remove_staging_directory(&controls, &name, &control, control_identity);
|
||||||
|
remove_staging_directory(&bundles, &name, &bundle, bundle_identity);
|
||||||
|
return Err(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let overlay_identity = FileIdentity::capture(&overlay)?;
|
||||||
let staging = Self {
|
let staging = Self {
|
||||||
bundles,
|
bundles,
|
||||||
bundle,
|
bundle,
|
||||||
controls,
|
controls,
|
||||||
control,
|
control,
|
||||||
|
overlays,
|
||||||
|
overlay,
|
||||||
name,
|
name,
|
||||||
bundle_identity,
|
bundle_identity,
|
||||||
control_identity,
|
control_identity,
|
||||||
|
overlay_identity,
|
||||||
};
|
};
|
||||||
staging.populate_control(escalation_client, sandbox_init)?;
|
staging.populate_control(escalation_client, sandbox_init)?;
|
||||||
Ok(staging)
|
Ok(staging)
|
||||||
|
|
@ -535,6 +581,12 @@ impl FileIdentity {
|
||||||
|
|
||||||
impl Drop for RuntimeStaging {
|
impl Drop for RuntimeStaging {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
|
remove_staging_directory(
|
||||||
|
&self.overlays,
|
||||||
|
&self.name,
|
||||||
|
&self.overlay,
|
||||||
|
self.overlay_identity,
|
||||||
|
);
|
||||||
remove_staging_directory(
|
remove_staging_directory(
|
||||||
&self.controls,
|
&self.controls,
|
||||||
&self.name,
|
&self.name,
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
use std::collections::BTreeSet;
|
use std::collections::BTreeSet;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
|
use std::os::unix::ffi::OsStringExt;
|
||||||
use std::os::unix::fs::PermissionsExt;
|
use std::os::unix::fs::PermissionsExt;
|
||||||
use std::path::Path;
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use lazaret_core::configuration::ResolvedDomainConfiguration;
|
use lazaret_core::configuration::ResolvedDomainConfiguration;
|
||||||
|
|
@ -11,14 +12,19 @@ use lazaret_core::identity::{
|
||||||
PrepareOperationId, SandboxId,
|
PrepareOperationId, SandboxId,
|
||||||
};
|
};
|
||||||
use lazaret_core::network::{DnsQueryExpectation, ReachabilityExpectation};
|
use lazaret_core::network::{DnsQueryExpectation, ReachabilityExpectation};
|
||||||
use lazaret_core::records::{DurableDecision, Generation, IpAddress, PreparedArtifact};
|
use lazaret_core::records::{DurableDecision, Generation, IpAddress, PreparedArtifact, VolumeKind};
|
||||||
use lazaret_linux::filesystem::namespace_identity;
|
use lazaret_linux::filesystem::namespace_identity;
|
||||||
use lazaret_linux::platform::{PlatformObservation, check_platform};
|
use lazaret_linux::platform::{PlatformObservation, check_platform};
|
||||||
use lazaret_prepared_lower::{
|
use lazaret_prepared_lower::{
|
||||||
IdMap, IdMapRange, OciRegistryTransport, Platform, PreparedLowerStore, RegistryCredentials,
|
IdMap, IdMapRange, OciRegistryTransport, Platform, PreparedDestination,
|
||||||
|
PreparedDestinationKind, PreparedLowerStore, RegistryCredentials,
|
||||||
};
|
};
|
||||||
use lazaret_runtime_supervisor::network::{DnsProbeTarget, NetworkProbeRequest, TcpProbeTarget};
|
use lazaret_runtime_supervisor::network::{DnsProbeTarget, NetworkProbeRequest, TcpProbeTarget};
|
||||||
use lazaret_runtime_supervisor::sandbox::{RootProcess, SANDBOX_INIT_DESTINATION};
|
use lazaret_runtime_supervisor::sandbox::{
|
||||||
|
CREDENTIALS_DESTINATION, ESCALATION_CLIENT_DESTINATION, MAILBOX_REQUESTS_DESTINATION,
|
||||||
|
MAILBOX_RESULTS_DESTINATION, RootProcess, SANDBOX_INIT_DESTINATION, TERMINFO_ROOT_DESTINATION,
|
||||||
|
WORKSPACE_DESTINATION,
|
||||||
|
};
|
||||||
use lazaret_storage_catalog::{Catalog, CatalogError};
|
use lazaret_storage_catalog::{Catalog, CatalogError};
|
||||||
use rustix::fs::{Mode, OFlags, ResolveFlags, openat2};
|
use rustix::fs::{Mode, OFlags, ResolveFlags, openat2};
|
||||||
|
|
||||||
|
|
@ -54,6 +60,8 @@ pub struct ProductionSelection {
|
||||||
configuration_fingerprint: ConfigurationFingerprint,
|
configuration_fingerprint: ConfigurationFingerprint,
|
||||||
catalog: Catalog,
|
catalog: Catalog,
|
||||||
artifact: PreparedArtifact,
|
artifact: PreparedArtifact,
|
||||||
|
artifact_authority: File,
|
||||||
|
artifact_label: PathBuf,
|
||||||
generation: Generation,
|
generation: Generation,
|
||||||
resolver_addresses: Vec<IpAddress>,
|
resolver_addresses: Vec<IpAddress>,
|
||||||
network_probe: NetworkProbeRequest,
|
network_probe: NetworkProbeRequest,
|
||||||
|
|
@ -83,6 +91,8 @@ impl ProductionSelection {
|
||||||
configuration,
|
configuration,
|
||||||
generation: self.generation.clone(),
|
generation: self.generation.clone(),
|
||||||
prepared_artifact: &self.artifact,
|
prepared_artifact: &self.artifact,
|
||||||
|
prepared_artifact_authority: &self.artifact_authority,
|
||||||
|
prepared_artifact_label: &self.artifact_label,
|
||||||
root_process: RootProcess::new(
|
root_process: RootProcess::new(
|
||||||
SANDBOX_INIT_DESTINATION,
|
SANDBOX_INIT_DESTINATION,
|
||||||
[PRIVATE_SANDBOX_INIT_ROLE.to_owned()],
|
[PRIVATE_SANDBOX_INIT_ROLE.to_owned()],
|
||||||
|
|
@ -149,14 +159,29 @@ async fn select_production_cold_start_for_platform(
|
||||||
random_id()?,
|
random_id()?,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|error| failure(format!("pulling configured anonymous OCI image: {error}")))?;
|
.map_err(|error| failure(format!("pulling configured anonymous OCI image: {error:#}")))?;
|
||||||
let artifact = prepared
|
let artifact = prepared
|
||||||
.prepare_transported(
|
.prepare_transported(
|
||||||
PrepareOperationId::new(random_id()?),
|
PrepareOperationId::new(random_id()?),
|
||||||
&image,
|
&image,
|
||||||
rootless_id_map(),
|
rootless_id_map(),
|
||||||
|
prepared_destinations(configuration),
|
||||||
)
|
)
|
||||||
.map_err(failure)?;
|
.map_err(failure)?;
|
||||||
|
let artifact_authority = prepared
|
||||||
|
.open_artifact_authority(&artifact)
|
||||||
|
.map_err(failure)?;
|
||||||
|
let artifact_label = PathBuf::from(std::ffi::OsString::from_vec(
|
||||||
|
configuration
|
||||||
|
.storage_root()
|
||||||
|
.canonical_label()
|
||||||
|
.as_bytes()
|
||||||
|
.to_vec(),
|
||||||
|
))
|
||||||
|
.join("prepared-lower")
|
||||||
|
.join(PathBuf::from(std::ffi::OsString::from_vec(
|
||||||
|
artifact.path.as_bytes().to_vec(),
|
||||||
|
)));
|
||||||
image.cleanup().map_err(failure)?;
|
image.cleanup().map_err(failure)?;
|
||||||
|
|
||||||
let next_generation = next_generation(&store)?;
|
let next_generation = next_generation(&store)?;
|
||||||
|
|
@ -179,6 +204,8 @@ async fn select_production_cold_start_for_platform(
|
||||||
configuration_fingerprint: configuration.configuration_fingerprint(),
|
configuration_fingerprint: configuration.configuration_fingerprint(),
|
||||||
catalog,
|
catalog,
|
||||||
artifact,
|
artifact,
|
||||||
|
artifact_authority,
|
||||||
|
artifact_label,
|
||||||
generation,
|
generation,
|
||||||
resolver_addresses,
|
resolver_addresses,
|
||||||
network_probe,
|
network_probe,
|
||||||
|
|
@ -189,6 +216,66 @@ async fn select_production_cold_start_for_platform(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn prepared_destinations(configuration: &ResolvedDomainConfiguration) -> Vec<PreparedDestination> {
|
||||||
|
let mut destinations = [
|
||||||
|
"/dev",
|
||||||
|
"/proc",
|
||||||
|
"/sys",
|
||||||
|
"/tmp",
|
||||||
|
"/etc",
|
||||||
|
"/home",
|
||||||
|
"/run",
|
||||||
|
WORKSPACE_DESTINATION,
|
||||||
|
CREDENTIALS_DESTINATION,
|
||||||
|
MAILBOX_REQUESTS_DESTINATION,
|
||||||
|
MAILBOX_RESULTS_DESTINATION,
|
||||||
|
TERMINFO_ROOT_DESTINATION,
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.map(|path| PreparedDestination {
|
||||||
|
path: path.into(),
|
||||||
|
kind: PreparedDestinationKind::Directory,
|
||||||
|
})
|
||||||
|
.chain(
|
||||||
|
[
|
||||||
|
"/etc/resolv.conf",
|
||||||
|
"/etc/hosts",
|
||||||
|
"/etc/hostname",
|
||||||
|
ESCALATION_CLIENT_DESTINATION,
|
||||||
|
SANDBOX_INIT_DESTINATION,
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.map(|path| PreparedDestination {
|
||||||
|
path: path.into(),
|
||||||
|
kind: PreparedDestinationKind::RegularFile,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
destinations.extend(
|
||||||
|
configuration
|
||||||
|
.fingerprint_record()
|
||||||
|
.persistent_dirs
|
||||||
|
.iter()
|
||||||
|
.map(|path| PreparedDestination {
|
||||||
|
path: path.into(),
|
||||||
|
kind: PreparedDestinationKind::Directory,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
destinations.extend(
|
||||||
|
configuration
|
||||||
|
.volumes()
|
||||||
|
.iter()
|
||||||
|
.map(|volume| PreparedDestination {
|
||||||
|
path: volume.record().container_path.as_str().into(),
|
||||||
|
kind: match volume.record().kind {
|
||||||
|
VolumeKind::Directory => PreparedDestinationKind::Directory,
|
||||||
|
VolumeKind::File => PreparedDestinationKind::RegularFile,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
destinations
|
||||||
|
}
|
||||||
|
|
||||||
fn next_generation(store: &HostCoordinatorDomainStore) -> Result<u64, ProductionSelectionError> {
|
fn next_generation(store: &HostCoordinatorDomainStore) -> Result<u64, ProductionSelectionError> {
|
||||||
store
|
store
|
||||||
.snapshot()
|
.snapshot()
|
||||||
|
|
@ -380,6 +467,15 @@ mod tests {
|
||||||
id_map.gid[0].namespace_id,
|
id_map.gid[0].namespace_id,
|
||||||
rustix::process::getgid().as_raw()
|
rustix::process::getgid().as_raw()
|
||||||
);
|
);
|
||||||
|
let destinations = prepared_destinations(&configuration);
|
||||||
|
assert!(destinations.iter().any(|destination| {
|
||||||
|
destination.path == Path::new(WORKSPACE_DESTINATION)
|
||||||
|
&& destination.kind == PreparedDestinationKind::Directory
|
||||||
|
}));
|
||||||
|
assert!(destinations.iter().any(|destination| {
|
||||||
|
destination.path == Path::new(SANDBOX_INIT_DESTINATION)
|
||||||
|
&& destination.kind == PreparedDestinationKind::RegularFile
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -411,8 +411,13 @@ impl AwaitingAttachedRunsc {
|
||||||
.acknowledge_staged_daemon(staged)
|
.acknowledge_staged_daemon(staged)
|
||||||
.map_err(|error| SandboxCoordinatorError::Io(io::Error::other(error)))?;
|
.map_err(|error| SandboxCoordinatorError::Io(io::Error::other(error)))?;
|
||||||
control.acknowledge_state_anchor(acknowledgment)?;
|
control.acknowledge_state_anchor(acknowledgment)?;
|
||||||
let (ready_staged, mount) = match control.receive()? {
|
let (ready_staged, user_namespace, mount_namespace, mount) = match control.receive()? {
|
||||||
SupervisorEvent::StateAnchorReady { staged, mount } => (staged, mount),
|
SupervisorEvent::StateAnchorReady {
|
||||||
|
staged,
|
||||||
|
user_namespace,
|
||||||
|
mount_namespace,
|
||||||
|
mount,
|
||||||
|
} => (staged, user_namespace, mount_namespace, mount),
|
||||||
SupervisorEvent::Refused(detail) => {
|
SupervisorEvent::Refused(detail) => {
|
||||||
return Err(SandboxCoordinatorError::Io(io::Error::other(detail)));
|
return Err(SandboxCoordinatorError::Io(io::Error::other(detail)));
|
||||||
}
|
}
|
||||||
|
|
@ -430,7 +435,7 @@ impl AwaitingAttachedRunsc {
|
||||||
let readiness =
|
let readiness =
|
||||||
lazaret_state_anchor::VerifiedStateAnchorReadiness::from_parts(ready_staged, mount);
|
lazaret_state_anchor::VerifiedStateAnchorReadiness::from_parts(ready_staged, mount);
|
||||||
coordinator
|
coordinator
|
||||||
.commit_daemon_ready(readiness)
|
.commit_daemon_ready(readiness, user_namespace, mount_namespace)
|
||||||
.and_then(|()| {
|
.and_then(|()| {
|
||||||
coordinator.commit_prepared(staged.generation_id(), prepared_artifact_id, mount)
|
coordinator.commit_prepared(staged.generation_id(), prepared_artifact_id, mount)
|
||||||
})
|
})
|
||||||
|
|
@ -949,14 +954,16 @@ impl CommittedSandboxRuntime {
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// Refuses an unexpected RS response or channel failure.
|
/// Refuses an unexpected RS response or channel failure.
|
||||||
pub fn unmount_state_anchor(&mut self) -> Result<(), SandboxCoordinatorError> {
|
pub fn unmount_state_anchor(
|
||||||
|
&mut self,
|
||||||
|
expected_mount: MountIdentity,
|
||||||
|
) -> Result<(), SandboxCoordinatorError> {
|
||||||
self.control.unmount_state_anchor()?;
|
self.control.unmount_state_anchor()?;
|
||||||
if self.control.receive()? == SupervisorEvent::StateAnchorUnmounted {
|
match self.control.receive()? {
|
||||||
Ok(())
|
SupervisorEvent::StateAnchorUnmounted { mount } if mount == expected_mount => Ok(()),
|
||||||
} else {
|
_ => Err(SandboxCoordinatorError::InvalidState(
|
||||||
Err(SandboxCoordinatorError::InvalidState(
|
"Runtime Supervisor did not confirm the exact State Anchor unmount",
|
||||||
"Runtime Supervisor did not unmount the State Anchor",
|
)),
|
||||||
))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,15 +3,15 @@ use std::io;
|
||||||
use std::os::unix::ffi::OsStrExt;
|
use std::os::unix::ffi::OsStrExt;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use lazaret_core::identity::{GenerationId, MountIdentity, PidfsProcessIdentityV1};
|
use lazaret_core::identity::{
|
||||||
|
GenerationId, MountIdentity, NamespaceIdentity, PidfsProcessIdentityV1,
|
||||||
|
};
|
||||||
use lazaret_core::records::{
|
use lazaret_core::records::{
|
||||||
ArtifactLease, HostProcessOwnershipSlot, Participant, ParticipantOwnership, ParticipantRole,
|
ArtifactLease, HostProcessOwnershipSlot, Participant, ParticipantOwnership, ParticipantRole,
|
||||||
ParticipantState, PersistedHostProcessOwnership, PreparedArtifact, PreparedLowerOwnership,
|
ParticipantState, PersistedHostProcessOwnership, PreparedArtifact, PreparedLowerOwnership,
|
||||||
ProcessOwnershipCommitAck, StagedProcessOwnership, StateAnchorOwnership,
|
ProcessOwnershipCommitAck, StagedProcessOwnership, StateAnchorOwnership,
|
||||||
};
|
};
|
||||||
use lazaret_state_anchor::{
|
use lazaret_state_anchor::{StateAnchorError, StateAnchorLayout, VerifiedStateAnchorReadiness};
|
||||||
StateAnchorError, StateAnchorLayout, VerifiedStateAnchorReadiness, require_mount_absent,
|
|
||||||
};
|
|
||||||
use lazaret_storage_catalog::{Catalog, CatalogError};
|
use lazaret_storage_catalog::{Catalog, CatalogError};
|
||||||
|
|
||||||
use crate::domain_store::{DomainStoreError, DomainStoreMutation, HostCoordinatorDomainStore};
|
use crate::domain_store::{DomainStoreError, DomainStoreMutation, HostCoordinatorDomainStore};
|
||||||
|
|
@ -71,6 +71,8 @@ pub struct StateAnchorReleaseAuthority {
|
||||||
merged_label: PathBuf,
|
merged_label: PathBuf,
|
||||||
mount: MountIdentity,
|
mount: MountIdentity,
|
||||||
daemon: PidfsProcessIdentityV1,
|
daemon: PidfsProcessIdentityV1,
|
||||||
|
user_namespace: NamespaceIdentity,
|
||||||
|
mount_namespace: NamespaceIdentity,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StateAnchorReleaseAuthority {
|
impl StateAnchorReleaseAuthority {
|
||||||
|
|
@ -88,6 +90,16 @@ impl StateAnchorReleaseAuthority {
|
||||||
pub const fn daemon(&self) -> PidfsProcessIdentityV1 {
|
pub const fn daemon(&self) -> PidfsProcessIdentityV1 {
|
||||||
self.daemon
|
self.daemon
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub const fn user_namespace(&self) -> NamespaceIdentity {
|
||||||
|
self.user_namespace
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub const fn mount_namespace(&self) -> NamespaceIdentity {
|
||||||
|
self.mount_namespace
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct StateAnchorCoordinator<'a> {
|
pub struct StateAnchorCoordinator<'a> {
|
||||||
|
|
@ -252,6 +264,8 @@ impl<'a> StateAnchorCoordinator<'a> {
|
||||||
pub fn commit_daemon_ready(
|
pub fn commit_daemon_ready(
|
||||||
&self,
|
&self,
|
||||||
readiness: VerifiedStateAnchorReadiness,
|
readiness: VerifiedStateAnchorReadiness,
|
||||||
|
user_namespace: NamespaceIdentity,
|
||||||
|
mount_namespace: NamespaceIdentity,
|
||||||
) -> Result<(), StateAnchorCoordinatorError> {
|
) -> Result<(), StateAnchorCoordinatorError> {
|
||||||
let staged = readiness.staged();
|
let staged = readiness.staged();
|
||||||
self.require_staged_identity(staged)?;
|
self.require_staged_identity(staged)?;
|
||||||
|
|
@ -262,6 +276,8 @@ impl<'a> StateAnchorCoordinator<'a> {
|
||||||
self.update_daemon_and_mount(
|
self.update_daemon_and_mount(
|
||||||
staged.generation_id(),
|
staged.generation_id(),
|
||||||
PersistedHostProcessOwnership::committed(staged.identity()),
|
PersistedHostProcessOwnership::committed(staged.identity()),
|
||||||
|
Some(user_namespace),
|
||||||
|
Some(mount_namespace),
|
||||||
Some(readiness.mount()),
|
Some(readiness.mount()),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -365,7 +381,6 @@ impl<'a> StateAnchorCoordinator<'a> {
|
||||||
"release authority layout changed",
|
"release authority layout changed",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
require_mount_absent(layout)?;
|
|
||||||
let anchor = self.participant(authority.generation_id, ParticipantRole::StateAnchor)?;
|
let anchor = self.participant(authority.generation_id, ParticipantRole::StateAnchor)?;
|
||||||
let prepared = self.participant(authority.generation_id, ParticipantRole::PreparedLower)?;
|
let prepared = self.participant(authority.generation_id, ParticipantRole::PreparedLower)?;
|
||||||
match (anchor.state(), prepared.state()) {
|
match (anchor.state(), prepared.state()) {
|
||||||
|
|
@ -392,7 +407,7 @@ impl<'a> StateAnchorCoordinator<'a> {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.release_catalog_lease_if_present(authority, layout)?;
|
self.release_catalog_lease_if_present(authority)?;
|
||||||
self.domain_store
|
self.domain_store
|
||||||
.apply(&[DomainStoreMutation::CleanParticipantAfterAbsence(
|
.apply(&[DomainStoreMutation::CleanParticipantAfterAbsence(
|
||||||
clean_participant(authority.generation_id, ParticipantRole::PreparedLower)?,
|
clean_participant(authority.generation_id, ParticipantRole::PreparedLower)?,
|
||||||
|
|
@ -426,13 +441,15 @@ impl<'a> StateAnchorCoordinator<'a> {
|
||||||
generation_id: GenerationId,
|
generation_id: GenerationId,
|
||||||
daemon: PersistedHostProcessOwnership,
|
daemon: PersistedHostProcessOwnership,
|
||||||
) -> Result<(), StateAnchorCoordinatorError> {
|
) -> Result<(), StateAnchorCoordinatorError> {
|
||||||
self.update_daemon_and_mount(generation_id, daemon, None)
|
self.update_daemon_and_mount(generation_id, daemon, None, None, None)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_daemon_and_mount(
|
fn update_daemon_and_mount(
|
||||||
&self,
|
&self,
|
||||||
generation_id: GenerationId,
|
generation_id: GenerationId,
|
||||||
daemon: PersistedHostProcessOwnership,
|
daemon: PersistedHostProcessOwnership,
|
||||||
|
user_namespace: Option<NamespaceIdentity>,
|
||||||
|
mount_namespace: Option<NamespaceIdentity>,
|
||||||
mount: Option<MountIdentity>,
|
mount: Option<MountIdentity>,
|
||||||
) -> Result<(), StateAnchorCoordinatorError> {
|
) -> Result<(), StateAnchorCoordinatorError> {
|
||||||
let current = self.participant(generation_id, ParticipantRole::StateAnchor)?;
|
let current = self.participant(generation_id, ParticipantRole::StateAnchor)?;
|
||||||
|
|
@ -448,6 +465,8 @@ impl<'a> StateAnchorCoordinator<'a> {
|
||||||
}
|
}
|
||||||
let next = StateAnchorOwnership {
|
let next = StateAnchorOwnership {
|
||||||
daemon,
|
daemon,
|
||||||
|
user_namespace,
|
||||||
|
mount_namespace,
|
||||||
mount,
|
mount,
|
||||||
..ownership.clone()
|
..ownership.clone()
|
||||||
};
|
};
|
||||||
|
|
@ -512,7 +531,6 @@ impl<'a> StateAnchorCoordinator<'a> {
|
||||||
fn release_catalog_lease_if_present(
|
fn release_catalog_lease_if_present(
|
||||||
&self,
|
&self,
|
||||||
authority: &StateAnchorReleaseAuthority,
|
authority: &StateAnchorReleaseAuthority,
|
||||||
layout: &StateAnchorLayout<'_>,
|
|
||||||
) -> Result<(), StateAnchorCoordinatorError> {
|
) -> Result<(), StateAnchorCoordinatorError> {
|
||||||
let snapshot = self.catalog.snapshot()?;
|
let snapshot = self.catalog.snapshot()?;
|
||||||
let generation_lease = snapshot.artifact_leases.iter().find(|lease| {
|
let generation_lease = snapshot.artifact_leases.iter().find(|lease| {
|
||||||
|
|
@ -552,7 +570,7 @@ impl<'a> StateAnchorCoordinator<'a> {
|
||||||
"Domain release evidence is not committed",
|
"Domain release evidence is not committed",
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
require_mount_absent(layout).map_err(|error| CatalogError::Io(io::Error::other(error)))
|
Ok(())
|
||||||
})?;
|
})?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -610,6 +628,12 @@ fn release_authority(
|
||||||
.daemon
|
.daemon
|
||||||
.serviceable_pidfs()
|
.serviceable_pidfs()
|
||||||
.map_err(|_| StateAnchorCoordinatorError::InvalidState("daemon is not committed"))?,
|
.map_err(|_| StateAnchorCoordinatorError::InvalidState("daemon is not committed"))?,
|
||||||
|
user_namespace: anchor_ownership.user_namespace.ok_or(
|
||||||
|
StateAnchorCoordinatorError::InvalidState("user namespace identity is absent"),
|
||||||
|
)?,
|
||||||
|
mount_namespace: anchor_ownership.mount_namespace.ok_or(
|
||||||
|
StateAnchorCoordinatorError::InvalidState("mount namespace identity is absent"),
|
||||||
|
)?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,10 @@ use lazaret_core::configuration::{
|
||||||
};
|
};
|
||||||
use lazaret_core::identity::{
|
use lazaret_core::identity::{
|
||||||
BootId, ConfigurationFingerprint, CoordinatorId, DeviceId, GenerationId, HostPath,
|
BootId, ConfigurationFingerprint, CoordinatorId, DeviceId, GenerationId, HostPath,
|
||||||
HostPidLocator, IdMapDigest, InodeId, MountId, MountIdentity, NetworkSidecarId, PidfsDeviceId,
|
HostPidLocator, IdMapDigest, InodeId, MountId, MountIdentity, NamespaceIdentity,
|
||||||
PidfsInodeId, PidfsProcessAuthorityV1, PidfsProcessIdentityV1, PolicyId, PrepareOperationId,
|
NetworkSidecarId, PidfsDeviceId, PidfsInodeId, PidfsProcessAuthorityV1, PidfsProcessIdentityV1,
|
||||||
PreparedArtifactId, ProcessStartTimeTicks, SandboxId, SelectedManifestDigest, SourceIdentity,
|
PolicyId, PrepareOperationId, PreparedArtifactId, ProcessStartTimeTicks, SandboxId,
|
||||||
|
SelectedManifestDigest, SourceIdentity,
|
||||||
};
|
};
|
||||||
use lazaret_core::records::{
|
use lazaret_core::records::{
|
||||||
ArtifactState, Domain, DurableDecision, Generation, ParticipantRole, ParticipantState,
|
ArtifactState, Domain, DurableDecision, Generation, ParticipantRole, ParticipantState,
|
||||||
|
|
@ -138,6 +139,14 @@ fn durable_phases_resume_release_without_collapsing_lease_order() {
|
||||||
ParticipantState::Intent,
|
ParticipantState::Intent,
|
||||||
ParticipantOwnership::StateAnchor(StateAnchorOwnership {
|
ParticipantOwnership::StateAnchor(StateAnchorOwnership {
|
||||||
daemon: PersistedHostProcessOwnership::committed(staged.identity()),
|
daemon: PersistedHostProcessOwnership::committed(staged.identity()),
|
||||||
|
user_namespace: Some(NamespaceIdentity {
|
||||||
|
device: DeviceId::new(93),
|
||||||
|
inode: InodeId::new(94),
|
||||||
|
}),
|
||||||
|
mount_namespace: Some(NamespaceIdentity {
|
||||||
|
device: DeviceId::new(95),
|
||||||
|
inode: InodeId::new(96),
|
||||||
|
}),
|
||||||
mount: Some(MountIdentity {
|
mount: Some(MountIdentity {
|
||||||
mount_id: MountId::new(90),
|
mount_id: MountId::new(90),
|
||||||
device: DeviceId::new(91),
|
device: DeviceId::new(91),
|
||||||
|
|
|
||||||
|
|
@ -27,10 +27,13 @@ fn validated_inputs_stage_one_complete_plan_and_drop_exact_runtime_artifacts() {
|
||||||
let staged = ProductionColdStart::stage(fixture.inputs(&fixture.escalation_client)).unwrap();
|
let staged = ProductionColdStart::stage(fixture.inputs(&fixture.escalation_client)).unwrap();
|
||||||
let bundle = fixture.bundle();
|
let bundle = fixture.bundle();
|
||||||
let control = fixture.control();
|
let control = fixture.control();
|
||||||
|
let overlay = fixture.overlay();
|
||||||
assert_private_artifacts(&bundle, &control);
|
assert_private_artifacts(&bundle, &control);
|
||||||
|
assert!(overlay.is_dir());
|
||||||
drop(staged);
|
drop(staged);
|
||||||
assert!(!bundle.exists());
|
assert!(!bundle.exists());
|
||||||
assert!(!control.exists());
|
assert!(!control.exists());
|
||||||
|
assert!(!overlay.exists());
|
||||||
assert!(
|
assert!(
|
||||||
ProductionColdStart::stage(fixture.inputs(&fixture.invalid_escalation_client)).is_err()
|
ProductionColdStart::stage(fixture.inputs(&fixture.invalid_escalation_client)).is_err()
|
||||||
);
|
);
|
||||||
|
|
@ -90,6 +93,7 @@ struct ProductionFixture {
|
||||||
catalog: Catalog,
|
catalog: Catalog,
|
||||||
store: Arc<HostCoordinatorDomainStore>,
|
store: Arc<HostCoordinatorDomainStore>,
|
||||||
artifact: PreparedArtifact,
|
artifact: PreparedArtifact,
|
||||||
|
artifact_authority: File,
|
||||||
generation: Generation,
|
generation: Generation,
|
||||||
container_id: ContainerId,
|
container_id: ContainerId,
|
||||||
escalation_client: File,
|
escalation_client: File,
|
||||||
|
|
@ -139,6 +143,7 @@ impl ProductionFixture {
|
||||||
let lower = tree.path().join("prepared-lower");
|
let lower = tree.path().join("prepared-lower");
|
||||||
fs::create_dir(&lower).unwrap();
|
fs::create_dir(&lower).unwrap();
|
||||||
let metadata = lower.metadata().unwrap();
|
let metadata = lower.metadata().unwrap();
|
||||||
|
let artifact_authority = File::open(&lower).unwrap();
|
||||||
let artifact = PreparedArtifact {
|
let artifact = PreparedArtifact {
|
||||||
prepared_artifact_id: PreparedArtifactId::new([8; 32]),
|
prepared_artifact_id: PreparedArtifactId::new([8; 32]),
|
||||||
source_identity: SourceIdentity::new(b"synthetic-spike".to_vec()),
|
source_identity: SourceIdentity::new(b"synthetic-spike".to_vec()),
|
||||||
|
|
@ -166,6 +171,7 @@ impl ProductionFixture {
|
||||||
catalog,
|
catalog,
|
||||||
store,
|
store,
|
||||||
artifact,
|
artifact,
|
||||||
|
artifact_authority,
|
||||||
generation,
|
generation,
|
||||||
container_id: ContainerId::new([5; 32]),
|
container_id: ContainerId::new([5; 32]),
|
||||||
escalation_client: File::open("/bin/true").unwrap(),
|
escalation_client: File::open("/bin/true").unwrap(),
|
||||||
|
|
@ -180,6 +186,10 @@ impl ProductionFixture {
|
||||||
configuration: &self.configuration,
|
configuration: &self.configuration,
|
||||||
generation: self.generation.clone(),
|
generation: self.generation.clone(),
|
||||||
prepared_artifact: &self.artifact,
|
prepared_artifact: &self.artifact,
|
||||||
|
prepared_artifact_authority: &self.artifact_authority,
|
||||||
|
prepared_artifact_label: Path::new(
|
||||||
|
std::str::from_utf8(self.artifact.path.as_bytes()).unwrap(),
|
||||||
|
),
|
||||||
root_process: RootProcess::new("/bin/sh", ["-c".into(), "sleep infinity".into()])
|
root_process: RootProcess::new("/bin/sh", ["-c".into(), "sleep infinity".into()])
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
resolver_addresses: vec![IpAddress::V4([10, 0, 2, 3])],
|
resolver_addresses: vec![IpAddress::V4([10, 0, 2, 3])],
|
||||||
|
|
@ -214,6 +224,12 @@ impl ProductionFixture {
|
||||||
.join("sandbox-control")
|
.join("sandbox-control")
|
||||||
.join(hex(self.container_id.as_bytes()))
|
.join(hex(self.container_id.as_bytes()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn overlay(&self) -> PathBuf {
|
||||||
|
self.runtime_domain
|
||||||
|
.join("runsc-overlay")
|
||||||
|
.join(hex(self.container_id.as_bytes()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn assert_private_artifacts(bundle: &Path, control: &Path) {
|
fn assert_private_artifacts(bundle: &Path, control: &Path) {
|
||||||
|
|
|
||||||
|
|
@ -686,6 +686,7 @@ impl Fixture {
|
||||||
expected_network_namespace: namespace,
|
expected_network_namespace: namespace,
|
||||||
root_process,
|
root_process,
|
||||||
terminal: None,
|
terminal: None,
|
||||||
|
prepared_root: runtime_domain.join("prepared-root"),
|
||||||
})
|
})
|
||||||
.map_err(failure)?;
|
.map_err(failure)?;
|
||||||
Self::finish_build(
|
Self::finish_build(
|
||||||
|
|
@ -743,6 +744,7 @@ impl Fixture {
|
||||||
expected_network_namespace: namespace,
|
expected_network_namespace: namespace,
|
||||||
root_process,
|
root_process,
|
||||||
terminal: None,
|
terminal: None,
|
||||||
|
prepared_root: runtime_domain.join("prepared-root"),
|
||||||
})
|
})
|
||||||
.map_err(failure)?;
|
.map_err(failure)?;
|
||||||
Self::finish_build(
|
Self::finish_build(
|
||||||
|
|
@ -846,6 +848,8 @@ impl Fixture {
|
||||||
)
|
)
|
||||||
.map_err(failure)?,
|
.map_err(failure)?,
|
||||||
terminal: None,
|
terminal: None,
|
||||||
|
prepared_root: host_path(self.configuration.runtime_domain_root().canonical_label())
|
||||||
|
.join("prepared-root"),
|
||||||
})
|
})
|
||||||
.map_err(failure)
|
.map_err(failure)
|
||||||
}
|
}
|
||||||
|
|
@ -888,7 +892,7 @@ impl NetworkFixture {
|
||||||
let runtime_domain = host_path(configuration.runtime_domain_root().canonical_label());
|
let runtime_domain = host_path(configuration.runtime_domain_root().canonical_label());
|
||||||
let container_id = ContainerId::new([16; 32]);
|
let container_id = ContainerId::new([16; 32]);
|
||||||
stage_rootfs_and_control(&runtime_domain, container_id)?;
|
stage_rootfs_and_control(&runtime_domain, container_id)?;
|
||||||
let rootfs = runtime_domain.join("state-anchor/merged");
|
let rootfs = runtime_domain.join("prepared-root");
|
||||||
fs::copy(
|
fs::copy(
|
||||||
std::env::current_exe()?,
|
std::env::current_exe()?,
|
||||||
rootfs.join("bin/lazaret-network-probe"),
|
rootfs.join("bin/lazaret-network-probe"),
|
||||||
|
|
@ -917,6 +921,7 @@ impl NetworkFixture {
|
||||||
expected_network_namespace: namespace,
|
expected_network_namespace: namespace,
|
||||||
root_process,
|
root_process,
|
||||||
terminal: None,
|
terminal: None,
|
||||||
|
prepared_root: runtime_domain.join("prepared-root"),
|
||||||
})
|
})
|
||||||
.map_err(failure)?;
|
.map_err(failure)?;
|
||||||
let bundle_path = runtime_domain
|
let bundle_path = runtime_domain
|
||||||
|
|
@ -1177,10 +1182,44 @@ fn stage_rootfs_and_control(
|
||||||
runtime_domain: &Path,
|
runtime_domain: &Path,
|
||||||
container_id: ContainerId,
|
container_id: ContainerId,
|
||||||
) -> Result<(), MatrixError> {
|
) -> Result<(), MatrixError> {
|
||||||
let rootfs = runtime_domain.join("state-anchor/merged");
|
let rootfs = runtime_domain.join("prepared-root");
|
||||||
for directory in ["bin", "lib64"] {
|
for directory in [
|
||||||
|
"bin",
|
||||||
|
"lib64",
|
||||||
|
"dev",
|
||||||
|
"proc",
|
||||||
|
"sys",
|
||||||
|
"tmp",
|
||||||
|
"etc",
|
||||||
|
"home",
|
||||||
|
"workspace",
|
||||||
|
"run/lazaret/credentials",
|
||||||
|
"run/lazaret/mailbox/requests",
|
||||||
|
"run/lazaret/mailbox/results",
|
||||||
|
"run/lazaret/bin",
|
||||||
|
] {
|
||||||
fs::create_dir_all(rootfs.join(directory))?;
|
fs::create_dir_all(rootfs.join(directory))?;
|
||||||
}
|
}
|
||||||
|
for file in [
|
||||||
|
"etc/resolv.conf",
|
||||||
|
"etc/hosts",
|
||||||
|
"etc/hostname",
|
||||||
|
"run/lazaret/bin/lazaret-escalate",
|
||||||
|
"run/lazaret/bin/lazaret-sandbox-init",
|
||||||
|
] {
|
||||||
|
fs::write(rootfs.join(file), b"")?;
|
||||||
|
}
|
||||||
|
fs::create_dir_all(runtime_domain.join("state-anchor/merged"))?;
|
||||||
|
let runtime_files = runtime_domain.join("state-anchor/runtime-files");
|
||||||
|
fs::create_dir_all(&runtime_files)?;
|
||||||
|
for file in ["resolv.conf", "hosts", "hostname"] {
|
||||||
|
fs::write(runtime_files.join(file), b"fixture")?;
|
||||||
|
}
|
||||||
|
fs::create_dir_all(
|
||||||
|
runtime_domain
|
||||||
|
.join("runsc-overlay")
|
||||||
|
.join(hex(container_id.as_bytes())),
|
||||||
|
)?;
|
||||||
for (source, destination) in [
|
for (source, destination) in [
|
||||||
("/bin/sh", "bin/sh"),
|
("/bin/sh", "bin/sh"),
|
||||||
("/bin/sleep", "bin/sleep"),
|
("/bin/sleep", "bin/sleep"),
|
||||||
|
|
@ -1199,11 +1238,12 @@ fn stage_rootfs_and_control(
|
||||||
fs::create_dir_all(control.join(directory))?;
|
fs::create_dir_all(control.join(directory))?;
|
||||||
}
|
}
|
||||||
fs::copy("/bin/true", control.join("bin/lazaret-escalate"))?;
|
fs::copy("/bin/true", control.join("bin/lazaret-escalate"))?;
|
||||||
|
fs::copy("/bin/true", control.join("bin/lazaret-sandbox-init"))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn stage_benchmark_rootfs_and_control(runtime_domain: &Path) -> Result<(), MatrixError> {
|
fn stage_benchmark_rootfs_and_control(runtime_domain: &Path) -> Result<(), MatrixError> {
|
||||||
let rootfs = runtime_domain.join("state-anchor/merged");
|
let rootfs = runtime_domain.join("prepared-root");
|
||||||
for directory in [
|
for directory in [
|
||||||
"bin",
|
"bin",
|
||||||
"tooling/usr",
|
"tooling/usr",
|
||||||
|
|
@ -1213,6 +1253,17 @@ fn stage_benchmark_rootfs_and_control(runtime_domain: &Path) -> Result<(), Matri
|
||||||
"fixtures/host",
|
"fixtures/host",
|
||||||
"fixtures/output",
|
"fixtures/output",
|
||||||
"fixtures/repository",
|
"fixtures/repository",
|
||||||
|
"dev",
|
||||||
|
"proc",
|
||||||
|
"sys",
|
||||||
|
"tmp",
|
||||||
|
"etc",
|
||||||
|
"home",
|
||||||
|
"workspace",
|
||||||
|
"run/lazaret/credentials",
|
||||||
|
"run/lazaret/mailbox/requests",
|
||||||
|
"run/lazaret/mailbox/results",
|
||||||
|
"run/lazaret/bin",
|
||||||
] {
|
] {
|
||||||
fs::create_dir_all(rootfs.join(directory))?;
|
fs::create_dir_all(rootfs.join(directory))?;
|
||||||
}
|
}
|
||||||
|
|
@ -1224,12 +1275,31 @@ fn stage_benchmark_rootfs_and_control(runtime_domain: &Path) -> Result<(), Matri
|
||||||
symlink("usr/lib", rootfs.join("lib"))?;
|
symlink("usr/lib", rootfs.join("lib"))?;
|
||||||
symlink("usr/lib64", rootfs.join("lib64"))?;
|
symlink("usr/lib64", rootfs.join("lib64"))?;
|
||||||
symlink("usr/sbin", rootfs.join("sbin"))?;
|
symlink("usr/sbin", rootfs.join("sbin"))?;
|
||||||
|
for file in [
|
||||||
|
"etc/resolv.conf",
|
||||||
|
"etc/hosts",
|
||||||
|
"etc/hostname",
|
||||||
|
"run/lazaret/bin/lazaret-escalate",
|
||||||
|
"run/lazaret/bin/lazaret-sandbox-init",
|
||||||
|
] {
|
||||||
|
fs::write(rootfs.join(file), b"")?;
|
||||||
|
}
|
||||||
|
for directory in ["tooling", "fixtures"] {
|
||||||
|
fs::create_dir_all(runtime_domain.join("state-anchor/merged").join(directory))?;
|
||||||
|
}
|
||||||
|
let runtime_files = runtime_domain.join("state-anchor/runtime-files");
|
||||||
|
fs::create_dir_all(&runtime_files)?;
|
||||||
|
for file in ["resolv.conf", "hosts", "hostname"] {
|
||||||
|
fs::write(runtime_files.join(file), b"fixture")?;
|
||||||
|
}
|
||||||
|
fs::create_dir_all(runtime_domain.join("runsc-overlay").join(hex(&[6; 32])))?;
|
||||||
|
|
||||||
let control = runtime_domain.join("sandbox-control").join(hex(&[6; 32]));
|
let control = runtime_domain.join("sandbox-control").join(hex(&[6; 32]));
|
||||||
for directory in ["credentials", "mailbox/requests", "mailbox/results", "bin"] {
|
for directory in ["credentials", "mailbox/requests", "mailbox/results", "bin"] {
|
||||||
fs::create_dir_all(control.join(directory))?;
|
fs::create_dir_all(control.join(directory))?;
|
||||||
}
|
}
|
||||||
fs::copy("/bin/true", control.join("bin/lazaret-escalate"))?;
|
fs::copy("/bin/true", control.join("bin/lazaret-escalate"))?;
|
||||||
|
fs::copy("/bin/true", control.join("bin/lazaret-sandbox-init"))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,18 @@ pub struct IdMap {
|
||||||
pub gid: Vec<IdMapRange>,
|
pub gid: Vec<IdMapRange>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
|
||||||
|
pub enum PreparedDestinationKind {
|
||||||
|
Directory,
|
||||||
|
RegularFile,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
|
||||||
|
pub struct PreparedDestination {
|
||||||
|
pub path: PathBuf,
|
||||||
|
pub kind: PreparedDestinationKind,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
pub struct PreparationLimits {
|
pub struct PreparationLimits {
|
||||||
pub max_layers: usize,
|
pub max_layers: usize,
|
||||||
|
|
@ -189,11 +201,91 @@ pub(crate) fn apply_layout(
|
||||||
bail!("layer {index} diff-ID mismatch");
|
bail!("layer {index} diff-ID mismatch");
|
||||||
}
|
}
|
||||||
validate_tar(&spool, id_map, limits, &mut usage)?;
|
validate_tar(&spool, id_map, limits, &mut usage)?;
|
||||||
state.apply(&spool, rootfs)?;
|
state.apply(&spool, rootfs, id_map)?;
|
||||||
}
|
}
|
||||||
state.finish(rootfs)
|
state.finish(rootfs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn materialize_destinations(
|
||||||
|
root: &File,
|
||||||
|
destinations: &[PreparedDestination],
|
||||||
|
id_map: &IdMap,
|
||||||
|
) -> Result<()> {
|
||||||
|
if destinations.is_empty() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let uid = map_owner(0, &id_map.uid, "uid")?;
|
||||||
|
let gid = map_owner(0, &id_map.gid, "gid")?;
|
||||||
|
for destination in destinations {
|
||||||
|
let relative = destination
|
||||||
|
.path
|
||||||
|
.strip_prefix("/")
|
||||||
|
.context("prepared destination is not absolute")?;
|
||||||
|
let relative = normalize_relative(relative)?;
|
||||||
|
let (parent, leaf) = materialize_parent(root, &relative, uid, gid)?;
|
||||||
|
let expected = match destination.kind {
|
||||||
|
PreparedDestinationKind::Directory => FileType::Directory,
|
||||||
|
PreparedDestinationKind::RegularFile => FileType::RegularFile,
|
||||||
|
};
|
||||||
|
match path_type(&parent, Path::new(&leaf))? {
|
||||||
|
Some(actual) if actual == expected => {}
|
||||||
|
Some(_) => bail!("prepared destination conflicts with the image layout"),
|
||||||
|
None => match destination.kind {
|
||||||
|
PreparedDestinationKind::Directory => {
|
||||||
|
mkdirat(&parent, &leaf, Mode::from_raw_mode(0o755))?;
|
||||||
|
set_materialized_metadata(open_component(&parent, &leaf)?, uid, gid)?;
|
||||||
|
}
|
||||||
|
PreparedDestinationKind::RegularFile => {
|
||||||
|
let file = openat2(
|
||||||
|
&parent,
|
||||||
|
&leaf,
|
||||||
|
OFlags::WRONLY
|
||||||
|
| OFlags::CREATE
|
||||||
|
| OFlags::EXCL
|
||||||
|
| OFlags::CLOEXEC
|
||||||
|
| OFlags::NOFOLLOW,
|
||||||
|
Mode::from_raw_mode(0o755),
|
||||||
|
secure_resolve(),
|
||||||
|
)?;
|
||||||
|
set_materialized_metadata(file, uid, gid)?;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn materialize_parent(root: &File, path: &Path, uid: u32, gid: u32) -> Result<(OwnedFd, OsString)> {
|
||||||
|
let leaf = path
|
||||||
|
.file_name()
|
||||||
|
.context("prepared destination has no final component")?
|
||||||
|
.to_os_string();
|
||||||
|
let mut current = dup(root)?;
|
||||||
|
for component in path.parent().into_iter().flat_map(Path::components) {
|
||||||
|
let Component::Normal(name) = component else {
|
||||||
|
bail!("prepared destination is not normalized");
|
||||||
|
};
|
||||||
|
match mkdirat(¤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>(
|
fn read_json<T: serde::de::DeserializeOwned>(
|
||||||
layout: &LayoutSource<'_>,
|
layout: &LayoutSource<'_>,
|
||||||
path: &Path,
|
path: &Path,
|
||||||
|
|
@ -315,7 +407,8 @@ fn validate_tar(
|
||||||
let mut paths = HashSet::new();
|
let mut paths = HashSet::new();
|
||||||
for entry in archive.entries()? {
|
for entry in archive.entries()? {
|
||||||
let mut entry = entry?;
|
let mut entry = entry?;
|
||||||
let path = normalize_relative(&entry.path()?)?;
|
let entry_type = entry.header().entry_type();
|
||||||
|
let path = normalize_layer_path(&entry.path()?, entry_type)?;
|
||||||
if !paths.insert(path.clone()) {
|
if !paths.insert(path.clone()) {
|
||||||
bail!("policy {POLICY_NAME} rejects duplicate layer path");
|
bail!("policy {POLICY_NAME} rejects duplicate layer path");
|
||||||
}
|
}
|
||||||
|
|
@ -353,9 +446,8 @@ fn validate_tar(
|
||||||
bail!("policy {POLICY_NAME} rejects PAX metadata");
|
bail!("policy {POLICY_NAME} rejects PAX metadata");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
validate_owner(entry.header().uid()?, &id_map.uid, "uid")?;
|
map_owner(entry.header().uid()?, &id_map.uid, "uid")?;
|
||||||
validate_owner(entry.header().gid()?, &id_map.gid, "gid")?;
|
map_owner(entry.header().gid()?, &id_map.gid, "gid")?;
|
||||||
let entry_type = entry.header().entry_type();
|
|
||||||
if !matches!(
|
if !matches!(
|
||||||
entry_type,
|
entry_type,
|
||||||
EntryType::Regular
|
EntryType::Regular
|
||||||
|
|
@ -407,17 +499,35 @@ fn normalize_relative(path: &Path) -> Result<PathBuf> {
|
||||||
Ok(normalized)
|
Ok(normalized)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_owner(value: u64, ranges: &[IdMapRange], name: &str) -> Result<()> {
|
fn normalize_layer_path(path: &Path, entry_type: EntryType) -> Result<PathBuf> {
|
||||||
|
match normalize_relative(path) {
|
||||||
|
Ok(path) => Ok(path),
|
||||||
|
Err(_)
|
||||||
|
if entry_type == EntryType::Directory
|
||||||
|
&& path.components().next().is_some()
|
||||||
|
&& path
|
||||||
|
.components()
|
||||||
|
.all(|component| component == Component::CurDir) =>
|
||||||
|
{
|
||||||
|
Ok(PathBuf::from("."))
|
||||||
|
}
|
||||||
|
Err(error) => Err(error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn map_owner(value: u64, ranges: &[IdMapRange], name: &str) -> Result<u32> {
|
||||||
let Ok(value) = u32::try_from(value) else {
|
let Ok(value) = u32::try_from(value) else {
|
||||||
bail!("{name} {value} is outside the target ID map");
|
bail!("{name} {value} is outside the target ID map");
|
||||||
};
|
};
|
||||||
if !ranges
|
for range in ranges {
|
||||||
.iter()
|
if value >= range.container_id && value - range.container_id < range.size {
|
||||||
.any(|range| value >= range.container_id && value - range.container_id < range.size)
|
return range
|
||||||
{
|
.namespace_id
|
||||||
bail!("{name} {value} is outside the target ID map");
|
.checked_add(value - range.container_id)
|
||||||
|
.context("mapped owner overflows");
|
||||||
}
|
}
|
||||||
Ok(())
|
}
|
||||||
|
bail!("{name} {value} is outside the target ID map")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_id_map(id_map: &IdMap) -> Result<()> {
|
fn validate_id_map(id_map: &IdMap) -> Result<()> {
|
||||||
|
|
@ -483,7 +593,7 @@ struct LayerState {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LayerState {
|
impl LayerState {
|
||||||
fn apply(&mut self, tar_source: &File, root: &File) -> Result<()> {
|
fn apply(&mut self, tar_source: &File, root: &File, id_map: &IdMap) -> Result<()> {
|
||||||
apply_whiteouts(tar_source, root, &mut self.directories)?;
|
apply_whiteouts(tar_source, root, &mut self.directories)?;
|
||||||
let mut archive = tar::Archive::new(rewound(tar_source)?);
|
let mut archive = tar::Archive::new(rewound(tar_source)?);
|
||||||
let mut hardlinks = Vec::new();
|
let mut hardlinks = Vec::new();
|
||||||
|
|
@ -497,7 +607,7 @@ impl LayerState {
|
||||||
if name == ".wh..wh..opq" || name.starts_with(".wh.") {
|
if name == ".wh..wh..opq" || name.starts_with(".wh.") {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let metadata = EntryMetadata::from_header(path.clone(), entry.header())?;
|
let metadata = EntryMetadata::from_header(path.clone(), entry.header(), id_map)?;
|
||||||
match entry.header().entry_type() {
|
match entry.header().entry_type() {
|
||||||
EntryType::Regular | EntryType::Continuous => {
|
EntryType::Regular | EntryType::Continuous => {
|
||||||
forget_directory_tree(&mut self.directories, &path);
|
forget_directory_tree(&mut self.directories, &path);
|
||||||
|
|
@ -557,11 +667,11 @@ struct EntryMetadata {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EntryMetadata {
|
impl EntryMetadata {
|
||||||
fn from_header(path: PathBuf, header: &Header) -> Result<Self> {
|
fn from_header(path: PathBuf, header: &Header, id_map: &IdMap) -> Result<Self> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
path,
|
path,
|
||||||
uid: u32::try_from(header.uid()?).context("entry uid exceeds u32")?,
|
uid: map_owner(header.uid()?, &id_map.uid, "uid")?,
|
||||||
gid: u32::try_from(header.gid()?).context("entry gid exceeds u32")?,
|
gid: map_owner(header.gid()?, &id_map.gid, "gid")?,
|
||||||
mode: header.mode()?,
|
mode: header.mode()?,
|
||||||
mtime: header.mtime()?,
|
mtime: header.mtime()?,
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@ mod layer;
|
||||||
mod store;
|
mod store;
|
||||||
mod transport;
|
mod transport;
|
||||||
|
|
||||||
pub use layer::{IdMap, IdMapRange, PreparationLimits};
|
pub use layer::{
|
||||||
|
IdMap, IdMapRange, PreparationLimits, PreparedDestination, PreparedDestinationKind,
|
||||||
|
};
|
||||||
pub use store::{PrepareRecovery, PrepareRequest, PreparedLowerStore, ReclaimOutcome};
|
pub use store::{PrepareRecovery, PrepareRequest, PreparedLowerStore, ReclaimOutcome};
|
||||||
pub use transport::{OciRegistryTransport, Platform, RegistryCredentials, TransportedImage};
|
pub use transport::{OciRegistryTransport, Platform, RegistryCredentials, TransportedImage};
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,8 @@ use rustix::fs::{
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
|
|
||||||
use crate::layer::{
|
use crate::layer::{
|
||||||
IdMap, LayoutSource, POLICY_NAME, PreparationLimits, apply_layout, remove_children,
|
IdMap, LayoutSource, POLICY_NAME, PreparationLimits, PreparedDestination,
|
||||||
|
PreparedDestinationKind, apply_layout, materialize_destinations, remove_children,
|
||||||
verify_layout,
|
verify_layout,
|
||||||
};
|
};
|
||||||
use crate::transport::TransportedImage;
|
use crate::transport::TransportedImage;
|
||||||
|
|
@ -38,6 +39,7 @@ pub struct PrepareRequest {
|
||||||
pub source_identity: Vec<u8>,
|
pub source_identity: Vec<u8>,
|
||||||
pub selected_manifest_digest: [u8; 32],
|
pub selected_manifest_digest: [u8; 32],
|
||||||
pub id_map: IdMap,
|
pub id_map: IdMap,
|
||||||
|
pub destinations: Vec<PreparedDestination>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
|
@ -115,6 +117,22 @@ impl PreparedLowerStore {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Opens the exact published artifact beneath the retained prepared-lower root.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Refuses a non-canonical artifact record, catalog drift, or physical identity drift.
|
||||||
|
pub fn open_artifact_authority(&self, artifact: &PreparedArtifact) -> Result<File> {
|
||||||
|
let catalog_artifact = self
|
||||||
|
.catalog_artifact(artifact.prepared_artifact_id)?
|
||||||
|
.context("prepared artifact is absent from the catalog")?;
|
||||||
|
if catalog_artifact != *artifact {
|
||||||
|
bail!("prepared artifact does not match the catalog");
|
||||||
|
}
|
||||||
|
self.require_artifact_identity(artifact)?;
|
||||||
|
self.open_relative(&artifact_path(artifact.prepared_artifact_id))?
|
||||||
|
.context("prepared artifact path is absent")
|
||||||
|
}
|
||||||
|
|
||||||
/// Materializes and atomically publishes one verified immutable prepared lower.
|
/// Materializes and atomically publishes one verified immutable prepared lower.
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
|
|
@ -134,6 +152,7 @@ impl PreparedLowerStore {
|
||||||
operation_id: PrepareOperationId,
|
operation_id: PrepareOperationId,
|
||||||
image: &TransportedImage,
|
image: &TransportedImage,
|
||||||
id_map: IdMap,
|
id_map: IdMap,
|
||||||
|
destinations: Vec<PreparedDestination>,
|
||||||
) -> Result<PreparedArtifact> {
|
) -> Result<PreparedArtifact> {
|
||||||
let layout = image
|
let layout = image
|
||||||
.retained_layout()
|
.retained_layout()
|
||||||
|
|
@ -144,6 +163,7 @@ impl PreparedLowerStore {
|
||||||
source_identity: image.source_identity.clone(),
|
source_identity: image.source_identity.clone(),
|
||||||
selected_manifest_digest: image.selected_manifest_digest,
|
selected_manifest_digest: image.selected_manifest_digest,
|
||||||
id_map,
|
id_map,
|
||||||
|
destinations,
|
||||||
};
|
};
|
||||||
self.prepare_from_source(&request, &LayoutSource::Retained(layout.root()))
|
self.prepare_from_source(&request, &LayoutSource::Retained(layout.root()))
|
||||||
}
|
}
|
||||||
|
|
@ -194,6 +214,7 @@ impl PreparedLowerStore {
|
||||||
return Err(error.into());
|
return Err(error.into());
|
||||||
}
|
}
|
||||||
if let Err(error) = apply_layout(layout, &stage, &verified, &request.id_map, self.limits)
|
if let Err(error) = apply_layout(layout, &stage, &verified, &request.id_map, self.limits)
|
||||||
|
.and_then(|()| materialize_destinations(&stage, &request.destinations, &request.id_map))
|
||||||
.and_then(|()| sync_tree(&stage))
|
.and_then(|()| sync_tree(&stage))
|
||||||
{
|
{
|
||||||
if self
|
if self
|
||||||
|
|
@ -494,7 +515,13 @@ impl Identities {
|
||||||
fn new(request: &PrepareRequest) -> Result<Self> {
|
fn new(request: &PrepareRequest) -> Result<Self> {
|
||||||
let source = SourceIdentity::new(request.source_identity.clone());
|
let source = SourceIdentity::new(request.source_identity.clone());
|
||||||
let manifest = SelectedManifestDigest::new(request.selected_manifest_digest);
|
let manifest = SelectedManifestDigest::new(request.selected_manifest_digest);
|
||||||
let policy = PolicyId::new(hash_parts(&[b"lazaret-policy-v1", POLICY_NAME.as_bytes()]));
|
let destinations = canonical_destinations(&request.destinations)?;
|
||||||
|
let destination_bytes = destination_policy_bytes(&destinations);
|
||||||
|
let policy = PolicyId::new(hash_parts(&[
|
||||||
|
b"lazaret-policy-v2",
|
||||||
|
POLICY_NAME.as_bytes(),
|
||||||
|
&destination_bytes,
|
||||||
|
]));
|
||||||
let id_map_bytes = postcard::to_stdvec(&request.id_map)?;
|
let id_map_bytes = postcard::to_stdvec(&request.id_map)?;
|
||||||
let id_map = IdMapDigest::new(hash_parts(&[b"lazaret-id-map-v1", &id_map_bytes]));
|
let id_map = IdMapDigest::new(hash_parts(&[b"lazaret-id-map-v1", &id_map_bytes]));
|
||||||
let artifact_id = PreparedArtifactId::new(hash_parts(&[
|
let artifact_id = PreparedArtifactId::new(hash_parts(&[
|
||||||
|
|
@ -555,6 +582,42 @@ impl Identities {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn canonical_destinations(
|
||||||
|
destinations: &[PreparedDestination],
|
||||||
|
) -> Result<Vec<PreparedDestination>> {
|
||||||
|
let mut destinations = destinations.to_vec();
|
||||||
|
for destination in &destinations {
|
||||||
|
let mut components = destination.path.components();
|
||||||
|
if components.next() != Some(std::path::Component::RootDir)
|
||||||
|
|| components.any(|component| !matches!(component, std::path::Component::Normal(_)))
|
||||||
|
{
|
||||||
|
bail!("prepared destination is not an absolute normalized path");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
destinations.sort();
|
||||||
|
destinations.dedup();
|
||||||
|
for pair in destinations.windows(2) {
|
||||||
|
if pair[0].path == pair[1].path && pair[0].kind != pair[1].kind {
|
||||||
|
bail!("prepared destination has conflicting types");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(destinations)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn destination_policy_bytes(destinations: &[PreparedDestination]) -> Vec<u8> {
|
||||||
|
let mut bytes = b"lazaret-prepared-layout-policy-v1\0".to_vec();
|
||||||
|
for destination in destinations {
|
||||||
|
bytes.extend_from_slice(destination.path.as_os_str().as_bytes());
|
||||||
|
bytes.push(0);
|
||||||
|
bytes.push(match destination.kind {
|
||||||
|
PreparedDestinationKind::Directory => b'd',
|
||||||
|
PreparedDestinationKind::RegularFile => b'f',
|
||||||
|
});
|
||||||
|
bytes.push(0);
|
||||||
|
}
|
||||||
|
bytes
|
||||||
|
}
|
||||||
|
|
||||||
fn artifact_from_operation(
|
fn artifact_from_operation(
|
||||||
operation: &PrepareOperation,
|
operation: &PrepareOperation,
|
||||||
path: &Path,
|
path: &Path,
|
||||||
|
|
|
||||||
|
|
@ -223,8 +223,10 @@ impl OciRegistryTransport {
|
||||||
}
|
}
|
||||||
verify_platform(&config_json, platform)?;
|
verify_platform(&config_json, platform)?;
|
||||||
validate_manifest(&manifest)?;
|
validate_manifest(&manifest)?;
|
||||||
let layout = LayoutWriter::create(destination)?;
|
let layout = LayoutWriter::create(destination).context("creating private OCI layout")?;
|
||||||
layout.write_verified_blob(&manifest.config.digest, config_json.as_bytes())?;
|
layout
|
||||||
|
.write_verified_blob(&manifest.config.digest, config_json.as_bytes())
|
||||||
|
.context("writing verified OCI config")?;
|
||||||
for descriptor in &manifest.layers {
|
for descriptor in &manifest.layers {
|
||||||
let size = u64::try_from(descriptor.size).context("negative OCI layer size")?;
|
let size = u64::try_from(descriptor.size).context("negative OCI layer size")?;
|
||||||
if size > MAX_LAYER_BYTES {
|
if size > MAX_LAYER_BYTES {
|
||||||
|
|
@ -235,23 +237,33 @@ impl OciRegistryTransport {
|
||||||
layout.require_existing_blob(&descriptor.digest, size)?;
|
layout.require_existing_blob(&descriptor.digest, size)?;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let (file, temporary) = layout.create_blob(digest_component)?;
|
let (file, temporary) = layout
|
||||||
|
.create_blob(digest_component)
|
||||||
|
.with_context(|| format!("creating OCI layer {}", descriptor.digest))?;
|
||||||
let file = tokio::fs::File::from_std(file);
|
let file = tokio::fs::File::from_std(file);
|
||||||
if let Err(error) = client.pull_blob(&reference, descriptor, file).await {
|
if let Err(error) = client.pull_blob(&reference, descriptor, file).await {
|
||||||
layout.remove_temporary_blob(&temporary);
|
layout.remove_temporary_blob(&temporary);
|
||||||
return Err(error)
|
return Err(error)
|
||||||
.with_context(|| format!("pulling OCI layer {}", descriptor.digest));
|
.with_context(|| format!("pulling OCI layer {}", descriptor.digest));
|
||||||
}
|
}
|
||||||
let observed = layout.temporary_blob_size(&temporary)?;
|
let observed = layout
|
||||||
|
.temporary_blob_size(&temporary)
|
||||||
|
.with_context(|| format!("measuring OCI layer {}", descriptor.digest))?;
|
||||||
if observed != size {
|
if observed != size {
|
||||||
bail!("OCI layer {} size mismatch", descriptor.digest);
|
bail!("OCI layer {} size mismatch", descriptor.digest);
|
||||||
}
|
}
|
||||||
layout.publish_blob(&temporary, digest_component)?;
|
layout
|
||||||
|
.publish_blob(&temporary, digest_component)
|
||||||
|
.with_context(|| format!("publishing OCI layer {}", descriptor.digest))?;
|
||||||
}
|
}
|
||||||
let local_manifest = serde_json::to_vec(&manifest)?;
|
let local_manifest = serde_json::to_vec(&manifest)?;
|
||||||
let local_digest = format!("sha256:{}", hex(&Sha256::digest(&local_manifest)));
|
let local_digest = format!("sha256:{}", hex(&Sha256::digest(&local_manifest)));
|
||||||
layout.write_verified_blob(&local_digest, &local_manifest)?;
|
layout
|
||||||
layout.write_layout_index(&local_digest, local_manifest.len())?;
|
.write_verified_blob(&local_digest, &local_manifest)
|
||||||
|
.context("writing verified OCI manifest")?;
|
||||||
|
layout
|
||||||
|
.write_layout_index(&local_digest, local_manifest.len())
|
||||||
|
.context("writing OCI layout index")?;
|
||||||
let selected_manifest_digest = parse_sha256(&local_digest)?;
|
let selected_manifest_digest = parse_sha256(&local_digest)?;
|
||||||
let source_identity = format!(
|
let source_identity = format!(
|
||||||
"oci-v1\0{}\0{}\0{manifest_digest}",
|
"oci-v1\0{}\0{}\0{manifest_digest}",
|
||||||
|
|
@ -406,13 +418,19 @@ impl LayoutWriter {
|
||||||
bail!("OCI descriptor digest mismatch for {digest}");
|
bail!("OCI descriptor digest mismatch for {digest}");
|
||||||
}
|
}
|
||||||
let component = digest_component(digest)?;
|
let component = digest_component(digest)?;
|
||||||
if self.blob_exists(component)? {
|
if self
|
||||||
|
.blob_exists(component)
|
||||||
|
.context("checking verified OCI blob destination")?
|
||||||
|
{
|
||||||
return self.require_existing_blob(digest, bytes.len() as u64);
|
return self.require_existing_blob(digest, bytes.len() as u64);
|
||||||
}
|
}
|
||||||
let (mut file, temporary) = self.create_blob(component)?;
|
let (mut file, temporary) = self
|
||||||
file.write_all(bytes)?;
|
.create_blob(component)
|
||||||
file.sync_all()?;
|
.context("creating verified OCI blob")?;
|
||||||
|
file.write_all(bytes).context("writing verified OCI blob")?;
|
||||||
|
file.sync_all().context("syncing verified OCI blob")?;
|
||||||
self.publish_blob(&temporary, component)
|
self.publish_blob(&temporary, component)
|
||||||
|
.context("publishing verified OCI blob")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn blob_exists(&self, component: &str) -> Result<bool> {
|
fn blob_exists(&self, component: &str) -> Result<bool> {
|
||||||
|
|
@ -552,11 +570,16 @@ fn open_directory(parent: &File, path: &Path) -> std::io::Result<File> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn open_relative_file(parent: &File, path: &Path, flags: OFlags) -> std::io::Result<File> {
|
fn open_relative_file(parent: &File, path: &Path, flags: OFlags) -> std::io::Result<File> {
|
||||||
|
let mode = if flags.contains(OFlags::CREATE) {
|
||||||
|
Mode::from_raw_mode(0o600)
|
||||||
|
} else {
|
||||||
|
Mode::empty()
|
||||||
|
};
|
||||||
openat2(
|
openat2(
|
||||||
parent,
|
parent,
|
||||||
path,
|
path,
|
||||||
flags | OFlags::NOFOLLOW | OFlags::CLOEXEC,
|
flags | OFlags::NOFOLLOW | OFlags::CLOEXEC,
|
||||||
Mode::from_raw_mode(0o600),
|
mode,
|
||||||
confined_resolve(),
|
confined_resolve(),
|
||||||
)
|
)
|
||||||
.map(File::from)
|
.map(File::from)
|
||||||
|
|
@ -566,7 +589,11 @@ fn open_relative_file(parent: &File, path: &Path, flags: OFlags) -> std::io::Res
|
||||||
fn write_relative_atomic(root: &File, path: &Path, bytes: &[u8]) -> Result<()> {
|
fn write_relative_atomic(root: &File, path: &Path, bytes: &[u8]) -> Result<()> {
|
||||||
let name = path.file_name().context("layout file has no name")?;
|
let name = path.file_name().context("layout file has no name")?;
|
||||||
let parent_path = path.parent().unwrap_or(Path::new("."));
|
let parent_path = path.parent().unwrap_or(Path::new("."));
|
||||||
let parent = open_directory(root, parent_path)?;
|
let parent = if parent_path.as_os_str().is_empty() {
|
||||||
|
File::from(rustix::io::dup(root)?)
|
||||||
|
} else {
|
||||||
|
open_directory(root, parent_path)?
|
||||||
|
};
|
||||||
let temporary = format!(".{}.partial", name.to_string_lossy());
|
let temporary = format!(".{}.partial", name.to_string_lossy());
|
||||||
let mut file = open_relative_file(
|
let mut file = open_relative_file(
|
||||||
&parent,
|
&parent,
|
||||||
|
|
@ -709,6 +736,25 @@ mod tests {
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn atomic_layout_write_accepts_a_root_level_destination() {
|
||||||
|
let tree = tempfile::tempdir().unwrap();
|
||||||
|
let root = File::open(tree.path()).unwrap();
|
||||||
|
write_relative_atomic(&root, Path::new("oci-layout"), b"layout").unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
std::fs::read(tree.path().join("oci-layout")).unwrap(),
|
||||||
|
b"layout"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn confined_read_open_uses_an_empty_creation_mode() {
|
||||||
|
let tree = tempfile::tempdir().unwrap();
|
||||||
|
let root = File::open(tree.path()).unwrap();
|
||||||
|
let error = open_relative_file(&root, Path::new("absent"), OFlags::RDONLY).unwrap_err();
|
||||||
|
assert_eq!(error.kind(), std::io::ErrorKind::NotFound);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn preexisting_transport_parents_require_exact_private_mode() {
|
fn preexisting_transport_parents_require_exact_private_mode() {
|
||||||
for name in ["image-transports", "blobs", "sha256"] {
|
for name in ["image-transports", "blobs", "sha256"] {
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,8 @@ use lazaret_core::identity::{
|
||||||
};
|
};
|
||||||
use lazaret_core::records::{PrepareOperation, PrepareStageState};
|
use lazaret_core::records::{PrepareOperation, PrepareStageState};
|
||||||
use lazaret_prepared_lower::{
|
use lazaret_prepared_lower::{
|
||||||
IdMap, IdMapRange, PrepareRecovery, PrepareRequest, PreparedLowerStore, ReclaimOutcome,
|
IdMap, IdMapRange, PrepareRecovery, PrepareRequest, PreparedDestination,
|
||||||
|
PreparedDestinationKind, PreparedLowerStore, ReclaimOutcome,
|
||||||
};
|
};
|
||||||
use lazaret_storage_catalog::{Catalog, CatalogError};
|
use lazaret_storage_catalog::{Catalog, CatalogError};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
@ -59,6 +60,7 @@ impl Fixture {
|
||||||
source_identity: b"oci-v1\0registry.invalid\0fixture\0sha256:source".to_vec(),
|
source_identity: b"oci-v1\0registry.invalid\0fixture\0sha256:source".to_vec(),
|
||||||
selected_manifest_digest: manifest,
|
selected_manifest_digest: manifest,
|
||||||
id_map: current_id_map(),
|
id_map: current_id_map(),
|
||||||
|
destinations: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -85,6 +87,55 @@ fn preexisting_fixed_children_require_exact_private_mode() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn published_artifact_authority_is_revalidated_beneath_the_retained_root() {
|
||||||
|
let fixture = Fixture::new();
|
||||||
|
let request = fixture.request(
|
||||||
|
1,
|
||||||
|
&[(
|
||||||
|
layer(&[Entry {
|
||||||
|
path: "payload",
|
||||||
|
kind: EntryType::Regular,
|
||||||
|
data: b"content",
|
||||||
|
target: None,
|
||||||
|
mode: 0o644,
|
||||||
|
}]),
|
||||||
|
OCI_TAR,
|
||||||
|
)],
|
||||||
|
);
|
||||||
|
let artifact = fixture.store.prepare(&request).unwrap();
|
||||||
|
|
||||||
|
let authority = fixture.store.open_artifact_authority(&artifact).unwrap();
|
||||||
|
let metadata = authority.metadata().unwrap();
|
||||||
|
assert_eq!(metadata.dev(), artifact.device.get());
|
||||||
|
assert_eq!(metadata.ino(), artifact.inode.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn replaced_published_artifact_is_refused() {
|
||||||
|
let fixture = Fixture::new();
|
||||||
|
let request = fixture.request(
|
||||||
|
1,
|
||||||
|
&[(
|
||||||
|
layer(&[Entry {
|
||||||
|
path: "payload",
|
||||||
|
kind: EntryType::Regular,
|
||||||
|
data: b"content",
|
||||||
|
target: None,
|
||||||
|
mode: 0o644,
|
||||||
|
}]),
|
||||||
|
OCI_TAR,
|
||||||
|
)],
|
||||||
|
);
|
||||||
|
let artifact = fixture.store.prepare(&request).unwrap();
|
||||||
|
let artifact_root = fixture.artifact_root(&artifact);
|
||||||
|
let displaced = fixture.root.join("displaced-artifact");
|
||||||
|
std::fs::rename(&artifact_root, &displaced).unwrap();
|
||||||
|
std::fs::create_dir(&artifact_root).unwrap();
|
||||||
|
|
||||||
|
assert!(fixture.store.open_artifact_authority(&artifact).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[allow(clippy::disallowed_methods, clippy::disallowed_types)]
|
#[allow(clippy::disallowed_methods, clippy::disallowed_types)]
|
||||||
fn retained_fixed_child_bind_mount_is_refused_when_mount_setup_is_available() {
|
fn retained_fixed_child_bind_mount_is_refused_when_mount_setup_is_available() {
|
||||||
|
|
@ -155,6 +206,10 @@ struct Entry<'a> {
|
||||||
fn layer(entries: &[Entry<'_>]) -> Vec<u8> {
|
fn layer(entries: &[Entry<'_>]) -> Vec<u8> {
|
||||||
let uid = rustix::process::geteuid().as_raw();
|
let uid = rustix::process::geteuid().as_raw();
|
||||||
let gid = rustix::process::getegid().as_raw();
|
let gid = rustix::process::getegid().as_raw();
|
||||||
|
layer_with_owner(entries, uid, gid)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn layer_with_owner(entries: &[Entry<'_>], uid: u32, gid: u32) -> Vec<u8> {
|
||||||
let mut bytes = Vec::new();
|
let mut bytes = Vec::new();
|
||||||
{
|
{
|
||||||
let mut builder = Builder::new(&mut bytes);
|
let mut builder = Builder::new(&mut bytes);
|
||||||
|
|
@ -514,6 +569,115 @@ fn layer_entry_matching_internal_spool_name_is_preserved() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn root_directory_layer_entry_is_applied() {
|
||||||
|
let fixture = Fixture::new();
|
||||||
|
let bytes = layer(&[
|
||||||
|
Entry {
|
||||||
|
path: ".",
|
||||||
|
kind: EntryType::Directory,
|
||||||
|
data: b"",
|
||||||
|
target: None,
|
||||||
|
mode: 0o755,
|
||||||
|
},
|
||||||
|
Entry {
|
||||||
|
path: "payload",
|
||||||
|
kind: EntryType::Regular,
|
||||||
|
data: b"content",
|
||||||
|
target: None,
|
||||||
|
mode: 0o644,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
let request = fixture.request(5, &[(bytes, OCI_TAR)]);
|
||||||
|
|
||||||
|
let artifact = fixture.store.prepare(&request).unwrap();
|
||||||
|
let root = fixture.artifact_root(&artifact);
|
||||||
|
assert_eq!(std::fs::read(root.join("payload")).unwrap(), b"content");
|
||||||
|
assert_eq!(
|
||||||
|
root.metadata().unwrap().permissions().mode() & 0o7777,
|
||||||
|
0o755
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn container_root_owner_maps_to_invoking_host_ids() {
|
||||||
|
let fixture = Fixture::new();
|
||||||
|
let bytes = layer_with_owner(
|
||||||
|
&[Entry {
|
||||||
|
path: "payload",
|
||||||
|
kind: EntryType::Regular,
|
||||||
|
data: b"content",
|
||||||
|
target: None,
|
||||||
|
mode: 0o640,
|
||||||
|
}],
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
let mut request = fixture.request(6, &[(bytes, OCI_TAR)]);
|
||||||
|
request.id_map = IdMap {
|
||||||
|
uid: vec![IdMapRange {
|
||||||
|
container_id: 0,
|
||||||
|
namespace_id: rustix::process::geteuid().as_raw(),
|
||||||
|
size: 1,
|
||||||
|
}],
|
||||||
|
gid: vec![IdMapRange {
|
||||||
|
container_id: 0,
|
||||||
|
namespace_id: rustix::process::getegid().as_raw(),
|
||||||
|
size: 1,
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
|
||||||
|
let artifact = fixture.store.prepare(&request).unwrap();
|
||||||
|
let metadata = fixture
|
||||||
|
.artifact_root(&artifact)
|
||||||
|
.join("payload")
|
||||||
|
.metadata()
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(metadata.uid(), rustix::process::geteuid().as_raw());
|
||||||
|
assert_eq!(metadata.gid(), rustix::process::getegid().as_raw());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unmapped_nonzero_owners_refuse_before_publication() {
|
||||||
|
for (operation, uid, gid) in [(7, 1, 0), (8, 0, 1)] {
|
||||||
|
let fixture = Fixture::new();
|
||||||
|
let bytes = layer_with_owner(
|
||||||
|
&[Entry {
|
||||||
|
path: "payload",
|
||||||
|
kind: EntryType::Regular,
|
||||||
|
data: b"content",
|
||||||
|
target: None,
|
||||||
|
mode: 0o640,
|
||||||
|
}],
|
||||||
|
uid,
|
||||||
|
gid,
|
||||||
|
);
|
||||||
|
let mut request = fixture.request(operation, &[(bytes, OCI_TAR)]);
|
||||||
|
request.id_map = IdMap {
|
||||||
|
uid: vec![IdMapRange {
|
||||||
|
container_id: 0,
|
||||||
|
namespace_id: rustix::process::geteuid().as_raw(),
|
||||||
|
size: 1,
|
||||||
|
}],
|
||||||
|
gid: vec![IdMapRange {
|
||||||
|
container_id: 0,
|
||||||
|
namespace_id: rustix::process::getegid().as_raw(),
|
||||||
|
size: 1,
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(fixture.store.prepare(&request).is_err());
|
||||||
|
assert!(
|
||||||
|
fixture
|
||||||
|
.catalog
|
||||||
|
.snapshot()
|
||||||
|
.unwrap()
|
||||||
|
.prepared_artifacts
|
||||||
|
.is_empty()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn root_opaque_whiteout_removes_only_lower_layer_entries() {
|
fn root_opaque_whiteout_removes_only_lower_layer_entries() {
|
||||||
let fixture = Fixture::new();
|
let fixture = Fixture::new();
|
||||||
|
|
@ -971,6 +1135,98 @@ fn reclaim_refuses_forged_identity_and_leases() {
|
||||||
assert!(fixture.artifact_root(&artifact).exists());
|
assert!(fixture.artifact_root(&artifact).exists());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn destination_layout_is_materialized_and_canonical_in_artifact_identity() {
|
||||||
|
let fixture = Fixture::new();
|
||||||
|
let bytes = layer_with_owner(&[], 0, 0);
|
||||||
|
let mut first = fixture.request(72, &[(bytes.clone(), OCI_TAR)]);
|
||||||
|
first.id_map = id_map_with_container_root();
|
||||||
|
first.destinations = vec![
|
||||||
|
PreparedDestination {
|
||||||
|
path: "/workspace".into(),
|
||||||
|
kind: PreparedDestinationKind::Directory,
|
||||||
|
},
|
||||||
|
PreparedDestination {
|
||||||
|
path: "/run/lazaret/bin/entry".into(),
|
||||||
|
kind: PreparedDestinationKind::RegularFile,
|
||||||
|
},
|
||||||
|
PreparedDestination {
|
||||||
|
path: "/workspace".into(),
|
||||||
|
kind: PreparedDestinationKind::Directory,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
let artifact = fixture.store.prepare(&first).unwrap();
|
||||||
|
let root = fixture.artifact_root(&artifact);
|
||||||
|
assert!(root.join("workspace").is_dir());
|
||||||
|
assert!(root.join("run/lazaret/bin/entry").is_file());
|
||||||
|
|
||||||
|
let mut reordered = fixture.request(73, &[(bytes.clone(), OCI_TAR)]);
|
||||||
|
reordered.id_map = id_map_with_container_root();
|
||||||
|
reordered.destinations = first.destinations.iter().rev().cloned().collect();
|
||||||
|
assert_eq!(fixture.store.prepare(&reordered).unwrap(), artifact);
|
||||||
|
|
||||||
|
let mut unequal = fixture.request(74, &[(bytes, OCI_TAR)]);
|
||||||
|
unequal.id_map = id_map_with_container_root();
|
||||||
|
unequal.destinations = vec![PreparedDestination {
|
||||||
|
path: "/opt/other".into(),
|
||||||
|
kind: PreparedDestinationKind::Directory,
|
||||||
|
}];
|
||||||
|
assert_ne!(
|
||||||
|
fixture
|
||||||
|
.store
|
||||||
|
.prepare(&unequal)
|
||||||
|
.unwrap()
|
||||||
|
.prepared_artifact_id,
|
||||||
|
artifact.prepared_artifact_id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn destination_type_conflict_refuses_before_publication() {
|
||||||
|
let fixture = Fixture::new();
|
||||||
|
let bytes = layer_with_owner(
|
||||||
|
&[Entry {
|
||||||
|
path: "opt/lazaret-state",
|
||||||
|
kind: EntryType::Regular,
|
||||||
|
data: b"conflict",
|
||||||
|
target: None,
|
||||||
|
mode: 0o644,
|
||||||
|
}],
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
let mut request = fixture.request(75, &[(bytes, OCI_TAR)]);
|
||||||
|
request.id_map = id_map_with_container_root();
|
||||||
|
request.destinations = vec![PreparedDestination {
|
||||||
|
path: "/opt/lazaret-state".into(),
|
||||||
|
kind: PreparedDestinationKind::Directory,
|
||||||
|
}];
|
||||||
|
assert!(fixture.store.prepare(&request).is_err());
|
||||||
|
assert!(
|
||||||
|
fixture
|
||||||
|
.catalog
|
||||||
|
.snapshot()
|
||||||
|
.unwrap()
|
||||||
|
.prepared_artifacts
|
||||||
|
.is_empty()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn id_map_with_container_root() -> IdMap {
|
||||||
|
IdMap {
|
||||||
|
uid: vec![IdMapRange {
|
||||||
|
container_id: 0,
|
||||||
|
namespace_id: rustix::process::geteuid().as_raw(),
|
||||||
|
size: 1,
|
||||||
|
}],
|
||||||
|
gid: vec![IdMapRange {
|
||||||
|
container_id: 0,
|
||||||
|
namespace_id: rustix::process::getegid().as_raw(),
|
||||||
|
size: 1,
|
||||||
|
}],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn hex(bytes: &[u8]) -> String {
|
fn hex(bytes: &[u8]) -> String {
|
||||||
use std::fmt::Write as _;
|
use std::fmt::Write as _;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,8 @@ use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use lazaret_core::codec::decode;
|
use lazaret_core::codec::decode;
|
||||||
use lazaret_core::identity::{
|
use lazaret_core::identity::{
|
||||||
ConfigurationFingerprint, DerivedPolicyDigest, DomainId, GenerationId, NetworkSidecarId,
|
ConfigurationFingerprint, DerivedPolicyDigest, DomainId, GenerationId, NamespaceIdentity,
|
||||||
PidfsProcessIdentityV1,
|
NetworkSidecarId, PidfsProcessIdentityV1,
|
||||||
};
|
};
|
||||||
use lazaret_core::network::{CanonicalDerivedNetworkPolicy, derive_policy};
|
use lazaret_core::network::{CanonicalDerivedNetworkPolicy, derive_policy};
|
||||||
use lazaret_core::records::{
|
use lazaret_core::records::{
|
||||||
|
|
@ -175,7 +175,7 @@ where
|
||||||
},
|
},
|
||||||
stage,
|
stage,
|
||||||
acknowledge,
|
acknowledge,
|
||||||
enter_domain_namespaces,
|
enter_generation_network_namespace,
|
||||||
)?;
|
)?;
|
||||||
drop(helper_ready_signal);
|
drop(helper_ready_signal);
|
||||||
drop(helper_exit);
|
drop(helper_exit);
|
||||||
|
|
@ -214,18 +214,28 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
fn enter_domain_namespaces() -> io::Result<()> {
|
pub(crate) fn enter_generation_filesystem_namespaces()
|
||||||
|
-> io::Result<(NamespaceIdentity, NamespaceIdentity)> {
|
||||||
let uid = getuid().as_raw();
|
let uid = getuid().as_raw();
|
||||||
let gid = getgid().as_raw();
|
let gid = getgid().as_raw();
|
||||||
rustix::thread::unshare(UnshareFlags::NEWUSER)?;
|
rustix::thread::unshare(UnshareFlags::NEWUSER)?;
|
||||||
std::fs::write("/proc/self/setgroups", "deny")?;
|
std::fs::write("/proc/self/setgroups", "deny")?;
|
||||||
std::fs::write("/proc/self/uid_map", format!("0 {uid} 1\n"))?;
|
std::fs::write("/proc/self/uid_map", format!("0 {uid} 1\n"))?;
|
||||||
std::fs::write("/proc/self/gid_map", format!("0 {gid} 1\n"))?;
|
std::fs::write("/proc/self/gid_map", format!("0 {gid} 1\n"))?;
|
||||||
rustix::thread::unshare(UnshareFlags::NEWNS | UnshareFlags::NEWNET)?;
|
rustix::thread::unshare(UnshareFlags::NEWNS)?;
|
||||||
mount_change(
|
mount_change(
|
||||||
"/",
|
"/",
|
||||||
MountPropagationFlags::PRIVATE | MountPropagationFlags::REC,
|
MountPropagationFlags::PRIVATE | MountPropagationFlags::REC,
|
||||||
)?;
|
)?;
|
||||||
|
Ok((
|
||||||
|
namespace_identity(Path::new("/proc/self/ns/user"))?,
|
||||||
|
namespace_identity(Path::new("/proc/self/ns/mnt"))?,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(deprecated)]
|
||||||
|
fn enter_generation_network_namespace() -> io::Result<()> {
|
||||||
|
rustix::thread::unshare(UnshareFlags::NEWNET)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -219,6 +219,8 @@ pub struct SandboxHandoffAuthority<'configuration> {
|
||||||
configuration_authority: ConfigurationAuthorityIdentity,
|
configuration_authority: ConfigurationAuthorityIdentity,
|
||||||
command: RunscCommand,
|
command: RunscCommand,
|
||||||
derived: Vec<DerivedHandoffAuthority>,
|
derived: Vec<DerivedHandoffAuthority>,
|
||||||
|
prepared_root: PathBuf,
|
||||||
|
prepared_root_identity: FileIdentity,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SandboxHandoffAuthority<'_> {
|
impl SandboxHandoffAuthority<'_> {
|
||||||
|
|
@ -241,17 +243,30 @@ impl SandboxHandoffAuthority<'_> {
|
||||||
self.configuration
|
self.configuration
|
||||||
.workspace()
|
.workspace()
|
||||||
.open_authority()
|
.open_authority()
|
||||||
.map_err(changed_configuration_authority)?;
|
.map_err(|error| changed_authority_at("workspace", error))?;
|
||||||
for volume in self.configuration.volumes() {
|
for volume in self.configuration.volumes() {
|
||||||
volume
|
volume
|
||||||
.open_source()
|
.open_source()
|
||||||
.map_err(changed_configuration_authority)?;
|
.map_err(|error| changed_authority_at("volume", error))?;
|
||||||
|
}
|
||||||
|
let prepared_root = open_absolute_directory(&self.prepared_root)?;
|
||||||
|
if descriptor_identity(&prepared_root).map_err(|error| {
|
||||||
|
SandboxBuildError::new(
|
||||||
|
SandboxBuildErrorKind::ChangedAuthority,
|
||||||
|
format!("prepared root identity: {error}"),
|
||||||
|
)
|
||||||
|
})? != self.prepared_root_identity
|
||||||
|
{
|
||||||
|
return Err(SandboxBuildError::new(
|
||||||
|
SandboxBuildErrorKind::ChangedAuthority,
|
||||||
|
"prepared root changed identity after bundle construction",
|
||||||
|
));
|
||||||
}
|
}
|
||||||
let runtime_root = self
|
let runtime_root = self
|
||||||
.configuration
|
.configuration
|
||||||
.runtime_domain_root()
|
.runtime_domain_root()
|
||||||
.open_authority()
|
.open_authority()
|
||||||
.map_err(changed_configuration_authority)?;
|
.map_err(|error| changed_authority_at("Runtime Domain", error))?;
|
||||||
for authority in &self.derived {
|
for authority in &self.derived {
|
||||||
let descriptor = open_runtime_source(
|
let descriptor = open_runtime_source(
|
||||||
&runtime_root,
|
&runtime_root,
|
||||||
|
|
@ -259,7 +274,16 @@ impl SandboxHandoffAuthority<'_> {
|
||||||
authority.expected_type,
|
authority.expected_type,
|
||||||
authority.expected_bytes.is_some(),
|
authority.expected_bytes.is_some(),
|
||||||
)?;
|
)?;
|
||||||
if descriptor_identity(&descriptor)? != authority.identity {
|
if descriptor_identity(&descriptor).map_err(|error| {
|
||||||
|
SandboxBuildError::new(
|
||||||
|
SandboxBuildErrorKind::ChangedAuthority,
|
||||||
|
format!(
|
||||||
|
"runtime handoff source {} identity: {error}",
|
||||||
|
authority.relative_path.display()
|
||||||
|
),
|
||||||
|
)
|
||||||
|
})? != authority.identity
|
||||||
|
{
|
||||||
return Err(SandboxBuildError::new(
|
return Err(SandboxBuildError::new(
|
||||||
SandboxBuildErrorKind::ChangedAuthority,
|
SandboxBuildErrorKind::ChangedAuthority,
|
||||||
"runtime handoff source changed identity",
|
"runtime handoff source changed identity",
|
||||||
|
|
@ -416,6 +440,7 @@ pub struct SandboxBuildRequest<'a> {
|
||||||
pub expected_network_namespace: NamespaceIdentity,
|
pub expected_network_namespace: NamespaceIdentity,
|
||||||
pub root_process: RootProcess,
|
pub root_process: RootProcess,
|
||||||
pub terminal: Option<StagedHostTerminal>,
|
pub terminal: Option<StagedHostTerminal>,
|
||||||
|
pub prepared_root: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
|
@ -458,6 +483,8 @@ pub struct SandboxBundle {
|
||||||
configuration_fingerprint: ConfigurationFingerprint,
|
configuration_fingerprint: ConfigurationFingerprint,
|
||||||
configuration_authority: ConfigurationAuthorityIdentity,
|
configuration_authority: ConfigurationAuthorityIdentity,
|
||||||
runtime_sources: Vec<RuntimeHandoffSource>,
|
runtime_sources: Vec<RuntimeHandoffSource>,
|
||||||
|
prepared_root: PathBuf,
|
||||||
|
prepared_root_identity: FileIdentity,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SandboxBundle {
|
impl SandboxBundle {
|
||||||
|
|
@ -497,16 +524,29 @@ impl SandboxBundle {
|
||||||
configuration
|
configuration
|
||||||
.workspace()
|
.workspace()
|
||||||
.open_authority()
|
.open_authority()
|
||||||
.map_err(changed_configuration_authority)?;
|
.map_err(|error| changed_authority_at("workspace", error))?;
|
||||||
for volume in configuration.volumes() {
|
for volume in configuration.volumes() {
|
||||||
volume
|
volume
|
||||||
.open_source()
|
.open_source()
|
||||||
.map_err(changed_configuration_authority)?;
|
.map_err(|error| changed_authority_at("volume", error))?;
|
||||||
}
|
}
|
||||||
let runtime_root = configuration
|
let runtime_root = configuration
|
||||||
.runtime_domain_root()
|
.runtime_domain_root()
|
||||||
.open_authority()
|
.open_authority()
|
||||||
.map_err(changed_configuration_authority)?;
|
.map_err(|error| changed_authority_at("Runtime Domain", error))?;
|
||||||
|
let prepared_root = open_absolute_directory(&self.prepared_root)?;
|
||||||
|
if descriptor_identity(&prepared_root).map_err(|error| {
|
||||||
|
SandboxBuildError::new(
|
||||||
|
SandboxBuildErrorKind::ChangedAuthority,
|
||||||
|
format!("prepared root identity: {error}"),
|
||||||
|
)
|
||||||
|
})? != self.prepared_root_identity
|
||||||
|
{
|
||||||
|
return Err(SandboxBuildError::new(
|
||||||
|
SandboxBuildErrorKind::ChangedAuthority,
|
||||||
|
"prepared root changed identity after bundle construction",
|
||||||
|
));
|
||||||
|
}
|
||||||
let derived = self
|
let derived = self
|
||||||
.runtime_sources
|
.runtime_sources
|
||||||
.iter()
|
.iter()
|
||||||
|
|
@ -517,7 +557,15 @@ impl SandboxBundle {
|
||||||
source.expected_type,
|
source.expected_type,
|
||||||
source.expected_bytes.is_some(),
|
source.expected_bytes.is_some(),
|
||||||
)?;
|
)?;
|
||||||
let identity = descriptor_identity(&descriptor)?;
|
let identity = descriptor_identity(&descriptor).map_err(|error| {
|
||||||
|
SandboxBuildError::new(
|
||||||
|
SandboxBuildErrorKind::ChangedAuthority,
|
||||||
|
format!(
|
||||||
|
"runtime handoff source {} identity: {error}",
|
||||||
|
source.relative_path.display()
|
||||||
|
),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
if source
|
if source
|
||||||
.build_identity
|
.build_identity
|
||||||
.is_some_and(|expected| expected != identity)
|
.is_some_and(|expected| expected != identity)
|
||||||
|
|
@ -545,6 +593,8 @@ impl SandboxBundle {
|
||||||
configuration_authority: self.configuration_authority.clone(),
|
configuration_authority: self.configuration_authority.clone(),
|
||||||
command: self.command.clone(),
|
command: self.command.clone(),
|
||||||
derived,
|
derived,
|
||||||
|
prepared_root: self.prepared_root.clone(),
|
||||||
|
prepared_root_identity: self.prepared_root_identity,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -590,6 +640,8 @@ pub fn build_sandbox_bundle(
|
||||||
if let Some(terminal) = &request.terminal {
|
if let Some(terminal) = &request.terminal {
|
||||||
terminal.revalidate(request.configuration)?;
|
terminal.revalidate(request.configuration)?;
|
||||||
}
|
}
|
||||||
|
let prepared_root = open_absolute_directory(&request.prepared_root)?;
|
||||||
|
let prepared_root_identity = descriptor_identity(&prepared_root)?;
|
||||||
|
|
||||||
let paths = runtime_paths(request.configuration, request.container_id)?;
|
let paths = runtime_paths(request.configuration, request.container_id)?;
|
||||||
let mounts = build_mounts(
|
let mounts = build_mounts(
|
||||||
|
|
@ -604,7 +656,7 @@ pub fn build_sandbox_bundle(
|
||||||
)?;
|
)?;
|
||||||
let linux = build_linux()?;
|
let linux = build_linux()?;
|
||||||
let root = RootBuilder::default()
|
let root = RootBuilder::default()
|
||||||
.path(paths.rootfs.clone())
|
.path(request.prepared_root.clone())
|
||||||
.readonly(false)
|
.readonly(false)
|
||||||
.build()
|
.build()
|
||||||
.map_err(oci_error)?;
|
.map_err(oci_error)?;
|
||||||
|
|
@ -632,6 +684,8 @@ pub fn build_sandbox_bundle(
|
||||||
configuration_fingerprint: request.configuration.configuration_fingerprint(),
|
configuration_fingerprint: request.configuration.configuration_fingerprint(),
|
||||||
configuration_authority: ConfigurationAuthorityIdentity::capture(request.configuration),
|
configuration_authority: ConfigurationAuthorityIdentity::capture(request.configuration),
|
||||||
runtime_sources,
|
runtime_sources,
|
||||||
|
prepared_root: request.prepared_root,
|
||||||
|
prepared_root_identity,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -639,7 +693,7 @@ struct RuntimePaths {
|
||||||
container_name: String,
|
container_name: String,
|
||||||
bundle: PathBuf,
|
bundle: PathBuf,
|
||||||
runsc_root: PathBuf,
|
runsc_root: PathBuf,
|
||||||
rootfs: PathBuf,
|
overlay: PathBuf,
|
||||||
control_sources: PathBuf,
|
control_sources: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -662,7 +716,7 @@ fn handoff_runtime_sources(
|
||||||
),
|
),
|
||||||
(PathBuf::from("runsc"), HandoffPathType::Directory),
|
(PathBuf::from("runsc"), HandoffPathType::Directory),
|
||||||
(
|
(
|
||||||
PathBuf::from("state-anchor/merged"),
|
PathBuf::from("runsc-overlay").join(&paths.container_name),
|
||||||
HandoffPathType::Directory,
|
HandoffPathType::Directory,
|
||||||
),
|
),
|
||||||
(container.clone(), HandoffPathType::Directory),
|
(container.clone(), HandoffPathType::Directory),
|
||||||
|
|
@ -699,6 +753,14 @@ fn handoff_runtime_sources(
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
for name in ["resolv.conf", "hosts", "hostname"] {
|
||||||
|
sources.push(RuntimeHandoffSource {
|
||||||
|
relative_path: Path::new("state-anchor/runtime-files").join(name),
|
||||||
|
expected_type: HandoffPathType::RegularFile,
|
||||||
|
build_identity: None,
|
||||||
|
expected_bytes: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
if let Some(terminal) = terminal {
|
if let Some(terminal) = terminal {
|
||||||
sources.push(RuntimeHandoffSource {
|
sources.push(RuntimeHandoffSource {
|
||||||
relative_path: terminal.relative_source.clone(),
|
relative_path: terminal.relative_source.clone(),
|
||||||
|
|
@ -727,7 +789,15 @@ fn open_runtime_source(
|
||||||
Mode::empty(),
|
Mode::empty(),
|
||||||
ResolveFlags::BENEATH | ResolveFlags::NO_MAGICLINKS | ResolveFlags::NO_SYMLINKS,
|
ResolveFlags::BENEATH | ResolveFlags::NO_MAGICLINKS | ResolveFlags::NO_SYMLINKS,
|
||||||
)
|
)
|
||||||
.map_err(staging_error)?;
|
.map_err(|error| {
|
||||||
|
SandboxBuildError::new(
|
||||||
|
SandboxBuildErrorKind::ChangedAuthority,
|
||||||
|
format!(
|
||||||
|
"runtime handoff source {}: {error}",
|
||||||
|
relative_path.display()
|
||||||
|
),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
let stat = rustix::fs::fstat(&descriptor).map_err(staging_error)?;
|
let stat = rustix::fs::fstat(&descriptor).map_err(staging_error)?;
|
||||||
let actual = FileType::from_raw_mode(stat.st_mode);
|
let actual = FileType::from_raw_mode(stat.st_mode);
|
||||||
let matches = match expected_type {
|
let matches = match expected_type {
|
||||||
|
|
@ -793,7 +863,7 @@ fn runtime_paths(
|
||||||
Ok(RuntimePaths {
|
Ok(RuntimePaths {
|
||||||
bundle: runtime_domain.join("bundles").join(&container_name),
|
bundle: runtime_domain.join("bundles").join(&container_name),
|
||||||
runsc_root: runtime_domain.join("runsc"),
|
runsc_root: runtime_domain.join("runsc"),
|
||||||
rootfs: runtime_domain.join("state-anchor").join("merged"),
|
overlay: runtime_domain.join("runsc-overlay").join(&container_name),
|
||||||
control_sources: runtime_domain.join("sandbox-control").join(&container_name),
|
control_sources: runtime_domain.join("sandbox-control").join(&container_name),
|
||||||
container_name,
|
container_name,
|
||||||
})
|
})
|
||||||
|
|
@ -811,6 +881,17 @@ fn build_mounts(
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
)?);
|
)?);
|
||||||
|
let runtime_domain = host_path(configuration.runtime_domain_root().canonical_label())?;
|
||||||
|
for destination in &configuration.fingerprint_record().persistent_dirs {
|
||||||
|
mounts.push(bind_mount(
|
||||||
|
runtime_domain
|
||||||
|
.join("state-anchor/merged")
|
||||||
|
.join(destination.strip_prefix('/').unwrap_or(destination)),
|
||||||
|
PathBuf::from(destination),
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
)?);
|
||||||
|
}
|
||||||
for volume in configuration.volumes() {
|
for volume in configuration.volumes() {
|
||||||
let record = volume.record();
|
let record = volume.record();
|
||||||
mounts.push(bind_mount(
|
mounts.push(bind_mount(
|
||||||
|
|
@ -821,6 +902,14 @@ fn build_mounts(
|
||||||
)?);
|
)?);
|
||||||
}
|
}
|
||||||
mounts.extend(control_mounts(control_sources)?);
|
mounts.extend(control_mounts(control_sources)?);
|
||||||
|
for name in ["resolv.conf", "hosts", "hostname"] {
|
||||||
|
mounts.push(bind_mount(
|
||||||
|
runtime_domain.join("state-anchor/runtime-files").join(name),
|
||||||
|
Path::new("/etc").join(name),
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
)?);
|
||||||
|
}
|
||||||
if let Some(terminal) = terminal {
|
if let Some(terminal) = terminal {
|
||||||
mounts.push(bind_mount(
|
mounts.push(bind_mount(
|
||||||
host_path(configuration.runtime_domain_root().canonical_label())?
|
host_path(configuration.runtime_domain_root().canonical_label())?
|
||||||
|
|
@ -858,13 +947,13 @@ fn build_linux() -> Result<oci_spec::runtime::Linux, SandboxBuildError> {
|
||||||
.collect::<Result<Vec<_>, _>>()?;
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
let uid_mapping = LinuxIdMappingBuilder::default()
|
let uid_mapping = LinuxIdMappingBuilder::default()
|
||||||
.container_id(0_u32)
|
.container_id(0_u32)
|
||||||
.host_id(rustix::process::getuid().as_raw())
|
.host_id(0_u32)
|
||||||
.size(1_u32)
|
.size(1_u32)
|
||||||
.build()
|
.build()
|
||||||
.map_err(oci_error)?;
|
.map_err(oci_error)?;
|
||||||
let gid_mapping = LinuxIdMappingBuilder::default()
|
let gid_mapping = LinuxIdMappingBuilder::default()
|
||||||
.container_id(0_u32)
|
.container_id(0_u32)
|
||||||
.host_id(rustix::process::getgid().as_raw())
|
.host_id(0_u32)
|
||||||
.size(1_u32)
|
.size(1_u32)
|
||||||
.build()
|
.build()
|
||||||
.map_err(oci_error)?;
|
.map_err(oci_error)?;
|
||||||
|
|
@ -891,6 +980,13 @@ fn build_runsc_command(paths: &RuntimePaths, bundle_authorization: [u8; 32]) ->
|
||||||
OsString::from("-rootless"),
|
OsString::from("-rootless"),
|
||||||
OsString::from("-network=host"),
|
OsString::from("-network=host"),
|
||||||
OsString::from("-ignore-cgroups"),
|
OsString::from("-ignore-cgroups"),
|
||||||
|
OsString::from(format!(
|
||||||
|
"-overlay2=root:dir={}",
|
||||||
|
paths
|
||||||
|
.overlay
|
||||||
|
.to_str()
|
||||||
|
.expect("validated runtime path remains UTF-8")
|
||||||
|
)),
|
||||||
OsString::from("run"),
|
OsString::from("run"),
|
||||||
OsString::from(format!(
|
OsString::from(format!(
|
||||||
"-bundle={}",
|
"-bundle={}",
|
||||||
|
|
@ -905,6 +1001,23 @@ fn build_runsc_command(paths: &RuntimePaths, bundle_authorization: [u8; 32]) ->
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn open_absolute_directory(path: &Path) -> Result<OwnedFd, SandboxBuildError> {
|
||||||
|
validate_absolute_normalized(path)?;
|
||||||
|
openat2(
|
||||||
|
rustix::fs::CWD,
|
||||||
|
path,
|
||||||
|
OFlags::PATH | OFlags::DIRECTORY | OFlags::CLOEXEC | OFlags::NOFOLLOW,
|
||||||
|
Mode::empty(),
|
||||||
|
ResolveFlags::NO_MAGICLINKS | ResolveFlags::NO_SYMLINKS,
|
||||||
|
)
|
||||||
|
.map_err(|error| {
|
||||||
|
SandboxBuildError::new(
|
||||||
|
SandboxBuildErrorKind::ChangedAuthority,
|
||||||
|
format!("prepared root {}: {error}", path.display()),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn build_process(
|
fn build_process(
|
||||||
configuration: &ResolvedDomainConfiguration,
|
configuration: &ResolvedDomainConfiguration,
|
||||||
root_process: RootProcess,
|
root_process: RootProcess,
|
||||||
|
|
@ -1094,6 +1207,13 @@ fn changed_configuration_authority(error: impl fmt::Display) -> SandboxBuildErro
|
||||||
SandboxBuildError::new(SandboxBuildErrorKind::ChangedAuthority, error.to_string())
|
SandboxBuildError::new(SandboxBuildErrorKind::ChangedAuthority, error.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn changed_authority_at(label: &str, error: impl fmt::Display) -> SandboxBuildError {
|
||||||
|
SandboxBuildError::new(
|
||||||
|
SandboxBuildErrorKind::ChangedAuthority,
|
||||||
|
format!("{label}: {error}"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fn oci_error(error: impl fmt::Display) -> SandboxBuildError {
|
fn oci_error(error: impl fmt::Display) -> SandboxBuildError {
|
||||||
SandboxBuildError::new(SandboxBuildErrorKind::OciSpecification, error.to_string())
|
SandboxBuildError::new(SandboxBuildErrorKind::OciSpecification, error.to_string())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ use std::fs::File;
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::os::fd::{AsFd, AsRawFd, OwnedFd};
|
use std::os::fd::{AsFd, AsRawFd, OwnedFd};
|
||||||
use std::os::unix::fs::FileExt;
|
use std::os::unix::fs::FileExt;
|
||||||
|
use std::path::PathBuf;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use lazaret_core::codec::{decode, encode};
|
use lazaret_core::codec::{decode, encode};
|
||||||
|
|
@ -32,8 +33,8 @@ use rustix::thread::CapabilitySet;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::network::{
|
use crate::network::{
|
||||||
NetworkAttestationContext, NetworkProbeRequest, PreparedNetwork, prepare_network,
|
NetworkAttestationContext, NetworkProbeRequest, PreparedNetwork,
|
||||||
probe_current_network,
|
enter_generation_filesystem_namespaces, prepare_network, probe_current_network,
|
||||||
};
|
};
|
||||||
use crate::sandbox::RunscCommand;
|
use crate::sandbox::RunscCommand;
|
||||||
|
|
||||||
|
|
@ -64,6 +65,8 @@ pub enum SupervisorEvent {
|
||||||
StateAnchorStaged(StagedProcessOwnership),
|
StateAnchorStaged(StagedProcessOwnership),
|
||||||
StateAnchorReady {
|
StateAnchorReady {
|
||||||
staged: StagedProcessOwnership,
|
staged: StagedProcessOwnership,
|
||||||
|
user_namespace: lazaret_core::identity::NamespaceIdentity,
|
||||||
|
mount_namespace: lazaret_core::identity::NamespaceIdentity,
|
||||||
mount: lazaret_core::identity::MountIdentity,
|
mount: lazaret_core::identity::MountIdentity,
|
||||||
},
|
},
|
||||||
SidecarStarted,
|
SidecarStarted,
|
||||||
|
|
@ -80,7 +83,9 @@ pub enum SupervisorEvent {
|
||||||
SignalApplied(AttachedRunscSignal),
|
SignalApplied(AttachedRunscSignal),
|
||||||
SandboxStopped,
|
SandboxStopped,
|
||||||
SidecarStopped,
|
SidecarStopped,
|
||||||
StateAnchorUnmounted,
|
StateAnchorUnmounted {
|
||||||
|
mount: lazaret_core::identity::MountIdentity,
|
||||||
|
},
|
||||||
Exited,
|
Exited,
|
||||||
AttachedRunscExited,
|
AttachedRunscExited,
|
||||||
Stopped,
|
Stopped,
|
||||||
|
|
@ -362,14 +367,31 @@ impl RuntimeSupervisorLaunch {
|
||||||
target = target
|
target = target
|
||||||
.argument("--state-lower-fd")
|
.argument("--state-lower-fd")
|
||||||
.argument(authority.lower_fd().to_string())
|
.argument(authority.lower_fd().to_string())
|
||||||
|
.argument("--state-lower-label")
|
||||||
|
.argument(authority.lower_label())
|
||||||
|
.argument("--state-persistent-root-fd")
|
||||||
|
.argument(authority.persistent_root_fd().to_string())
|
||||||
.argument("--state-persistent-fd")
|
.argument("--state-persistent-fd")
|
||||||
.argument(authority.persistent_anchor_fd().to_string())
|
.argument(authority.persistent_anchor_fd().to_string())
|
||||||
|
.argument("--state-persistent-label")
|
||||||
|
.argument(authority.persistent_anchor_label())
|
||||||
|
.argument("--state-runtime-root-fd")
|
||||||
|
.argument(authority.runtime_root_fd().to_string())
|
||||||
.argument("--state-runtime-fd")
|
.argument("--state-runtime-fd")
|
||||||
.argument(authority.runtime_anchor_fd().to_string())
|
.argument(authority.runtime_anchor_fd().to_string())
|
||||||
|
.argument("--state-runtime-label")
|
||||||
|
.argument(authority.runtime_anchor_label())
|
||||||
.argument("--state-lower-device")
|
.argument("--state-lower-device")
|
||||||
.argument(device.to_string())
|
.argument(device.to_string())
|
||||||
.argument("--state-lower-inode")
|
.argument("--state-lower-inode")
|
||||||
.argument(inode.to_string());
|
.argument(inode.to_string())
|
||||||
|
.argument("--state-persistent-count")
|
||||||
|
.argument(authority.persistent_destinations().len().to_string());
|
||||||
|
for destination in authority.persistent_destinations() {
|
||||||
|
target = target
|
||||||
|
.argument("--state-persistent-destination")
|
||||||
|
.argument(destination);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if let (Some(policy_file), Some(digest), Some(fingerprint), Some(sidecar)) = (
|
if let (Some(policy_file), Some(digest), Some(fingerprint), Some(sidecar)) = (
|
||||||
&self.policy_file,
|
&self.policy_file,
|
||||||
|
|
@ -468,9 +490,15 @@ struct RuntimeArguments {
|
||||||
|
|
||||||
struct RuntimeStateAnchorArguments {
|
struct RuntimeStateAnchorArguments {
|
||||||
lower_fd: i32,
|
lower_fd: i32,
|
||||||
|
lower_label: PathBuf,
|
||||||
|
persistent_root_fd: i32,
|
||||||
persistent_anchor_fd: i32,
|
persistent_anchor_fd: i32,
|
||||||
|
persistent_anchor_label: PathBuf,
|
||||||
|
runtime_root_fd: i32,
|
||||||
runtime_anchor_fd: i32,
|
runtime_anchor_fd: i32,
|
||||||
|
runtime_anchor_label: PathBuf,
|
||||||
expected_lower: (u64, u64),
|
expected_lower: (u64, u64),
|
||||||
|
persistent_destinations: Vec<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct RuntimeNetworkArguments {
|
struct RuntimeNetworkArguments {
|
||||||
|
|
@ -492,14 +520,35 @@ fn parse_runtime_arguments(arguments: &[OsString]) -> io::Result<RuntimeArgument
|
||||||
.peek()
|
.peek()
|
||||||
.is_some_and(|value| *value == "--state-lower-fd")
|
.is_some_and(|value| *value == "--state-lower-fd")
|
||||||
{
|
{
|
||||||
Some(RuntimeStateAnchorArguments {
|
let lower_fd = parse_number(named(&mut arguments, "--state-lower-fd")?)?;
|
||||||
lower_fd: parse_number(named(&mut arguments, "--state-lower-fd")?)?,
|
let lower_label = PathBuf::from(named(&mut arguments, "--state-lower-label")?);
|
||||||
persistent_anchor_fd: parse_number(named(&mut arguments, "--state-persistent-fd")?)?,
|
let persistent_root_fd =
|
||||||
runtime_anchor_fd: parse_number(named(&mut arguments, "--state-runtime-fd")?)?,
|
parse_number(named(&mut arguments, "--state-persistent-root-fd")?)?;
|
||||||
expected_lower: (
|
let persistent_anchor_fd = parse_number(named(&mut arguments, "--state-persistent-fd")?)?;
|
||||||
|
let persistent_anchor_label =
|
||||||
|
PathBuf::from(named(&mut arguments, "--state-persistent-label")?);
|
||||||
|
let runtime_root_fd = parse_number(named(&mut arguments, "--state-runtime-root-fd")?)?;
|
||||||
|
let runtime_anchor_fd = parse_number(named(&mut arguments, "--state-runtime-fd")?)?;
|
||||||
|
let runtime_anchor_label = PathBuf::from(named(&mut arguments, "--state-runtime-label")?);
|
||||||
|
let expected_lower = (
|
||||||
parse_number(named(&mut arguments, "--state-lower-device")?)?,
|
parse_number(named(&mut arguments, "--state-lower-device")?)?,
|
||||||
parse_number(named(&mut arguments, "--state-lower-inode")?)?,
|
parse_number(named(&mut arguments, "--state-lower-inode")?)?,
|
||||||
),
|
);
|
||||||
|
let count: usize = parse_number(named(&mut arguments, "--state-persistent-count")?)?;
|
||||||
|
let persistent_destinations = (0..count)
|
||||||
|
.map(|_| named(&mut arguments, "--state-persistent-destination").map(PathBuf::from))
|
||||||
|
.collect::<io::Result<Vec<_>>>()?;
|
||||||
|
Some(RuntimeStateAnchorArguments {
|
||||||
|
lower_fd,
|
||||||
|
lower_label,
|
||||||
|
persistent_root_fd,
|
||||||
|
persistent_anchor_fd,
|
||||||
|
persistent_anchor_label,
|
||||||
|
runtime_root_fd,
|
||||||
|
runtime_anchor_fd,
|
||||||
|
runtime_anchor_label,
|
||||||
|
expected_lower,
|
||||||
|
persistent_destinations,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
|
@ -604,9 +653,10 @@ fn parse_fixed_hex(value: &OsStr) -> io::Result<[u8; 32]> {
|
||||||
|
|
||||||
fn run_supervisor(arguments: &RuntimeArguments) -> io::Result<()> {
|
fn run_supervisor(arguments: &RuntimeArguments) -> io::Result<()> {
|
||||||
require_lifetime_held()?;
|
require_lifetime_held()?;
|
||||||
|
let namespaces = enter_generation_filesystem_namespaces()?;
|
||||||
let control = std::io::stdout();
|
let control = std::io::stdout();
|
||||||
let parent = current_process_identity()?;
|
let parent = current_process_identity()?;
|
||||||
let mut state_anchor = prepare_state_anchor(arguments, parent, &control)?;
|
let mut state_anchor = prepare_state_anchor(arguments, parent, namespaces, &control)?;
|
||||||
let result = run_supervisor_with_state(arguments, parent, &control, &mut state_anchor);
|
let result = run_supervisor_with_state(arguments, parent, &control, &mut state_anchor);
|
||||||
let cleanup = state_anchor.take().map(RuntimeStateAnchorMount::unmount);
|
let cleanup = state_anchor.take().map(RuntimeStateAnchorMount::unmount);
|
||||||
match (result, cleanup) {
|
match (result, cleanup) {
|
||||||
|
|
@ -623,6 +673,10 @@ fn run_supervisor(arguments: &RuntimeArguments) -> io::Result<()> {
|
||||||
fn prepare_state_anchor(
|
fn prepare_state_anchor(
|
||||||
arguments: &RuntimeArguments,
|
arguments: &RuntimeArguments,
|
||||||
parent: lazaret_core::identity::PidfsProcessIdentityV1,
|
parent: lazaret_core::identity::PidfsProcessIdentityV1,
|
||||||
|
namespaces: (
|
||||||
|
lazaret_core::identity::NamespaceIdentity,
|
||||||
|
lazaret_core::identity::NamespaceIdentity,
|
||||||
|
),
|
||||||
control: &std::io::Stdout,
|
control: &std::io::Stdout,
|
||||||
) -> io::Result<Option<RuntimeStateAnchorMount>> {
|
) -> io::Result<Option<RuntimeStateAnchorMount>> {
|
||||||
let Some(state) = &arguments.state_anchor else {
|
let Some(state) = &arguments.state_anchor else {
|
||||||
|
|
@ -636,8 +690,13 @@ fn prepare_state_anchor(
|
||||||
}
|
}
|
||||||
let authority = RuntimeStateAnchorAuthority::from_inherited(
|
let authority = RuntimeStateAnchorAuthority::from_inherited(
|
||||||
state.lower_fd,
|
state.lower_fd,
|
||||||
|
&state.lower_label,
|
||||||
|
state.persistent_root_fd,
|
||||||
state.persistent_anchor_fd,
|
state.persistent_anchor_fd,
|
||||||
|
&state.persistent_anchor_label,
|
||||||
|
state.runtime_root_fd,
|
||||||
state.runtime_anchor_fd,
|
state.runtime_anchor_fd,
|
||||||
|
&state.runtime_anchor_label,
|
||||||
state.expected_lower,
|
state.expected_lower,
|
||||||
)
|
)
|
||||||
.map_err(io::Error::other)?;
|
.map_err(io::Error::other)?;
|
||||||
|
|
@ -645,7 +704,7 @@ fn prepare_state_anchor(
|
||||||
slot: HostProcessOwnershipSlot::StateAnchorDaemon,
|
slot: HostProcessOwnershipSlot::StateAnchorDaemon,
|
||||||
..arguments.ownership
|
..arguments.ownership
|
||||||
};
|
};
|
||||||
authority
|
let mount = authority
|
||||||
.acquire(
|
.acquire(
|
||||||
parent,
|
parent,
|
||||||
ownership,
|
ownership,
|
||||||
|
|
@ -665,13 +724,18 @@ fn prepare_state_anchor(
|
||||||
control,
|
control,
|
||||||
&SupervisorEvent::StateAnchorReady {
|
&SupervisorEvent::StateAnchorReady {
|
||||||
staged: readiness.staged(),
|
staged: readiness.staged(),
|
||||||
|
user_namespace: namespaces.0,
|
||||||
|
mount_namespace: namespaces.1,
|
||||||
mount: readiness.mount(),
|
mount: readiness.mount(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.map(Some)
|
.map_err(io::Error::other)?;
|
||||||
.map_err(io::Error::other)
|
mount
|
||||||
|
.require_persistent_destinations(&state.persistent_destinations)
|
||||||
|
.map_err(io::Error::other)?;
|
||||||
|
Ok(Some(mount))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_supervisor_with_state(
|
fn run_supervisor_with_state(
|
||||||
|
|
@ -698,6 +762,7 @@ fn run_supervisor_with_state(
|
||||||
let pid_file = File::from(memfd_create("lazaret-runsc-pid", MemfdFlags::empty())?);
|
let pid_file = File::from(memfd_create("lazaret-runsc-pid", MemfdFlags::empty())?);
|
||||||
fcntl_setfd(pid_file.as_fd(), FdFlags::empty())?;
|
fcntl_setfd(pid_file.as_fd(), FdFlags::empty())?;
|
||||||
let runsc = runsc_target(arguments, pid_file.as_raw_fd())?;
|
let runsc = runsc_target(arguments, pid_file.as_raw_fd())?;
|
||||||
|
let delete = runsc_delete_target(arguments)?;
|
||||||
let staged = std::cell::Cell::new(None);
|
let staged = std::cell::Cell::new(None);
|
||||||
let process = match child_broker().acquire_with_stdio(
|
let process = match child_broker().acquire_with_stdio(
|
||||||
parent,
|
parent,
|
||||||
|
|
@ -762,7 +827,7 @@ fn run_supervisor_with_state(
|
||||||
io::Error::other("sandbox commit acknowledgment was not received"),
|
io::Error::other("sandbox commit acknowledgment was not received"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
supervise(control, process, network, state_anchor)
|
supervise(control, process, network, state_anchor, &delete)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prepare_runtime_network(
|
fn prepare_runtime_network(
|
||||||
|
|
@ -858,6 +923,29 @@ fn runsc_target(arguments: &RuntimeArguments, pid_file: i32) -> io::Result<Targe
|
||||||
Ok(target.with_parent_death_kill_edge())
|
Ok(target.with_parent_death_kill_edge())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn runsc_delete_target(arguments: &RuntimeArguments) -> io::Result<TargetCommand> {
|
||||||
|
let run = arguments
|
||||||
|
.runsc_arguments
|
||||||
|
.iter()
|
||||||
|
.position(|argument| argument == "run")
|
||||||
|
.ok_or_else(|| io::Error::other("attached runsc command has no run operation"))?;
|
||||||
|
let container = arguments
|
||||||
|
.runsc_arguments
|
||||||
|
.last()
|
||||||
|
.filter(|container| {
|
||||||
|
!container.is_empty() && !container.as_encoded_bytes().starts_with(b"-")
|
||||||
|
})
|
||||||
|
.ok_or_else(|| io::Error::other("attached runsc command has no container ID"))?;
|
||||||
|
let mut target = TargetCommand::new(arguments.runsc_program.clone());
|
||||||
|
for argument in &arguments.runsc_arguments[..run] {
|
||||||
|
target = target.argument(argument);
|
||||||
|
}
|
||||||
|
Ok(target
|
||||||
|
.argument("delete")
|
||||||
|
.argument("--force")
|
||||||
|
.argument(container))
|
||||||
|
}
|
||||||
|
|
||||||
fn attached_runsc_standard_io(target: &TargetCommand) -> io::Result<ChildStandardIo> {
|
fn attached_runsc_standard_io(target: &TargetCommand) -> io::Result<ChildStandardIo> {
|
||||||
if target.parent_edge() != ParentEdge::ParentDeathKill {
|
if target.parent_edge() != ParentEdge::ParentDeathKill {
|
||||||
return Err(io::Error::other(
|
return Err(io::Error::other(
|
||||||
|
|
@ -896,6 +984,7 @@ fn supervise(
|
||||||
process: RetainedProcess,
|
process: RetainedProcess,
|
||||||
mut network: Option<PreparedNetwork>,
|
mut network: Option<PreparedNetwork>,
|
||||||
state_anchor: &mut Option<RuntimeStateAnchorMount>,
|
state_anchor: &mut Option<RuntimeStateAnchorMount>,
|
||||||
|
delete: &TargetCommand,
|
||||||
) -> io::Result<()> {
|
) -> io::Result<()> {
|
||||||
let mut process = Some(process);
|
let mut process = Some(process);
|
||||||
loop {
|
loop {
|
||||||
|
|
@ -912,6 +1001,7 @@ fn supervise(
|
||||||
.take()
|
.take()
|
||||||
.ok_or_else(|| io::Error::other("attached runsc handle is absent"))?,
|
.ok_or_else(|| io::Error::other("attached runsc handle is absent"))?,
|
||||||
)?;
|
)?;
|
||||||
|
child_broker().run_transient(delete)?;
|
||||||
stop_network(network)?;
|
stop_network(network)?;
|
||||||
send_message(control, &SupervisorEvent::AttachedRunscExited)?;
|
send_message(control, &SupervisorEvent::AttachedRunscExited)?;
|
||||||
return Ok(());
|
return Ok(());
|
||||||
|
|
@ -933,6 +1023,7 @@ fn supervise(
|
||||||
if lease_lost {
|
if lease_lost {
|
||||||
if let Some(process) = process.take() {
|
if let Some(process) = process.take() {
|
||||||
terminate_and_reap(process)?;
|
terminate_and_reap(process)?;
|
||||||
|
child_broker().run_transient(delete)?;
|
||||||
}
|
}
|
||||||
stop_network(network)?;
|
stop_network(network)?;
|
||||||
return Ok(());
|
return Ok(());
|
||||||
|
|
@ -969,6 +1060,7 @@ fn supervise(
|
||||||
&mut process,
|
&mut process,
|
||||||
&mut network,
|
&mut network,
|
||||||
state_anchor,
|
state_anchor,
|
||||||
|
delete,
|
||||||
) {
|
) {
|
||||||
Ok(true) => return Ok(()),
|
Ok(true) => return Ok(()),
|
||||||
Ok(false) => {}
|
Ok(false) => {}
|
||||||
|
|
@ -984,6 +1076,7 @@ fn handle_supervisor_message(
|
||||||
process: &mut Option<RetainedProcess>,
|
process: &mut Option<RetainedProcess>,
|
||||||
network: &mut Option<PreparedNetwork>,
|
network: &mut Option<PreparedNetwork>,
|
||||||
state_anchor: &mut Option<RuntimeStateAnchorMount>,
|
state_anchor: &mut Option<RuntimeStateAnchorMount>,
|
||||||
|
delete: &TargetCommand,
|
||||||
) -> io::Result<bool> {
|
) -> io::Result<bool> {
|
||||||
match message {
|
match message {
|
||||||
CoordinatorMessage::SignalAttachedRunsc(signal) => {
|
CoordinatorMessage::SignalAttachedRunsc(signal) => {
|
||||||
|
|
@ -999,6 +1092,7 @@ fn handle_supervisor_message(
|
||||||
.take()
|
.take()
|
||||||
.ok_or_else(|| io::Error::other("attached runsc is already stopped"))?,
|
.ok_or_else(|| io::Error::other("attached runsc is already stopped"))?,
|
||||||
)?;
|
)?;
|
||||||
|
child_broker().run_transient(delete)?;
|
||||||
send_message(control, &SupervisorEvent::SandboxStopped)?;
|
send_message(control, &SupervisorEvent::SandboxStopped)?;
|
||||||
}
|
}
|
||||||
CoordinatorMessage::StopSidecar => {
|
CoordinatorMessage::StopSidecar => {
|
||||||
|
|
@ -1014,12 +1108,16 @@ fn handle_supervisor_message(
|
||||||
"State Anchor unmount preceded runtime resource stop",
|
"State Anchor unmount preceded runtime resource stop",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
let mount = state_anchor
|
||||||
|
.as_ref()
|
||||||
|
.ok_or_else(|| io::Error::other("State Anchor is absent"))?
|
||||||
|
.mount_identity();
|
||||||
state_anchor
|
state_anchor
|
||||||
.take()
|
.take()
|
||||||
.ok_or_else(|| io::Error::other("State Anchor is absent"))?
|
.ok_or_else(|| io::Error::other("State Anchor is absent"))?
|
||||||
.unmount()
|
.unmount()
|
||||||
.map_err(io::Error::other)?;
|
.map_err(io::Error::other)?;
|
||||||
send_message(control, &SupervisorEvent::StateAnchorUnmounted)?;
|
send_message(control, &SupervisorEvent::StateAnchorUnmounted { mount })?;
|
||||||
}
|
}
|
||||||
CoordinatorMessage::Exit => {
|
CoordinatorMessage::Exit => {
|
||||||
if process.is_some() || network.is_some() || state_anchor.is_some() {
|
if process.is_some() || network.is_some() || state_anchor.is_some() {
|
||||||
|
|
@ -1307,6 +1405,14 @@ mod tests {
|
||||||
attached_runsc_standard_io(&runsc).unwrap().policy(),
|
attached_runsc_standard_io(&runsc).unwrap().policy(),
|
||||||
ChildStandardIoPolicy::Silenced
|
ChildStandardIoPolicy::Silenced
|
||||||
);
|
);
|
||||||
|
let delete = runsc_delete_target(&runsc_arguments).unwrap();
|
||||||
|
assert_eq!(delete.program(), "runsc");
|
||||||
|
assert_eq!(
|
||||||
|
delete.arguments(),
|
||||||
|
["-root=/runtime", "delete", "--force", "container"]
|
||||||
|
.map(OsString::from)
|
||||||
|
.as_slice()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue