Fix rootless launches with subordinate ID maps
Resolve and verify canonical caller subordinate ranges, launch preparation and runtime workers through mapped causal acquisition, and keep runsc in the existing user namespace. Repair descriptor-relative OCI cleanup and absent-mount recovery so failed launches can progress to the corrected path.
This commit is contained in:
parent
b9dbd78087
commit
73e3e9fdee
14 changed files with 1424 additions and 172 deletions
|
|
@ -43,6 +43,8 @@ tokio.workspace = true
|
|||
|
||||
[dev-dependencies]
|
||||
tempfile = "=3.27.0"
|
||||
serde_json.workspace = true
|
||||
tar.workspace = true
|
||||
lazaret-client = { version = "=0.0.0", path = "../lazaret-client" }
|
||||
lazaret-escalation-client = { version = "=0.0.0", path = "../lazaret-escalation-client" }
|
||||
x509-parser.workspace = true
|
||||
|
|
@ -52,6 +54,11 @@ name = "escalation"
|
|||
path = "tests/escalation.rs"
|
||||
harness = false
|
||||
|
||||
[[test]]
|
||||
name = "prepared-worker"
|
||||
path = "tests/prepared-worker.rs"
|
||||
harness = false
|
||||
|
||||
[[test]]
|
||||
name = "cold-start"
|
||||
path = "tests/cold_start.rs"
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ use lazaret_linux::filesystem::{RegularFileMailbox, mount_identity, namespace_id
|
|||
use lazaret_linux::process::{
|
||||
ProcessOwnershipContext, TargetCommand, child_broker, current_process_identity,
|
||||
};
|
||||
use lazaret_linux::user_namespace::UserNamespaceMap;
|
||||
use lazaret_runtime_supervisor::network::NetworkProbeRequest;
|
||||
use lazaret_runtime_supervisor::sandbox::{
|
||||
RootProcess, SandboxBuildRequest, SandboxBundle, build_sandbox_bundle,
|
||||
|
|
@ -85,6 +86,7 @@ pub struct ColdStartInputs<'a> {
|
|||
pub invoking_uid: u32,
|
||||
pub escalation_client: &'a File,
|
||||
pub sandbox_init: &'a File,
|
||||
pub user_namespace_map: UserNamespaceMap,
|
||||
}
|
||||
|
||||
pub struct BootstrapOwnerGuard {
|
||||
|
|
@ -227,6 +229,8 @@ impl<'a> ColdStart<'a> {
|
|||
},
|
||||
)
|
||||
.map_err(|error| failure(format!("Runtime Supervisor launch: {error}")))?
|
||||
.with_user_namespace_map(inputs.user_namespace_map)
|
||||
.map_err(|error| failure(format!("Runtime Supervisor user namespace: {error}")))?
|
||||
.with_state_anchor(state_anchor)
|
||||
.map_err(|error| failure(format!("Runtime Supervisor handoff: {error}")))?;
|
||||
if let Some(git_routing) = &git_routing {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ pub mod escalation;
|
|||
pub mod git_routing;
|
||||
pub mod lifecycle;
|
||||
pub mod network;
|
||||
pub mod prepared_worker;
|
||||
pub mod private_roles;
|
||||
pub mod recovery;
|
||||
pub mod runtime;
|
||||
|
|
|
|||
266
crates/lazaret-host-coordinator/src/prepared_worker.rs
Normal file
266
crates/lazaret-host-coordinator/src/prepared_worker.rs
Normal file
|
|
@ -0,0 +1,266 @@
|
|||
use std::ffi::OsString;
|
||||
use std::fs::{File, OpenOptions};
|
||||
use std::io::{self, Read, Seek, SeekFrom, Write};
|
||||
use std::os::fd::AsRawFd;
|
||||
use std::os::unix::ffi::{OsStrExt, OsStringExt};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use lazaret_core::codec::{decode, encode};
|
||||
use lazaret_core::configuration::ResolvedDomainConfiguration;
|
||||
use lazaret_core::identity::PrepareOperationId;
|
||||
use lazaret_core::records::{PreparedArtifact, SchemaType};
|
||||
use lazaret_linux::filesystem::{require_write_sealed, sealed_memfd};
|
||||
use lazaret_linux::process::{TargetCommand, child_broker};
|
||||
use lazaret_linux::user_namespace::UserNamespaceMap;
|
||||
use lazaret_prepared_lower::{
|
||||
IdMap, IdMapRange, PrepareRequest, PreparedDestination, PreparedLowerStore, TransportedImage,
|
||||
};
|
||||
use lazaret_storage_catalog::{Catalog, CatalogError};
|
||||
use rustix::fs::{FileType, MemfdFlags, fstat, memfd_create};
|
||||
use rustix::io::{FdFlags, fcntl_setfd};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub const PRIVATE_PREPARED_WORKER_ROLE: &str = "--lazaret-prepared-worker-role";
|
||||
const MAX_HANDOFF_BYTES: u64 = 64 * 1024;
|
||||
const MAX_RESULT_BYTES: u64 = 64 * 1024;
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
enum PreparedWorkerHandoff {
|
||||
V1 {
|
||||
storage_root: i32,
|
||||
storage_root_label: Vec<u8>,
|
||||
layout: i32,
|
||||
request: PrepareRequest,
|
||||
},
|
||||
}
|
||||
|
||||
impl SchemaType for PreparedWorkerHandoff {
|
||||
const NAME: &'static str = "PreparedWorkerHandoff";
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
enum PreparedWorkerResult {
|
||||
Prepared(PreparedArtifact),
|
||||
Refused(String),
|
||||
}
|
||||
|
||||
impl SchemaType for PreparedWorkerResult {
|
||||
const NAME: &'static str = "PreparedWorkerResult";
|
||||
}
|
||||
|
||||
/// Prepares one transported image inside the canonical caller-configured user namespace.
|
||||
///
|
||||
/// # Errors
|
||||
/// Refuses unavailable authority, mapped launch, worker result, or preparation failure.
|
||||
pub fn prepare_in_user_namespace(
|
||||
configuration: &ResolvedDomainConfiguration,
|
||||
image: &TransportedImage,
|
||||
operation_id: PrepareOperationId,
|
||||
destinations: Vec<PreparedDestination>,
|
||||
map: &UserNamespaceMap,
|
||||
) -> io::Result<PreparedArtifact> {
|
||||
let storage_root = File::from(
|
||||
configuration
|
||||
.storage_root()
|
||||
.open_authority()
|
||||
.map_err(|error| io::Error::other(error.to_string()))?,
|
||||
);
|
||||
let layout = image
|
||||
.layout_authority()
|
||||
.map_err(|error| io::Error::other(error.to_string()))?;
|
||||
let storage_root_label = PathBuf::from(std::ffi::OsString::from_vec(
|
||||
configuration
|
||||
.storage_root()
|
||||
.canonical_label()
|
||||
.as_bytes()
|
||||
.to_vec(),
|
||||
));
|
||||
prepare_request_in_user_namespace(
|
||||
&storage_root,
|
||||
&storage_root_label,
|
||||
&layout,
|
||||
PrepareRequest {
|
||||
operation_id,
|
||||
layout_path: image.layout_path.clone(),
|
||||
source_identity: image.source_identity.clone(),
|
||||
selected_manifest_digest: image.selected_manifest_digest,
|
||||
id_map: prepared_id_map(map),
|
||||
destinations,
|
||||
},
|
||||
map,
|
||||
)
|
||||
}
|
||||
|
||||
/// Runs one descriptor-bound preparation worker under the supplied canonical user namespace.
|
||||
///
|
||||
/// # Errors
|
||||
/// Refuses descriptor handoff, mapped launch, worker result, or preparation failure.
|
||||
pub fn prepare_request_in_user_namespace(
|
||||
storage_root: &File,
|
||||
storage_root_label: &Path,
|
||||
layout: &File,
|
||||
request: PrepareRequest,
|
||||
map: &UserNamespaceMap,
|
||||
) -> io::Result<PreparedArtifact> {
|
||||
fcntl_setfd(storage_root, FdFlags::empty())?;
|
||||
fcntl_setfd(layout, FdFlags::empty())?;
|
||||
let result = File::from(memfd_create(
|
||||
"lazaret-prepared-worker-result",
|
||||
MemfdFlags::CLOEXEC | MemfdFlags::ALLOW_SEALING,
|
||||
)?);
|
||||
fcntl_setfd(&result, FdFlags::empty())?;
|
||||
let handoff = PreparedWorkerHandoff::V1 {
|
||||
storage_root: storage_root.as_raw_fd(),
|
||||
storage_root_label: storage_root_label.as_os_str().as_bytes().to_vec(),
|
||||
layout: layout.as_raw_fd(),
|
||||
request,
|
||||
};
|
||||
let encoded = encode(&handoff).map_err(io::Error::other)?;
|
||||
let handoff = sealed_memfd("lazaret-prepared-worker-handoff", &encoded)?;
|
||||
fcntl_setfd(&handoff, FdFlags::empty())?;
|
||||
let target = TargetCommand::new("/proc/self/exe")
|
||||
.argument(PRIVATE_PREPARED_WORKER_ROLE)
|
||||
.argument(handoff.as_raw_fd().to_string())
|
||||
.argument(result.as_raw_fd().to_string());
|
||||
child_broker().run_transient_in_user_namespace(&target, map)?;
|
||||
read_result(result)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn dispatch_private_prepared_worker(arguments: &[OsString]) -> Option<io::Result<()>> {
|
||||
if arguments
|
||||
.first()
|
||||
.is_none_or(|argument| argument != PRIVATE_PREPARED_WORKER_ROLE)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
Some(run_worker(&arguments[1..]))
|
||||
}
|
||||
|
||||
fn run_worker(arguments: &[OsString]) -> io::Result<()> {
|
||||
let [handoff, result] = arguments else {
|
||||
return Err(io::Error::other(
|
||||
"prepared worker requires handoff and result descriptors",
|
||||
));
|
||||
};
|
||||
let handoff = inherited_file(parse_descriptor(handoff)?, FileType::RegularFile, false)?;
|
||||
require_write_sealed(&handoff)?;
|
||||
let result = inherited_file(parse_descriptor(result)?, FileType::RegularFile, true)?;
|
||||
let outcome = run_worker_inner(handoff)
|
||||
.unwrap_or_else(|error| PreparedWorkerResult::Refused(error.to_string()));
|
||||
let encoded = encode(&outcome).map_err(io::Error::other)?;
|
||||
if encoded.len() as u64 > MAX_RESULT_BYTES {
|
||||
return Err(io::Error::other("prepared worker result is oversized"));
|
||||
}
|
||||
(&result).write_all(&encoded)?;
|
||||
result.sync_all()
|
||||
}
|
||||
|
||||
fn run_worker_inner(mut handoff: File) -> io::Result<PreparedWorkerResult> {
|
||||
let length = handoff.metadata()?.len();
|
||||
if length == 0 || length > MAX_HANDOFF_BYTES {
|
||||
return Err(io::Error::other("prepared worker handoff is oversized"));
|
||||
}
|
||||
let mut encoded = vec![
|
||||
0_u8;
|
||||
usize::try_from(length).map_err(|_| io::Error::other(
|
||||
"prepared worker handoff length is invalid"
|
||||
))?
|
||||
];
|
||||
handoff.read_exact(&mut encoded)?;
|
||||
let PreparedWorkerHandoff::V1 {
|
||||
storage_root,
|
||||
storage_root_label,
|
||||
layout,
|
||||
request,
|
||||
} = decode::<PreparedWorkerHandoff>(&encoded).map_err(io::Error::other)?;
|
||||
if storage_root == layout {
|
||||
return Err(io::Error::other(
|
||||
"prepared worker authority descriptors are duplicated",
|
||||
));
|
||||
}
|
||||
let storage_root = inherited_file(storage_root, FileType::Directory, false)?;
|
||||
let layout = inherited_file(layout, FileType::Directory, false)?;
|
||||
let storage_root_label = PathBuf::from(std::ffi::OsString::from_vec(storage_root_label));
|
||||
let catalog =
|
||||
Catalog::under_storage_root_authority(storage_root.try_clone()?, storage_root_label)
|
||||
.map_err(io::Error::other)?;
|
||||
match catalog.initialize() {
|
||||
Ok(()) | Err(CatalogError::AlreadyInitialized) => {}
|
||||
Err(error) => return Err(io::Error::other(error)),
|
||||
}
|
||||
let store = PreparedLowerStore::under_storage_root_authority(&storage_root, catalog)
|
||||
.map_err(|error| io::Error::other(error.to_string()))?;
|
||||
let artifact = store
|
||||
.prepare_from_layout_authority_in_target_namespace(&request, &layout)
|
||||
.map_err(|error| io::Error::other(format!("{error:#}")))?;
|
||||
Ok(PreparedWorkerResult::Prepared(artifact))
|
||||
}
|
||||
|
||||
fn read_result(mut result: File) -> io::Result<PreparedArtifact> {
|
||||
result.seek(SeekFrom::Start(0))?;
|
||||
let length = result.metadata()?.len();
|
||||
if length == 0 || length > MAX_RESULT_BYTES {
|
||||
return Err(io::Error::other(
|
||||
"prepared worker returned no bounded result",
|
||||
));
|
||||
}
|
||||
let mut encoded = vec![
|
||||
0_u8;
|
||||
usize::try_from(length).map_err(|_| io::Error::other(
|
||||
"prepared worker result length is invalid"
|
||||
))?
|
||||
];
|
||||
result.read_exact(&mut encoded)?;
|
||||
match decode::<PreparedWorkerResult>(&encoded).map_err(io::Error::other)? {
|
||||
PreparedWorkerResult::Prepared(artifact) => Ok(artifact),
|
||||
PreparedWorkerResult::Refused(detail) => Err(io::Error::other(detail)),
|
||||
}
|
||||
}
|
||||
|
||||
fn inherited_file(number: i32, expected: FileType, writable: bool) -> io::Result<File> {
|
||||
if number <= 2 {
|
||||
return Err(io::Error::other("prepared worker descriptor is invalid"));
|
||||
}
|
||||
let file = OpenOptions::new()
|
||||
.read(true)
|
||||
.write(writable)
|
||||
.open(format!("/proc/self/fd/{number}"))?;
|
||||
if FileType::from_raw_mode(fstat(&file)?.st_mode) != expected {
|
||||
return Err(io::Error::other(
|
||||
"prepared worker descriptor has the wrong type",
|
||||
));
|
||||
}
|
||||
Ok(file)
|
||||
}
|
||||
|
||||
fn parse_descriptor(value: &OsString) -> io::Result<i32> {
|
||||
value
|
||||
.to_str()
|
||||
.and_then(|value| value.parse().ok())
|
||||
.filter(|number| *number > 2)
|
||||
.ok_or_else(|| io::Error::other("prepared worker descriptor is invalid"))
|
||||
}
|
||||
|
||||
fn prepared_id_map(map: &UserNamespaceMap) -> IdMap {
|
||||
IdMap {
|
||||
uid: map
|
||||
.uid
|
||||
.iter()
|
||||
.map(|range| IdMapRange {
|
||||
container_id: range.container_id,
|
||||
namespace_id: range.parent_id,
|
||||
size: range.size,
|
||||
})
|
||||
.collect(),
|
||||
gid: map
|
||||
.gid
|
||||
.iter()
|
||||
.map(|range| IdMapRange {
|
||||
container_id: range.container_id,
|
||||
namespace_id: range.parent_id,
|
||||
size: range.size,
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@ use lazaret_state_anchor::{
|
|||
};
|
||||
|
||||
use crate::client_bootstrap::dispatch_private_client_bootstrap;
|
||||
use crate::prepared_worker::{PRIVATE_PREPARED_WORKER_ROLE, dispatch_private_prepared_worker};
|
||||
use crate::sandbox_init::{
|
||||
PRIVATE_SANDBOX_INIT_ROLE, PRIVATE_SESSION_EXEC_ROLE, PRIVATE_SESSION_IDENTITY_ROLE,
|
||||
dispatch_private_sandbox_init,
|
||||
|
|
@ -42,8 +43,12 @@ fn dispatch_exact(role: &str, arguments: &[OsString]) -> Option<io::Result<()>>
|
|||
})
|
||||
}
|
||||
|
||||
static ROLES: [PrivateRole; 8] = [
|
||||
static ROLES: [PrivateRole; 9] = [
|
||||
PrivateRole::new(PRIVATE_CLIENT_BOOTSTRAP_ROLE, client_bootstrap),
|
||||
PrivateRole::new(
|
||||
PRIVATE_PREPARED_WORKER_ROLE,
|
||||
dispatch_private_prepared_worker,
|
||||
),
|
||||
PrivateRole::new(PRIVATE_SANDBOX_INIT_ROLE, sandbox_init),
|
||||
PrivateRole::new(PRIVATE_SESSION_EXEC_ROLE, session_exec),
|
||||
PrivateRole::new(PRIVATE_SESSION_IDENTITY_ROLE, session_identity),
|
||||
|
|
@ -68,6 +73,7 @@ mod tests {
|
|||
REGISTRY.names(),
|
||||
[
|
||||
PRIVATE_CLIENT_BOOTSTRAP_ROLE,
|
||||
PRIVATE_PREPARED_WORKER_ROLE,
|
||||
PRIVATE_SANDBOX_INIT_ROLE,
|
||||
PRIVATE_SESSION_EXEC_ROLE,
|
||||
PRIVATE_SESSION_IDENTITY_ROLE,
|
||||
|
|
|
|||
|
|
@ -115,8 +115,13 @@ pub(super) fn require_mount_absent(
|
|||
match (path, mount) {
|
||||
(None, None) => Ok(()),
|
||||
(Some(path), Some(expected)) => {
|
||||
let observed =
|
||||
current_mount_identity(Path::new(std::ffi::OsStr::from_bytes(path.as_bytes())))?;
|
||||
let observed = match current_mount_identity(Path::new(std::ffi::OsStr::from_bytes(
|
||||
path.as_bytes(),
|
||||
))) {
|
||||
Ok(observed) => observed,
|
||||
Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(()),
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
if observed == expected {
|
||||
Err(io::Error::other(format!(
|
||||
"{label} exact mount remains present"
|
||||
|
|
|
|||
|
|
@ -60,6 +60,19 @@ fn generation() -> Generation {
|
|||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recorded_mount_path_absence_is_successful_recovery_evidence() {
|
||||
let tree = tempfile::tempdir().unwrap();
|
||||
let absent = tree.path().join("absent");
|
||||
let path = HostPath::new(absent.as_os_str().as_bytes().to_vec());
|
||||
let mount = MountIdentity {
|
||||
mount_id: MountId::new(10),
|
||||
device: DeviceId::new(11),
|
||||
inode: InodeId::new(12),
|
||||
};
|
||||
require_mount_absent(Some(&path), Some(mount), "fixture").unwrap();
|
||||
}
|
||||
|
||||
fn participant(role: ParticipantRole, ownership: ParticipantOwnership) -> Participant {
|
||||
Participant::snapshot(
|
||||
GenerationId::new(1),
|
||||
|
|
|
|||
|
|
@ -16,7 +16,9 @@ use lazaret_core::records::{
|
|||
SandboxOwnership, StagedProcessOwnership,
|
||||
};
|
||||
use lazaret_linux::ipc::private_seqpacket_pair;
|
||||
use lazaret_linux::process::{RetainedProcess, child_broker, current_process_identity};
|
||||
use lazaret_linux::process::{
|
||||
AcknowledgedChildAcquisition, RetainedProcess, child_broker, current_process_identity,
|
||||
};
|
||||
use lazaret_runtime_supervisor::network::NetworkProbeRequest;
|
||||
use lazaret_runtime_supervisor::sandbox::SandboxHandoffAuthority;
|
||||
use lazaret_runtime_supervisor::supervision::{
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
use super::{
|
||||
CanonicalDerivedNetworkPolicy, CommittedSandboxRuntime, CoordinatorChannel,
|
||||
GitRoutingCoordinator, MountIdentity, NamespaceIdentity, NetworkCoordinator,
|
||||
NetworkProbeRequest, NetworkReadinessEvidence, PidfsProcessIdentityV1, PipeFlags,
|
||||
PreparedArtifactId, ProcessOwnershipCommitAck, ReachabilityExpectation, RetainedProcess,
|
||||
RuntimeSupervisorLaunch, SandboxCoordinator, SandboxCoordinatorError, SandboxHandoffAuthority,
|
||||
StagedProcessOwnership, StateAnchorCoordinator, SupervisorEvent, child_broker,
|
||||
cleanup_supervisor, current_process_identity, io, pipe_with, private_seqpacket_pair,
|
||||
AcknowledgedChildAcquisition, CanonicalDerivedNetworkPolicy, CommittedSandboxRuntime,
|
||||
CoordinatorChannel, GitRoutingCoordinator, MountIdentity, NamespaceIdentity,
|
||||
NetworkCoordinator, NetworkProbeRequest, NetworkReadinessEvidence, PidfsProcessIdentityV1,
|
||||
PipeFlags, PreparedArtifactId, ProcessOwnershipCommitAck, ReachabilityExpectation,
|
||||
RetainedProcess, RuntimeSupervisorLaunch, SandboxCoordinator, SandboxCoordinatorError,
|
||||
SandboxHandoffAuthority, StagedProcessOwnership, StateAnchorCoordinator, SupervisorEvent,
|
||||
child_broker, cleanup_supervisor, current_process_identity, io, pipe_with,
|
||||
private_seqpacket_pair,
|
||||
};
|
||||
|
||||
pub struct AwaitingAttachedRunsc {
|
||||
|
|
@ -216,11 +217,20 @@ impl AwaitingAttachedRunsc {
|
|||
let (coordinator_control, supervisor_control) = private_seqpacket_pair()?;
|
||||
let (supervisor_lifetime, coordinator_lifetime) =
|
||||
pipe_with(PipeFlags::CLOEXEC).map_err(io::Error::from)?;
|
||||
let process = child_broker().acquire_with_stdio(
|
||||
parent,
|
||||
launch.ownership(),
|
||||
launch.target(),
|
||||
launch.child_standard_io(supervisor_control, supervisor_lifetime)?,
|
||||
let user_namespace_map =
|
||||
launch
|
||||
.user_namespace_map()
|
||||
.ok_or(SandboxCoordinatorError::InvalidState(
|
||||
"Runtime Supervisor user namespace map is absent",
|
||||
))?;
|
||||
let process = child_broker().acquire_with_stdio_in_user_namespace(
|
||||
AcknowledgedChildAcquisition {
|
||||
expected_parent: parent,
|
||||
ownership: launch.ownership(),
|
||||
target: launch.target(),
|
||||
standard_io: launch.child_standard_io(supervisor_control, supervisor_lifetime)?,
|
||||
},
|
||||
user_namespace_map,
|
||||
|staged| {
|
||||
coordinator
|
||||
.stage_runtime_supervisor(staged)
|
||||
|
|
|
|||
|
|
@ -15,9 +15,10 @@ use lazaret_core::network::{DnsQueryExpectation, ReachabilityExpectation};
|
|||
use lazaret_core::records::{DurableDecision, Generation, IpAddress, PreparedArtifact, VolumeKind};
|
||||
use lazaret_linux::filesystem::namespace_identity;
|
||||
use lazaret_linux::platform::{PlatformObservation, check_platform};
|
||||
use lazaret_linux::user_namespace::UserNamespaceMap;
|
||||
use lazaret_prepared_lower::{
|
||||
IdMap, IdMapRange, OciRegistryTransport, Platform, PreparedDestination,
|
||||
PreparedDestinationKind, PreparedLowerError, PreparedLowerStore, RegistryCredentials,
|
||||
OciRegistryTransport, Platform, PreparedDestination, PreparedDestinationKind,
|
||||
PreparedLowerError, PreparedLowerStore, RegistryCredentials,
|
||||
};
|
||||
use lazaret_runtime_supervisor::network::{DnsProbeTarget, NetworkProbeRequest, TcpProbeTarget};
|
||||
use lazaret_runtime_supervisor::sandbox::{
|
||||
|
|
@ -32,6 +33,7 @@ use rustix::fs::{Mode, OFlags, ResolveFlags, openat2};
|
|||
|
||||
use crate::cold_start::ColdStartInputs;
|
||||
use crate::domain_store::HostCoordinatorDomainStore;
|
||||
use crate::prepared_worker::prepare_in_user_namespace;
|
||||
use crate::recovery::recover_before_cold_start;
|
||||
use crate::sandbox_init::PRIVATE_SANDBOX_INIT_ROLE;
|
||||
|
||||
|
|
@ -89,6 +91,7 @@ pub struct RuntimeSelection {
|
|||
container_id: ContainerId,
|
||||
escalation_client: File,
|
||||
sandbox_init: File,
|
||||
user_namespace_map: UserNamespaceMap,
|
||||
}
|
||||
|
||||
impl RuntimeSelection {
|
||||
|
|
@ -125,6 +128,7 @@ impl RuntimeSelection {
|
|||
invoking_uid: rustix::process::getuid().as_raw(),
|
||||
escalation_client: &self.escalation_client,
|
||||
sandbox_init: &self.sandbox_init,
|
||||
user_namespace_map: self.user_namespace_map.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -160,6 +164,7 @@ async fn select_runtime_for_platform(
|
|||
observation: &PlatformObservation<'_>,
|
||||
) -> Result<RuntimeSelection, RuntimeSelectionError> {
|
||||
check_platform(observation).map_err(failure)?;
|
||||
let user_namespace_map = UserNamespaceMap::resolve_current().map_err(failure)?;
|
||||
let escalation_client = open_installed_executable(product_paths.escalation_client)?;
|
||||
let sandbox_init = open_installed_executable(product_paths.sandbox_init)?;
|
||||
let catalog = Catalog::under_storage_root(configuration.storage_root()).map_err(failure)?;
|
||||
|
|
@ -180,14 +185,14 @@ async fn select_runtime_for_platform(
|
|||
)
|
||||
.await
|
||||
.map_err(RuntimeSelectionError::PreparedLower)?;
|
||||
let artifact = prepared
|
||||
.prepare_transported(
|
||||
PrepareOperationId::new(random_id()?),
|
||||
&image,
|
||||
rootless_id_map(),
|
||||
prepared_destinations(configuration),
|
||||
)
|
||||
.map_err(RuntimeSelectionError::PreparedLower)?;
|
||||
let artifact = prepare_in_user_namespace(
|
||||
configuration,
|
||||
&image,
|
||||
PrepareOperationId::new(random_id()?),
|
||||
prepared_destinations(configuration),
|
||||
&user_namespace_map,
|
||||
)
|
||||
.map_err(|error| failure(format!("mapped preparation worker failed: {error}")))?;
|
||||
let artifact_authority = prepared
|
||||
.open_artifact_authority(&artifact)
|
||||
.map_err(RuntimeSelectionError::PreparedLower)?;
|
||||
|
|
@ -204,7 +209,7 @@ async fn select_runtime_for_platform(
|
|||
)));
|
||||
image
|
||||
.cleanup()
|
||||
.map_err(RuntimeSelectionError::PreparedLower)?;
|
||||
.map_err(|error| failure(format!("OCI layout cleanup failed: {error:#}")))?;
|
||||
|
||||
let next_generation = next_generation(&store)?;
|
||||
let (coordinator_id, network_sidecar_id, sandbox_id) = distinct_generation_ids()?;
|
||||
|
|
@ -235,6 +240,7 @@ async fn select_runtime_for_platform(
|
|||
container_id: ContainerId::new(random_id()?),
|
||||
escalation_client,
|
||||
sandbox_init,
|
||||
user_namespace_map,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -387,21 +393,6 @@ fn open_installed_executable(path: &Path) -> Result<File, RuntimeSelectionError>
|
|||
Ok(file)
|
||||
}
|
||||
|
||||
fn rootless_id_map() -> IdMap {
|
||||
IdMap {
|
||||
uid: vec![IdMapRange {
|
||||
container_id: 0,
|
||||
namespace_id: rustix::process::getuid().as_raw(),
|
||||
size: 1,
|
||||
}],
|
||||
gid: vec![IdMapRange {
|
||||
container_id: 0,
|
||||
namespace_id: rustix::process::getgid().as_raw(),
|
||||
size: 1,
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
fn distinct_generation_ids()
|
||||
-> Result<(CoordinatorId, NetworkSidecarId, SandboxId), RuntimeSelectionError> {
|
||||
for _ in 0..8 {
|
||||
|
|
@ -446,7 +437,7 @@ mod tests {
|
|||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn resolved_readiness_maps_exactly_and_rootless_id_map_matches_builder_policy() {
|
||||
fn resolved_readiness_maps_exactly_and_destinations_match_builder_policy() {
|
||||
let tree = tempfile::tempdir().unwrap();
|
||||
let configuration = configuration(tree.path());
|
||||
let generation = Generation {
|
||||
|
|
@ -475,16 +466,6 @@ mod tests {
|
|||
);
|
||||
assert_eq!(expectation.dns_queries[0].resolver, request.dns[0].resolver);
|
||||
|
||||
let id_map = rootless_id_map();
|
||||
assert_eq!(id_map.uid[0].container_id, 0);
|
||||
assert_eq!(
|
||||
id_map.uid[0].namespace_id,
|
||||
rustix::process::getuid().as_raw()
|
||||
);
|
||||
assert_eq!(
|
||||
id_map.gid[0].namespace_id,
|
||||
rustix::process::getgid().as_raw()
|
||||
);
|
||||
let destinations = prepared_destinations(&configuration);
|
||||
assert!(destinations.iter().any(|destination| {
|
||||
destination.path == Path::new(WORKSPACE_DESTINATION)
|
||||
|
|
|
|||
|
|
@ -16,10 +16,13 @@ use lazaret_core::records::{
|
|||
use lazaret_host_coordinator::cold_start::{ColdStart, ColdStartInputs};
|
||||
use lazaret_host_coordinator::domain_store::HostCoordinatorDomainStore;
|
||||
use lazaret_linux::filesystem::{namespace_identity, try_exclusive_flock};
|
||||
use lazaret_linux::user_namespace::{
|
||||
REQUIRED_SUBORDINATE_IDS, UserNamespaceIdRange, UserNamespaceMap,
|
||||
};
|
||||
use lazaret_runtime_supervisor::network::NetworkProbeRequest;
|
||||
use lazaret_runtime_supervisor::sandbox::{RootProcess, default_sandbox_layout};
|
||||
use lazaret_storage_catalog::Catalog;
|
||||
use rustix::process::getuid;
|
||||
use rustix::process::{getgid, getuid};
|
||||
|
||||
#[test]
|
||||
fn validated_inputs_stage_one_complete_plan_and_drop_exact_runtime_artifacts() {
|
||||
|
|
@ -217,6 +220,7 @@ impl ColdStartFixture {
|
|||
invoking_uid: getuid().as_raw(),
|
||||
escalation_client,
|
||||
sandbox_init: escalation_client,
|
||||
user_namespace_map: test_user_namespace_map(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -239,6 +243,39 @@ impl ColdStartFixture {
|
|||
}
|
||||
}
|
||||
|
||||
fn test_user_namespace_map() -> UserNamespaceMap {
|
||||
let uid = getuid().as_raw();
|
||||
let gid = getgid().as_raw();
|
||||
let subordinate_user_start = if uid < 131_072 { 131_072 } else { 1 };
|
||||
let subordinate_group_start = if gid < 131_072 { 131_072 } else { 1 };
|
||||
UserNamespaceMap {
|
||||
uid: vec![
|
||||
UserNamespaceIdRange {
|
||||
container_id: 0,
|
||||
parent_id: uid,
|
||||
size: 1,
|
||||
},
|
||||
UserNamespaceIdRange {
|
||||
container_id: 1,
|
||||
parent_id: subordinate_user_start,
|
||||
size: REQUIRED_SUBORDINATE_IDS,
|
||||
},
|
||||
],
|
||||
gid: vec![
|
||||
UserNamespaceIdRange {
|
||||
container_id: 0,
|
||||
parent_id: gid,
|
||||
size: 1,
|
||||
},
|
||||
UserNamespaceIdRange {
|
||||
container_id: 1,
|
||||
parent_id: subordinate_group_start,
|
||||
size: REQUIRED_SUBORDINATE_IDS,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_private_artifacts(bundle: &Path, control: &Path) {
|
||||
let config = bundle.join("config.json");
|
||||
let escalation_client = control.join("bin/lazaret-escalate");
|
||||
|
|
|
|||
178
crates/lazaret-host-coordinator/tests/prepared-worker.rs
Normal file
178
crates/lazaret-host-coordinator/tests/prepared-worker.rs
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
use std::fs::File;
|
||||
use std::io::Cursor;
|
||||
use std::os::unix::ffi::OsStringExt;
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::ExitCode;
|
||||
|
||||
use lazaret_core::identity::PrepareOperationId;
|
||||
use lazaret_host_coordinator::prepared_worker::prepare_request_in_user_namespace;
|
||||
use lazaret_linux::user_namespace::UserNamespaceMap;
|
||||
use lazaret_prepared_lower::{IdMap, IdMapRange, PrepareRequest};
|
||||
use serde_json::json;
|
||||
use sha2::{Digest, Sha256};
|
||||
use tar::{Builder, EntryType, Header};
|
||||
|
||||
fn main() -> ExitCode {
|
||||
let arguments = std::env::args_os().skip(1).collect::<Vec<_>>();
|
||||
if let Some(outcome) = lazaret_host_coordinator::private_roles::REGISTRY.dispatch(&arguments) {
|
||||
return outcome.result.map_or_else(
|
||||
|error| {
|
||||
eprintln!("private role {} failed: {error}", outcome.name);
|
||||
ExitCode::FAILURE
|
||||
},
|
||||
|()| ExitCode::SUCCESS,
|
||||
);
|
||||
}
|
||||
run().map_or_else(
|
||||
|error| {
|
||||
eprintln!("prepared worker fixture failed: {error}");
|
||||
ExitCode::FAILURE
|
||||
},
|
||||
|()| ExitCode::SUCCESS,
|
||||
)
|
||||
}
|
||||
|
||||
fn run() -> std::io::Result<()> {
|
||||
let tree = tempfile::tempdir()?;
|
||||
let storage = tree.path().join("storage");
|
||||
std::fs::create_dir(&storage)?;
|
||||
let (layout, manifest) = create_layout(tree.path())?;
|
||||
let map = UserNamespaceMap::resolve_current()?;
|
||||
let artifact = prepare_request_in_user_namespace(
|
||||
&File::open(&storage)?,
|
||||
&storage,
|
||||
&File::open(&layout)?,
|
||||
PrepareRequest {
|
||||
operation_id: PrepareOperationId::new([7; 32]),
|
||||
layout_path: layout,
|
||||
source_identity: b"oci-v1\0registry.invalid\0owner-fixture".to_vec(),
|
||||
selected_manifest_digest: manifest,
|
||||
id_map: prepared_id_map(&map),
|
||||
destinations: Vec::new(),
|
||||
},
|
||||
&map,
|
||||
)?;
|
||||
let artifact_root =
|
||||
storage
|
||||
.join("prepared-lower")
|
||||
.join(PathBuf::from(std::ffi::OsString::from_vec(
|
||||
artifact.path.as_bytes().to_vec(),
|
||||
)));
|
||||
let metadata = std::fs::metadata(artifact_root.join("gid-42"))?;
|
||||
let expected_gid = map.gid[1]
|
||||
.parent_id
|
||||
.checked_add(41)
|
||||
.ok_or_else(|| std::io::Error::other("expected GID overflows"))?;
|
||||
if metadata.uid() != map.uid[0].parent_id || metadata.gid() != expected_gid {
|
||||
return Err(std::io::Error::other(format!(
|
||||
"mapped owner differs: observed {}:{}, expected {}:{expected_gid}",
|
||||
metadata.uid(),
|
||||
metadata.gid(),
|
||||
map.uid[0].parent_id
|
||||
)));
|
||||
}
|
||||
println!("PASS prepared worker materialized logical GID 42 through subordinate mapping");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_layout(root: &Path) -> std::io::Result<(PathBuf, [u8; 32])> {
|
||||
let path = root.join("layout");
|
||||
let blobs = path.join("blobs/sha256");
|
||||
std::fs::create_dir_all(&blobs)?;
|
||||
let layer = layer_with_gid_42()?;
|
||||
let layer_hash = Sha256::digest(&layer);
|
||||
std::fs::write(blobs.join(hex(&layer_hash)), &layer)?;
|
||||
let config = serde_json::to_vec(&json!({
|
||||
"architecture": "amd64",
|
||||
"os": "linux",
|
||||
"rootfs": {
|
||||
"type": "layers",
|
||||
"diff_ids": [format!("sha256:{}", hex(&layer_hash))]
|
||||
}
|
||||
}))?;
|
||||
let config_hash = Sha256::digest(&config);
|
||||
std::fs::write(blobs.join(hex(&config_hash)), &config)?;
|
||||
let manifest = serde_json::to_vec(&json!({
|
||||
"schemaVersion": 2,
|
||||
"config": {
|
||||
"mediaType": "application/vnd.oci.image.config.v1+json",
|
||||
"digest": format!("sha256:{}", hex(&config_hash)),
|
||||
"size": config.len()
|
||||
},
|
||||
"layers": [{
|
||||
"mediaType": "application/vnd.oci.image.layer.v1.tar",
|
||||
"digest": format!("sha256:{}", hex(&layer_hash)),
|
||||
"size": layer.len()
|
||||
}]
|
||||
}))?;
|
||||
let manifest_hash: [u8; 32] = Sha256::digest(&manifest).into();
|
||||
std::fs::write(blobs.join(hex(&manifest_hash)), &manifest)?;
|
||||
std::fs::write(
|
||||
path.join("index.json"),
|
||||
serde_json::to_vec(&json!({
|
||||
"schemaVersion": 2,
|
||||
"manifests": [{
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"digest": format!("sha256:{}", hex(&manifest_hash)),
|
||||
"size": manifest.len()
|
||||
}]
|
||||
}))?,
|
||||
)?;
|
||||
std::fs::write(
|
||||
path.join("oci-layout"),
|
||||
b"{\"imageLayoutVersion\":\"1.0.0\"}\n",
|
||||
)?;
|
||||
Ok((path, manifest_hash))
|
||||
}
|
||||
|
||||
fn layer_with_gid_42() -> std::io::Result<Vec<u8>> {
|
||||
let mut bytes = Vec::new();
|
||||
{
|
||||
let mut builder = Builder::new(&mut bytes);
|
||||
let mut header = Header::new_gnu();
|
||||
header.set_path("gid-42")?;
|
||||
header.set_entry_type(EntryType::Regular);
|
||||
header.set_size(5);
|
||||
header.set_uid(0);
|
||||
header.set_gid(42);
|
||||
header.set_mode(0o644);
|
||||
header.set_mtime(1_600_000_123);
|
||||
header.set_cksum();
|
||||
builder.append(&header, Cursor::new(b"probe"))?;
|
||||
builder.finish()?;
|
||||
}
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
fn prepared_id_map(map: &UserNamespaceMap) -> IdMap {
|
||||
IdMap {
|
||||
uid: map
|
||||
.uid
|
||||
.iter()
|
||||
.map(|range| IdMapRange {
|
||||
container_id: range.container_id,
|
||||
namespace_id: range.parent_id,
|
||||
size: range.size,
|
||||
})
|
||||
.collect(),
|
||||
gid: map
|
||||
.gid
|
||||
.iter()
|
||||
.map(|range| IdMapRange {
|
||||
container_id: range.container_id,
|
||||
namespace_id: range.parent_id,
|
||||
size: range.size,
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn hex(bytes: &[u8]) -> String {
|
||||
use std::fmt::Write as _;
|
||||
|
||||
bytes.iter().fold(String::new(), |mut output, byte| {
|
||||
write!(output, "{byte:02x}").expect("writing to String cannot fail");
|
||||
output
|
||||
})
|
||||
}
|
||||
|
|
@ -39,6 +39,7 @@ use lazaret_linux::process::{
|
|||
ProcessOwnershipContext, RetainedProcess, TargetCommand, child_broker,
|
||||
current_process_identity, read_start_time,
|
||||
};
|
||||
use lazaret_linux::user_namespace::UserNamespaceMap;
|
||||
use lazaret_runtime_supervisor::network::{DnsProbeTarget, NetworkProbeRequest, TcpProbeTarget};
|
||||
use lazaret_runtime_supervisor::sandbox::{
|
||||
NETWORK_NAMESPACE_PATH, RootProcess, RunscCommand, SandboxBuildRequest, SandboxBundle,
|
||||
|
|
|
|||
|
|
@ -8,9 +8,10 @@ use super::{
|
|||
RootProcess, RunscCommand, RuntimeNetworkLaunch, RuntimeSupervisorLaunch,
|
||||
SANDBOX_NETWORK_PROBE, SandboxBuildRequest, SandboxBundle, SandboxCoordinator, SandboxId,
|
||||
SocketAddr, SocketAddrExt, TcpListener, TcpProbeTarget, TempDir, UnixListener,
|
||||
build_sandbox_bundle, current_mount_identity, current_process_identity, default_sandbox_layout,
|
||||
derive_ipv4_policy, failure, fs, hex, host_path, load_trusted_configuration, lock_file,
|
||||
stage_benchmark_rootfs_and_control, stage_rootfs_and_control, try_exclusive_flock,
|
||||
UserNamespaceMap, build_sandbox_bundle, current_mount_identity, current_process_identity,
|
||||
default_sandbox_layout, derive_ipv4_policy, failure, fs, hex, host_path,
|
||||
load_trusted_configuration, lock_file, stage_benchmark_rootfs_and_control,
|
||||
stage_rootfs_and_control, try_exclusive_flock,
|
||||
};
|
||||
|
||||
pub(super) struct Fixture {
|
||||
|
|
@ -270,7 +271,8 @@ impl Fixture {
|
|||
slot: lazaret_core::records::HostProcessOwnershipSlot::SandboxRuntimeSupervisor,
|
||||
},
|
||||
command,
|
||||
)?;
|
||||
)?
|
||||
.with_user_namespace_map(UserNamespaceMap::resolve_current().map_err(failure)?)?;
|
||||
AwaitingAttachedRunsc::launch(&SandboxCoordinator::new(&self.store), &launch)
|
||||
.map_err(failure)
|
||||
}
|
||||
|
|
@ -416,7 +418,8 @@ impl NetworkFixture {
|
|||
configuration_fingerprint: self.configuration.configuration_fingerprint(),
|
||||
network_sidecar_id: NetworkSidecarId::new([14; 32]),
|
||||
},
|
||||
)?;
|
||||
)?
|
||||
.with_user_namespace_map(UserNamespaceMap::resolve_current().map_err(failure)?)?;
|
||||
let sandbox = SandboxCoordinator::new(&self.store);
|
||||
let network = NetworkCoordinator::new(&self.store);
|
||||
let mut pending = AwaitingAttachedRunsc::launch_networked(&sandbox, &network, &launch)
|
||||
|
|
|
|||
Loading…
Reference in a new issue