Skip to content
This repository was archived by the owner on Aug 18, 2026. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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<OwnedFd> {
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
Expand Down
46 changes: 44 additions & 2 deletions tests/integration_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<i64, Vec<SeccompRule>> =
[(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.
}