lazaret/crates/lazaret-host-coordinator/tests/cold_start.rs
ms2k 73e3e9fdee 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.
2026-07-19 19:51:47 +09:00

336 lines
13 KiB
Rust

use std::collections::BTreeMap;
use std::fs::{self, File, OpenOptions};
use std::os::unix::ffi::{OsStrExt, OsStringExt};
use std::os::unix::fs::{MetadataExt, PermissionsExt};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use lazaret_core::configuration::{ConfigurationLoadRequest, load_trusted_configuration};
use lazaret_core::identity::{
ContainerId, CoordinatorId, DeviceId, GenerationId, HostPath, IdMapDigest, InodeId, PolicyId,
PreparedArtifactId, SandboxId, SelectedManifestDigest, SourceIdentity,
};
use lazaret_core::records::{
ArtifactState, Domain, DurableDecision, Generation, IpAddress, PreparedArtifact,
};
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::{getgid, getuid};
#[test]
fn validated_inputs_stage_one_complete_plan_and_drop_exact_runtime_artifacts() {
let fixture = ColdStartFixture::new();
let staged = ColdStart::stage(fixture.inputs(&fixture.escalation_client)).unwrap();
let bundle = fixture.bundle();
let control = fixture.control();
let overlay = fixture.overlay();
assert_private_artifacts(&bundle, &control);
assert!(overlay.is_dir());
drop(staged);
assert!(!bundle.exists());
assert!(!control.exists());
assert!(!overlay.exists());
assert!(ColdStart::stage(fixture.inputs(&fixture.invalid_escalation_client)).is_err());
assert!(!bundle.exists());
assert!(!control.exists());
}
#[test]
fn non_git_workspace_stages_without_a_routing_runtime() {
let fixture = ColdStartFixture::new();
let staged = ColdStart::stage(fixture.inputs(&fixture.escalation_client)).unwrap();
assert!(!fixture.runtime_domain.join("git-routing").exists());
drop(staged);
}
#[test]
fn cleanup_leaves_a_replacement_directory_untouched() {
let fixture = ColdStartFixture::new();
let staged = ColdStart::stage(fixture.inputs(&fixture.escalation_client)).unwrap();
let bundle = fixture.bundle();
let moved = bundle.with_extension("moved");
fs::rename(&bundle, &moved).unwrap();
fs::create_dir(&bundle).unwrap();
fs::write(bundle.join("replacement"), b"trusted replacement").unwrap();
drop(staged);
assert_eq!(
fs::read(bundle.join("replacement")).unwrap(),
b"trusted replacement"
);
assert!(moved.is_dir());
}
#[test]
fn fixed_runtime_parents_refuse_symlinks_and_wrong_modes() {
let fixture = ColdStartFixture::new();
let bundles = fixture.runtime_domain.join("bundles");
fs::create_dir(&bundles).unwrap();
fs::set_permissions(&bundles, fs::Permissions::from_mode(0o755)).unwrap();
assert!(ColdStart::stage(fixture.inputs(&fixture.escalation_client)).is_err());
fs::remove_dir(&bundles).unwrap();
let controls = fixture.runtime_domain.join("sandbox-control");
let redirect = fixture.tree.path().join("redirect");
fs::create_dir(&redirect).unwrap();
std::os::unix::fs::symlink(&redirect, &controls).unwrap();
assert!(ColdStart::stage(fixture.inputs(&fixture.escalation_client)).is_err());
assert!(redirect.read_dir().unwrap().next().is_none());
}
#[test]
fn replaced_runtime_domain_authority_is_not_used_for_staging() {
let fixture = ColdStartFixture::new();
let moved = fixture.runtime_domain.with_extension("moved");
fs::rename(&fixture.runtime_domain, &moved).unwrap();
fs::create_dir(&fixture.runtime_domain).unwrap();
fs::set_permissions(&fixture.runtime_domain, fs::Permissions::from_mode(0o700)).unwrap();
assert!(ColdStart::stage(fixture.inputs(&fixture.escalation_client)).is_err());
assert!(fixture.runtime_domain.read_dir().unwrap().next().is_none());
}
struct ColdStartFixture {
tree: tempfile::TempDir,
configuration: lazaret_core::configuration::ResolvedDomainConfiguration,
runtime_domain: PathBuf,
catalog: Catalog,
store: Arc<HostCoordinatorDomainStore>,
artifact: PreparedArtifact,
artifact_authority: File,
generation: Generation,
container_id: ContainerId,
escalation_client: File,
invalid_escalation_client: File,
}
impl ColdStartFixture {
fn new() -> Self {
let tree = tempfile::tempdir().unwrap();
let configuration = configuration(tree.path());
let persistent_domain = host_path(
configuration
.persistent_domain_root()
.canonical_label()
.as_bytes(),
);
let runtime_domain = host_path(
configuration
.runtime_domain_root()
.canonical_label()
.as_bytes(),
);
let catalog = Catalog::new(
tree.path().join("catalog.redb"),
tree.path().join("catalog.lock"),
);
catalog.initialize().unwrap();
let domain = Domain {
domain_id: configuration.domain_id(),
profile_name: configuration.selected_profile().to_owned(),
canonical_workspace: configuration.workspace().canonical_label().clone(),
};
let lock = OpenOptions::new()
.read(true)
.write(true)
.create_new(true)
.open(persistent_domain.join("domain.lock"))
.unwrap();
let store = Arc::new(
HostCoordinatorDomainStore::open(
try_exclusive_flock(lock).unwrap(),
&persistent_domain.join("domain.redb"),
&domain,
)
.unwrap(),
);
let lower = tree.path().join("prepared-lower");
fs::create_dir(&lower).unwrap();
let metadata = lower.metadata().unwrap();
let artifact_authority = File::open(&lower).unwrap();
let artifact = PreparedArtifact {
prepared_artifact_id: PreparedArtifactId::new([8; 32]),
source_identity: SourceIdentity::new(b"synthetic-spike".to_vec()),
selected_manifest_digest: SelectedManifestDigest::new([9; 32]),
policy_id: PolicyId::new([10; 32]),
id_map_digest: IdMapDigest::new([11; 32]),
path: HostPath::new(lower.as_os_str().as_bytes().to_vec()),
device: DeviceId::new(metadata.dev()),
inode: InodeId::new(metadata.ino()),
state: ArtifactState::Available,
};
let generation = Generation {
domain_id: configuration.domain_id(),
generation_id: GenerationId::new(1),
configuration_fingerprint: configuration.configuration_fingerprint(),
coordinator_id: CoordinatorId::new([2; 32]),
network_sidecar_id: lazaret_core::identity::NetworkSidecarId::new([3; 32]),
sandbox_id: SandboxId::new([4; 32]),
durable_decision: DurableDecision::None,
};
Self {
tree,
configuration,
runtime_domain,
catalog,
store,
artifact,
artifact_authority,
generation,
container_id: ContainerId::new([5; 32]),
escalation_client: File::open("/bin/true").unwrap(),
invalid_escalation_client: File::open("/dev/null").unwrap(),
}
}
fn inputs<'a>(&'a self, escalation_client: &'a File) -> ColdStartInputs<'a> {
ColdStartInputs {
store: Arc::clone(&self.store),
catalog: &self.catalog,
configuration: &self.configuration,
generation: self.generation.clone(),
prepared_artifact: &self.artifact,
prepared_artifact_authority: &self.artifact_authority,
prepared_artifact_label: Path::new(
std::str::from_utf8(self.artifact.path.as_bytes()).unwrap(),
),
root_process: RootProcess::new("/bin/sh", ["-c".into(), "sleep infinity".into()])
.unwrap(),
resolver_addresses: vec![IpAddress::V4([10, 0, 2, 3])],
network_probe: NetworkProbeRequest {
domain_id: self.configuration.domain_id(),
generation_id: GenerationId::new(1),
enforcement_namespace: namespace_identity(Path::new("/proc/self/ns/net")).unwrap(),
denied: Vec::new(),
permitted: Vec::new(),
dns: Vec::new(),
},
reachability: lazaret_core::network::ReachabilityExpectation {
denied_destinations: Vec::new(),
permitted_destinations: Vec::new(),
dns_queries: Vec::new(),
},
container_id: self.container_id,
invoking_uid: getuid().as_raw(),
escalation_client,
sandbox_init: escalation_client,
user_namespace_map: test_user_namespace_map(),
}
}
fn bundle(&self) -> PathBuf {
self.runtime_domain
.join("bundles")
.join(hex(self.container_id.as_bytes()))
}
fn control(&self) -> PathBuf {
self.runtime_domain
.join("sandbox-control")
.join(hex(self.container_id.as_bytes()))
}
fn overlay(&self) -> PathBuf {
self.runtime_domain
.join("runsc-overlay")
.join(hex(self.container_id.as_bytes()))
}
}
fn 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");
assert!(config.is_file());
assert!(escalation_client.is_file());
assert!(control.join("credentials").is_dir());
assert_eq!(mode(bundle), 0o700);
assert_eq!(mode(control), 0o700);
assert_eq!(mode(&config), 0o600);
assert_eq!(mode(&escalation_client), 0o700);
}
fn mode(path: &Path) -> u32 {
path.metadata().unwrap().permissions().mode() & 0o777
}
fn configuration(root: &Path) -> lazaret_core::configuration::ResolvedDomainConfiguration {
let configuration_directory = root.join("configuration");
let workspace = root.join("workspace");
let runtime = root.join("runtime");
fs::create_dir(&configuration_directory).unwrap();
fs::create_dir(&workspace).unwrap();
fs::create_dir(&runtime).unwrap();
let configuration_path = configuration_directory.join("lazaret.toml");
fs::write(
&configuration_path,
format!(
"schema = \"lazaret-trusted-host-config-v1\"\n\n[storage]\ndata_root = \"{}\"\n\n[network]\ndefault_allow = [\"192.0.2.10/32\"]\n\n[profiles.default]\nimage = \"example.invalid/factory:v1\"\npersistent_dirs = []\n\n[profiles.default.readiness]\ndenied = [{{ address = \"10.20.30.40\", port = 443 }}]\npermitted = [{{ address = \"192.0.2.10\", port = 443 }}]\ndns = [{{ resolver = \"198.51.100.53\", query = \"example.invalid\" }}]\n",
root.join("data").display()
),
)
.unwrap();
load_trusted_configuration(ConfigurationLoadRequest {
explicit_configuration_path: Some(configuration_path),
selected_profile: "default".into(),
workspace,
home: root.join("home"),
xdg_config_home: None,
xdg_data_home: None,
xdg_runtime_dir: runtime,
environment: BTreeMap::new(),
sandbox_layout: default_sandbox_layout().unwrap(),
})
.unwrap()
}
fn host_path(bytes: &[u8]) -> PathBuf {
PathBuf::from(std::ffi::OsString::from_vec(bytes.to_vec()))
}
fn hex(bytes: &[u8]) -> String {
use std::fmt::Write as _;
bytes.iter().fold(String::new(), |mut output, byte| {
write!(output, "{byte:02x}").unwrap();
output
})
}