use std::{
io,
os::fd::{AsRawFd, OwnedFd, RawFd},
time::{Duration, Instant},
};
use crate::{
ProcessFork, ProcessGroupId, ProcessId, Signal, create_process_group,
descriptor::GuardPrepared, fork_process, signal_process, socket_pair_cloexec, wait_event,
wait_event_nohang,
};
const DISARM_RECORD: u8 = 0xa5;
const DROP_CLEANUP_TIMEOUT: Duration = Duration::from_secs(3);
const FAILURE_EXIT: libc::c_int = 70;
const INITIAL_POLL_INTERVAL: Duration = Duration::from_micros(50);
const MAX_POLL_INTERVAL: Duration = Duration::from_millis(5);
const YIELD_ATTEMPTS: u8 = 8;
#[must_use = "dropping an armed process-group guard triggers fail-closed cleanup"]
#[derive(Debug)]
pub struct ProcessGroupGuard {
anchor: Option<GuardHelper>,
group: ProcessGroupId,
guard: Option<GuardHelper>,
guard_process: ProcessId,
}
impl ProcessGroupGuard {
pub fn new() -> io::Result<Self> {
let anchor = GuardHelper::spawn_anchor()?;
let group = create_process_group(anchor.process)?;
let guard = GuardHelper::spawn_for_group(group)?;
let guard_process = guard.process;
Ok(Self {
anchor: Some(anchor),
group,
guard: Some(guard),
guard_process,
})
}
#[must_use]
pub const fn process_group(&self) -> ProcessGroupId {
self.group
}
#[must_use]
pub const fn guard_process(&self) -> ProcessId {
self.guard_process
}
pub fn activate(&mut self, timeout: Duration) -> io::Result<()> {
validate_timeout(timeout)?;
let Some(mut anchor) = self.anchor.take() else {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"process-group guard is already active",
));
};
anchor.disarm_and_reap(timeout)
}
pub fn disarm(mut self, timeout: Duration) -> io::Result<()> {
validate_timeout(timeout)?;
if let Some(mut anchor) = self.anchor.take() {
anchor.disarm_and_reap(timeout)?;
}
let Some(mut guard) = self.guard.take() else {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"process-group guard is already disarmed",
));
};
guard.disarm_and_reap(timeout)
}
}
impl Drop for ProcessGroupGuard {
fn drop(&mut self) {
drop(self.guard.take());
drop(self.anchor.take());
}
}
#[derive(Debug)]
struct GuardHelper {
owner: Option<OwnedFd>,
process: ProcessId,
}
impl GuardHelper {
fn spawn_anchor() -> io::Result<Self> {
let channel = socket_pair_cloexec()?;
let (reader, writer) = channel.into_parts();
let descriptors = GuardPrepared::new(&reader)?;
match fork_process()? {
ProcessFork::Parent(process) => {
drop(reader);
Ok(Self {
owner: Some(writer),
process,
})
}
ProcessFork::Child => {
descriptors.apply_in_child();
let group = child_create_process_group();
guard_loop(reader.as_raw_fd(), group);
}
}
}
fn spawn_for_group(group: ProcessGroupId) -> io::Result<Self> {
let channel = socket_pair_cloexec()?;
let (reader, writer) = channel.into_parts();
let descriptors = GuardPrepared::new(&reader)?;
match fork_process()? {
ProcessFork::Parent(process) => {
drop(reader);
Ok(Self {
owner: Some(writer),
process,
})
}
ProcessFork::Child => {
descriptors.apply_in_child();
guard_loop(reader.as_raw_fd(), group);
}
}
}
fn disarm_and_reap(&mut self, timeout: Duration) -> io::Result<()> {
let owner = self.owner.take().ok_or_else(|| {
io::Error::new(io::ErrorKind::InvalidInput, "guard is already disarmed")
})?;
let write_result = write_disarm(owner.as_raw_fd());
drop(owner);
if let Err(error) = write_result {
let _ = self.finish(timeout);
return Err(error);
}
self.finish(timeout)
}
fn finish(&self, timeout: Duration) -> io::Result<()> {
match wait_for_clean_exit(self.process, timeout) {
Ok(()) => Ok(()),
Err(HelperWaitFailure::StillLive(error)) => {
self.terminate_and_reap();
Err(error)
}
Err(HelperWaitFailure::ReapedOrUnknown(error)) => Err(error),
}
}
fn terminate_and_reap(&self) {
let _ = signal_process(self.process, Signal::KILL);
let _ = wait_event(self.process);
}
}
impl Drop for GuardHelper {
fn drop(&mut self) {
drop(self.owner.take());
let _ = self.finish(DROP_CLEANUP_TIMEOUT);
}
}
#[derive(Debug)]
enum HelperWaitFailure {
StillLive(io::Error),
ReapedOrUnknown(io::Error),
}
fn validate_timeout(timeout: Duration) -> io::Result<()> {
if timeout.is_zero() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"guard cleanup timeout must be greater than zero",
));
}
Instant::now().checked_add(timeout).map_or_else(
|| {
Err(io::Error::new(
io::ErrorKind::InvalidInput,
"guard cleanup timeout exceeds the monotonic clock range",
))
},
|_| Ok(()),
)
}
fn wait_for_clean_exit(process: ProcessId, timeout: Duration) -> Result<(), HelperWaitFailure> {
let deadline = Instant::now().checked_add(timeout).ok_or_else(|| {
HelperWaitFailure::ReapedOrUnknown(io::Error::new(
io::ErrorKind::InvalidInput,
"guard cleanup timeout exceeds the monotonic clock range",
))
})?;
let mut poll_interval = INITIAL_POLL_INTERVAL;
let mut yield_attempts = 0;
loop {
match wait_event_nohang(process) {
Ok(Some(crate::ChildEvent::Continued { .. }) | None) => {}
Ok(Some(event)) => {
return match event {
crate::ChildEvent::Exited { code: 0, .. } => Ok(()),
other if other.is_terminal() => {
Err(HelperWaitFailure::ReapedOrUnknown(io::Error::other(
format!("process-group helper terminated unexpectedly: {other:?}"),
)))
}
other => Err(HelperWaitFailure::StillLive(io::Error::other(format!(
"process-group helper entered an unexpected state: {other:?}"
)))),
};
}
Err(error) => return Err(HelperWaitFailure::ReapedOrUnknown(error)),
}
let now = Instant::now();
if now >= deadline {
return Err(HelperWaitFailure::StillLive(io::Error::new(
io::ErrorKind::TimedOut,
"timed out waiting for process-group helper",
)));
}
if yield_attempts < YIELD_ATTEMPTS {
std::thread::yield_now();
yield_attempts += 1;
} else {
std::thread::sleep(poll_interval.min(deadline.saturating_duration_since(now)));
poll_interval = poll_interval.saturating_mul(2).min(MAX_POLL_INTERVAL);
}
}
}
fn write_disarm(descriptor: RawFd) -> io::Result<()> {
let record = DISARM_RECORD;
loop {
let written = unsafe {
libc::send(
descriptor,
(&raw const record).cast::<libc::c_void>(),
1,
libc::MSG_NOSIGNAL,
)
};
if written == 1 {
return Ok(());
}
if written == 0 {
return Err(io::Error::new(
io::ErrorKind::WriteZero,
"process-group helper accepted no disarm data",
));
}
let error = io::Error::last_os_error();
if error.kind() != io::ErrorKind::Interrupted {
return Err(error);
}
}
}
fn child_create_process_group() -> ProcessGroupId {
loop {
if unsafe { libc::setpgid(0, 0) } == 0 {
let process = unsafe { libc::getpid() };
if let Some(group) = ProcessGroupId::new(process) {
return group;
}
child_exit(FAILURE_EXIT);
}
let error = io::Error::last_os_error();
if error.kind() != io::ErrorKind::Interrupted {
child_exit(FAILURE_EXIT);
}
}
}
fn guard_loop(descriptor: RawFd, group: ProcessGroupId) -> ! {
let mut record = 0_u8;
loop {
let read = unsafe { libc::read(descriptor, (&raw mut record).cast::<libc::c_void>(), 1) };
if read == 1 && record == DISARM_RECORD {
child_exit(0);
}
if read == -1 && io::Error::last_os_error().kind() == io::ErrorKind::Interrupted {
continue;
}
child_kill_group(group);
}
}
fn child_kill_group(group: ProcessGroupId) -> ! {
loop {
if unsafe { libc::kill(-group.get(), libc::SIGKILL) } == 0 {
child_exit(0);
}
let error = io::Error::last_os_error();
if error.kind() != io::ErrorKind::Interrupted {
child_exit(FAILURE_EXIT);
}
}
}
fn child_exit(code: libc::c_int) -> ! {
unsafe { libc::_exit(code) }
}