use std::cell::Cell; use std::os::fd::{AsFd, AsRawFd}; use std::rc::Rc; use super::recovery::{ authority_from_pidfd_with, cleanup_sigchld_probe_with, match_recovery_process_with, }; use super::*; #[test] fn broker_is_process_global() { assert!(std::ptr::eq(child_broker(), child_broker())); } #[test] fn parent_edges_require_their_exact_stdio_policy() { assert!( require_parent_edge_standard_io( ParentEdge::LifetimePipe, ChildStandardIoPolicy::PrivateControlWithLifetimePipe, ) .is_ok() ); assert!( require_parent_edge_standard_io(ParentEdge::LifetimePipe, ChildStandardIoPolicy::Silenced,) .is_err() ); assert!( require_parent_edge_standard_io( ParentEdge::ParentDeathKill, ChildStandardIoPolicy::Silenced, ) .is_ok() ); assert!( require_parent_edge_standard_io( ParentEdge::ParentDeathKill, ChildStandardIoPolicy::PrivateControlWithLifetimePipe, ) .is_err() ); } #[test] fn barrier_reader_is_inheritable_and_writer_is_close_on_exec() { let (release, wait) = launch_barrier().unwrap(); let reader_flags = rustix::io::fcntl_getfd(wait.0.as_fd()).unwrap(); let writer_flags = rustix::io::fcntl_getfd(release.0.as_fd()).unwrap(); assert!(!reader_flags.contains(FdFlags::CLOEXEC)); assert!(writer_flags.contains(FdFlags::CLOEXEC)); assert_ne!(wait.0.as_raw_fd(), release.0.as_raw_fd()); } #[test] fn bootstrap_child_retains_exact_pipe_owner_and_pidfd_reap() { let (reader, writer) = pipe_with(PipeFlags::CLOEXEC).unwrap(); let target = TargetCommand::new("/bin/sh") .argument("-c") .argument("printf LR; read -r _ || exit 0") .with_lifetime_pipe_parent_edge(); let mut child = child_broker() .spawn_bootstrap(&target, File::from(reader)) .unwrap(); assert_eq!(child.bootstrap_status().unwrap(), b'L'); assert_eq!(child.bootstrap_status().unwrap(), b'R'); assert!(!child.has_exited().unwrap()); drop(writer); let status = child.reap().unwrap(); assert_eq!(status.exit_code, Some(0)); assert_eq!(status.terminating_signal, None); let (reader, _writer) = pipe_with(PipeFlags::CLOEXEC).unwrap(); assert!( child_broker() .spawn_bootstrap(&TargetCommand::new("/bin/true"), File::from(reader)) .is_err() ); } #[test] fn boot_id_requires_exact_canonical_uuid_syntax() { parse_boot_id("00112233-4455-6677-8899-aabbccddeeff\n").unwrap(); for invalid in [ "00112233445566778899aabbccddeeff", "00112233_4455-6677-8899-aabbccddeeff", "00112233-4455-6677-8899-aabbccddeeff-extra", "00112233-4455-6677-8899-aabbccddeefg", "00112233-4455-6677-8899-aabbccddeeff\n\n", ] { assert!(parse_boot_id(invalid).is_err(), "accepted {invalid}"); } } #[test] fn recovery_checks_boot_before_pid_lookup() { let identity = PidfsProcessIdentityV1::new( PidfsProcessAuthorityV1 { boot_id: BootId::new([1; 16]), pidfs_st_dev: PidfsDeviceId::new(1), pidfs_st_ino: PidfsInodeId::new(1), }, HostPidLocator::new(1), ProcessStartTimeTicks::new(1), ); let opened = Cell::new(false); let result = match_recovery_process_with( identity, || Ok(BootId::new([2; 16])), |_| { opened.set(true); unreachable!() }, |_, _| unreachable!(), |_| unreachable!(), ); assert!(matches!(result, Ok(PidfsRecoveryMatch::BootMismatch))); assert!(!opened.get()); } #[test] fn recovery_refuses_each_post_open_mismatch_without_returning_authority() { let authority = PidfsProcessAuthorityV1 { boot_id: BootId::new([1; 16]), pidfs_st_dev: PidfsDeviceId::new(7), pidfs_st_ino: PidfsInodeId::new(9), }; let identity = PidfsProcessIdentityV1::new( authority, HostPidLocator::new(41), ProcessStartTimeTicks::new(43), ); for (observed, start) in [ ( PidfsProcessAuthorityV1 { pidfs_st_dev: PidfsDeviceId::new(8), ..authority }, 43, ), ( PidfsProcessAuthorityV1 { pidfs_st_ino: PidfsInodeId::new(10), ..authority }, 43, ), (authority, 44), ] { let events = Rc::new(std::cell::RefCell::new(Vec::new())); let open_events = Rc::clone(&events); let inspect_events = Rc::clone(&events); let start_events = Rc::clone(&events); let result = match_recovery_process_with( identity, || Ok(authority.boot_id), move |pid| { open_events.borrow_mut().push(("open", u64::from(pid))); Ok(File::open("/dev/null").unwrap().into()) }, move |fd, _| { inspect_events .borrow_mut() .push(("inspect", u64::try_from(fd.as_raw_fd()).unwrap())); Ok(observed) }, move |pid| { start_events.borrow_mut().push(("start", u64::from(pid))); Ok(start) }, ); assert!(result.is_err()); assert_eq!(events.borrow()[0], ("open", 41)); assert_eq!(events.borrow()[1].0, "inspect"); if observed == authority { assert_eq!(events.borrow()[2], ("start", 41)); } else { assert_eq!(events.borrow().len(), 2); } } for failure in ["open", "inspect", "start"] { let result = match_recovery_process_with( identity, || Ok(authority.boot_id), |_| { if failure == "open" { Err(io::Error::from_raw_os_error(3)) } else { Ok(File::open("/dev/null").unwrap().into()) } }, |_, _| { if failure == "inspect" { Err(io::Error::other("injected descriptor inspection error")) } else { Ok(authority) } }, |_| { if failure == "start" { Err(io::Error::other("injected start observation error")) } else { Ok(43) } }, ); assert!(result.is_err(), "accepted injected {failure} failure"); } } #[test] fn recovery_normalizes_vanished_proc_evidence_to_esrch() { let authority = PidfsProcessAuthorityV1 { boot_id: BootId::new([1; 16]), pidfs_st_dev: PidfsDeviceId::new(7), pidfs_st_ino: PidfsInodeId::new(9), }; let identity = PidfsProcessIdentityV1::new( authority, HostPidLocator::new(41), ProcessStartTimeTicks::new(43), ); for missing in ["open", "start"] { let result = match_recovery_process_with( identity, || Ok(authority.boot_id), |_| { if missing == "open" { Err(io::Error::from_raw_os_error( rustix::io::Errno::NOENT.raw_os_error(), )) } else { Ok(File::open("/dev/null").unwrap().into()) } }, |_, _| Ok(authority), |_| { if missing == "start" { Err(io::Error::from_raw_os_error( rustix::io::Errno::NOENT.raw_os_error(), )) } else { Ok(43) } }, ) .unwrap_err(); assert_eq!( result.raw_os_error(), Some(rustix::io::Errno::SRCH.raw_os_error()) ); } } #[test] fn recovery_action_reconciles_boot_without_process_action() { let mut old_boot = *read_boot_id().unwrap().as_bytes(); old_boot[0] ^= u8::MAX; let identity = PidfsProcessIdentityV1::new( PidfsProcessAuthorityV1 { boot_id: BootId::new(old_boot), pidfs_st_dev: PidfsDeviceId::new(1), pidfs_st_ino: PidfsInodeId::new(1), }, HostPidLocator::new(u32::MAX), ProcessStartTimeTicks::new(u64::MAX), ); let action_called = Cell::new(false); let result = recover_process_action( identity, || Ok("reconciled"), |_| { action_called.set(true); Ok("acted") }, ) .unwrap(); assert_eq!(result, RecoveryActionResult::BootReconciled("reconciled")); assert!(!action_called.get()); } #[test] fn pidfs_magic_precedes_same_descriptor_stat_and_errors_refuse() { let file = File::open("/dev/null").unwrap(); let raw = file.as_raw_fd(); let events = Rc::new(std::cell::RefCell::new(Vec::new())); let filesystem_events = Rc::clone(&events); let stat_events = Rc::clone(&events); let authority = authority_from_pidfd_with( file.as_fd(), BootId::new([1; 16]), move |fd| { filesystem_events .borrow_mut() .push(("fstatfs", fd.as_raw_fd())); Ok(PID_FS_MAGIC) }, move |fd| { stat_events.borrow_mut().push(("fstat", fd.as_raw_fd())); Ok((7, 9)) }, ) .unwrap(); assert_eq!(&*events.borrow(), &[("fstatfs", raw), ("fstat", raw)]); assert_eq!(authority.pidfs_st_dev.get(), 7); assert_eq!(authority.pidfs_st_ino.get(), 9); for filesystem_error in [false, true] { let stat_called = Cell::new(false); let result = authority_from_pidfd_with( file.as_fd(), BootId::new([1; 16]), |_| { if filesystem_error { Err(io::Error::other("injected fstatfs error")) } else { Ok(0) } }, |_| { stat_called.set(true); Ok((1, 1)) }, ); assert!(result.is_err()); assert!(!stat_called.get()); } assert!( authority_from_pidfd_with( file.as_fd(), BootId::new([1; 16]), |_| Ok(PID_FS_MAGIC), |_| Err(io::Error::other("injected fstat error")), ) .is_err() ); } #[test] fn sigchld_probe_epipe_terminates_and_reaps_exact_child() { let events = Rc::new(std::cell::RefCell::new(Vec::new())); let terminate_events = Rc::clone(&events); let wait_events = Rc::clone(&events); let result = cleanup_sigchld_probe_with( io::Error::new(io::ErrorKind::BrokenPipe, "probe died before release"), move || { terminate_events.borrow_mut().push("terminate-pidfd"); Err(io::Error::from_raw_os_error(3)) }, move || { wait_events.borrow_mut().push("wait-pidfd"); Ok(()) }, ); assert_eq!(result.unwrap_err().kind(), io::ErrorKind::BrokenPipe); assert_eq!(&*events.borrow(), &["terminate-pidfd", "wait-pidfd"]); } #[test] fn sigchld_probe_failed_wait_retries_cleanup_until_not_waitable() { let waits = Cell::new(0_u8); let result = cleanup_sigchld_probe_with( io::Error::other("injected semantic wait failure"), || Ok(()), || { waits.set(waits.get() + 1); match waits.get() { 1 => Err(io::Error::from(io::ErrorKind::Interrupted)), _ => Err(io::Error::from_raw_os_error( rustix::io::Errno::CHILD.raw_os_error(), )), } }, ); assert_eq!(result.unwrap_err().kind(), io::ErrorKind::Other); assert_eq!(waits.get(), 2); }