378 lines
12 KiB
Rust
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);
|
|
}
|