use libseccomp_sys::{
seccomp_notif, seccomp_notif_resp, seccomp_notify_alloc, seccomp_notify_free,
seccomp_notify_id_valid, seccomp_notify_receive, seccomp_notify_respond,
SECCOMP_USER_NOTIF_FLAG_CONTINUE,
};
use std::fs::{File, OpenOptions};
use std::io;
use std::io::ErrorKind;
use std::os::fd::{AsRawFd, RawFd};
use std::os::raw::c_int;
use std::pin::Pin;
use std::task::{Context, Poll};
use tokio::io::unix::AsyncFd;
use tokio::io::Interest;
use tokio_stream::Stream;
pub use syscalls::Sysno;
#[cfg(not(target_os = "linux"))]
compile_error!("There is little to no point to run this crate on non-Linux systems!");
#[derive(Debug, Copy, Clone)]
pub struct Notification {
id: u64,
pub pid: u32,
pub syscall: crate::Sysno,
pub args: [u64; 6],
fd: RawFd,
}
pub enum ResponseType {
Success(i64),
RawError(i32),
Error(io::Error),
}
fn cvt(result: c_int) -> Result<(), io::Error> {
match result {
0 => Ok(()),
_ => Err(io::Error::last_os_error()),
}
}
#[derive(Debug)]
struct RawNotification(*mut seccomp_notif);
impl Drop for RawNotification {
fn drop(&mut self) {
let ptr = std::mem::replace(&mut self.0, std::ptr::null_mut());
if !ptr.is_null() {
unsafe {
seccomp_notify_free(ptr, std::ptr::null_mut());
}
}
}
}
#[derive(Debug)]
struct RawResponse(*mut seccomp_notif_resp);
impl Drop for RawResponse {
fn drop(&mut self) {
let ptr = std::mem::replace(&mut self.0, std::ptr::null_mut());
if !ptr.is_null() {
unsafe {
seccomp_notify_free(std::ptr::null_mut(), ptr);
}
}
}
}
impl RawResponse {
pub fn new() -> Result<Self, io::Error> {
let mut response = std::ptr::null_mut();
cvt(unsafe { seccomp_notify_alloc(std::ptr::null_mut(), &mut response) })?;
Ok(Self(response))
}
pub unsafe fn send_continue(self, fd: RawFd, id: u64) -> Result<(), io::Error> {
(*self.0).id = id;
(*self.0).val = 0;
(*self.0).error = 0;
(*self.0).flags |= SECCOMP_USER_NOTIF_FLAG_CONTINUE;
cvt(unsafe { seccomp_notify_respond(fd, self.0) })?;
Ok(())
}
pub fn send(self, fd: RawFd, id: u64, response_type: ResponseType) -> Result<(), io::Error> {
unsafe {
(*self.0).id = id;
}
match response_type {
ResponseType::Success(val) => unsafe {
(*self.0).val = val;
(*self.0).error = 0;
},
ResponseType::RawError(err) => unsafe {
(*self.0).val = 0;
(*self.0).error = -err;
},
ResponseType::Error(err) => unsafe {
(*self.0).val = 0;
(*self.0).error = -err.raw_os_error().ok_or_else(|| {
io::Error::new(
ErrorKind::InvalidData,
"Supplied io::Error did not map to an OS error!",
)
})?;
},
}
cvt(unsafe { seccomp_notify_respond(fd, self.0) })?;
Ok(())
}
}
impl RawNotification {
pub fn new() -> Result<Self, io::Error> {
let mut notification = std::ptr::null_mut();
cvt(unsafe { seccomp_notify_alloc(&mut notification, std::ptr::null_mut()) })?;
Ok(Self(notification))
}
pub fn recv(self, fd: RawFd) -> Result<seccomp_notif, io::Error> {
cvt(unsafe { seccomp_notify_receive(fd, self.0) })?;
Ok(unsafe { *self.0 })
}
}
impl Notification {
pub fn from_raw(notif: seccomp_notif, fd: RawFd) -> Self {
Self {
id: notif.id,
pid: notif.pid,
syscall: Sysno::from(notif.data.nr),
args: notif.data.args,
fd: fd.as_raw_fd(),
}
}
pub fn valid(&self) -> bool {
cvt(unsafe { seccomp_notify_id_valid(self.fd, self.id) }).is_ok()
}
pub unsafe fn open(&self) -> Result<File, io::Error> {
let path = format!("/proc/{}/mem", self.pid);
let file = OpenOptions::new().read(true).write(true).open(path)?;
if !self.valid() {
return Err(io::Error::new(
ErrorKind::NotFound,
"Process has quit while trying to access its memory!",
));
}
Ok(file)
}
}
#[derive(Debug)]
struct SeccompFd(RawFd);
impl AsRawFd for SeccompFd {
fn as_raw_fd(&self) -> RawFd {
self.0.as_raw_fd()
}
}
#[derive(Debug)]
pub struct NotificationStream {
inner: AsyncFd<SeccompFd>,
}
impl NotificationStream {
pub fn new(fd: RawFd) -> Result<Self, io::Error> {
Ok(Self {
inner: AsyncFd::with_interest(SeccompFd(fd), Interest::READABLE | Interest::WRITABLE)?,
})
}
pub fn blocking_recv(&self) -> Result<Notification, io::Error> {
let raw = RawNotification::new()?.recv(self.inner.as_raw_fd())?;
Ok(Notification::from_raw(raw, self.inner.as_raw_fd()))
}
pub async fn recv(&self) -> Result<Notification, io::Error> {
let guard = self.inner.readable().await?;
let result = self.blocking_recv();
drop(guard);
result
}
pub fn send(&self, notif: Notification, response_type: ResponseType) -> Result<(), io::Error> {
let raw = RawResponse::new()?;
raw.send(self.inner.as_raw_fd(), notif.id, response_type)?;
Ok(())
}
pub unsafe fn send_continue(&self, notif: Notification) -> Result<(), io::Error> {
let raw = RawResponse::new()?;
raw.send_continue(self.inner.as_raw_fd(), notif.id)?;
Ok(())
}
}
impl Stream for NotificationStream {
type Item = Notification;
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
match self.inner.poll_read_ready(cx) {
Poll::Ready(Ok(mut guard)) => {
if guard.ready().is_read_closed() {
Poll::Ready(None)
} else {
let x = self.blocking_recv().ok();
guard.clear_ready();
Poll::Ready(x)
}
}
Poll::Ready(Err(_)) => Poll::Ready(None),
Poll::Pending => Poll::Pending,
}
}
}
#[cfg(test)]
#[allow(clippy::await_holding_lock)]
mod tests {
use super::*;
use libseccomp::{
reset_global_state, ScmpAction, ScmpArch, ScmpFd, ScmpFilterContext, ScmpSyscall,
};
use std::error::Error;
use std::ffi::CStr;
use std::fmt::Debug;
use std::io::{Seek, SeekFrom, Write};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Mutex;
use std::thread;
use std::time::Duration;
use std::thread::JoinHandle;
use tokio::sync::oneshot;
use tokio_stream::StreamExt;
static SECCOMP_MUTEX: Mutex<()> = Mutex::new(());
fn run_with_seccomp<F, Output>(fd_tx: oneshot::Sender<ScmpFd>, func: F) -> JoinHandle<Output>
where
F: FnOnce() -> Output + Send + 'static,
Output: Send + Clone + Debug + 'static,
{
thread::spawn(move || {
reset_global_state().unwrap();
let filter = setup().expect("Failed to setup SECCOMP!");
let fd = filter
.get_notify_fd()
.expect("Did not receive fd from seccomp()!");
fd_tx.send(fd).unwrap();
let result = func();
drop(filter);
result
})
}
fn setup() -> Result<ScmpFilterContext, Box<dyn Error>> {
let mut filter = ScmpFilterContext::new_filter(ScmpAction::Allow)?;
filter.add_arch(ScmpArch::Native)?;
let syscall = ScmpSyscall::from_name("uname")?;
filter.add_rule(ScmpAction::Notify, syscall)?;
filter.load()?;
Ok(filter)
}
#[tokio::test]
async fn test_drop() -> Result<(), io::Error> {
RawResponse(std::ptr::null_mut());
RawNotification(std::ptr::null_mut());
Ok(())
}
#[tokio::test]
async fn test_bad_error() -> Result<(), io::Error> {
let guard = SECCOMP_MUTEX.lock().unwrap();
let (fd_tx, fd_rx) = oneshot::channel::<ScmpFd>();
let handle = run_with_seccomp(fd_tx, move || {
let mut n = unsafe { std::mem::zeroed() };
let r = cvt(unsafe { libc::uname(&mut n) });
assert!(matches!(r, Ok(())));
});
let fd = fd_rx.await.expect("Did not receive FD!");
let mut stream =
NotificationStream::new(fd).expect("Failed to construct NotificationStream");
let notification = tokio::time::timeout(Duration::from_secs(5), stream.next())
.await
.expect("Did not receive a notification in time!")
.unwrap();
assert!(matches!(notification.syscall, Sysno::uname));
stream
.send(
notification,
ResponseType::Error(io::Error::new(ErrorKind::Other, "Custom Error")),
)
.expect_err("Expected error!");
unsafe { stream.send_continue(notification) }.unwrap();
handle.join().expect("Failed to wait for thread!");
drop(guard);
Ok(())
}
#[tokio::test]
async fn test_error() -> Result<(), io::Error> {
let guard = SECCOMP_MUTEX.lock().unwrap();
let (fd_tx, fd_rx) = oneshot::channel::<ScmpFd>();
let handle = run_with_seccomp(fd_tx, move || {
let mut n = unsafe { std::mem::zeroed() };
let r = cvt(unsafe { libc::uname(&mut n) });
assert!(matches!(r, Err(e) if e.kind() == ErrorKind::Unsupported));
});
let fd = fd_rx.await.expect("Did not receive FD!");
let mut stream =
NotificationStream::new(fd).expect("Failed to construct NotificationStream");
let notification = tokio::time::timeout(Duration::from_secs(5), stream.next())
.await
.expect("Did not receive a notification in time!")
.unwrap();
assert!(matches!(notification.syscall, Sysno::uname));
stream
.send(
notification,
ResponseType::Error(io::Error::from_raw_os_error(libc::ENOSYS)),
)
.expect("Failed to send response");
handle.join().expect("Failed to wait for thread!");
drop(guard);
Ok(())
}
#[tokio::test]
async fn test_raw_error() -> Result<(), io::Error> {
let guard = SECCOMP_MUTEX.lock().unwrap();
let (fd_tx, fd_rx) = oneshot::channel::<ScmpFd>();
let handle = run_with_seccomp(fd_tx, move || {
let mut n = unsafe { std::mem::zeroed() };
let r = cvt(unsafe { libc::uname(&mut n) });
assert!(matches!(r, Err(e) if e.kind() == ErrorKind::Unsupported));
});
let fd = fd_rx.await.expect("Did not receive FD!");
let mut stream =
NotificationStream::new(fd).expect("Failed to construct NotificationStream");
let notification = tokio::time::timeout(Duration::from_secs(5), stream.next())
.await
.expect("Did not receive a notification in time!")
.unwrap();
assert!(matches!(notification.syscall, Sysno::uname));
stream
.send(notification, ResponseType::RawError(libc::ENOSYS))
.expect("Failed to send response");
handle.join().expect("Failed to wait for thread!");
drop(guard);
Ok(())
}
#[tokio::test]
async fn test_continue() -> Result<(), io::Error> {
let guard = SECCOMP_MUTEX.lock().unwrap();
let (fd_tx, fd_rx) = oneshot::channel::<ScmpFd>();
let handle = run_with_seccomp(fd_tx, move || {
let mut n = unsafe { std::mem::zeroed() };
let r = unsafe { libc::uname(&mut n) };
assert_eq!(r, 0);
unsafe { CStr::from_ptr(&n.sysname[0]) }
.to_str()
.expect("Invalid UTF-8 reply!")
.to_owned()
});
let fd = fd_rx.await.expect("Did not receive FD!");
let mut stream =
NotificationStream::new(fd).expect("Failed to construct NotificationStream");
let notification = tokio::time::timeout(Duration::from_secs(5), stream.next())
.await
.expect("Did not receive a notification in time!")
.unwrap();
assert!(matches!(notification.syscall, Sysno::uname));
unsafe { stream.send_continue(notification) }.expect("Failed to send response");
let sysname = handle.join().expect("Failed to wait for thread!");
assert_eq!(sysname, "Linux");
drop(guard);
Ok(())
}
#[tokio::test]
async fn test_continue_recv() -> Result<(), io::Error> {
let guard = SECCOMP_MUTEX.lock().unwrap();
let (fd_tx, fd_rx) = oneshot::channel::<ScmpFd>();
let handle = run_with_seccomp(fd_tx, move || {
let mut n = unsafe { std::mem::zeroed() };
let r = unsafe { libc::uname(&mut n) };
assert_eq!(r, 0);
unsafe { CStr::from_ptr(&n.sysname[0]) }
.to_str()
.expect("Invalid UTF-8 reply!")
.to_owned()
});
let fd = fd_rx.await.expect("Did not receive FD!");
let stream = NotificationStream::new(fd).expect("Failed to construct NotificationStream");
let notification = tokio::time::timeout(Duration::from_secs(5), stream.recv())
.await
.expect("Did not receive a notification in time!")
.unwrap();
assert!(matches!(notification.syscall, Sysno::uname));
unsafe { stream.send_continue(notification) }.expect("Failed to send response");
let sysname = handle.join().expect("Failed to wait for thread!");
assert_eq!(sysname, "Linux");
drop(guard);
Ok(())
}
#[tokio::test]
async fn test_intercept() -> Result<(), io::Error> {
let guard = SECCOMP_MUTEX.lock().unwrap();
let (fd_tx, fd_rx) = oneshot::channel::<ScmpFd>();
let handle = run_with_seccomp(fd_tx, move || {
let mut n = unsafe { std::mem::zeroed() };
let r = unsafe { libc::uname(&mut n) };
assert_eq!(r, 0);
unsafe { CStr::from_ptr(&n.sysname[0]) }
.to_str()
.expect("Invalid UTF-8 reply!")
.to_owned()
});
let fd = fd_rx.await.expect("Did not receive FD!");
let mut stream =
NotificationStream::new(fd).expect("Failed to construct NotificationStream");
let notification = tokio::time::timeout(Duration::from_secs(5), stream.next())
.await
.expect("Did not receive a notification in time!")
.unwrap();
assert!(matches!(notification.syscall, Sysno::uname));
let mut file = unsafe { notification.open() }.expect("Failed to open memory!");
file.seek(SeekFrom::Start(notification.args[0]))
.expect("Failed to seek!");
file.write_all(b"seccomp")
.expect("Failed to write spoofed reply!");
stream
.send(notification, ResponseType::Success(0))
.expect("Failed to send response");
let sysname = handle.join().expect("Failed to wait for thread!");
assert_eq!(sysname, "seccomp");
drop(guard);
Ok(())
}
#[tokio::test]
async fn test_parallel() -> Result<(), io::Error> {
let guard = SECCOMP_MUTEX.lock().unwrap();
let (fd_tx, fd_rx) = oneshot::channel::<ScmpFd>();
let handle = run_with_seccomp(fd_tx, move || {
let first = std::thread::spawn(move || {
for _ in 0..20 {
let mut n = unsafe { std::mem::zeroed() };
let r = unsafe { libc::uname(&mut n) };
assert_eq!(r, 0);
}
});
let second = std::thread::spawn(move || {
for _ in 0..20 {
let mut n = unsafe { std::mem::zeroed() };
let r = unsafe { libc::uname(&mut n) };
assert_eq!(r, 0);
}
});
first.join().unwrap();
second.join().unwrap();
});
let fd = fd_rx.await.expect("Did not receive FD!");
let mut stream =
NotificationStream::new(fd).expect("Failed to construct NotificationStream");
let counter = AtomicUsize::new(0);
while let Some(notification) = stream.next().await {
counter.fetch_add(1, Ordering::Relaxed);
unsafe { stream.send_continue(notification) }.unwrap();
}
assert_eq!(counter.into_inner(), 40);
handle.join().unwrap();
drop(guard);
Ok(())
}
}