use lazaret_core::identity::{ GenerationId, MountIdentity, NamespaceIdentity, PidfsProcessIdentityV1, }; use lazaret_core::records::{ Participant, ParticipantOwnership, ParticipantRole, PersistedHostProcessOwnership, }; use lazaret_linux::process::{PidfsRecoveryMatch, match_recovery_process}; use crate::state_anchor::RuntimeOwnersAbsent; #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub struct RuntimeRecoveryOwnership { pub host_coordinator: Option, pub runtime_supervisor: Option, pub attached_runsc: Option, pub connectivity_helper: Option, pub state_anchor_daemon: Option, pub git_routing_daemon: Option, pub state_anchor_user_namespace: Option, pub state_anchor_mount_namespace: Option, pub state_anchor_mount: Option, } /// Extracts one generation's exact process and private-mount recovery authority. /// /// # Errors /// Refuses unequal copies of the constitutive HC or RS identities, and incomplete State Anchor /// namespace/mount evidence. Staged identities remain cleanup authority. pub fn runtime_recovery_ownership( participants: &[Participant], generation_id: GenerationId, ) -> Result { let mut result = RuntimeRecoveryOwnership::default(); for participant in participants .iter() .filter(|participant| participant.generation_id() == generation_id) { let Some(ownership) = participant.ownership() else { continue; }; match ownership { ParticipantOwnership::Sandbox(sandbox) => { merge_process( &mut result.host_coordinator, &sandbox.host_coordinator, "Sandbox HC identity mismatch", )?; merge_process( &mut result.runtime_supervisor, &sandbox.runtime_supervisor, "Sandbox RS identity mismatch", )?; merge_process( &mut result.attached_runsc, &sandbox.attached_runsc, "attached runsc identity mismatch", )?; } ParticipantOwnership::Network(network) => merge_process( &mut result.runtime_supervisor, &network.runtime_supervisor, "Network RS identity mismatch", )?, ParticipantOwnership::Helper(helper) => { merge_process( &mut result.connectivity_helper, &helper.connectivity_helper, "connectivity-helper identity mismatch", )?; merge_process( &mut result.runtime_supervisor, &helper.runtime_supervisor, "Helper RS identity mismatch", )?; } ParticipantOwnership::StateAnchor(anchor) => { merge_process( &mut result.state_anchor_daemon, &anchor.daemon, "State Anchor daemon identity mismatch", )?; let evidence = (anchor.user_namespace, anchor.mount_namespace, anchor.mount); if evidence == (None, None, None) { continue; } let (Some(user), Some(mount_namespace), Some(mount)) = evidence else { return Err("State Anchor recovery evidence is incomplete"); }; merge_value( &mut result.state_anchor_user_namespace, user, "State Anchor user namespace mismatch", )?; merge_value( &mut result.state_anchor_mount_namespace, mount_namespace, "State Anchor mount namespace mismatch", )?; merge_value( &mut result.state_anchor_mount, mount, "State Anchor mount identity mismatch", )?; } ParticipantOwnership::GitRouting(routing) => merge_process( &mut result.git_routing_daemon, &routing.daemon, "Git-routing daemon identity mismatch", )?, ParticipantOwnership::PreparedLower(_) | ParticipantOwnership::Publication(_) => {} } } Ok(result) } fn merge_process( target: &mut Option, observed: &PersistedHostProcessOwnership, mismatch: &'static str, ) -> Result<(), &'static str> { let Ok(observed) = observed.authoritative_pidfs() else { return Ok(()); }; merge_value(target, *observed, mismatch) } fn merge_value( target: &mut Option, observed: T, mismatch: &'static str, ) -> Result<(), &'static str> { match target { None => *target = Some(observed), Some(current) if *current == observed => {} Some(_) => return Err(mismatch), } Ok(()) } #[must_use] pub fn recovery_role_order() -> [ParticipantRole; 7] { [ ParticipantRole::Publication, ParticipantRole::Sandbox, ParticipantRole::Helper, ParticipantRole::Network, ParticipantRole::GitRouting, ParticipantRole::StateAnchor, ParticipantRole::PreparedLower, ] } /// Proves the constitutive RS and attached runsc are absent before private-mount reconciliation. /// /// # Errors /// Refuses a live or stopped exact authority and every unreadable or ambiguous observation. pub fn prove_runtime_owners_absent( generation_id: GenerationId, ownership: &RuntimeRecoveryOwnership, ) -> Result { for identity in [ownership.attached_runsc, ownership.runtime_supervisor] .into_iter() .flatten() { match match_recovery_process(identity) { Ok(PidfsRecoveryMatch::BootMismatch) => {} Ok(PidfsRecoveryMatch::Matched(process)) if process.has_exited()? => {} Ok(PidfsRecoveryMatch::Matched(_)) => { return Err(std::io::Error::other( "constitutive runtime authority remains present", )); } Err(error) if error.raw_os_error() == Some(rustix::io::Errno::SRCH.raw_os_error()) => {} Err(error) => return Err(error), } } Ok(RuntimeOwnersAbsent::for_generation(generation_id)) } #[cfg(test)] mod tests { use lazaret_core::identity::{ BootId, BundleDigest, ContainerId, DeviceId, HostPath, HostPidLocator, MountId, PidfsDeviceId, PidfsInodeId, PidfsProcessAuthorityV1, ProcessStartTimeTicks, }; use lazaret_core::records::{ HelperOwnership, ParticipantState, SandboxOwnership, StateAnchorOwnership, }; use super::*; fn process(value: u64) -> PidfsProcessIdentityV1 { PidfsProcessIdentityV1::new( PidfsProcessAuthorityV1 { boot_id: BootId::new([1; 16]), pidfs_st_dev: PidfsDeviceId::new(value), pidfs_st_ino: PidfsInodeId::new(value), }, HostPidLocator::new(u32::try_from(value).unwrap()), ProcessStartTimeTicks::new(value), ) } fn participant(role: ParticipantRole, ownership: ParticipantOwnership) -> Participant { Participant::snapshot( GenerationId::new(1), role, ParticipantState::Intent, Some(ownership), ) .unwrap() } #[test] fn inventory_accepts_staged_cleanup_authority_and_rejects_cross_role_rs_mismatch() { let rs = process(2); let sandbox = participant( ParticipantRole::Sandbox, ParticipantOwnership::Sandbox(SandboxOwnership { container_id: Some(ContainerId::new([3; 32])), runtime_root: Some(HostPath::new(b"/runtime".to_vec())), bundle_digest: Some(BundleDigest::new([4; 32])), host_coordinator: PersistedHostProcessOwnership::committed(process(1)), runtime_supervisor: PersistedHostProcessOwnership::staged(rs), attached_runsc: PersistedHostProcessOwnership::staged(process(3)), }), ); let helper = |runtime_supervisor| { participant( ParticipantRole::Helper, ParticipantOwnership::Helper(HelperOwnership { network_sidecar_id: None, connectivity_helper: PersistedHostProcessOwnership::staged(process(4)), runtime_supervisor: PersistedHostProcessOwnership::committed( runtime_supervisor, ), }), ) }; let inventory = runtime_recovery_ownership(&[sandbox.clone(), helper(rs)], GenerationId::new(1)) .unwrap(); assert_eq!(inventory.runtime_supervisor, Some(rs)); assert_eq!(inventory.attached_runsc, Some(process(3))); assert_eq!(inventory.connectivity_helper, Some(process(4))); assert!( runtime_recovery_ownership(&[sandbox, helper(process(99))], GenerationId::new(1),) .is_err() ); } #[test] fn inventory_requires_complete_private_mount_evidence() { let incomplete = participant( ParticipantRole::StateAnchor, ParticipantOwnership::StateAnchor(StateAnchorOwnership { lower_artifact_id: None, upper_path: Some(HostPath::new(b"upper".to_vec())), work_path: None, merged_path: None, daemon: PersistedHostProcessOwnership::staged(process(5)), user_namespace: Some(NamespaceIdentity { device: DeviceId::new(6), inode: lazaret_core::identity::InodeId::new(7), }), mount_namespace: None, mount: None, }), ); assert!(runtime_recovery_ownership(&[incomplete], GenerationId::new(1)).is_err()); let complete = participant( ParticipantRole::StateAnchor, ParticipantOwnership::StateAnchor(StateAnchorOwnership { lower_artifact_id: None, upper_path: Some(HostPath::new(b"upper".to_vec())), work_path: None, merged_path: None, daemon: PersistedHostProcessOwnership::staged(process(5)), user_namespace: Some(NamespaceIdentity { device: DeviceId::new(6), inode: lazaret_core::identity::InodeId::new(7), }), mount_namespace: Some(NamespaceIdentity { device: DeviceId::new(8), inode: lazaret_core::identity::InodeId::new(9), }), mount: Some(MountIdentity { mount_id: MountId::new(10), device: DeviceId::new(11), inode: lazaret_core::identity::InodeId::new(12), }), }), ); assert!(runtime_recovery_ownership(&[complete], GenerationId::new(1)).is_ok()); } }