lazaret/crates/lazaret-linux/src/process/tests.rs

378 lines
12 KiB
Rust

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);
}