use std::collections::BTreeSet;
use std::ffi::CString;
use std::io;
use std::sync::{Mutex, OnceLock};
use std::time::Duration;
use procage_policy::{RUN_MARKER_PARAM, RUN_PREFIX_PARAM};
use super::{SpawnError, Unsupported, sys};
pub(crate) const ROUNDS: usize = 64;
pub(crate) fn prefix() -> &'static str {
static PREFIX: OnceLock<String> = OnceLock::new();
PREFIX.get_or_init(|| {
use std::hash::{BuildHasher, Hasher};
let random = std::collections::hash_map::RandomState::new()
.build_hasher()
.finish();
format!("org.procage.run.{:x}.{random:016x}.", std::process::id())
})
}
fn taken() -> &'static Mutex<BTreeSet<u32>> {
static TAKEN: OnceLock<Mutex<BTreeSet<u32>>> = OnceLock::new();
TAKEN.get_or_init(Default::default)
}
#[derive(Debug)]
pub(crate) struct Run {
slot: u32,
marker: CString,
probe: CString,
}
impl Run {
pub(crate) fn take() -> Run {
let mut taken = taken().lock().unwrap_or_else(|e| e.into_inner());
let slot = (0..=u32::MAX)
.find(|s| !taken.contains(s))
.unwrap_or_else(|| unreachable!("fewer than 2^32 runs"));
taken.insert(slot);
Run::of(slot)
}
fn of(slot: u32) -> Run {
let name = |suffix: &str| {
CString::new(format!("{}{suffix}", prefix()))
.unwrap_or_else(|_| unreachable!("hex digits and dots only"))
};
Run {
slot,
marker: name(&slot.to_string()),
probe: name("probe"),
}
}
pub(crate) fn params(&self) -> [(String, String); 2] {
params(&self.marker)
}
pub(crate) fn marker(&self) -> &str {
self.marker
.to_str()
.unwrap_or_else(|_| unreachable!("hex digits and dots only"))
}
pub(crate) fn slot(&self) -> u32 {
self.slot
}
pub(crate) fn is_first(&self) -> bool {
self.slot == 0
}
pub(crate) fn free(self) {
taken()
.lock()
.unwrap_or_else(|e| e.into_inner())
.remove(&self.slot);
}
}
pub(crate) fn first_params() -> [(String, String); 2] {
params(&Run::of(0).marker)
}
fn params(marker: &CString) -> [(String, String); 2] {
[
(RUN_PREFIX_PARAM.to_string(), prefix().to_string()),
(
RUN_MARKER_PARAM.to_string(),
marker.to_string_lossy().into_owned(),
),
]
}
pub(crate) fn probe(lib: &sys::Libsandbox) -> Result<(), SpawnError> {
static RESULT: OnceLock<Result<(), String>> = OnceLock::new();
RESULT
.get_or_init(|| {
let probe = Run::of(0).probe;
let pid = std::process::id().cast_signed();
match lib.allows_lookup(pid, &probe) {
Ok(true) => Ok(()),
Ok(false) => Err("the caller's own lookup check was denied".into()),
Err(errno) => Err(format!("the lookup check failed with errno {errno}")),
}
})
.clone()
.map_err(|detail| {
SpawnError::Unsupported(Unsupported::RunMarker {
os_build: sys::os_build(),
detail,
})
})
}
pub(crate) fn members(lib: &sys::Libsandbox, run: &Run) -> io::Result<Vec<libc::pid_t>> {
let mut found = Vec::new();
for pid in sys::all_pids().map_err(io::Error::from_raw_os_error)? {
if pid <= 0 || !sys::live_of_user(pid) {
continue;
}
match (
lib.allows_lookup(pid, &run.marker),
lib.allows_lookup(pid, &run.probe),
) {
(Ok(true), Ok(false)) => found.push(pid),
(Ok(_), Ok(_)) => {}
_ if !sys::live_of_user(pid) => {}
(Err(errno), _) | (_, Err(errno)) => {
return Err(io::Error::from_raw_os_error(errno));
}
}
}
Ok(found)
}
pub(crate) fn reap(
mut scan: impl FnMut() -> io::Result<Vec<libc::pid_t>>,
mut kill: impl FnMut(libc::pid_t),
rounds: usize,
) -> io::Result<()> {
for _ in 0..rounds {
let found = scan()?;
if found.is_empty() {
return Ok(());
}
for pid in found {
kill(pid);
}
std::thread::sleep(Duration::from_millis(1));
}
let survivors = scan()?;
if survivors.is_empty() {
Ok(())
} else {
Err(io::Error::other(format!(
"processes of the run survived: {survivors:?}"
)))
}
}
pub(crate) fn end(run: &Run) -> io::Result<()> {
let lib = super::libsandbox().map_err(io::Error::other)?;
reap(|| members(lib, run), sys::kill, ROUNDS)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn slots_are_reused_only_when_freed() {
let a = Run::take();
let b = Run::take();
assert_ne!(a.slot, b.slot);
let (a_slot, b_slot) = (a.slot, b.slot);
a.free();
let c = Run::take();
assert_eq!(c.slot, a_slot);
drop(b);
let d = Run::take();
assert_ne!(d.slot, b_slot);
c.free();
d.free();
}
#[test]
fn names_are_plain() {
let run = Run::of(7);
for name in [&run.marker, &run.probe] {
let s = name.to_str().unwrap();
assert!(s.starts_with(prefix()), "{s}");
assert!(
s.bytes()
.all(|b| b.is_ascii_digit() || b.is_ascii_lowercase() || b == b'.'),
"{s}"
);
}
assert_ne!(run.marker, run.probe);
assert_eq!(run.params()[1].1, format!("{}7", prefix()));
}
#[test]
fn a_run_that_keeps_living_is_reported() {
let mut kills = 0;
let err = reap(|| Ok(vec![4242]), |_| kills += 1, 3).unwrap_err();
assert_eq!(kills, 3);
assert!(err.to_string().contains("[4242]"), "{err}");
}
#[test]
fn a_run_that_dies_is_done() {
let mut left = vec![vec![1, 2], vec![2], vec![]].into_iter();
let mut killed = Vec::new();
reap(|| Ok(left.next().unwrap()), |p| killed.push(p), 8).unwrap();
assert_eq!(killed, [1, 2, 2]);
}
}