diff --git a/src/lib.rs b/src/lib.rs index a0d122377..f5b4ed327 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -195,6 +195,7 @@ mod syscall_table; use std::convert::TryInto; #[cfg(feature = "json")] use std::io::Read; +use std::os::unix::io::{FromRawFd, OwnedFd, RawFd}; use std::collections::HashMap; use std::fmt::{Display, Formatter}; @@ -325,6 +326,52 @@ pub fn apply_filter_all_threads(bpf_filter: BpfProgramRef) -> Result<()> { apply_filter_with_flags(bpf_filter, libc::SECCOMP_FILTER_FLAG_TSYNC) } +/// Apply a BPF filter with `SECCOMP_FILTER_FLAG_NEW_LISTENER` and return the +/// listener fd (Linux 5.0+). [`SeccompAction::UserNotif`] notifications are +/// consumed via the `SECCOMP_IOCTL_NOTIF_*` ioctls (see `seccomp_unotify(2)`); +/// at least one rule should use UserNotif, or the listener never fires. +/// +/// Unlike [`apply_filter`], a successful call returns the positive listener fd. +/// +/// [`SeccompAction::UserNotif`]: enum.SeccompAction.html#variant.UserNotif +pub fn apply_filter_with_listener(bpf_filter: BpfProgramRef) -> Result { + if bpf_filter.is_empty() { + return Err(Error::EmptyFilter); + } + + // SAFETY: Safe because syscall arguments are valid. + let rc = unsafe { libc::prctl(libc::PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) }; + if rc != 0 { + return Err(Error::Prctl(io::Error::last_os_error())); + } + + let bpf_prog = sock_fprog { + len: bpf_filter.len() as u16, + filter: bpf_filter.as_ptr(), + }; + let bpf_prog_ptr = &bpf_prog as *const sock_fprog; + + // SAFETY: + // Safe because the kernel performs a `copy_from_user` on the filter and leaves the memory + // untouched. We can therefore use a reference to the BpfProgram, without needing ownership. + let rc = unsafe { + libc::syscall( + libc::SYS_seccomp, + libc::SECCOMP_SET_MODE_FILTER, + libc::SECCOMP_FILTER_FLAG_NEW_LISTENER, + bpf_prog_ptr, + ) + }; + + if rc < 0 { + return Err(Error::Seccomp(io::Error::last_os_error())); + } + + // SAFETY: on success seccomp(2) with NEW_LISTENER returns a newly-allocated, + // open file descriptor; we take ownership and close it on drop. + Ok(unsafe { OwnedFd::from_raw_fd(rc as RawFd) }) +} + /// Apply a BPF filter to the calling thread. /// /// # Arguments diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index cb6c1f350..f72b8bd6c 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -8,8 +8,8 @@ use std::collections::BTreeMap; use seccompiler::SeccompCmpArgLen::*; use seccompiler::SeccompCmpOp::*; use seccompiler::{ - apply_filter, sock_filter, BpfProgram, Error, SeccompAction, SeccompCondition as Cond, - SeccompFilter, SeccompRule, + apply_filter, apply_filter_with_listener, sock_filter, BpfProgram, Error, SeccompAction, + SeccompCondition as Cond, SeccompFilter, SeccompRule, }; use std::convert::TryInto; use std::env::consts::ARCH; @@ -789,3 +789,45 @@ fn test_filter_apply() { .join() .unwrap(); } + +#[test] +fn test_apply_filter_with_listener() { + use std::os::unix::io::{AsRawFd, OwnedFd}; + + // Empty program: rejected without enabling seccomp. + assert_eq!(unsafe { libc::prctl(libc::PR_GET_SECCOMP) }, 0); + assert!(matches!( + apply_filter_with_listener(&Vec::new()).unwrap_err(), + Error::EmptyFilter + )); + assert_eq!(unsafe { libc::prctl(libc::PR_GET_SECCOMP) }, 0); + + // Install in a thread (filters are process-wide). getpid routes to UserNotif + // but is never called, so the thread exits cleanly. Requires Linux 5.0+. + let fd: OwnedFd = thread::spawn(|| { + // Empty rule chain ⇒ filter match_action (no dependency on per-rule actions). + let rules: BTreeMap> = + [(libc::SYS_getpid, vec![])].into_iter().collect(); + + let filter = SeccompFilter::new( + rules, + SeccompAction::Allow, // mismatch: everything else allowed + SeccompAction::UserNotif, // match: getpid → USER_NOTIF + ARCH.try_into().unwrap(), + ) + .unwrap(); + let prog: BpfProgram = filter.try_into().unwrap(); + + apply_filter_with_listener(&prog).unwrap() + }) + .join() + .unwrap(); + + let raw = fd.as_raw_fd(); + assert!(raw >= 0, "listener fd must be non-negative, got {raw}"); + assert!( + unsafe { libc::fcntl(raw, libc::F_GETFD) } >= 0, + "listener fd {raw} should be a valid open fd" + ); + // OwnedFd closes on drop. +}