use super::confined_io::{ config_home, confined_existing, data_home, ensure_owned_directory, expand_host_path, hex_lower, host_path, io_error, is_strict_descendant, paths_intersect, read_stable_configuration, require_clear_of_protected, require_pairwise_disjoint, require_pairwise_non_ancestral, require_strict_child_of_one, resolve_environment_references, resolve_environment_values, resolve_network, validate_configuration_stat, validate_executable, validate_host_base, validate_persistent_destinations, }; use super::validation::{ validate_profile_name, validate_sandbox_path, validate_structural_configuration, validate_text, }; use super::{ ConfigurationError, ConfigurationErrorKind, ConfigurationFingerprintRecordV1, ConfigurationLoadRequest, ConfinedPathPlan, ConfinedPathType, MAX_CONFIGURATION_BYTES, OFlags, OsStrExt, PathBuf, ResolvedAutoApproval, ResolvedAutoApprovalRule, ResolvedDomainConfiguration, ResolvedVolume, ResolvedVolumeAuthority, TrustedHostConfigurationV1, VolumeKind, derive_configuration_fingerprint, derive_domain_id, }; /// Parses the closed trusted-host configuration document without resolving host authority. /// /// # Errors /// Unknown, duplicate, aliased, malformed, and unknown-version input is rejected. pub fn parse_trusted_configuration( bytes: &[u8], ) -> Result { if bytes.len() > MAX_CONFIGURATION_BYTES { return Err(ConfigurationError::new( ConfigurationErrorKind::InvalidConfigurationSource, "configuration exceeds 1,048,576 bytes", )); } let text = std::str::from_utf8(bytes).map_err(|_| { ConfigurationError::new( ConfigurationErrorKind::InvalidToml, "configuration is not UTF-8", ) })?; let configuration: TrustedHostConfigurationV1 = toml::from_str(text).map_err(|error| { ConfigurationError::new(ConfigurationErrorKind::InvalidToml, error.to_string()) })?; validate_structural_configuration(&configuration)?; Ok(configuration) } /// Resolves one selected profile into move-owned host authority. /// /// # Errors /// Loading fails atomically when source stability, strict syntax, confinement, environment, /// network, volume, executable, or fingerprint validation fails. #[allow(clippy::too_many_lines)] pub fn load_trusted_configuration( request: ConfigurationLoadRequest, ) -> Result { validate_host_base(&request.home, "HOME")?; validate_host_base(&request.xdg_runtime_dir, "XDG_RUNTIME_DIR")?; validate_profile_name(&request.selected_profile)?; let configuration_path = if let Some(explicit) = &request.explicit_configuration_path { let declaration = explicit.as_os_str().to_str().ok_or_else(|| { ConfigurationError::new( ConfigurationErrorKind::InvalidHostInput, "configuration path is not UTF-8", ) })?; expand_host_path(declaration, &request.home, None)? } else { config_home(&request).join("lazaret/config.toml") }; if request.explicit_configuration_path.is_none() { validate_host_base(&configuration_path, "configuration path")?; } let configuration_canonical = std::fs::canonicalize(&configuration_path).map_err(io_error)?; let configuration_source = ConfinedPathPlan::for_canonical_path( &configuration_canonical, ConfinedPathType::RegularFile, )?; let configuration_bytes = read_stable_configuration(&configuration_source)?; let configuration = parse_trusted_configuration(&configuration_bytes)?; let selected = configuration .profiles() .get(&request.selected_profile) .ok_or_else(|| { ConfigurationError::new( ConfigurationErrorKind::UnknownProfile, request.selected_profile.clone(), ) })?; let workspace_canonical = std::fs::canonicalize(&request.workspace).map_err(io_error)?; let workspace = confined_existing(&workspace_canonical, ConfinedPathType::Directory, false)?; let workspace_bytes = workspace_canonical.as_os_str().as_bytes(); let domain_id = derive_domain_id(&request.selected_profile, workspace_bytes)?; let data_root_declaration = configuration.storage().data_root.as_deref(); let storage_path = match data_root_declaration { Some(declaration) => expand_host_path(declaration, &request.home, None)?, None => data_home(&request).join("lazaret"), }; let runtime_path = request.xdg_runtime_dir.join("lazaret"); let invoking_uid = rustix::process::getuid().as_raw(); let storage_canonical = ensure_owned_directory(&storage_path, invoking_uid)?; let runtime_canonical = ensure_owned_directory(&runtime_path, invoking_uid)?; let profile_path = storage_canonical .join("profiles") .join(&request.selected_profile); let profile_canonical = ensure_owned_directory(&profile_path, invoking_uid)?; let domain_hex = hex_lower(domain_id.as_bytes()); let persistent_domain_canonical = ensure_owned_directory( &storage_canonical.join("domains").join(&domain_hex), invoking_uid, )?; let runtime_domain_canonical = ensure_owned_directory( &runtime_canonical.join("domains").join(&domain_hex), invoking_uid, )?; let storage_root = confined_existing(&storage_canonical, ConfinedPathType::Directory, false)?; let runtime_root = confined_existing(&runtime_canonical, ConfinedPathType::Directory, false)?; let profile_root = confined_existing(&profile_canonical, ConfinedPathType::Directory, false)?; let persistent_domain_root = confined_existing( &persistent_domain_canonical, ConfinedPathType::Directory, false, )?; let runtime_domain_root = confined_existing( &runtime_domain_canonical, ConfinedPathType::Directory, false, )?; let core_paths = [ configuration_canonical.as_path(), workspace_canonical.as_path(), storage_canonical.as_path(), runtime_canonical.as_path(), ]; let core_authorities = [ &configuration_source, &workspace, &storage_root, &runtime_root, ]; require_pairwise_disjoint( &core_paths, &core_authorities, ConfigurationErrorKind::PathConflict, )?; let persistent_dirs = validate_persistent_destinations( &selected.persistent_dirs, request.sandbox_layout.protected_destinations(), request.sandbox_layout.nested_transient_overrides(), )?; let (effective_allow_prefixes, effective_deny_prefixes) = resolve_network(&configuration, selected)?; let environment_references = resolve_environment_references(selected)?; let mut volumes = Vec::with_capacity(selected.volumes.len()); let mut resolved_volume_records = Vec::with_capacity(selected.volumes.len()); let mut volume_paths: Vec = Vec::with_capacity(selected.volumes.len()); for volume in &selected.volumes { validate_sandbox_path(&volume.container)?; require_strict_child_of_one(&volume.container, &persistent_dirs)?; require_clear_of_protected( &volume.container, request.sandbox_layout.protected_destinations(), )?; let under_profile = volume.host.starts_with("{profile_dir}/"); if volume.create && (volume.kind != VolumeKind::Directory || !under_profile) { return Err(ConfigurationError::new( ConfigurationErrorKind::InvalidVolume, "create=true requires a directory strictly beneath {profile_dir}", )); } let source_path = expand_host_path(&volume.host, &request.home, Some(&profile_canonical))?; let canonical = if volume.create { ensure_owned_directory(&source_path, invoking_uid)? } else { std::fs::canonicalize(&source_path).map_err(io_error)? }; if !volume.create && canonical.as_os_str().as_bytes() != source_path.as_os_str().as_bytes() { return Err(ConfigurationError::new( ConfigurationErrorKind::InvalidVolume, "volume source contains a symlink", )); } let expected = match volume.kind { VolumeKind::File => ConfinedPathType::RegularFile, VolumeKind::Directory => ConfinedPathType::Directory, }; let source_plan = confined_existing(&canonical, expected, false)?; for (forbidden, forbidden_authority) in core_paths.iter().zip(core_authorities) { if paths_intersect(&canonical, forbidden) { let allowed_profile_child = under_profile && *forbidden == storage_canonical.as_path() && is_strict_descendant(&canonical, &profile_canonical); if !allowed_profile_child { return Err(ConfigurationError::new( ConfigurationErrorKind::PathConflict, "volume source intersects a protected host tree", )); } } if source_plan.aliases(forbidden_authority) { return Err(ConfigurationError::new( ConfigurationErrorKind::PathConflict, "volume source aliases a protected host authority", )); } } if volume_paths .iter() .any(|other| paths_intersect(&canonical, other)) || volumes .iter() .any(|other: &ResolvedVolumeAuthority| source_plan.aliases(&other.source_plan)) { return Err(ConfigurationError::new( ConfigurationErrorKind::PathConflict, "volume sources overlap", )); } let record = ResolvedVolume { canonical_host_source: host_path(&canonical), container_path: volume.container.clone(), kind: volume.kind, access: volume.access, create: volume.create, }; volume_paths.push(canonical); resolved_volume_records.push(record.clone()); volumes.push(ResolvedVolumeAuthority { record, source_plan, }); } require_pairwise_non_ancestral( selected .volumes .iter() .map(|volume| volume.container.as_str()), ConfigurationErrorKind::InvalidVolume, )?; let mut auto_approval_rules = Vec::new(); let mut resolved_auto_approval_records = Vec::new(); if !selected.escalation.enabled && !selected.escalation.auto_approve.is_empty() { return Err(ConfigurationError::new( ConfigurationErrorKind::InvalidAutoApproval, "disabled escalation profile contains auto-approval rules", )); } for rule in &selected.escalation.auto_approve { validate_text(&rule.executable, "auto-approval executable")?; for argument in &rule.args { validate_text(argument, "auto-approval argument")?; } if rule.allow_extra_args && rule.args.is_empty() { return Err(ConfigurationError::new( ConfigurationErrorKind::InvalidAutoApproval, "allow_extra_args requires a nonempty configured prefix", )); } let executable_path = expand_host_path(&rule.executable, &request.home, None)?; let canonical = std::fs::canonicalize(&executable_path).map_err(io_error)?; if core_paths .iter() .copied() .chain(volume_paths.iter().map(PathBuf::as_path)) .chain([ profile_canonical.as_path(), persistent_domain_canonical.as_path(), runtime_domain_canonical.as_path(), ]) .any(|forbidden| paths_intersect(&canonical, forbidden)) { return Err(ConfigurationError::new( ConfigurationErrorKind::InvalidAutoApproval, "auto-approval executable intersects a writable or control tree", )); } let executable_plan = confined_existing(&canonical, ConfinedPathType::RegularFile, true)?; if core_authorities .iter() .copied() .chain([&profile_root, &persistent_domain_root, &runtime_domain_root]) .any(|forbidden| executable_plan.aliases(forbidden)) || volumes .iter() .any(|volume| executable_plan.aliases(&volume.source_plan)) { return Err(ConfigurationError::new( ConfigurationErrorKind::InvalidAutoApproval, "auto-approval executable aliases a writable or control authority", )); } let executable = executable_plan.reopen_exact(OFlags::RDONLY | OFlags::NONBLOCK)?; validate_executable(&executable, invoking_uid)?; let record = ResolvedAutoApprovalRule { canonical_executable: host_path(&canonical), args: rule.args.clone(), allow_extra_args: rule.allow_extra_args, }; if resolved_auto_approval_records.contains(&record) { return Err(ConfigurationError::new( ConfigurationErrorKind::InvalidAutoApproval, "duplicate normalized auto-approval rule", )); } resolved_auto_approval_records.push(record.clone()); auto_approval_rules.push(ResolvedAutoApproval { record, executable_plan, invoking_uid, }); } let configuration_descriptor = configuration_source.open_authority()?; validate_configuration_stat(&rustix::fs::fstat(&configuration_descriptor).map_err(io_error)?)?; for authority in [ &workspace, &storage_root, &runtime_root, &profile_root, &persistent_domain_root, &runtime_domain_root, ] { drop(authority.open_authority()?); } for volume in &volumes { drop(volume.open_source()?); } for rule in &auto_approval_rules { let executable = rule .executable_plan .reopen_exact(OFlags::RDONLY | OFlags::NONBLOCK)?; validate_executable(&executable, invoking_uid)?; } let fingerprint_record = ConfigurationFingerprintRecordV1 { selected_profile: request.selected_profile.clone(), storage_root: host_path(&storage_canonical), runtime_root: host_path(&runtime_canonical), image: selected.image.clone(), isolate_gitignored: selected.isolate_gitignored, persistent_dirs, environment_references, volumes: resolved_volume_records, effective_allow_prefixes: effective_allow_prefixes .iter() .map(|prefix| (prefix.address, prefix.prefix_length)) .collect(), effective_deny_prefixes: effective_deny_prefixes .iter() .map(|prefix| (prefix.address, prefix.prefix_length)) .collect(), escalation_enabled: selected.escalation.enabled, auto_approval_rules: resolved_auto_approval_records, }; let configuration_fingerprint = derive_configuration_fingerprint(&fingerprint_record)?; let ephemeral_environment = resolve_environment_values( &fingerprint_record.environment_references, &request.environment, )?; Ok(ResolvedDomainConfiguration { selected_profile: request.selected_profile, domain_id, configuration_fingerprint, fingerprint_record, effective_allow_prefixes, effective_deny_prefixes, ephemeral_environment, configuration_source, workspace, storage_root, runtime_root, profile_root, persistent_domain_root, runtime_domain_root, volumes, auto_approval_rules, }) }