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
34 changed files with 1424 additions and 172 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
|
@ -1731,7 +1731,9 @@ dependencies = [
|
||||||
"rustix",
|
"rustix",
|
||||||
"rustls",
|
"rustls",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"sha2 0.11.0",
|
"sha2 0.11.0",
|
||||||
|
"tar",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"time",
|
"time",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
|
|
||||||
|
|
@ -509,31 +509,40 @@ fn validate_reachability(
|
||||||
|| expected_results.permitted_destinations.is_empty()
|
|| expected_results.permitted_destinations.is_empty()
|
||||||
|| expected_results.dns_queries.is_empty()
|
|| expected_results.dns_queries.is_empty()
|
||||||
{
|
{
|
||||||
return reachability_error();
|
return reachability_error("network probe identity or coverage mismatch");
|
||||||
}
|
}
|
||||||
|
|
||||||
let denied = exact_observations(
|
let denied = exact_observations(
|
||||||
&expected_results.denied_destinations,
|
&expected_results.denied_destinations,
|
||||||
&probe.denied_destinations,
|
&probe.denied_destinations,
|
||||||
|item| (item.destination, item.unreachable),
|
|item| (item.destination, item.unreachable),
|
||||||
|
"denied destination observations differ",
|
||||||
)?;
|
)?;
|
||||||
let permitted = exact_observations(
|
let permitted = exact_observations(
|
||||||
&expected_results.permitted_destinations,
|
&expected_results.permitted_destinations,
|
||||||
&probe.permitted_destinations,
|
&probe.permitted_destinations,
|
||||||
|item| (item.destination, item.reachable),
|
|item| (item.destination, item.reachable),
|
||||||
|
"permitted destination observations differ",
|
||||||
)?;
|
)?;
|
||||||
if denied.iter().any(|(_, passed)| !passed)
|
if denied.iter().any(|(_, passed)| !passed) {
|
||||||
|| permitted.iter().any(|(_, passed)| !passed)
|
return reachability_error("a configured denied destination was reachable");
|
||||||
|| expected_results
|
}
|
||||||
.denied_destinations
|
if permitted.iter().any(|(_, passed)| !passed) {
|
||||||
.iter()
|
return reachability_error("a configured permitted destination was unreachable");
|
||||||
.any(|address| effective_action(policy, *address) != Some(PolicyAction::Deny))
|
}
|
||||||
|| expected_results
|
if expected_results
|
||||||
.permitted_destinations
|
.denied_destinations
|
||||||
.iter()
|
.iter()
|
||||||
.any(|address| effective_action(policy, *address) != Some(PolicyAction::Allow))
|
.any(|address| effective_action(policy, *address) != Some(PolicyAction::Deny))
|
||||||
{
|
{
|
||||||
return reachability_error();
|
return reachability_error("a denied probe destination is not denied by policy");
|
||||||
|
}
|
||||||
|
if expected_results
|
||||||
|
.permitted_destinations
|
||||||
|
.iter()
|
||||||
|
.any(|address| effective_action(policy, *address) != Some(PolicyAction::Allow))
|
||||||
|
{
|
||||||
|
return reachability_error("a permitted probe destination is not allowed by policy");
|
||||||
}
|
}
|
||||||
|
|
||||||
let expected_dns: BTreeSet<_> = expected_results.dns_queries.iter().cloned().collect();
|
let expected_dns: BTreeSet<_> = expected_results.dns_queries.iter().cloned().collect();
|
||||||
|
|
@ -563,7 +572,7 @@ fn validate_reachability(
|
||||||
.any(|address| address_family(*address) != AddressFamily::Ipv4)
|
.any(|address| address_family(*address) != AddressFamily::Ipv4)
|
||||||
})
|
})
|
||||||
{
|
{
|
||||||
return reachability_error();
|
return reachability_error("DNS readiness observations differ or contain no IPv4 answer");
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -572,6 +581,7 @@ fn exact_observations<T, F>(
|
||||||
expected: &[IpAddress],
|
expected: &[IpAddress],
|
||||||
actual: &[T],
|
actual: &[T],
|
||||||
project: F,
|
project: F,
|
||||||
|
mismatch: &'static str,
|
||||||
) -> Result<BTreeSet<(IpAddress, bool)>, NetworkValidationError>
|
) -> Result<BTreeSet<(IpAddress, bool)>, NetworkValidationError>
|
||||||
where
|
where
|
||||||
F: Fn(&T) -> (IpAddress, bool),
|
F: Fn(&T) -> (IpAddress, bool),
|
||||||
|
|
@ -585,7 +595,7 @@ where
|
||||||
|| expected_items.len() != actual_addresses.len()
|
|| expected_items.len() != actual_addresses.len()
|
||||||
|| actual_addresses != expected_items
|
|| actual_addresses != expected_items
|
||||||
{
|
{
|
||||||
return reachability_error();
|
return reachability_error(mismatch);
|
||||||
}
|
}
|
||||||
Ok(actual_items)
|
Ok(actual_items)
|
||||||
}
|
}
|
||||||
|
|
@ -646,9 +656,9 @@ fn domain_digest(schema_id: &str, canonical_bytes: &[u8]) -> [u8; 32] {
|
||||||
hasher.finalize().into()
|
hasher.finalize().into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn reachability_error<T>() -> Result<T, NetworkValidationError> {
|
fn reachability_error<T>(detail: &'static str) -> Result<T, NetworkValidationError> {
|
||||||
Err(NetworkValidationError::new(
|
Err(NetworkValidationError::new(
|
||||||
NetworkValidationErrorKind::Reachability,
|
NetworkValidationErrorKind::Reachability,
|
||||||
"network reachability evidence mismatch",
|
detail,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,8 @@ tokio.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = "=3.27.0"
|
tempfile = "=3.27.0"
|
||||||
|
serde_json.workspace = true
|
||||||
|
tar.workspace = true
|
||||||
lazaret-client = { version = "=0.0.0", path = "../lazaret-client" }
|
lazaret-client = { version = "=0.0.0", path = "../lazaret-client" }
|
||||||
lazaret-escalation-client = { version = "=0.0.0", path = "../lazaret-escalation-client" }
|
lazaret-escalation-client = { version = "=0.0.0", path = "../lazaret-escalation-client" }
|
||||||
x509-parser.workspace = true
|
x509-parser.workspace = true
|
||||||
|
|
@ -52,6 +54,11 @@ name = "escalation"
|
||||||
path = "tests/escalation.rs"
|
path = "tests/escalation.rs"
|
||||||
harness = false
|
harness = false
|
||||||
|
|
||||||
|
[[test]]
|
||||||
|
name = "prepared-worker"
|
||||||
|
path = "tests/prepared-worker.rs"
|
||||||
|
harness = false
|
||||||
|
|
||||||
[[test]]
|
[[test]]
|
||||||
name = "cold-start"
|
name = "cold-start"
|
||||||
path = "tests/cold_start.rs"
|
path = "tests/cold_start.rs"
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ use lazaret_linux::filesystem::{RegularFileMailbox, mount_identity, namespace_id
|
||||||
use lazaret_linux::process::{
|
use lazaret_linux::process::{
|
||||||
ProcessOwnershipContext, TargetCommand, child_broker, current_process_identity,
|
ProcessOwnershipContext, TargetCommand, child_broker, current_process_identity,
|
||||||
};
|
};
|
||||||
|
use lazaret_linux::user_namespace::UserNamespaceMap;
|
||||||
use lazaret_runtime_supervisor::network::NetworkProbeRequest;
|
use lazaret_runtime_supervisor::network::NetworkProbeRequest;
|
||||||
use lazaret_runtime_supervisor::sandbox::{
|
use lazaret_runtime_supervisor::sandbox::{
|
||||||
RootProcess, SandboxBuildRequest, SandboxBundle, build_sandbox_bundle,
|
RootProcess, SandboxBuildRequest, SandboxBundle, build_sandbox_bundle,
|
||||||
|
|
@ -85,6 +86,7 @@ pub struct ColdStartInputs<'a> {
|
||||||
pub invoking_uid: u32,
|
pub invoking_uid: u32,
|
||||||
pub escalation_client: &'a File,
|
pub escalation_client: &'a File,
|
||||||
pub sandbox_init: &'a File,
|
pub sandbox_init: &'a File,
|
||||||
|
pub user_namespace_map: UserNamespaceMap,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct BootstrapOwnerGuard {
|
pub struct BootstrapOwnerGuard {
|
||||||
|
|
@ -227,6 +229,8 @@ impl<'a> ColdStart<'a> {
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.map_err(|error| failure(format!("Runtime Supervisor launch: {error}")))?
|
.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)
|
.with_state_anchor(state_anchor)
|
||||||
.map_err(|error| failure(format!("Runtime Supervisor handoff: {error}")))?;
|
.map_err(|error| failure(format!("Runtime Supervisor handoff: {error}")))?;
|
||||||
if let Some(git_routing) = &git_routing {
|
if let Some(git_routing) = &git_routing {
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ pub mod escalation;
|
||||||
pub mod git_routing;
|
pub mod git_routing;
|
||||||
pub mod lifecycle;
|
pub mod lifecycle;
|
||||||
pub mod network;
|
pub mod network;
|
||||||
|
pub mod prepared_worker;
|
||||||
pub mod private_roles;
|
pub mod private_roles;
|
||||||
pub mod recovery;
|
pub mod recovery;
|
||||||
pub mod runtime;
|
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::client_bootstrap::dispatch_private_client_bootstrap;
|
||||||
|
use crate::prepared_worker::{PRIVATE_PREPARED_WORKER_ROLE, dispatch_private_prepared_worker};
|
||||||
use crate::sandbox_init::{
|
use crate::sandbox_init::{
|
||||||
PRIVATE_SANDBOX_INIT_ROLE, PRIVATE_SESSION_EXEC_ROLE, PRIVATE_SESSION_IDENTITY_ROLE,
|
PRIVATE_SANDBOX_INIT_ROLE, PRIVATE_SESSION_EXEC_ROLE, PRIVATE_SESSION_IDENTITY_ROLE,
|
||||||
dispatch_private_sandbox_init,
|
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_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_SANDBOX_INIT_ROLE, sandbox_init),
|
||||||
PrivateRole::new(PRIVATE_SESSION_EXEC_ROLE, session_exec),
|
PrivateRole::new(PRIVATE_SESSION_EXEC_ROLE, session_exec),
|
||||||
PrivateRole::new(PRIVATE_SESSION_IDENTITY_ROLE, session_identity),
|
PrivateRole::new(PRIVATE_SESSION_IDENTITY_ROLE, session_identity),
|
||||||
|
|
@ -68,6 +73,7 @@ mod tests {
|
||||||
REGISTRY.names(),
|
REGISTRY.names(),
|
||||||
[
|
[
|
||||||
PRIVATE_CLIENT_BOOTSTRAP_ROLE,
|
PRIVATE_CLIENT_BOOTSTRAP_ROLE,
|
||||||
|
PRIVATE_PREPARED_WORKER_ROLE,
|
||||||
PRIVATE_SANDBOX_INIT_ROLE,
|
PRIVATE_SANDBOX_INIT_ROLE,
|
||||||
PRIVATE_SESSION_EXEC_ROLE,
|
PRIVATE_SESSION_EXEC_ROLE,
|
||||||
PRIVATE_SESSION_IDENTITY_ROLE,
|
PRIVATE_SESSION_IDENTITY_ROLE,
|
||||||
|
|
|
||||||
|
|
@ -115,8 +115,13 @@ pub(super) fn require_mount_absent(
|
||||||
match (path, mount) {
|
match (path, mount) {
|
||||||
(None, None) => Ok(()),
|
(None, None) => Ok(()),
|
||||||
(Some(path), Some(expected)) => {
|
(Some(path), Some(expected)) => {
|
||||||
let observed =
|
let observed = match current_mount_identity(Path::new(std::ffi::OsStr::from_bytes(
|
||||||
current_mount_identity(Path::new(std::ffi::OsStr::from_bytes(path.as_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 {
|
if observed == expected {
|
||||||
Err(io::Error::other(format!(
|
Err(io::Error::other(format!(
|
||||||
"{label} exact mount remains present"
|
"{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 {
|
fn participant(role: ParticipantRole, ownership: ParticipantOwnership) -> Participant {
|
||||||
Participant::snapshot(
|
Participant::snapshot(
|
||||||
GenerationId::new(1),
|
GenerationId::new(1),
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,9 @@ use lazaret_core::records::{
|
||||||
SandboxOwnership, StagedProcessOwnership,
|
SandboxOwnership, StagedProcessOwnership,
|
||||||
};
|
};
|
||||||
use lazaret_linux::ipc::private_seqpacket_pair;
|
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::network::NetworkProbeRequest;
|
||||||
use lazaret_runtime_supervisor::sandbox::SandboxHandoffAuthority;
|
use lazaret_runtime_supervisor::sandbox::SandboxHandoffAuthority;
|
||||||
use lazaret_runtime_supervisor::supervision::{
|
use lazaret_runtime_supervisor::supervision::{
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
use super::{
|
use super::{
|
||||||
CanonicalDerivedNetworkPolicy, CommittedSandboxRuntime, CoordinatorChannel,
|
AcknowledgedChildAcquisition, CanonicalDerivedNetworkPolicy, CommittedSandboxRuntime,
|
||||||
GitRoutingCoordinator, MountIdentity, NamespaceIdentity, NetworkCoordinator,
|
CoordinatorChannel, GitRoutingCoordinator, MountIdentity, NamespaceIdentity,
|
||||||
NetworkProbeRequest, NetworkReadinessEvidence, PidfsProcessIdentityV1, PipeFlags,
|
NetworkCoordinator, NetworkProbeRequest, NetworkReadinessEvidence, PidfsProcessIdentityV1,
|
||||||
PreparedArtifactId, ProcessOwnershipCommitAck, ReachabilityExpectation, RetainedProcess,
|
PipeFlags, PreparedArtifactId, ProcessOwnershipCommitAck, ReachabilityExpectation,
|
||||||
RuntimeSupervisorLaunch, SandboxCoordinator, SandboxCoordinatorError, SandboxHandoffAuthority,
|
RetainedProcess, RuntimeSupervisorLaunch, SandboxCoordinator, SandboxCoordinatorError,
|
||||||
StagedProcessOwnership, StateAnchorCoordinator, SupervisorEvent, child_broker,
|
SandboxHandoffAuthority, StagedProcessOwnership, StateAnchorCoordinator, SupervisorEvent,
|
||||||
cleanup_supervisor, current_process_identity, io, pipe_with, private_seqpacket_pair,
|
child_broker, cleanup_supervisor, current_process_identity, io, pipe_with,
|
||||||
|
private_seqpacket_pair,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct AwaitingAttachedRunsc {
|
pub struct AwaitingAttachedRunsc {
|
||||||
|
|
@ -216,11 +217,20 @@ impl AwaitingAttachedRunsc {
|
||||||
let (coordinator_control, supervisor_control) = private_seqpacket_pair()?;
|
let (coordinator_control, supervisor_control) = private_seqpacket_pair()?;
|
||||||
let (supervisor_lifetime, coordinator_lifetime) =
|
let (supervisor_lifetime, coordinator_lifetime) =
|
||||||
pipe_with(PipeFlags::CLOEXEC).map_err(io::Error::from)?;
|
pipe_with(PipeFlags::CLOEXEC).map_err(io::Error::from)?;
|
||||||
let process = child_broker().acquire_with_stdio(
|
let user_namespace_map =
|
||||||
parent,
|
launch
|
||||||
launch.ownership(),
|
.user_namespace_map()
|
||||||
launch.target(),
|
.ok_or(SandboxCoordinatorError::InvalidState(
|
||||||
launch.child_standard_io(supervisor_control, supervisor_lifetime)?,
|
"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| {
|
|staged| {
|
||||||
coordinator
|
coordinator
|
||||||
.stage_runtime_supervisor(staged)
|
.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_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_linux::user_namespace::UserNamespaceMap;
|
||||||
use lazaret_prepared_lower::{
|
use lazaret_prepared_lower::{
|
||||||
IdMap, IdMapRange, OciRegistryTransport, Platform, PreparedDestination,
|
OciRegistryTransport, Platform, PreparedDestination, PreparedDestinationKind,
|
||||||
PreparedDestinationKind, PreparedLowerError, PreparedLowerStore, RegistryCredentials,
|
PreparedLowerError, PreparedLowerStore, RegistryCredentials,
|
||||||
};
|
};
|
||||||
use lazaret_runtime_supervisor::network::{DnsProbeTarget, NetworkProbeRequest, TcpProbeTarget};
|
use lazaret_runtime_supervisor::network::{DnsProbeTarget, NetworkProbeRequest, TcpProbeTarget};
|
||||||
use lazaret_runtime_supervisor::sandbox::{
|
use lazaret_runtime_supervisor::sandbox::{
|
||||||
|
|
@ -32,6 +33,7 @@ use rustix::fs::{Mode, OFlags, ResolveFlags, openat2};
|
||||||
|
|
||||||
use crate::cold_start::ColdStartInputs;
|
use crate::cold_start::ColdStartInputs;
|
||||||
use crate::domain_store::HostCoordinatorDomainStore;
|
use crate::domain_store::HostCoordinatorDomainStore;
|
||||||
|
use crate::prepared_worker::prepare_in_user_namespace;
|
||||||
use crate::recovery::recover_before_cold_start;
|
use crate::recovery::recover_before_cold_start;
|
||||||
use crate::sandbox_init::PRIVATE_SANDBOX_INIT_ROLE;
|
use crate::sandbox_init::PRIVATE_SANDBOX_INIT_ROLE;
|
||||||
|
|
||||||
|
|
@ -89,6 +91,7 @@ pub struct RuntimeSelection {
|
||||||
container_id: ContainerId,
|
container_id: ContainerId,
|
||||||
escalation_client: File,
|
escalation_client: File,
|
||||||
sandbox_init: File,
|
sandbox_init: File,
|
||||||
|
user_namespace_map: UserNamespaceMap,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RuntimeSelection {
|
impl RuntimeSelection {
|
||||||
|
|
@ -125,6 +128,7 @@ impl RuntimeSelection {
|
||||||
invoking_uid: rustix::process::getuid().as_raw(),
|
invoking_uid: rustix::process::getuid().as_raw(),
|
||||||
escalation_client: &self.escalation_client,
|
escalation_client: &self.escalation_client,
|
||||||
sandbox_init: &self.sandbox_init,
|
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<'_>,
|
observation: &PlatformObservation<'_>,
|
||||||
) -> Result<RuntimeSelection, RuntimeSelectionError> {
|
) -> Result<RuntimeSelection, RuntimeSelectionError> {
|
||||||
check_platform(observation).map_err(failure)?;
|
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 escalation_client = open_installed_executable(product_paths.escalation_client)?;
|
||||||
let sandbox_init = open_installed_executable(product_paths.sandbox_init)?;
|
let sandbox_init = open_installed_executable(product_paths.sandbox_init)?;
|
||||||
let catalog = Catalog::under_storage_root(configuration.storage_root()).map_err(failure)?;
|
let catalog = Catalog::under_storage_root(configuration.storage_root()).map_err(failure)?;
|
||||||
|
|
@ -180,14 +185,14 @@ async fn select_runtime_for_platform(
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(RuntimeSelectionError::PreparedLower)?;
|
.map_err(RuntimeSelectionError::PreparedLower)?;
|
||||||
let artifact = prepared
|
let artifact = prepare_in_user_namespace(
|
||||||
.prepare_transported(
|
configuration,
|
||||||
PrepareOperationId::new(random_id()?),
|
&image,
|
||||||
&image,
|
PrepareOperationId::new(random_id()?),
|
||||||
rootless_id_map(),
|
prepared_destinations(configuration),
|
||||||
prepared_destinations(configuration),
|
&user_namespace_map,
|
||||||
)
|
)
|
||||||
.map_err(RuntimeSelectionError::PreparedLower)?;
|
.map_err(|error| failure(format!("mapped preparation worker failed: {error}")))?;
|
||||||
let artifact_authority = prepared
|
let artifact_authority = prepared
|
||||||
.open_artifact_authority(&artifact)
|
.open_artifact_authority(&artifact)
|
||||||
.map_err(RuntimeSelectionError::PreparedLower)?;
|
.map_err(RuntimeSelectionError::PreparedLower)?;
|
||||||
|
|
@ -204,7 +209,7 @@ async fn select_runtime_for_platform(
|
||||||
)));
|
)));
|
||||||
image
|
image
|
||||||
.cleanup()
|
.cleanup()
|
||||||
.map_err(RuntimeSelectionError::PreparedLower)?;
|
.map_err(|error| failure(format!("OCI layout cleanup failed: {error:#}")))?;
|
||||||
|
|
||||||
let next_generation = next_generation(&store)?;
|
let next_generation = next_generation(&store)?;
|
||||||
let (coordinator_id, network_sidecar_id, sandbox_id) = distinct_generation_ids()?;
|
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()?),
|
container_id: ContainerId::new(random_id()?),
|
||||||
escalation_client,
|
escalation_client,
|
||||||
sandbox_init,
|
sandbox_init,
|
||||||
|
user_namespace_map,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -387,21 +393,6 @@ fn open_installed_executable(path: &Path) -> Result<File, RuntimeSelectionError>
|
||||||
Ok(file)
|
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()
|
fn distinct_generation_ids()
|
||||||
-> Result<(CoordinatorId, NetworkSidecarId, SandboxId), RuntimeSelectionError> {
|
-> Result<(CoordinatorId, NetworkSidecarId, SandboxId), RuntimeSelectionError> {
|
||||||
for _ in 0..8 {
|
for _ in 0..8 {
|
||||||
|
|
@ -446,7 +437,7 @@ mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[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 tree = tempfile::tempdir().unwrap();
|
||||||
let configuration = configuration(tree.path());
|
let configuration = configuration(tree.path());
|
||||||
let generation = Generation {
|
let generation = Generation {
|
||||||
|
|
@ -475,16 +466,6 @@ mod tests {
|
||||||
);
|
);
|
||||||
assert_eq!(expectation.dns_queries[0].resolver, request.dns[0].resolver);
|
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);
|
let destinations = prepared_destinations(&configuration);
|
||||||
assert!(destinations.iter().any(|destination| {
|
assert!(destinations.iter().any(|destination| {
|
||||||
destination.path == Path::new(WORKSPACE_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::cold_start::{ColdStart, ColdStartInputs};
|
||||||
use lazaret_host_coordinator::domain_store::HostCoordinatorDomainStore;
|
use lazaret_host_coordinator::domain_store::HostCoordinatorDomainStore;
|
||||||
use lazaret_linux::filesystem::{namespace_identity, try_exclusive_flock};
|
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::network::NetworkProbeRequest;
|
||||||
use lazaret_runtime_supervisor::sandbox::{RootProcess, default_sandbox_layout};
|
use lazaret_runtime_supervisor::sandbox::{RootProcess, default_sandbox_layout};
|
||||||
use lazaret_storage_catalog::Catalog;
|
use lazaret_storage_catalog::Catalog;
|
||||||
use rustix::process::getuid;
|
use rustix::process::{getgid, getuid};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn validated_inputs_stage_one_complete_plan_and_drop_exact_runtime_artifacts() {
|
fn validated_inputs_stage_one_complete_plan_and_drop_exact_runtime_artifacts() {
|
||||||
|
|
@ -217,6 +220,7 @@ impl ColdStartFixture {
|
||||||
invoking_uid: getuid().as_raw(),
|
invoking_uid: getuid().as_raw(),
|
||||||
escalation_client,
|
escalation_client,
|
||||||
sandbox_init: 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) {
|
fn assert_private_artifacts(bundle: &Path, control: &Path) {
|
||||||
let config = bundle.join("config.json");
|
let config = bundle.join("config.json");
|
||||||
let escalation_client = control.join("bin/lazaret-escalate");
|
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,
|
ProcessOwnershipContext, RetainedProcess, TargetCommand, child_broker,
|
||||||
current_process_identity, read_start_time,
|
current_process_identity, read_start_time,
|
||||||
};
|
};
|
||||||
|
use lazaret_linux::user_namespace::UserNamespaceMap;
|
||||||
use lazaret_runtime_supervisor::network::{DnsProbeTarget, NetworkProbeRequest, TcpProbeTarget};
|
use lazaret_runtime_supervisor::network::{DnsProbeTarget, NetworkProbeRequest, TcpProbeTarget};
|
||||||
use lazaret_runtime_supervisor::sandbox::{
|
use lazaret_runtime_supervisor::sandbox::{
|
||||||
NETWORK_NAMESPACE_PATH, RootProcess, RunscCommand, SandboxBuildRequest, SandboxBundle,
|
NETWORK_NAMESPACE_PATH, RootProcess, RunscCommand, SandboxBuildRequest, SandboxBundle,
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,10 @@ use super::{
|
||||||
RootProcess, RunscCommand, RuntimeNetworkLaunch, RuntimeSupervisorLaunch,
|
RootProcess, RunscCommand, RuntimeNetworkLaunch, RuntimeSupervisorLaunch,
|
||||||
SANDBOX_NETWORK_PROBE, SandboxBuildRequest, SandboxBundle, SandboxCoordinator, SandboxId,
|
SANDBOX_NETWORK_PROBE, SandboxBuildRequest, SandboxBundle, SandboxCoordinator, SandboxId,
|
||||||
SocketAddr, SocketAddrExt, TcpListener, TcpProbeTarget, TempDir, UnixListener,
|
SocketAddr, SocketAddrExt, TcpListener, TcpProbeTarget, TempDir, UnixListener,
|
||||||
build_sandbox_bundle, current_mount_identity, current_process_identity, default_sandbox_layout,
|
UserNamespaceMap, build_sandbox_bundle, current_mount_identity, current_process_identity,
|
||||||
derive_ipv4_policy, failure, fs, hex, host_path, load_trusted_configuration, lock_file,
|
default_sandbox_layout, derive_ipv4_policy, failure, fs, hex, host_path,
|
||||||
stage_benchmark_rootfs_and_control, stage_rootfs_and_control, try_exclusive_flock,
|
load_trusted_configuration, lock_file, stage_benchmark_rootfs_and_control,
|
||||||
|
stage_rootfs_and_control, try_exclusive_flock,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub(super) struct Fixture {
|
pub(super) struct Fixture {
|
||||||
|
|
@ -270,7 +271,8 @@ impl Fixture {
|
||||||
slot: lazaret_core::records::HostProcessOwnershipSlot::SandboxRuntimeSupervisor,
|
slot: lazaret_core::records::HostProcessOwnershipSlot::SandboxRuntimeSupervisor,
|
||||||
},
|
},
|
||||||
command,
|
command,
|
||||||
)?;
|
)?
|
||||||
|
.with_user_namespace_map(UserNamespaceMap::resolve_current().map_err(failure)?)?;
|
||||||
AwaitingAttachedRunsc::launch(&SandboxCoordinator::new(&self.store), &launch)
|
AwaitingAttachedRunsc::launch(&SandboxCoordinator::new(&self.store), &launch)
|
||||||
.map_err(failure)
|
.map_err(failure)
|
||||||
}
|
}
|
||||||
|
|
@ -416,7 +418,8 @@ impl NetworkFixture {
|
||||||
configuration_fingerprint: self.configuration.configuration_fingerprint(),
|
configuration_fingerprint: self.configuration.configuration_fingerprint(),
|
||||||
network_sidecar_id: NetworkSidecarId::new([14; 32]),
|
network_sidecar_id: NetworkSidecarId::new([14; 32]),
|
||||||
},
|
},
|
||||||
)?;
|
)?
|
||||||
|
.with_user_namespace_map(UserNamespaceMap::resolve_current().map_err(failure)?)?;
|
||||||
let sandbox = SandboxCoordinator::new(&self.store);
|
let sandbox = SandboxCoordinator::new(&self.store);
|
||||||
let network = NetworkCoordinator::new(&self.store);
|
let network = NetworkCoordinator::new(&self.store);
|
||||||
let mut pending = AwaitingAttachedRunsc::launch_networked(&sandbox, &network, &launch)
|
let mut pending = AwaitingAttachedRunsc::launch_networked(&sandbox, &network, &launch)
|
||||||
|
|
|
||||||
|
|
@ -7,3 +7,4 @@ pub mod ipc;
|
||||||
pub mod platform;
|
pub mod platform;
|
||||||
pub mod private_role;
|
pub mod private_role;
|
||||||
pub mod process;
|
pub mod process;
|
||||||
|
pub mod user_namespace;
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ pub const PID_FS_MAGIC: i64 = 0x5049_4446;
|
||||||
const BARRIER_RELEASE: &[u8; 32] = b"LAZARET-CAUSAL-BARRIER-RELEASE!!";
|
const BARRIER_RELEASE: &[u8; 32] = b"LAZARET-CAUSAL-BARRIER-RELEASE!!";
|
||||||
pub const PRIVATE_PROCESS_ROLE: &str = "--lazaret-private-process-role";
|
pub const PRIVATE_PROCESS_ROLE: &str = "--lazaret-private-process-role";
|
||||||
const WRAPPER_ROLE: &str = "launch-wrapper";
|
const WRAPPER_ROLE: &str = "launch-wrapper";
|
||||||
|
const MAPPED_WRAPPER_ROLE: &str = "mapped-launch-wrapper";
|
||||||
const SIGCHLD_PROBE_ROLE: &str = "sigchld-probe";
|
const SIGCHLD_PROBE_ROLE: &str = "sigchld-probe";
|
||||||
static CHILD_BROKER: OnceLock<ChildBroker> = OnceLock::new();
|
static CHILD_BROKER: OnceLock<ChildBroker> = OnceLock::new();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,15 +4,18 @@ use super::recovery::{
|
||||||
read_start_time, require_default_sigchld, require_host_pid_namespace, require_reapable_sigchld,
|
read_start_time, require_default_sigchld, require_host_pid_namespace, require_reapable_sigchld,
|
||||||
};
|
};
|
||||||
use super::{
|
use super::{
|
||||||
AcknowledgedChildAcquisition, AsFd, BarrierRelease, BootstrapChild, CapturedProcess,
|
AcknowledgedChildAcquisition, AsFd, BARRIER_RELEASE, BarrierRelease, BootstrapChild,
|
||||||
ChildBroker, ChildStandardIo, ChildStandardIoPolicy, File, HostPidLocator, OFlags, OsString,
|
CapturedProcess, ChildBroker, ChildStandardIo, ChildStandardIoPolicy, File, HostPidLocator,
|
||||||
OwnedFd, PRIVATE_PROCESS_ROLE, ParentEdge, Pid, PidfdFlags, PidfsProcessIdentityV1, PollFd,
|
MAPPED_WRAPPER_ROLE, OFlags, OsString, OwnedFd, PRIVATE_PROCESS_ROLE, ParentEdge, Pid,
|
||||||
PollFlags, ProcessExit, ProcessOwnershipCommitAck, ProcessOwnershipContext,
|
PidfdFlags, PidfsProcessIdentityV1, PollFd, PollFlags, ProcessExit, ProcessOwnershipCommitAck,
|
||||||
ProcessStartTimeTicks, RetainedProcess, SIGCHLD_PROBE_ROLE, Signal, StagedProcessOwnership,
|
ProcessOwnershipContext, ProcessStartTimeTicks, RetainedProcess, SIGCHLD_PROBE_ROLE, Signal,
|
||||||
Stdio, TargetCommand, Timespec, WRAPPER_ROLE, WaitId, WaitIdOptions, fcntl_getfl, fcntl_setfl,
|
StagedProcessOwnership, Stdio, TargetCommand, Timespec, WRAPPER_ROLE, WaitId, WaitIdOptions,
|
||||||
io, launch_barrier, poll, recover_process, require_captured_parent_edge,
|
fcntl_getfl, fcntl_setfl, io, launch_barrier, poll, recover_process,
|
||||||
require_parent_edge_standard_io,
|
require_captured_parent_edge, require_parent_edge_standard_io,
|
||||||
};
|
};
|
||||||
|
use crate::ipc::private_stream_pair;
|
||||||
|
use crate::user_namespace::UserNamespaceMap;
|
||||||
|
use std::io::{Read as _, Write as _};
|
||||||
|
|
||||||
impl ChildBroker {
|
impl ChildBroker {
|
||||||
/// Spawns the private Host Coordinator bootstrap with exact pipe ownership and pidfd reaping.
|
/// Spawns the private Host Coordinator bootstrap with exact pipe ownership and pidfd reaping.
|
||||||
|
|
@ -132,6 +135,64 @@ impl ChildBroker {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Runs a non-durable direct child as root in the supplied caller-configured user namespace.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Refuses invalid mapping, causal launch, helper execution, map attestation, or child exit.
|
||||||
|
#[allow(clippy::disallowed_methods, clippy::disallowed_types)]
|
||||||
|
pub fn run_transient_in_user_namespace(
|
||||||
|
&self,
|
||||||
|
target: &TargetCommand,
|
||||||
|
map: &UserNamespaceMap,
|
||||||
|
) -> io::Result<ProcessExit> {
|
||||||
|
let _guard = self
|
||||||
|
.exclusion
|
||||||
|
.lock()
|
||||||
|
.map_err(|_| io::Error::other("child broker lock poisoned"))?;
|
||||||
|
require_parent_edge_standard_io(target.parent_edge(), ChildStandardIoPolicy::Inherit)?;
|
||||||
|
require_default_sigchld()?;
|
||||||
|
require_reapable_sigchld()?;
|
||||||
|
map.validate()?;
|
||||||
|
let parent = current_process_identity()?;
|
||||||
|
let (handshake, child_handshake) = private_stream_pair()?;
|
||||||
|
let mut command = mapped_wrapper_command();
|
||||||
|
append_parent_identity(&mut command, parent);
|
||||||
|
target.append_to(&mut command);
|
||||||
|
command
|
||||||
|
.stdin(Stdio::from(child_handshake))
|
||||||
|
.stdout(Stdio::null())
|
||||||
|
.stderr(Stdio::inherit());
|
||||||
|
let mut child = command.spawn()?;
|
||||||
|
let process = match acquire_spawned_child(child.id()) {
|
||||||
|
Ok(process) => process,
|
||||||
|
Err(error) => {
|
||||||
|
drop(handshake);
|
||||||
|
let _ = child.wait();
|
||||||
|
return Err(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if let Err(error) = configure_user_namespace(&process, File::from(handshake), map) {
|
||||||
|
return fail_running_mapped(process, child, error);
|
||||||
|
}
|
||||||
|
let status =
|
||||||
|
rustix::process::waitid(WaitId::PidFd(process.pidfd.as_fd()), WaitIdOptions::EXITED)?
|
||||||
|
.ok_or_else(|| io::Error::other("mapped transient child exited without status"))?;
|
||||||
|
let exit = ProcessExit {
|
||||||
|
exit_code: status.exit_status(),
|
||||||
|
terminating_signal: status.terminating_signal(),
|
||||||
|
};
|
||||||
|
drop(child);
|
||||||
|
drop(process);
|
||||||
|
if exit.exit_code == Some(0) && exit.terminating_signal.is_none() {
|
||||||
|
Ok(exit)
|
||||||
|
} else {
|
||||||
|
Err(io::Error::other(format!(
|
||||||
|
"mapped transient child failed with exit {:?} and signal {:?}",
|
||||||
|
exit.exit_code, exit.terminating_signal
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Starts a non-durable direct child with split captured output and exact retained authority.
|
/// Starts a non-durable direct child with split captured output and exact retained authority.
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
|
|
@ -223,6 +284,89 @@ impl ChildBroker {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Acquires an exact child as root in a helper-configured subordinate user namespace.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Refuses staging, acknowledgment, namespace helper, live-map attestation, or release
|
||||||
|
/// failures and reaps the child on every failure.
|
||||||
|
#[allow(clippy::disallowed_methods, clippy::disallowed_types)]
|
||||||
|
pub fn acquire_with_stdio_in_user_namespace<Stage, Acknowledge>(
|
||||||
|
&self,
|
||||||
|
acquisition: AcknowledgedChildAcquisition<'_>,
|
||||||
|
map: &UserNamespaceMap,
|
||||||
|
stage: Stage,
|
||||||
|
acknowledge: Acknowledge,
|
||||||
|
) -> io::Result<RetainedProcess>
|
||||||
|
where
|
||||||
|
Stage: FnOnce(StagedProcessOwnership) -> io::Result<()>,
|
||||||
|
Acknowledge: FnOnce(StagedProcessOwnership) -> io::Result<ProcessOwnershipCommitAck>,
|
||||||
|
{
|
||||||
|
let _guard = self
|
||||||
|
.exclusion
|
||||||
|
.lock()
|
||||||
|
.map_err(|_| io::Error::other("child broker lock poisoned"))?;
|
||||||
|
require_parent_edge_standard_io(
|
||||||
|
acquisition.target.parent_edge(),
|
||||||
|
acquisition.standard_io.policy(),
|
||||||
|
)?;
|
||||||
|
require_default_sigchld()?;
|
||||||
|
require_reapable_sigchld()?;
|
||||||
|
map.validate()?;
|
||||||
|
let (handshake, child_handshake) = private_stream_pair()?;
|
||||||
|
let mut command = mapped_wrapper_command();
|
||||||
|
append_parent_identity(&mut command, acquisition.expected_parent);
|
||||||
|
acquisition.target.append_to(&mut command);
|
||||||
|
command.stdin(Stdio::from(child_handshake));
|
||||||
|
let mut standard_io = acquisition.standard_io;
|
||||||
|
if let Some(stdout) = standard_io.stdout.take() {
|
||||||
|
command.stdout(stdout);
|
||||||
|
}
|
||||||
|
if let Some(stderr) = standard_io.stderr.take() {
|
||||||
|
command.stderr(stderr);
|
||||||
|
}
|
||||||
|
let mut child = command.spawn()?;
|
||||||
|
let process = match acquire_spawned_child(child.id()) {
|
||||||
|
Ok(process) => process,
|
||||||
|
Err(error) => {
|
||||||
|
drop(handshake);
|
||||||
|
let _ = child.wait();
|
||||||
|
return Err(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let staged = StagedProcessOwnership::v1(
|
||||||
|
acquisition.ownership.domain_id,
|
||||||
|
acquisition.ownership.generation_id,
|
||||||
|
acquisition.ownership.coordinator_id,
|
||||||
|
acquisition.ownership.slot,
|
||||||
|
process.identity,
|
||||||
|
);
|
||||||
|
if let Err(error) = stage(staged) {
|
||||||
|
drop(handshake);
|
||||||
|
return fail_after_pidfd_closed(process, error);
|
||||||
|
}
|
||||||
|
let acknowledgment = match acknowledge(staged) {
|
||||||
|
Ok(acknowledgment) => acknowledgment,
|
||||||
|
Err(error) => {
|
||||||
|
drop(handshake);
|
||||||
|
return fail_after_pidfd_closed(process, error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if !acknowledgment.acknowledges(&staged) {
|
||||||
|
drop(handshake);
|
||||||
|
return fail_after_pidfd_closed(
|
||||||
|
process,
|
||||||
|
io::Error::other(
|
||||||
|
"process ownership acknowledgment does not match staged ownership",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if let Err(error) = configure_user_namespace(&process, File::from(handshake), map) {
|
||||||
|
return fail_running_mapped(process, child, error);
|
||||||
|
}
|
||||||
|
drop(child);
|
||||||
|
Ok(process)
|
||||||
|
}
|
||||||
|
|
||||||
/// Acquires an exact child while retaining its blocked wrapper across one acknowledged
|
/// Acquires an exact child while retaining its blocked wrapper across one acknowledged
|
||||||
/// parent transition.
|
/// parent transition.
|
||||||
///
|
///
|
||||||
|
|
@ -439,6 +583,84 @@ fn child_pipe_file(fd: OwnedFd) -> io::Result<File> {
|
||||||
Ok(File::from(fd))
|
Ok(File::from(fd))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::disallowed_types)]
|
||||||
|
fn mapped_wrapper_command() -> std::process::Command {
|
||||||
|
private_role_command(MAPPED_WRAPPER_ROLE)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn configure_user_namespace(
|
||||||
|
process: &RetainedProcess,
|
||||||
|
mut handshake: File,
|
||||||
|
map: &UserNamespaceMap,
|
||||||
|
) -> io::Result<()> {
|
||||||
|
handshake.write_all(BARRIER_RELEASE)?;
|
||||||
|
handshake.flush()?;
|
||||||
|
let mut marker = [0_u8; 1];
|
||||||
|
handshake.read_exact(&mut marker)?;
|
||||||
|
if marker != *b"u" {
|
||||||
|
return Err(io::Error::other(
|
||||||
|
"mapped child sent an invalid user-namespace marker",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let pid = process.identity.host_pid().get();
|
||||||
|
run_mapping_helper("/usr/bin/newuidmap", &map.uid_helper_arguments(pid))?;
|
||||||
|
run_mapping_helper("/usr/bin/newgidmap", &map.gid_helper_arguments(pid))?;
|
||||||
|
map.verify_process(pid)?;
|
||||||
|
handshake.write_all(b"m")?;
|
||||||
|
handshake.flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::disallowed_methods, clippy::disallowed_types)]
|
||||||
|
fn run_mapping_helper(program: &str, arguments: &[String]) -> io::Result<()> {
|
||||||
|
let mut command = std::process::Command::new(program);
|
||||||
|
command
|
||||||
|
.args(arguments)
|
||||||
|
.stdin(Stdio::null())
|
||||||
|
.stdout(Stdio::null())
|
||||||
|
.stderr(Stdio::piped());
|
||||||
|
let mut child = command.spawn()?;
|
||||||
|
let process = match acquire_spawned_child(child.id()) {
|
||||||
|
Ok(process) => process,
|
||||||
|
Err(error) => {
|
||||||
|
let _ = child.kill();
|
||||||
|
let _ = child.wait();
|
||||||
|
return Err(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let status =
|
||||||
|
rustix::process::waitid(WaitId::PidFd(process.pidfd.as_fd()), WaitIdOptions::EXITED)?
|
||||||
|
.ok_or_else(|| io::Error::other("ID-map helper exited without status"))?;
|
||||||
|
let mut detail = String::new();
|
||||||
|
if let Some(stderr) = child.stderr.take() {
|
||||||
|
stderr.take(4096).read_to_string(&mut detail)?;
|
||||||
|
}
|
||||||
|
drop(child);
|
||||||
|
drop(process);
|
||||||
|
if status.exit_status() == Some(0) && status.terminating_signal().is_none() {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(io::Error::other(format!(
|
||||||
|
"{program} failed with exit {:?} and signal {:?}: {}",
|
||||||
|
status.exit_status(),
|
||||||
|
status.terminating_signal(),
|
||||||
|
detail.trim()
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::disallowed_methods, clippy::disallowed_types)]
|
||||||
|
fn fail_running_mapped<T>(
|
||||||
|
process: RetainedProcess,
|
||||||
|
child: std::process::Child,
|
||||||
|
error: io::Error,
|
||||||
|
) -> io::Result<T> {
|
||||||
|
let _ = process.send_signal(Signal::KILL);
|
||||||
|
rustix::process::waitid(WaitId::PidFd(process.pidfd.as_fd()), WaitIdOptions::EXITED)?;
|
||||||
|
drop(child);
|
||||||
|
drop(process);
|
||||||
|
Err(error)
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(clippy::disallowed_methods, clippy::disallowed_types)]
|
#[allow(clippy::disallowed_methods, clippy::disallowed_types)]
|
||||||
fn fail_running_captured<T>(
|
fn fail_running_captured<T>(
|
||||||
process: RetainedProcess,
|
process: RetainedProcess,
|
||||||
|
|
@ -476,12 +698,57 @@ pub fn dispatch_private_process_role(arguments: &[OsString]) -> Option<io::Resul
|
||||||
Some(BarrierWait::from_stdin().and_then(BarrierWait::wait))
|
Some(BarrierWait::from_stdin().and_then(BarrierWait::wait))
|
||||||
}
|
}
|
||||||
Some(WRAPPER_ROLE) => Some(run_wrapper_role(arguments[2..].to_vec())),
|
Some(WRAPPER_ROLE) => Some(run_wrapper_role(arguments[2..].to_vec())),
|
||||||
|
Some(MAPPED_WRAPPER_ROLE) => Some(run_mapped_wrapper_role(arguments[2..].to_vec())),
|
||||||
_ => Some(Err(io::Error::other(
|
_ => Some(Err(io::Error::other(
|
||||||
"unknown or malformed private process role",
|
"unknown or malformed private process role",
|
||||||
))),
|
))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(deprecated)]
|
||||||
|
fn run_mapped_wrapper_role(arguments: Vec<OsString>) -> io::Result<()> {
|
||||||
|
let (expected_parent, target) = parse_wrapper_arguments(arguments)?;
|
||||||
|
let mut release = [0_u8; BARRIER_RELEASE.len()];
|
||||||
|
std::io::stdin().read_exact(&mut release)?;
|
||||||
|
if release != *BARRIER_RELEASE {
|
||||||
|
return Err(io::Error::other(
|
||||||
|
"mapped wrapper launch release is malformed",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
match target.parent_edge {
|
||||||
|
ParentEdge::ParentDeathKill => {
|
||||||
|
arm_parent_death_and_recheck(Signal::KILL, expected_parent)?;
|
||||||
|
}
|
||||||
|
ParentEdge::LifetimePipe => {
|
||||||
|
require_lifetime_pipe_and_recheck(expected_parent)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rustix::thread::unshare(rustix::thread::UnshareFlags::NEWUSER)?;
|
||||||
|
let stdin = std::io::stdin();
|
||||||
|
rustix::io::write(&stdin, b"u")?;
|
||||||
|
let mut marker = [0_u8; 1];
|
||||||
|
if rustix::io::read(&stdin, &mut marker)? != 1 || marker != *b"m" {
|
||||||
|
return Err(io::Error::other(
|
||||||
|
"mapped wrapper did not receive a valid map release",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
rustix::io::fcntl_setfd(&stdin, rustix::io::FdFlags::CLOEXEC)?;
|
||||||
|
rustix::thread::set_thread_res_gid(
|
||||||
|
rustix::process::Gid::ROOT,
|
||||||
|
rustix::process::Gid::ROOT,
|
||||||
|
rustix::process::Gid::ROOT,
|
||||||
|
)?;
|
||||||
|
rustix::thread::set_thread_res_uid(
|
||||||
|
rustix::process::Uid::ROOT,
|
||||||
|
rustix::process::Uid::ROOT,
|
||||||
|
rustix::process::Uid::ROOT,
|
||||||
|
)?;
|
||||||
|
if let Some(stdin) = target.stdin.as_ref() {
|
||||||
|
rustix::stdio::dup2_stdin(stdin)?;
|
||||||
|
}
|
||||||
|
Err(exec_target(target))
|
||||||
|
}
|
||||||
|
|
||||||
fn run_wrapper_role(arguments: Vec<OsString>) -> io::Result<()> {
|
fn run_wrapper_role(arguments: Vec<OsString>) -> io::Result<()> {
|
||||||
let (expected_parent, target) = parse_wrapper_arguments(arguments)?;
|
let (expected_parent, target) = parse_wrapper_arguments(arguments)?;
|
||||||
BarrierWait::from_stdin()?.wait()?;
|
BarrierWait::from_stdin()?.wait()?;
|
||||||
|
|
|
||||||
315
crates/lazaret-linux/src/user_namespace.rs
Normal file
315
crates/lazaret-linux/src/user_namespace.rs
Normal file
|
|
@ -0,0 +1,315 @@
|
||||||
|
use std::collections::BTreeSet;
|
||||||
|
use std::fmt::Write as _;
|
||||||
|
use std::io;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use rustix::process::{getgid, getuid};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
pub const REQUIRED_SUBORDINATE_IDS: u32 = 65_536;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||||
|
pub struct UserNamespaceIdRange {
|
||||||
|
pub container_id: u32,
|
||||||
|
pub parent_id: u32,
|
||||||
|
pub size: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||||
|
pub struct UserNamespaceMap {
|
||||||
|
pub uid: Vec<UserNamespaceIdRange>,
|
||||||
|
pub gid: Vec<UserNamespaceIdRange>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UserNamespaceMap {
|
||||||
|
/// Resolves the invoking identity and the lowest sufficient configured subordinate ranges.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Refuses missing, malformed, overlapping, or insufficient `/etc/subuid` and `/etc/subgid`
|
||||||
|
/// allocations.
|
||||||
|
pub fn resolve_current() -> io::Result<Self> {
|
||||||
|
let uid = getuid().as_raw();
|
||||||
|
let gid = getgid().as_raw();
|
||||||
|
let names = account_names(uid)?;
|
||||||
|
let subordinate_user_start = select_subordinate_range(Path::new("/etc/subuid"), &names)?;
|
||||||
|
let subordinate_group_start = select_subordinate_range(Path::new("/etc/subgid"), &names)?;
|
||||||
|
let map = Self {
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
map.validate()?;
|
||||||
|
Ok(map)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # Errors
|
||||||
|
/// Refuses empty, zero-sized, overflowing, overlapping, or rootless-incompatible maps.
|
||||||
|
pub fn validate(&self) -> io::Result<()> {
|
||||||
|
validate_ranges("UID", &self.uid, getuid().as_raw())?;
|
||||||
|
validate_ranges("GID", &self.gid, getgid().as_raw())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn uid_helper_arguments(&self, pid: u32) -> Vec<String> {
|
||||||
|
helper_arguments(pid, &self.uid)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn gid_helper_arguments(&self, pid: u32) -> Vec<String> {
|
||||||
|
helper_arguments(pid, &self.gid)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn uid_proc_text(&self) -> String {
|
||||||
|
proc_text(&self.uid)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn gid_proc_text(&self) -> String {
|
||||||
|
proc_text(&self.gid)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # Errors
|
||||||
|
/// Refuses an unreadable process or a live UID/GID map that differs from this map.
|
||||||
|
pub fn verify_process(&self, pid: u32) -> io::Result<()> {
|
||||||
|
verify_proc_map(&format!("/proc/{pid}/uid_map"), &self.uid, "UID")?;
|
||||||
|
verify_proc_map(&format!("/proc/{pid}/gid_map"), &self.gid, "GID")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn account_names(uid: u32) -> io::Result<BTreeSet<String>> {
|
||||||
|
let mut names = BTreeSet::from([uid.to_string()]);
|
||||||
|
for line in std::fs::read_to_string("/etc/passwd")?.lines() {
|
||||||
|
let fields = line.split(':').collect::<Vec<_>>();
|
||||||
|
if fields.len() >= 3 && fields[2].parse::<u32>().ok() == Some(uid) {
|
||||||
|
names.insert(fields[0].to_owned());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(names)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn select_subordinate_range(path: &Path, names: &BTreeSet<String>) -> io::Result<u32> {
|
||||||
|
select_subordinate_range_contents(
|
||||||
|
&path.display().to_string(),
|
||||||
|
&std::fs::read_to_string(path)?,
|
||||||
|
names,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn select_subordinate_range_contents(
|
||||||
|
label: &str,
|
||||||
|
contents: &str,
|
||||||
|
names: &BTreeSet<String>,
|
||||||
|
) -> io::Result<u32> {
|
||||||
|
let mut candidates = Vec::new();
|
||||||
|
for (line_number, line) in contents.lines().enumerate() {
|
||||||
|
if line.trim().is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let fields = line.split(':').collect::<Vec<_>>();
|
||||||
|
if fields.len() != 3 {
|
||||||
|
return Err(io::Error::other(format!(
|
||||||
|
"{} row {} is malformed",
|
||||||
|
label,
|
||||||
|
line_number + 1
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
if !names.contains(fields[0]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let start = fields[1].parse::<u32>().map_err(|_| {
|
||||||
|
io::Error::other(format!(
|
||||||
|
"{} row {} has an invalid start",
|
||||||
|
label,
|
||||||
|
line_number + 1
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
let count = fields[2].parse::<u32>().map_err(|_| {
|
||||||
|
io::Error::other(format!(
|
||||||
|
"{} row {} has an invalid count",
|
||||||
|
label,
|
||||||
|
line_number + 1
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
if count >= REQUIRED_SUBORDINATE_IDS
|
||||||
|
&& start.checked_add(REQUIRED_SUBORDINATE_IDS - 1).is_some()
|
||||||
|
{
|
||||||
|
candidates.push(start);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
candidates.into_iter().min().ok_or_else(|| {
|
||||||
|
io::Error::other(format!(
|
||||||
|
"{label} has no contiguous {REQUIRED_SUBORDINATE_IDS}-ID allocation for the invoking user"
|
||||||
|
))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_ranges(
|
||||||
|
name: &str,
|
||||||
|
ranges: &[UserNamespaceIdRange],
|
||||||
|
invoking_id: u32,
|
||||||
|
) -> io::Result<()> {
|
||||||
|
if ranges.len() != 2
|
||||||
|
|| ranges[0]
|
||||||
|
!= (UserNamespaceIdRange {
|
||||||
|
container_id: 0,
|
||||||
|
parent_id: invoking_id,
|
||||||
|
size: 1,
|
||||||
|
})
|
||||||
|
|| ranges[1].container_id != 1
|
||||||
|
|| ranges[1].size != REQUIRED_SUBORDINATE_IDS
|
||||||
|
{
|
||||||
|
return Err(io::Error::other(format!(
|
||||||
|
"{name} map does not match the canonical rootless shape"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
for range in ranges {
|
||||||
|
range
|
||||||
|
.container_id
|
||||||
|
.checked_add(range.size - 1)
|
||||||
|
.and_then(|_| range.parent_id.checked_add(range.size - 1))
|
||||||
|
.ok_or_else(|| io::Error::other(format!("{name} map overflows")))?;
|
||||||
|
}
|
||||||
|
let subordinate = ranges[1];
|
||||||
|
if invoking_id >= subordinate.parent_id
|
||||||
|
&& invoking_id - subordinate.parent_id < subordinate.size
|
||||||
|
{
|
||||||
|
return Err(io::Error::other(format!(
|
||||||
|
"{name} subordinate range contains the invoking identity"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn helper_arguments(pid: u32, ranges: &[UserNamespaceIdRange]) -> Vec<String> {
|
||||||
|
std::iter::once(pid.to_string())
|
||||||
|
.chain(ranges.iter().flat_map(|range| {
|
||||||
|
[
|
||||||
|
range.container_id.to_string(),
|
||||||
|
range.parent_id.to_string(),
|
||||||
|
range.size.to_string(),
|
||||||
|
]
|
||||||
|
}))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn proc_text(ranges: &[UserNamespaceIdRange]) -> String {
|
||||||
|
ranges.iter().fold(String::new(), |mut text, range| {
|
||||||
|
writeln!(
|
||||||
|
text,
|
||||||
|
"{} {} {}",
|
||||||
|
range.container_id, range.parent_id, range.size
|
||||||
|
)
|
||||||
|
.expect("writing to String cannot fail");
|
||||||
|
text
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn verify_proc_map(path: &str, expected: &[UserNamespaceIdRange], name: &str) -> io::Result<()> {
|
||||||
|
let observed = std::fs::read_to_string(path)?
|
||||||
|
.lines()
|
||||||
|
.map(|line| {
|
||||||
|
let fields = line.split_whitespace().collect::<Vec<_>>();
|
||||||
|
if fields.len() != 3 {
|
||||||
|
return Err(io::Error::other(format!("{name} map row is malformed")));
|
||||||
|
}
|
||||||
|
Ok(UserNamespaceIdRange {
|
||||||
|
container_id: fields[0]
|
||||||
|
.parse()
|
||||||
|
.map_err(|_| io::Error::other(format!("{name} container ID is invalid")))?,
|
||||||
|
parent_id: fields[1]
|
||||||
|
.parse()
|
||||||
|
.map_err(|_| io::Error::other(format!("{name} parent ID is invalid")))?,
|
||||||
|
size: fields[2]
|
||||||
|
.parse()
|
||||||
|
.map_err(|_| io::Error::other(format!("{name} range size is invalid")))?,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<io::Result<Vec<_>>>()?;
|
||||||
|
if observed != expected {
|
||||||
|
return Err(io::Error::other(format!(
|
||||||
|
"live {name} map differs from the canonical map"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn helper_arguments_preserve_canonical_range_order() {
|
||||||
|
let map = UserNamespaceMap {
|
||||||
|
uid: vec![
|
||||||
|
UserNamespaceIdRange {
|
||||||
|
container_id: 0,
|
||||||
|
parent_id: 1000,
|
||||||
|
size: 1,
|
||||||
|
},
|
||||||
|
UserNamespaceIdRange {
|
||||||
|
container_id: 1,
|
||||||
|
parent_id: 524_288,
|
||||||
|
size: 65_536,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
gid: Vec::new(),
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
map.uid_helper_arguments(42),
|
||||||
|
["42", "0", "1000", "1", "1", "524288", "65536"]
|
||||||
|
);
|
||||||
|
assert_eq!(map.uid_proc_text(), "0 1000 1\n1 524288 65536\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn subordinate_selection_uses_the_lowest_complete_allocation() {
|
||||||
|
let names = BTreeSet::from(["1000".to_owned(), "developer".to_owned()]);
|
||||||
|
assert_eq!(
|
||||||
|
select_subordinate_range_contents(
|
||||||
|
"/etc/subuid",
|
||||||
|
"other:100000:65536\ndeveloper:700000:131072\n1000:524288:65536\n",
|
||||||
|
&names,
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
524_288
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn subordinate_selection_refuses_partial_and_overflowing_allocations() {
|
||||||
|
let names = BTreeSet::from(["developer".to_owned()]);
|
||||||
|
let error = select_subordinate_range_contents(
|
||||||
|
"/etc/subgid",
|
||||||
|
"developer:524288:65535\ndeveloper:4294967295:65536\n",
|
||||||
|
&names,
|
||||||
|
)
|
||||||
|
.unwrap_err();
|
||||||
|
assert!(
|
||||||
|
error
|
||||||
|
.to_string()
|
||||||
|
.contains("no contiguous 65536-ID allocation")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue