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:
ms2k 2026-07-19 19:51:47 +09:00
commit 73e3e9fdee
14 changed files with 1424 additions and 172 deletions

View file

@ -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"

View file

@ -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 {

View file

@ -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;

View 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(),
}
}

View file

@ -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,

View file

@ -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"

View file

@ -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),

View file

@ -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::{

View file

@ -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)

View file

@ -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)

View file

@ -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");

View 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
})
}

View file

@ -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,

View file

@ -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)