use std::{
env,
error::Error,
fs, io, mem,
path::PathBuf,
process::{Command, ExitStatus, Stdio},
ptr, thread,
time::{Duration, Instant},
};
use fork::{ChildEvent, ProcessGroupGuard, ProcessId, Signal};
#[path = "inherited_reaper.rs"]
mod inherited_reaper;
#[path = "waitid_fallback.rs"]
mod waitid_fallback;
const BOUND: Duration = Duration::from_secs(5);
const POLL: Duration = Duration::from_millis(5);
const CLEANUP: Duration = Duration::from_millis(50);
pub(super) fn run() -> Result<(), Box<dyn Error>> {
if let Ok(mode) = env::var("FORK_CLEANUP_MODE") {
fork::acquire_subreaper()?;
let _children = Children;
return match mode.as_str() {
"guard-ignore" | "guard-nocldwait" => stopped_auto_owner(&mode),
"spawn-ignore" | "spawn-nocldwait" => failed_auto_spawn(&mode),
"guard-deadline" => deadline_collection(),
"continue-handler" => inherited_continue_handler(),
"continue-denied" => unavailable_continue(),
"reaper-delivery" => inherited_reaper::run(false),
"reaper-loss" => inherited_reaper::run(true),
"waitid-daemon" | "waitid-failure" | "waitid-guard" => waitid_fallback::run(&mode),
_ => Err("unknown cleanup mode".into()),
};
}
fork::acquire_subreaper()?;
let directory =
Temporary(env::temp_dir().join(format!("fork-owner-cleanup-{}", std::process::id())));
fs::create_dir(&directory.0)?;
let _children = Children;
let source = directory.0.join("schedule.c");
let library = directory.0.join("schedule.so");
fs::write(&source, include_str!("schedule.c"))?;
if !run_command(
Command::new("cc")
.args(["-shared", "-fPIC", "-Wall", "-Werror"])
.arg(&source)
.args(["-ldl", "-o"])
.arg(&library),
)?
.success()
{
return Err("cleanup shim compilation failed".into());
}
for (mode, denial) in cases() {
if env::var("FORK_ONLY_CASE").is_ok_and(|selected| selected != mode)
|| env::var("FORK_ONLY_ERRNO")
.is_ok_and(|selected| denial.is_none_or(|errno| selected != errno.to_string()))
{
continue;
}
let case = denial.map_or_else(|| mode.to_owned(), |errno| format!("{mode}-{errno}"));
let log = directory.0.join(&case);
let pin_log = directory.0.join(format!("pins-{case}"));
let mut command = Command::new(env::current_exe()?);
command
.env("FORK_CLEANUP_MODE", mode)
.env(
"FORK_CLEANUP_RELEASE",
directory.0.join(format!("release-{case}")),
)
.env("LD_PRELOAD", &library)
.env("FORK_PIN_LOG", &pin_log)
.stderr(Stdio::from(fs::File::create(&log)?));
if let Some(errno) = denial {
command.env("FORK_WAITID_ERRNO", errno.to_string());
}
let status = run_command(&mut command)?;
let output = fs::read_to_string(&log)?;
let pins = fs::read_to_string(&pin_log).unwrap_or_default();
eprintln!("{pins}");
eprintln!("{case}: {status}: {output}");
if !status.success()
|| output.contains("UNSAFE")
|| pins.contains("UNSAFE")
|| (mode.starts_with("reaper-") && !pins.contains("PINNED"))
|| (mode == "guard-deadline" && !output.contains("STOLE"))
|| (mode == "waitid-guard" && !output.contains("CONSUMED"))
{
return Err(format!("cleanup regression failed: {case}").into());
}
}
Ok(())
}
fn cases() -> Vec<(&'static str, Option<libc::c_int>)> {
let mut cases: Vec<_> = [
"guard-ignore",
"guard-nocldwait",
"spawn-ignore",
"spawn-nocldwait",
"guard-deadline",
"continue-handler",
"continue-denied",
"reaper-delivery",
"reaper-loss",
]
.into_iter()
.map(|mode| (mode, None))
.collect();
for mode in ["waitid-daemon", "waitid-failure", "waitid-guard"] {
for errno in [
libc::ENOSYS,
libc::EPERM,
libc::EACCES,
libc::EINVAL,
libc::EIO,
] {
cases.push((mode, Some(errno)));
}
}
cases
}
fn stopped_auto_owner(mode: &str) -> Result<(), Box<dyn Error>> {
let guard = ProcessGroupGuard::new()?;
let helper = guard.guard_process();
stop(helper)?;
let action = install_auto_policy(mode)?;
let result = guard.disarm(CLEANUP);
drop(action);
finish(helper)?;
assert!(matches!(result, Err(error) if error.kind() == io::ErrorKind::InvalidInput));
Ok(())
}
fn failed_auto_spawn(mode: &str) -> Result<(), Box<dyn Error>> {
let action = install_auto_policy(mode)?;
let result = fork::PreparedCommand::new("/bin/true")?.spawn(CLEANUP);
drop(action);
let error = result
.err()
.ok_or("stopped pre-exec child unexpectedly succeeded")?;
let child = error
.cleanup_pending()
.ok_or("failed spawn did not report its unkillable child")?;
finish(child)?;
assert!(
matches!(error, fork::SpawnError::TimedOut { .. }),
"{error:?}"
);
Ok(())
}
fn deadline_collection() -> Result<(), Box<dyn Error>> {
let guard = ProcessGroupGuard::new()?;
let helper = guard.guard_process();
stop(helper)?;
let result = guard.disarm(CLEANUP);
finish(helper)?;
assert!(result.is_err(), "lost helper ownership was hidden");
Ok(())
}
extern "C" fn exit_on_continue(_: libc::c_int) {
unsafe { libc::_exit(42) }
}
fn inherited_continue_handler() -> Result<(), Box<dyn Error>> {
let _action = install(libc::SIGCONT, exit_on_continue as *const () as usize, 0)?;
let guard = ProcessGroupGuard::new()?;
guard.disarm(BOUND)?;
Ok(())
}
fn unavailable_continue() -> Result<(), Box<dyn Error>> {
ProcessGroupGuard::new()?.disarm(BOUND)?;
Ok(())
}
fn stop(child: ProcessId) -> io::Result<()> {
fork::signal_process(child, Signal::STOP)?;
let deadline = Instant::now() + BOUND;
loop {
if matches!(
fork::wait_event_nohang(child)?,
Some(ChildEvent::Stopped { .. })
) {
return Ok(());
}
if Instant::now() >= deadline {
return Err(io::Error::new(
io::ErrorKind::TimedOut,
"helper did not stop",
));
}
thread::sleep(POLL);
}
}
fn finish(child: ProcessId) -> io::Result<()> {
match fork::wait_event_nohang(child) {
Ok(Some(event)) if event.is_terminal() => return Ok(()),
Err(error) if error.raw_os_error() == Some(libc::ECHILD) => return Ok(()),
Err(error) => return Err(error),
Ok(_) => {}
}
fork::signal_process(child, Signal::CONT)?;
let deadline = Instant::now() + BOUND;
loop {
match fork::wait_event_nohang(child) {
Ok(Some(event)) if event.is_terminal() => return Ok(()),
Err(error) if error.raw_os_error() == Some(libc::ECHILD) => return Ok(()),
Err(error) => return Err(error),
Ok(_) => {}
}
if Instant::now() >= deadline {
fork::signal_process(child, Signal::KILL)?;
fork::wait_event(child)?;
return Err(io::Error::new(
io::ErrorKind::TimedOut,
"fixture child did not finish",
));
}
thread::sleep(POLL);
}
}
struct Action {
signal: libc::c_int,
previous: libc::sigaction,
}
impl Drop for Action {
fn drop(&mut self) {
unsafe {
libc::sigaction(self.signal, &raw const self.previous, ptr::null_mut());
}
}
}
fn install_auto_policy(mode: &str) -> io::Result<Action> {
if mode.ends_with("ignore") {
install(libc::SIGCHLD, libc::SIG_IGN, 0)
} else {
install(libc::SIGCHLD, libc::SIG_DFL, libc::SA_NOCLDWAIT)
}
}
fn install(
signal: libc::c_int,
handler: libc::sighandler_t,
flags: libc::c_int,
) -> io::Result<Action> {
let mut action: libc::sigaction = unsafe { mem::zeroed() };
let mut previous: libc::sigaction = unsafe { mem::zeroed() };
action.sa_sigaction = handler;
action.sa_flags = flags;
unsafe {
libc::sigemptyset(&raw mut action.sa_mask);
if libc::sigaction(signal, &raw const action, &raw mut previous) != 0 {
return Err(io::Error::last_os_error());
}
}
Ok(Action { signal, previous })
}
fn run_command(command: &mut Command) -> io::Result<ExitStatus> {
let mut child = command.spawn()?;
let deadline = Instant::now() + Duration::from_secs(15);
loop {
if let Some(status) = child.try_wait()? {
return Ok(status);
}
if Instant::now() >= deadline {
child.kill()?;
child.wait()?;
return Err(io::Error::new(
io::ErrorKind::TimedOut,
"cleanup contract command timed out",
));
}
thread::sleep(POLL);
}
}
struct Children;
impl Drop for Children {
fn drop(&mut self) {
let deadline = Instant::now() + BOUND;
loop {
match fork::wait_any_event_nohang() {
Err(_) => return,
Ok(Some(_)) => continue,
Ok(None) => {}
}
if let Ok(raw) =
fs::read_to_string(format!("/proc/self/task/{}/children", std::process::id()))
{
for child in raw
.split_whitespace()
.filter_map(|s| s.parse::<i32>().ok())
.filter_map(ProcessId::new)
{
if matches!(fork::wait_event_nohang(child), Ok(None)) {
let _ = fork::signal_process(child, Signal::KILL);
}
}
}
if Instant::now() >= deadline {
return;
}
thread::sleep(POLL);
}
}
}
struct Temporary(PathBuf);
impl Drop for Temporary {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.0);
}
}