use std::os::fd::OwnedFd;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::SystemTime;
use procage_policy::linux::{Action, SeccompRule, Syscall};
use super::{seccomp, sys};
use crate::violation::{DeniedBy, Sink, Violation, lock};
pub(crate) fn start(
listener: OwnedFd,
rules: Arc<[SeccompRule]>,
record: Sink,
) -> std::io::Result<Arc<AtomicBool>> {
let done = Arc::new(AtomicBool::new(false));
let flag = done.clone();
std::thread::Builder::new()
.name("procage-supervisor".into())
.spawn(move || {
supervise(&listener, &rules, &record);
flag.store(true, Ordering::Release);
})?;
Ok(done)
}
fn supervise(listener: &OwnedFd, rules: &[SeccompRule], record: &Sink) {
let numbered: Vec<(u32, &SeccompRule)> = rules
.iter()
.filter(|r| r.action == Action::Eperm)
.filter_map(|r| match (seccomp::NATIVE.number)(&r.syscall) {
Ok(Some(nr)) => Some((nr, r)),
_ => None,
})
.collect();
loop {
match sys::wait_listener(listener, 1000) {
Ok(sys::Wait::Ready) => {}
Ok(sys::Wait::Idle) => continue,
Ok(sys::Wait::HungUp) | Err(_) => return,
}
let Ok(n) = sys::receive_notification(listener) else {
continue;
};
let (pid, name) = process_of(n.pid)
.filter(|_| sys::notification_valid(listener, n.id))
.unwrap_or((n.pid, "?".to_string()));
let _ = sys::answer_eperm(listener, n.id);
let rule = numbered
.iter()
.find(|(nr, r)| *nr == n.nr && r.condition.matches(&n.args))
.map(|(_, r)| *r);
let violation = Violation {
pid,
process: name,
operation: rule.map_or_else(|| format!("syscall {}", n.nr), |r| r.syscall.to_string()),
target: rule.and_then(|r| target(&r.syscall, &n.args)),
denied_by: rule.map_or(DeniedBy::Unknown("unmatched".into()), |r| {
DeniedBy::from_reason(r.reason)
}),
received: SystemTime::now(),
repeats: 1,
};
lock(record).push(violation);
}
}
fn process_of(tid: u32) -> Option<(u32, String)> {
let status = std::fs::read_to_string(format!("/proc/{tid}/status")).ok()?;
let tgid: u32 = status
.lines()
.find_map(|l| l.strip_prefix("Tgid:"))?
.trim()
.parse()
.ok()?;
let name = std::fs::read_to_string(format!("/proc/{tgid}/comm")).ok()?;
Some((tgid, name.trim_end().to_string()))
}
fn target(syscall: &Syscall, args: &[u64; 6]) -> Option<String> {
let low = |n: usize| args[n] as u32;
match syscall {
Syscall::Socket => Some(format!("family {}", family(low(0)))),
Syscall::Ioctl => Some(match low(1) {
0x5412 => "command TIOCSTI".to_string(),
0x541c => "command TIOCLINUX".to_string(),
c => format!("command {c:#x}"),
}),
Syscall::Clone => Some(format!("flags {:#x}", low(0))),
Syscall::Open => Some(format!("flags {:#o}", low(1))),
Syscall::Openat => Some(format!("flags {:#o}", low(2))),
Syscall::Kill | Syscall::Tkill => Some(format!("pid {}", args[0] as i32)),
Syscall::Tgkill => Some(format!("pid {}", args[1] as i32)),
Syscall::Ptrace => Some(format!("pid {}", args[1] as i32)),
_ => None,
}
}
fn family(af: u32) -> String {
match af as i32 {
libc::AF_UNIX => "AF_UNIX".into(),
libc::AF_INET => "AF_INET".into(),
libc::AF_INET6 => "AF_INET6".into(),
libc::AF_NETLINK => "AF_NETLINK".into(),
libc::AF_PACKET => "AF_PACKET".into(),
libc::AF_VSOCK => "AF_VSOCK".into(),
other => other.to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn targets_name_what_the_arguments_name() {
let args = |a0: u64, a1: u64, a2: u64| [a0, a1, a2, 0, 0, 0];
assert_eq!(
target(&Syscall::Socket, &args(libc::AF_INET as u64, 1, 0)).as_deref(),
Some("family AF_INET")
);
assert_eq!(
target(&Syscall::Ioctl, &args(0, 0x5412, 0)).as_deref(),
Some("command TIOCSTI")
);
assert_eq!(
target(&Syscall::Openat, &args(0, 0, 0o1000)).as_deref(),
Some("flags 0o1000")
);
assert_eq!(target(&Syscall::Setsid, &args(0, 0, 0)), None);
}
}