use std::ffi::OsString;
use std::os::unix::process::{CommandExt as _, ExitStatusExt as _};
use std::process::{Child, Command};
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
use super::gate::Refusal;
use super::{END_BUDGET, EXIT_CONTAINMENT, EXIT_USAGE, GroupEnd, SESSION_ACK};
use crate::shell::exec::{exited_unreaped, reap_within};
const TICK: Duration = Duration::from_millis(50);
const REAP_BUDGET: Duration = Duration::from_secs(2);
static STOP_REQUESTED: AtomicBool = AtomicBool::new(false);
extern "C" fn request_stop(_signal: libc::c_int) {
STOP_REQUESTED.store(true, Ordering::SeqCst);
}
pub(super) struct Options {
pub(super) ack: bool,
pub(super) watch_parent: bool,
pub(super) control_fd: Option<libc::c_int>,
pub(super) program: OsString,
pub(super) args: Vec<OsString>,
#[cfg(any(test, feature = "test-support"))]
pub(super) engine_in_own_group: bool,
}
#[cfg(any(test, feature = "test-support"))]
fn engine_in_own_group(options: &Options) -> bool {
options.engine_in_own_group
}
#[cfg(not(any(test, feature = "test-support")))]
fn engine_in_own_group(_: &Options) -> bool {
true
}
pub(super) fn main_explicit(args: impl Iterator<Item = OsString>) -> i32 {
parse(args).map_or(EXIT_USAGE, |options| run(&options))
}
pub(super) fn main_requested() -> Option<i32> {
let program = std::env::current_exe().ok()?;
Some(run(&Options {
ack: true,
watch_parent: true,
control_fd: None,
program: program.into_os_string(),
args: std::env::args_os().skip(1).collect(),
#[cfg(any(test, feature = "test-support"))]
engine_in_own_group: true,
}))
}
pub(super) fn parse(mut args: impl Iterator<Item = OsString>) -> Option<Options> {
let mut ack = false;
let mut watch_parent = false;
let mut control_fd = None;
loop {
let flag = args.next()?;
if flag == "--" {
break;
} else if flag == "--ack" {
ack = true;
} else if flag == "--watch-parent" {
watch_parent = true;
} else if flag == "--control-fd" {
let value = args.next()?;
control_fd = Some(
value
.to_str()?
.parse::<libc::c_int>()
.ok()
.filter(|fd| *fd > libc::STDERR_FILENO)?,
);
} else {
return None;
}
}
Some(Options {
ack,
watch_parent,
control_fd,
program: args.next()?,
args: args.collect(),
#[cfg(any(test, feature = "test-support"))]
engine_in_own_group: true,
})
}
struct Control(libc::c_int);
impl Control {
fn open(fd: libc::c_int) -> Option<Self> {
unsafe {
let status = libc::fcntl(fd, libc::F_GETFL);
let flags = libc::fcntl(fd, libc::F_GETFD);
if status < 0
|| flags < 0
|| libc::fcntl(fd, libc::F_SETFL, status | libc::O_NONBLOCK) < 0
|| libc::fcntl(fd, libc::F_SETFD, flags | libc::FD_CLOEXEC) < 0
{
return None;
}
}
Some(Self(fd))
}
fn closed(&self) -> bool {
let mut buffer = [0_u8; 64];
loop {
let read = unsafe { libc::read(self.0, buffer.as_mut_ptr().cast(), buffer.len()) };
if read == 0 {
return true;
}
if read < 0 {
return !matches!(
std::io::Error::last_os_error().kind(),
std::io::ErrorKind::WouldBlock | std::io::ErrorKind::Interrupted
);
}
}
}
}
fn report(message: &str) {
let line = format!("leanctx-containment: {message}\n");
let _ = super::write_fd(libc::STDERR_FILENO, line.as_bytes());
}
pub(super) fn run(options: &Options) -> i32 {
let parent = unsafe { libc::getppid() };
let control = match options.control_fd.map(Control::open) {
Some(None) => return EXIT_USAGE,
Some(control) => control,
None => None,
};
STOP_REQUESTED.store(false, Ordering::SeqCst);
super::set_termination_signals(
request_stop as extern "C" fn(libc::c_int) as libc::sighandler_t,
);
if options.ack && !super::write_fd(libc::STDOUT_FILENO, SESSION_ACK) {
return EXIT_CONTAINMENT;
}
let mut command = Command::new(&options.program);
command.args(&options.args).env_remove(super::SESSION_ENV);
if engine_in_own_group(options) {
command.process_group(0);
}
let (mut engine, backstop) = match super::gate::start_behind_guard(&mut command) {
Ok(started) => started,
Err(Refusal::NotStarted(error)) => {
report(&format!("the instance could not be started: {error}"));
return EXIT_CONTAINMENT;
}
Err(Refusal::NotLeader) => {
report("the instance's process group could not be verified; no group was signalled");
return EXIT_CONTAINMENT;
}
Err(Refusal::NoGuard(error)) => {
report(&format!("the session guard could not be attached: {error}"));
return EXIT_CONTAINMENT;
}
};
let group = engine.id() as libc::pid_t;
release_session_pipes();
while !must_end(&mut engine, parent, options.watch_parent, control.as_ref()) {
std::thread::sleep(TICK);
}
let end = super::end_pinned_group(group, END_BUDGET);
let _ = backstop.finish();
let status = reap_within(&mut engine, REAP_BUDGET);
match end {
GroupEnd::Empty => {}
GroupEnd::Unconfirmed => {
report("the instance's group was signalled; its end could not be observed");
}
GroupEnd::Survivors | GroupEnd::Refused => {
report("the instance's group could not be ended");
return EXIT_CONTAINMENT;
}
}
let Some(status) = status else {
report("the instance could not be reaped");
return EXIT_CONTAINMENT;
};
mirror(status)
}
fn must_end(
engine: &mut Child,
parent: libc::pid_t,
watch_parent: bool,
control: Option<&Control>,
) -> bool {
!matches!(exited_unreaped(engine), Ok(false))
|| STOP_REQUESTED.load(Ordering::SeqCst)
|| (watch_parent && parent > 1 && unsafe { libc::getppid() } != parent)
|| control.is_some_and(Control::closed)
}
fn release_session_pipes() {
use std::os::fd::AsRawFd as _;
let Ok(null) = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open("/dev/null")
else {
return;
};
for fd in [libc::STDIN_FILENO, libc::STDOUT_FILENO] {
unsafe { libc::dup2(null.as_raw_fd(), fd) };
}
}
fn mirror(status: std::process::ExitStatus) -> i32 {
if let Some(code) = status.code() {
return code;
}
let Some(signal) = status.signal() else {
return EXIT_CONTAINMENT;
};
unsafe {
libc::signal(signal, libc::SIG_DFL);
libc::raise(signal);
}
128 + signal
}