use std::ffi::{OsStr, OsString}; use std::fs::File; use std::io::{self, Read, Write}; use std::os::fd::{AsFd, AsRawFd, BorrowedFd, OwnedFd}; use std::process::Stdio; use std::sync::{Mutex, OnceLock}; use lazaret_core::identity::{ BootId, CoordinatorId, DomainId, GenerationId, HostPidLocator, PidfsDeviceId, PidfsInodeId, PidfsProcessAuthorityV1, PidfsProcessIdentityV1, ProcessStartTimeTicks, }; use lazaret_core::records::{ HostProcessOwnershipSlot, ProcessOwnershipCommitAck, StagedProcessOwnership, }; use rustix::event::{PollFd, PollFlags, Timespec, poll}; use rustix::fs::{OFlags, fcntl_getfl, fcntl_setfl, fstat, fstatfs}; use rustix::io::{FdFlags, fcntl_setfd}; use rustix::pipe::{PipeFlags, pipe_with}; use rustix::process::{Pid, PidfdFlags, Signal, WaitId, WaitIdOptions}; pub const PID_FS_MAGIC: i64 = 0x5049_4446; const BARRIER_RELEASE: &[u8; 32] = b"LAZARET-CAUSAL-BARRIER-RELEASE!!"; const PRIVATE_ROLE_ARGUMENT: &str = "--lazaret-private-process-role"; const WRAPPER_ROLE: &str = "launch-wrapper"; const SIGCHLD_PROBE_ROLE: &str = "sigchld-probe"; static CHILD_BROKER: OnceLock = OnceLock::new(); #[derive(Debug)] pub struct TargetCommand { program: OsString, arguments: Vec, parent_edge: ParentEdge, executable: Option, stdin: Option, } #[derive(Clone, Copy, Debug, Eq, PartialEq)] enum TargetStdinAccess { ReadOnly, WriteOnly, ReadWrite, } impl TargetStdinAccess { fn from_flags(flags: OFlags) -> io::Result { if flags.contains(OFlags::PATH) { return Err(io::Error::other( "target stdin descriptor has no read or write authority", )); } match flags & OFlags::ACCMODE { flags if flags == OFlags::RDONLY => Ok(Self::ReadOnly), flags if flags == OFlags::WRONLY => Ok(Self::WriteOnly), flags if flags == OFlags::RDWR => Ok(Self::ReadWrite), _ => Err(io::Error::other( "target stdin descriptor has an unsupported access mode", )), } } fn from_descriptor(descriptor: i32) -> io::Result { let metadata = std::fs::read_to_string(format!("/proc/self/fdinfo/{descriptor}"))?; let flags = metadata .lines() .find_map(|line| line.strip_prefix("flags:")) .map(str::trim) .ok_or_else(|| io::Error::other("target stdin descriptor flags are missing")) .and_then(|flags| { u32::from_str_radix(flags, 8) .map(OFlags::from_bits_retain) .map_err(|_| io::Error::other("target stdin descriptor flags are invalid")) })?; Self::from_flags(flags) } fn reopen(self, descriptor: i32) -> io::Result { let mut options = File::options(); match self { Self::ReadOnly => { options.read(true); } Self::WriteOnly => { options.write(true); } Self::ReadWrite => { options.read(true).write(true); } } options.open(format!("/proc/self/fd/{descriptor}")) } } #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum ParentEdge { ParentDeathKill, LifetimePipe, } impl TargetCommand { #[must_use] pub fn new(program: impl Into) -> Self { Self { program: program.into(), arguments: Vec::new(), parent_edge: ParentEdge::ParentDeathKill, executable: None, stdin: None, } } /// Constructs an argv-only target bound to the exact inherited executable descriptor. /// /// # Errors /// Returns an error when the descriptor cannot be made inheritable by the launch wrapper. pub fn from_executable( executable: OwnedFd, arguments: impl IntoIterator, ) -> io::Result { fcntl_setfd(executable.as_fd(), FdFlags::empty())?; Ok(Self { program: OsString::from(format!("/proc/self/fd/{}", executable.as_raw_fd())), arguments: arguments.into_iter().collect(), parent_edge: ParentEdge::ParentDeathKill, executable: Some(executable), stdin: None, }) } #[must_use] pub fn argument(mut self, argument: impl Into) -> Self { self.arguments.push(argument.into()); self } #[must_use] pub fn with_lifetime_pipe_parent_edge(mut self) -> Self { self.parent_edge = ParentEdge::LifetimePipe; self } #[must_use] pub fn with_parent_death_kill_edge(mut self) -> Self { self.parent_edge = ParentEdge::ParentDeathKill; self } /// Attaches the target's standard input after the private wrapper consumes its barrier. /// /// # Errors /// Returns an error when the descriptor cannot be inherited by the wrapper. pub fn with_target_stdin(mut self, stdin: File) -> io::Result { TargetStdinAccess::from_flags(fcntl_getfl(&stdin)?)?; fcntl_setfd(stdin.as_fd(), FdFlags::empty())?; self.stdin = Some(stdin); Ok(self) } #[must_use] pub const fn parent_edge(&self) -> ParentEdge { self.parent_edge } #[must_use] pub fn program(&self) -> &OsStr { &self.program } #[must_use] pub fn arguments(&self) -> &[OsString] { &self.arguments } #[allow(clippy::disallowed_types)] fn append_to(&self, command: &mut std::process::Command) { let _ = &self.executable; command .arg("--parent-action") .arg(match self.parent_edge { ParentEdge::ParentDeathKill => "kill", ParentEdge::LifetimePipe => "lifetime-pipe", }) .args( self.stdin .as_ref() .map(|stdin| { [ OsString::from("--target-stdin-fd"), OsString::from(stdin.as_raw_fd().to_string()), ] }) .into_iter() .flatten(), ) .arg("--") .arg(&self.program) .args(&self.arguments); } } pub struct ChildStandardIo { stdout: Option, stderr: Option, policy: ChildStandardIoPolicy, } #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum ChildStandardIoPolicy { Inherit, PrivateControlWithLifetimePipe, Silenced, Terminal, } impl ChildStandardIo { #[must_use] pub const fn inherit() -> Self { Self { stdout: None, stderr: None, policy: ChildStandardIoPolicy::Inherit, } } #[must_use] pub fn private_control_with_lifetime_pipe( control: impl Into, lifetime_pipe: impl Into, ) -> Self { Self { stdout: Some(control.into()), stderr: Some(lifetime_pipe.into()), policy: ChildStandardIoPolicy::PrivateControlWithLifetimePipe, } } #[must_use] pub fn silenced() -> Self { Self { stdout: Some(Stdio::null()), stderr: Some(Stdio::null()), policy: ChildStandardIoPolicy::Silenced, } } /// # Errors /// Returns an error when the output descriptor cannot be duplicated for merged terminal I/O. pub fn terminal(output: File) -> io::Result { let error = output.try_clone()?; Ok(Self { stdout: Some(Stdio::from(output)), stderr: Some(Stdio::from(error)), policy: ChildStandardIoPolicy::Terminal, }) } #[must_use] pub const fn policy(&self) -> ChildStandardIoPolicy { self.policy } } #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct ProcessOwnershipContext { pub domain_id: DomainId, pub generation_id: GenerationId, pub coordinator_id: CoordinatorId, pub slot: HostProcessOwnershipSlot, } pub struct AcknowledgedChildAcquisition<'target> { pub expected_parent: PidfsProcessIdentityV1, pub ownership: ProcessOwnershipContext, pub target: &'target TargetCommand, pub standard_io: ChildStandardIo, } #[derive(Debug)] pub struct RetainedProcess { pidfd: OwnedFd, identity: PidfsProcessIdentityV1, } #[derive(Debug)] pub enum PidfsRecoveryMatch { BootMismatch, Matched(RetainedProcess), } #[derive(Clone, Debug, Eq, PartialEq)] pub enum RecoveryActionResult { BootReconciled(T), ProcessAction(T), } #[derive(Debug)] pub struct CapturedProcess { process: RetainedProcess, stdout: File, stderr: File, } pub struct BootstrapChild { process: RetainedProcess, status: File, } impl BootstrapChild { /// Reads the private bootstrap acquisition status byte. /// /// # Errors /// Returns an error when the child closes or corrupts its status channel. pub fn acquisition_status(&mut self) -> io::Result { let mut status = [0_u8; 1]; self.status.read_exact(&mut status)?; Ok(status[0]) } /// # Errors /// Returns an error when polling the exact child pidfd fails. pub fn has_exited(&self) -> io::Result { self.process.has_exited() } /// Reaps the exact retained child through its pidfd. /// /// # Errors /// Returns an error when serialized exact reaping fails. pub fn reap(self) -> io::Result { child_broker().reap_status(self.process) } } impl CapturedProcess { #[must_use] pub const fn process(&self) -> &RetainedProcess { &self.process } pub fn streams(&mut self) -> (&mut File, &mut File) { (&mut self.stdout, &mut self.stderr) } #[must_use] pub fn into_process(self) -> RetainedProcess { self.process } } #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct ProcessExit { pub exit_code: Option, pub terminating_signal: Option, } impl RetainedProcess { #[must_use] pub const fn identity(&self) -> PidfsProcessIdentityV1 { self.identity } /// # Errors /// Returns an error when the exact retained-pidfd action fails. #[allow(clippy::disallowed_methods)] pub fn send_signal(&self, signal: Signal) -> io::Result<()> { rustix::process::pidfd_send_signal(self.pidfd.as_fd(), signal).map_err(Into::into) } /// # Errors /// Returns an error when polling the retained pidfd fails. pub fn has_exited(&self) -> io::Result { let mut descriptors = [PollFd::new(&self.pidfd, PollFlags::IN)]; poll(&mut descriptors, Some(&Timespec::default()))?; Ok(descriptors[0] .revents() .intersects(PollFlags::IN | PollFlags::HUP)) } } impl AsFd for RetainedProcess { fn as_fd(&self) -> BorrowedFd<'_> { self.pidfd.as_fd() } } #[derive(Debug)] pub struct ChildBroker { exclusion: Mutex<()>, } #[must_use] pub fn child_broker() -> &'static ChildBroker { CHILD_BROKER.get_or_init(|| ChildBroker { exclusion: Mutex::new(()), }) } fn require_parent_edge_standard_io( parent_edge: ParentEdge, standard_io: ChildStandardIoPolicy, ) -> io::Result<()> { match (parent_edge, standard_io) { (ParentEdge::LifetimePipe, ChildStandardIoPolicy::PrivateControlWithLifetimePipe) | ( ParentEdge::ParentDeathKill, ChildStandardIoPolicy::Inherit | ChildStandardIoPolicy::Silenced | ChildStandardIoPolicy::Terminal, ) => Ok(()), (ParentEdge::LifetimePipe, _) => Err(io::Error::other( "lifetime-pipe parent edge requires private control stdout and lifetime-pipe stderr", )), (ParentEdge::ParentDeathKill, ChildStandardIoPolicy::PrivateControlWithLifetimePipe) => { Err(io::Error::other( "parent-death edge cannot substitute a lifetime-pipe stdio policy", )) } } } fn require_captured_parent_edge(parent_edge: ParentEdge) -> io::Result<()> { if parent_edge == ParentEdge::ParentDeathKill { Ok(()) } else { Err(io::Error::other( "captured output requires the parent-death edge", )) } } include!("process/broker.rs"); include!("process/recovery.rs"); include!("process/launch.rs"); include!("process/tests.rs");