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
4 changed files with 1424 additions and 172 deletions
|
|
@ -16,10 +16,13 @@ use lazaret_core::records::{
|
|||
use lazaret_host_coordinator::cold_start::{ColdStart, ColdStartInputs};
|
||||
use lazaret_host_coordinator::domain_store::HostCoordinatorDomainStore;
|
||||
use lazaret_linux::filesystem::{namespace_identity, try_exclusive_flock};
|
||||
use lazaret_linux::user_namespace::{
|
||||
REQUIRED_SUBORDINATE_IDS, UserNamespaceIdRange, UserNamespaceMap,
|
||||
};
|
||||
use lazaret_runtime_supervisor::network::NetworkProbeRequest;
|
||||
use lazaret_runtime_supervisor::sandbox::{RootProcess, default_sandbox_layout};
|
||||
use lazaret_storage_catalog::Catalog;
|
||||
use rustix::process::getuid;
|
||||
use rustix::process::{getgid, getuid};
|
||||
|
||||
#[test]
|
||||
fn validated_inputs_stage_one_complete_plan_and_drop_exact_runtime_artifacts() {
|
||||
|
|
@ -217,6 +220,7 @@ impl ColdStartFixture {
|
|||
invoking_uid: getuid().as_raw(),
|
||||
escalation_client,
|
||||
sandbox_init: escalation_client,
|
||||
user_namespace_map: test_user_namespace_map(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -239,6 +243,39 @@ impl ColdStartFixture {
|
|||
}
|
||||
}
|
||||
|
||||
fn test_user_namespace_map() -> UserNamespaceMap {
|
||||
let uid = getuid().as_raw();
|
||||
let gid = getgid().as_raw();
|
||||
let subordinate_user_start = if uid < 131_072 { 131_072 } else { 1 };
|
||||
let subordinate_group_start = if gid < 131_072 { 131_072 } else { 1 };
|
||||
UserNamespaceMap {
|
||||
uid: vec![
|
||||
UserNamespaceIdRange {
|
||||
container_id: 0,
|
||||
parent_id: uid,
|
||||
size: 1,
|
||||
},
|
||||
UserNamespaceIdRange {
|
||||
container_id: 1,
|
||||
parent_id: subordinate_user_start,
|
||||
size: REQUIRED_SUBORDINATE_IDS,
|
||||
},
|
||||
],
|
||||
gid: vec![
|
||||
UserNamespaceIdRange {
|
||||
container_id: 0,
|
||||
parent_id: gid,
|
||||
size: 1,
|
||||
},
|
||||
UserNamespaceIdRange {
|
||||
container_id: 1,
|
||||
parent_id: subordinate_group_start,
|
||||
size: REQUIRED_SUBORDINATE_IDS,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_private_artifacts(bundle: &Path, control: &Path) {
|
||||
let config = bundle.join("config.json");
|
||||
let escalation_client = control.join("bin/lazaret-escalate");
|
||||
|
|
|
|||
178
crates/lazaret-host-coordinator/tests/prepared-worker.rs
Normal file
178
crates/lazaret-host-coordinator/tests/prepared-worker.rs
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
use std::fs::File;
|
||||
use std::io::Cursor;
|
||||
use std::os::unix::ffi::OsStringExt;
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::ExitCode;
|
||||
|
||||
use lazaret_core::identity::PrepareOperationId;
|
||||
use lazaret_host_coordinator::prepared_worker::prepare_request_in_user_namespace;
|
||||
use lazaret_linux::user_namespace::UserNamespaceMap;
|
||||
use lazaret_prepared_lower::{IdMap, IdMapRange, PrepareRequest};
|
||||
use serde_json::json;
|
||||
use sha2::{Digest, Sha256};
|
||||
use tar::{Builder, EntryType, Header};
|
||||
|
||||
fn main() -> ExitCode {
|
||||
let arguments = std::env::args_os().skip(1).collect::<Vec<_>>();
|
||||
if let Some(outcome) = lazaret_host_coordinator::private_roles::REGISTRY.dispatch(&arguments) {
|
||||
return outcome.result.map_or_else(
|
||||
|error| {
|
||||
eprintln!("private role {} failed: {error}", outcome.name);
|
||||
ExitCode::FAILURE
|
||||
},
|
||||
|()| ExitCode::SUCCESS,
|
||||
);
|
||||
}
|
||||
run().map_or_else(
|
||||
|error| {
|
||||
eprintln!("prepared worker fixture failed: {error}");
|
||||
ExitCode::FAILURE
|
||||
},
|
||||
|()| ExitCode::SUCCESS,
|
||||
)
|
||||
}
|
||||
|
||||
fn run() -> std::io::Result<()> {
|
||||
let tree = tempfile::tempdir()?;
|
||||
let storage = tree.path().join("storage");
|
||||
std::fs::create_dir(&storage)?;
|
||||
let (layout, manifest) = create_layout(tree.path())?;
|
||||
let map = UserNamespaceMap::resolve_current()?;
|
||||
let artifact = prepare_request_in_user_namespace(
|
||||
&File::open(&storage)?,
|
||||
&storage,
|
||||
&File::open(&layout)?,
|
||||
PrepareRequest {
|
||||
operation_id: PrepareOperationId::new([7; 32]),
|
||||
layout_path: layout,
|
||||
source_identity: b"oci-v1\0registry.invalid\0owner-fixture".to_vec(),
|
||||
selected_manifest_digest: manifest,
|
||||
id_map: prepared_id_map(&map),
|
||||
destinations: Vec::new(),
|
||||
},
|
||||
&map,
|
||||
)?;
|
||||
let artifact_root =
|
||||
storage
|
||||
.join("prepared-lower")
|
||||
.join(PathBuf::from(std::ffi::OsString::from_vec(
|
||||
artifact.path.as_bytes().to_vec(),
|
||||
)));
|
||||
let metadata = std::fs::metadata(artifact_root.join("gid-42"))?;
|
||||
let expected_gid = map.gid[1]
|
||||
.parent_id
|
||||
.checked_add(41)
|
||||
.ok_or_else(|| std::io::Error::other("expected GID overflows"))?;
|
||||
if metadata.uid() != map.uid[0].parent_id || metadata.gid() != expected_gid {
|
||||
return Err(std::io::Error::other(format!(
|
||||
"mapped owner differs: observed {}:{}, expected {}:{expected_gid}",
|
||||
metadata.uid(),
|
||||
metadata.gid(),
|
||||
map.uid[0].parent_id
|
||||
)));
|
||||
}
|
||||
println!("PASS prepared worker materialized logical GID 42 through subordinate mapping");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_layout(root: &Path) -> std::io::Result<(PathBuf, [u8; 32])> {
|
||||
let path = root.join("layout");
|
||||
let blobs = path.join("blobs/sha256");
|
||||
std::fs::create_dir_all(&blobs)?;
|
||||
let layer = layer_with_gid_42()?;
|
||||
let layer_hash = Sha256::digest(&layer);
|
||||
std::fs::write(blobs.join(hex(&layer_hash)), &layer)?;
|
||||
let config = serde_json::to_vec(&json!({
|
||||
"architecture": "amd64",
|
||||
"os": "linux",
|
||||
"rootfs": {
|
||||
"type": "layers",
|
||||
"diff_ids": [format!("sha256:{}", hex(&layer_hash))]
|
||||
}
|
||||
}))?;
|
||||
let config_hash = Sha256::digest(&config);
|
||||
std::fs::write(blobs.join(hex(&config_hash)), &config)?;
|
||||
let manifest = serde_json::to_vec(&json!({
|
||||
"schemaVersion": 2,
|
||||
"config": {
|
||||
"mediaType": "application/vnd.oci.image.config.v1+json",
|
||||
"digest": format!("sha256:{}", hex(&config_hash)),
|
||||
"size": config.len()
|
||||
},
|
||||
"layers": [{
|
||||
"mediaType": "application/vnd.oci.image.layer.v1.tar",
|
||||
"digest": format!("sha256:{}", hex(&layer_hash)),
|
||||
"size": layer.len()
|
||||
}]
|
||||
}))?;
|
||||
let manifest_hash: [u8; 32] = Sha256::digest(&manifest).into();
|
||||
std::fs::write(blobs.join(hex(&manifest_hash)), &manifest)?;
|
||||
std::fs::write(
|
||||
path.join("index.json"),
|
||||
serde_json::to_vec(&json!({
|
||||
"schemaVersion": 2,
|
||||
"manifests": [{
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"digest": format!("sha256:{}", hex(&manifest_hash)),
|
||||
"size": manifest.len()
|
||||
}]
|
||||
}))?,
|
||||
)?;
|
||||
std::fs::write(
|
||||
path.join("oci-layout"),
|
||||
b"{\"imageLayoutVersion\":\"1.0.0\"}\n",
|
||||
)?;
|
||||
Ok((path, manifest_hash))
|
||||
}
|
||||
|
||||
fn layer_with_gid_42() -> std::io::Result<Vec<u8>> {
|
||||
let mut bytes = Vec::new();
|
||||
{
|
||||
let mut builder = Builder::new(&mut bytes);
|
||||
let mut header = Header::new_gnu();
|
||||
header.set_path("gid-42")?;
|
||||
header.set_entry_type(EntryType::Regular);
|
||||
header.set_size(5);
|
||||
header.set_uid(0);
|
||||
header.set_gid(42);
|
||||
header.set_mode(0o644);
|
||||
header.set_mtime(1_600_000_123);
|
||||
header.set_cksum();
|
||||
builder.append(&header, Cursor::new(b"probe"))?;
|
||||
builder.finish()?;
|
||||
}
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
fn prepared_id_map(map: &UserNamespaceMap) -> IdMap {
|
||||
IdMap {
|
||||
uid: map
|
||||
.uid
|
||||
.iter()
|
||||
.map(|range| IdMapRange {
|
||||
container_id: range.container_id,
|
||||
namespace_id: range.parent_id,
|
||||
size: range.size,
|
||||
})
|
||||
.collect(),
|
||||
gid: map
|
||||
.gid
|
||||
.iter()
|
||||
.map(|range| IdMapRange {
|
||||
container_id: range.container_id,
|
||||
namespace_id: range.parent_id,
|
||||
size: range.size,
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn hex(bytes: &[u8]) -> String {
|
||||
use std::fmt::Write as _;
|
||||
|
||||
bytes.iter().fold(String::new(), |mut output, byte| {
|
||||
write!(output, "{byte:02x}").expect("writing to String cannot fail");
|
||||
output
|
||||
})
|
||||
}
|
||||
|
|
@ -39,6 +39,7 @@ use lazaret_linux::process::{
|
|||
ProcessOwnershipContext, RetainedProcess, TargetCommand, child_broker,
|
||||
current_process_identity, read_start_time,
|
||||
};
|
||||
use lazaret_linux::user_namespace::UserNamespaceMap;
|
||||
use lazaret_runtime_supervisor::network::{DnsProbeTarget, NetworkProbeRequest, TcpProbeTarget};
|
||||
use lazaret_runtime_supervisor::sandbox::{
|
||||
NETWORK_NAMESPACE_PATH, RootProcess, RunscCommand, SandboxBuildRequest, SandboxBundle,
|
||||
|
|
|
|||
|
|
@ -8,9 +8,10 @@ use super::{
|
|||
RootProcess, RunscCommand, RuntimeNetworkLaunch, RuntimeSupervisorLaunch,
|
||||
SANDBOX_NETWORK_PROBE, SandboxBuildRequest, SandboxBundle, SandboxCoordinator, SandboxId,
|
||||
SocketAddr, SocketAddrExt, TcpListener, TcpProbeTarget, TempDir, UnixListener,
|
||||
build_sandbox_bundle, current_mount_identity, current_process_identity, default_sandbox_layout,
|
||||
derive_ipv4_policy, failure, fs, hex, host_path, load_trusted_configuration, lock_file,
|
||||
stage_benchmark_rootfs_and_control, stage_rootfs_and_control, try_exclusive_flock,
|
||||
UserNamespaceMap, build_sandbox_bundle, current_mount_identity, current_process_identity,
|
||||
default_sandbox_layout, derive_ipv4_policy, failure, fs, hex, host_path,
|
||||
load_trusted_configuration, lock_file, stage_benchmark_rootfs_and_control,
|
||||
stage_rootfs_and_control, try_exclusive_flock,
|
||||
};
|
||||
|
||||
pub(super) struct Fixture {
|
||||
|
|
@ -270,7 +271,8 @@ impl Fixture {
|
|||
slot: lazaret_core::records::HostProcessOwnershipSlot::SandboxRuntimeSupervisor,
|
||||
},
|
||||
command,
|
||||
)?;
|
||||
)?
|
||||
.with_user_namespace_map(UserNamespaceMap::resolve_current().map_err(failure)?)?;
|
||||
AwaitingAttachedRunsc::launch(&SandboxCoordinator::new(&self.store), &launch)
|
||||
.map_err(failure)
|
||||
}
|
||||
|
|
@ -416,7 +418,8 @@ impl NetworkFixture {
|
|||
configuration_fingerprint: self.configuration.configuration_fingerprint(),
|
||||
network_sidecar_id: NetworkSidecarId::new([14; 32]),
|
||||
},
|
||||
)?;
|
||||
)?
|
||||
.with_user_namespace_map(UserNamespaceMap::resolve_current().map_err(failure)?)?;
|
||||
let sandbox = SandboxCoordinator::new(&self.store);
|
||||
let network = NetworkCoordinator::new(&self.store);
|
||||
let mut pending = AwaitingAttachedRunsc::launch_networked(&sandbox, &network, &launch)
|
||||
|
|
|
|||
Loading…
Reference in a new issue