use std::fs::File;
use std::io::{self, Read, Seek, SeekFrom};
use std::os::fd::{AsRawFd, OwnedFd, RawFd};
use evalbox_sys::seccomp_notify::{
SECCOMP_ADDFD_FLAG_SEND, SECCOMP_USER_NOTIF_FLAG_CONTINUE, SeccompNotif, SeccompNotifAddfd,
SeccompNotifResp, notif_addfd, notif_id_valid, notif_recv, notif_send,
};
use super::virtual_fs::VirtualFs;
use crate::plan::NotifyMode;
#[derive(Debug)]
pub enum NotifyEvent {
SyscallHandled {
pid: u32,
syscall_nr: i32,
allowed: bool,
},
}
pub struct Supervisor {
listener_fd: OwnedFd,
mode: NotifyMode,
vfs: VirtualFs,
}
impl Supervisor {
pub fn new(listener_fd: OwnedFd, mode: NotifyMode, vfs: VirtualFs) -> Self {
Self {
listener_fd,
mode,
vfs,
}
}
pub fn fd(&self) -> RawFd {
self.listener_fd.as_raw_fd()
}
pub fn handle_event(&self) -> io::Result<Option<NotifyEvent>> {
let mut notif = SeccompNotif::default();
if let Err(e) = notif_recv(self.listener_fd.as_raw_fd(), &mut notif) {
if e == rustix::io::Errno::NOENT {
return Ok(None);
}
return Err(io::Error::from_raw_os_error(e.raw_os_error()));
}
match self.mode {
NotifyMode::Disabled => {
debug_assert!(
false,
"supervisor received notification with NotifyMode::Disabled"
);
self.respond_continue(¬if)?;
Ok(None)
}
NotifyMode::Monitor => self.handle_monitor(¬if),
NotifyMode::Virtualize => self.handle_virtualize(¬if),
}
}
fn handle_monitor(&self, notif: &SeccompNotif) -> io::Result<Option<NotifyEvent>> {
let syscall_name = syscall_name(notif.data.nr);
eprintln!(
"[notify] pid={} syscall={}({}) args=[{:#x}, {:#x}, {:#x}]",
notif.pid,
syscall_name,
notif.data.nr,
notif.data.args[0],
notif.data.args[1],
notif.data.args[2],
);
self.respond_continue(notif)?;
Ok(Some(NotifyEvent::SyscallHandled {
pid: notif.pid,
syscall_nr: notif.data.nr,
allowed: true,
}))
}
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
fn handle_virtualize(&self, notif: &SeccompNotif) -> io::Result<Option<NotifyEvent>> {
let syscall_nr = notif.data.nr;
let path_addr = if is_at_syscall(syscall_nr) {
notif.data.args[1]
} else {
notif.data.args[0]
};
let path = match self.read_child_string(notif.pid, path_addr) {
Ok(p) => p,
Err(_) => {
self.respond_continue(notif)?;
return Ok(None);
}
};
if notif_id_valid(self.listener_fd.as_raw_fd(), notif.id).is_err() {
return Ok(None); }
if let Some(real_path) = self.vfs.translate(&path) {
if is_open_syscall(syscall_nr) {
let flags = if syscall_nr == libc::SYS_openat as i32 {
notif.data.args[2] as i32
} else {
notif.data.args[1] as i32
};
match self.open_and_inject(notif, &real_path, flags) {
Ok(()) => {
return Ok(Some(NotifyEvent::SyscallHandled {
pid: notif.pid,
syscall_nr,
allowed: true,
}));
}
Err(_) => {
}
}
}
}
self.respond_continue(notif)?;
Ok(Some(NotifyEvent::SyscallHandled {
pid: notif.pid,
syscall_nr,
allowed: true,
}))
}
fn respond_continue(&self, notif: &SeccompNotif) -> io::Result<()> {
let resp = SeccompNotifResp {
id: notif.id,
val: 0,
error: 0,
flags: SECCOMP_USER_NOTIF_FLAG_CONTINUE,
};
notif_send(self.listener_fd.as_raw_fd(), &resp)
.map_err(|e| io::Error::from_raw_os_error(e.raw_os_error()))
}
#[allow(clippy::cast_sign_loss)]
fn open_and_inject(
&self,
notif: &SeccompNotif,
real_path: &std::path::Path,
flags: i32,
) -> io::Result<()> {
use std::ffi::CString;
use std::os::unix::ffi::OsStrExt;
let path_c = CString::new(real_path.as_os_str().as_bytes())
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid path"))?;
let fd = unsafe { libc::open(path_c.as_ptr(), flags & !libc::O_CLOEXEC, 0o666) };
if fd < 0 {
return Err(io::Error::last_os_error());
}
let addfd = SeccompNotifAddfd {
id: notif.id,
flags: SECCOMP_ADDFD_FLAG_SEND,
srcfd: fd as u32,
newfd: 0,
newfd_flags: 0,
};
let result = notif_addfd(self.listener_fd.as_raw_fd(), &addfd)
.map_err(|e| io::Error::from_raw_os_error(e.raw_os_error()));
unsafe { libc::close(fd) };
result.map(|_| ())
}
fn read_child_string(&self, pid: u32, addr: u64) -> io::Result<String> {
let mem_path = format!("/proc/{pid}/mem");
let mut file = File::open(&mem_path)?;
file.seek(SeekFrom::Start(addr))?;
let mut buf = vec![0u8; 4096];
let n = file.read(&mut buf)?;
buf.truncate(n);
if let Some(nul_pos) = buf.iter().position(|&b| b == 0) {
buf.truncate(nul_pos);
}
String::from_utf8(buf)
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "invalid UTF-8 in path"))
}
}
#[allow(clippy::cast_possible_truncation)]
fn is_at_syscall(nr: i32) -> bool {
let nr = nr as i64;
nr == libc::SYS_openat
|| nr == libc::SYS_newfstatat
|| nr == libc::SYS_faccessat
|| nr == libc::SYS_faccessat2
|| nr == libc::SYS_readlinkat
}
#[allow(clippy::cast_possible_truncation)]
fn is_open_syscall(nr: i32) -> bool {
let nr = nr as i64;
if nr == libc::SYS_openat {
return true;
}
#[cfg(target_arch = "x86_64")]
if nr == libc::SYS_open || nr == libc::SYS_creat {
return true;
}
false
}
#[allow(clippy::cast_possible_truncation)]
fn syscall_name(nr: i32) -> &'static str {
match nr as i64 {
libc::SYS_openat => "openat",
libc::SYS_faccessat => "faccessat",
libc::SYS_faccessat2 => "faccessat2",
libc::SYS_newfstatat => "newfstatat",
libc::SYS_statx => "statx",
libc::SYS_readlinkat => "readlinkat",
#[cfg(target_arch = "x86_64")]
libc::SYS_open => "open",
#[cfg(target_arch = "x86_64")]
libc::SYS_creat => "creat",
#[cfg(target_arch = "x86_64")]
libc::SYS_access => "access",
#[cfg(target_arch = "x86_64")]
libc::SYS_stat => "stat",
#[cfg(target_arch = "x86_64")]
libc::SYS_lstat => "lstat",
#[cfg(target_arch = "x86_64")]
libc::SYS_readlink => "readlink",
_ => "unknown",
}
}
#[cfg(test)]
#[allow(clippy::cast_possible_truncation)]
mod tests {
use super::*;
#[test]
fn syscall_names() {
assert_eq!(syscall_name(libc::SYS_openat as i32), "openat");
assert_eq!(syscall_name(libc::SYS_newfstatat as i32), "newfstatat");
assert_eq!(syscall_name(9999), "unknown");
}
}