From c63ac88c26acd3180776a7b1097d37f906fb7118 Mon Sep 17 00:00:00 2001 From: Harry Stern Date: Tue, 5 Sep 2023 22:10:00 -0400 Subject: [PATCH] Add support for seccomp thread sync feature - Adds SeccompFlag and SeccompFlagset with initial support for just SyncThreads/SECCOMP_FILTER_FLAG_TSYNC - Adds public functions `seccompiler::apply_filter_all_threads` and `apply_filter_with_flags` - Moves the body of apply_filter into apply_filter_with_flags Resolves #57 Signed-off-by: Harry Stern --- CHANGELOG.md | 6 ++++ src/backend/flags.rs | 76 +++++++++++++++++++++++++++++++++++++++++ src/backend/mod.rs | 3 ++ src/lib.rs | 34 ++++++++++++++++--- tests/multi_thread.rs | 78 +++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 193 insertions(+), 4 deletions(-) create mode 100644 src/backend/flags.rs create mode 100644 tests/multi_thread.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index e5b0d70..5b3f632 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +# Upcoming Release + +- Add `apply_filter_with_flags` to pass seccomp flags + - Add SeccompFlag and SeccompFlagset +- Add `apply_filter_all_threads` convenience function + # v0.3.0 ## Changed diff --git a/src/backend/flags.rs b/src/backend/flags.rs new file mode 100644 index 0000000..dd2f2df --- /dev/null +++ b/src/backend/flags.rs @@ -0,0 +1,76 @@ +use std::ops::{BitOr, BitOrAssign}; + +#[derive(Copy, Clone, Debug)] +/// A flag accepted by the `seccomp` syscall. +pub enum SeccompFlag { + /// Synchronize the current filter across all threads in the process. + ThreadSync, + // TODO: other flags +} + +impl SeccompFlag { + fn as_u64(&self) -> u64 { + // Values given in linux/seccomp.h + // https://github.com/torvalds/linux/blob/master/include/uapi/linux/seccomp.h + match self { + SeccompFlag::ThreadSync => 1, + } + } + + /// Convert this flag to a new [`SeccompFlagset`] containing it. + pub fn to_flagset(self) -> SeccompFlagset { + SeccompFlagset(self.as_u64()) + } +} + +#[derive(Copy, Clone, Debug, Default)] +/// A set of [`SeccompFlag`]s to be passed to the `seccomp` syscall. +pub struct SeccompFlagset(u64); + +impl SeccompFlagset { + /// Create a new, empty SeccompFlagset. + pub fn new() -> SeccompFlagset { + SeccompFlagset(0) + } + + /// Return a u64 representing the flags to be passed to the seccomp syscall. + pub fn as_u64(&self) -> u64 { + self.0 + } +} + +impl BitOr for SeccompFlag { + type Output = SeccompFlagset; + + fn bitor(self, rhs: Self) -> Self::Output { + SeccompFlagset(self.as_u64() | rhs.as_u64()) + } +} + +impl BitOr for SeccompFlagset { + type Output = SeccompFlagset; + + fn bitor(self, rhs: SeccompFlagset) -> Self::Output { + SeccompFlagset(self.0 | rhs.0) + } +} + +impl BitOrAssign for SeccompFlagset { + fn bitor_assign(&mut self, rhs: SeccompFlagset) { + self.0 |= rhs.0; + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + /// Test all the bitwise or options for SeccompFlag and SeccompFlagset + fn test_bitwise() { + let mut flagset = SeccompFlag::ThreadSync | SeccompFlag::ThreadSync; + assert_eq!(flagset.as_u64(), 1); + flagset |= SeccompFlag::ThreadSync.to_flagset() | SeccompFlag::ThreadSync.to_flagset(); + assert_eq!(flagset.as_u64(), 1); + } +} diff --git a/src/backend/mod.rs b/src/backend/mod.rs index 7ae49a8..8a040d8 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -7,6 +7,7 @@ mod bpf; mod condition; mod filter; +mod flags; mod rule; pub use condition::SeccompCondition; @@ -27,6 +28,8 @@ use bpf::{ pub use bpf::{sock_filter, BpfProgram, BpfProgramRef}; +pub use flags::{SeccompFlag, SeccompFlagset}; + /// Backend Result type. type Result = std::result::Result; diff --git a/src/lib.rs b/src/lib.rs index edcfe58..c0e3696 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -205,7 +205,8 @@ use frontend::json::{Error as JsonFrontendError, JsonCompiler}; // Re-export the IR public types. pub use backend::{ sock_filter, BpfProgram, BpfProgramRef, Error as BackendError, SeccompAction, SeccompCmpArgLen, - SeccompCmpOp, SeccompCondition, SeccompFilter, SeccompRule, TargetArch, + SeccompCmpOp, SeccompCondition, SeccompFilter, SeccompFlag, SeccompFlagset, SeccompRule, + TargetArch, }; // BPF structure definition for filter array. @@ -292,6 +293,30 @@ impl From for Error { /// /// [`BpfProgram`]: type.BpfProgram.html pub fn apply_filter(bpf_filter: BpfProgramRef) -> Result<()> { + apply_filter_with_flags(bpf_filter, SeccompFlagset::new()) +} + +/// Apply a BPF filter to the all threads in the process. +/// +/// # Arguments +/// +/// * `bpf_filter` - A reference to the [`BpfProgram`] to be installed. +/// +/// [`BpfProgram`]: type.BpfProgram.html +pub fn apply_filter_all_threads(bpf_filter: BpfProgramRef) -> Result<()> { + apply_filter_with_flags(bpf_filter, SeccompFlag::ThreadSync.to_flagset()) +} + +/// Apply a BPF filter to the calling thread. +/// +/// # Arguments +/// +/// * `bpf_filter` - A reference to the [`BpfProgram`] to be installed. +/// * `flags` - A u64 representing a bitset of flags, typically created by or-ing [`SeccompFlags`] +/// together +/// +/// [`BpfProgram`]: type.BpfProgram.html +pub fn apply_filter_with_flags(bpf_filter: BpfProgramRef, flags: SeccompFlagset) -> Result<()> { // If the program is empty, don't install the filter. if bpf_filter.is_empty() { return Err(Error::EmptyFilter); @@ -314,9 +339,10 @@ pub fn apply_filter(bpf_filter: BpfProgramRef) -> Result<()> { // 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::prctl( - libc::PR_SET_SECCOMP, - libc::SECCOMP_MODE_FILTER, + libc::syscall( + libc::SYS_seccomp, + libc::SECCOMP_SET_MODE_FILTER, + flags.as_u64(), bpf_prog_ptr, ) }; diff --git a/tests/multi_thread.rs b/tests/multi_thread.rs new file mode 100644 index 0000000..73d8f2f --- /dev/null +++ b/tests/multi_thread.rs @@ -0,0 +1,78 @@ +#![allow(clippy::undocumented_unsafe_blocks)] + +/// This test is in a separate top-level test file so that it is isolated from the other tests - +/// each file in the tests/ directory gets compiled to a separate binary and is run as a separate +/// process. +use std::collections::BTreeMap; + +use std::sync::mpsc::sync_channel; +use std::thread; + +use seccompiler::{ + apply_filter_all_threads, BpfProgram, SeccompAction, SeccompFilter, SeccompRule, +}; +use std::env::consts::ARCH; + +#[test] +/// Test seccomp's TSYNC functionality, which syncs the current filter to all threads in the +/// process. +fn test_tsync() { + // These channels will block on send until the receiver has called recv. + let (setup_tx, setup_rx) = sync_channel::<()>(0); + let (finish_tx, finish_rx) = sync_channel::<()>(0); + + let seccomp_thread = thread::spawn(move || { + let rules = vec![(libc::SYS_getpid, vec![])]; + + let rule_map: BTreeMap> = rules.into_iter().collect(); + + // Build seccomp filter only disallowing getpid + let filter = SeccompFilter::new( + rule_map, + SeccompAction::Allow, + SeccompAction::Errno(1u32), + ARCH.try_into().unwrap(), + ) + .unwrap(); + + let filter: BpfProgram = filter.try_into().unwrap(); + apply_filter_all_threads(&filter).unwrap(); + + // seccomp setup done, let the other thread start + setup_tx.send(()).unwrap(); + + // don't close this thread until the other thread is done asserting. This way we can be + // sure the thread that loaded the filter is definitely active when the other thread runs. + finish_rx.recv().unwrap(); + println!("exit seccomp thread"); + }); + + let test_thread = thread::spawn(move || { + // wait until seccomp setup is done + setup_rx.recv().unwrap(); + + // try to use getpid, which we have disallowed on another thread + let pid = unsafe { libc::getpid() }; + let errno = std::io::Error::last_os_error().raw_os_error().unwrap(); + + assert_eq!(pid, -1, "getpid should return -1 as set in SeccompFilter"); + assert_eq!(errno, 0, "there should be no errors"); + + // let other thread know we've passed + finish_tx.send(()).unwrap(); + println!("exit io thread"); + }); + + let seccomp_res = seccomp_thread.join(); + assert!( + seccomp_res.is_ok(), + "seccomp thread failed: {:?}", + seccomp_res.unwrap_err() + ); + let test_res = test_thread.join(); + assert!( + test_res.is_ok(), + "test thread failed: {:?}", + test_res.unwrap_err() + ); +}