diff --git a/CHANGELOG.md b/CHANGELOG.md index e5b0d705..d5aa4e22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +# Upcoming Release + +- Seccomp is now activated via the seccomp syscall, not prctl +- A new Error::Seccomp variant is added to indictate seccomp syscall failures +- Add `apply_filter_all_threads` convenience function which uses the seccomp + TSYNC feature to synchronize all threads in the process to the same filter +- A new Error::ThreadSync variant is added to indicate failure to sync threads + # v0.3.0 ## Changed diff --git a/coverage_config_x86_64.json b/coverage_config_x86_64.json index 69d20fc9..1c4fd6a2 100644 --- a/coverage_config_x86_64.json +++ b/coverage_config_x86_64.json @@ -1,5 +1,5 @@ { - "coverage_score": 93.6, + "coverage_score": 93.0, "exclude_path": "tests/integration_tests.rs,tests/json.rs", "crate_features": "json" } diff --git a/src/lib.rs b/src/lib.rs index edcfe58f..2e93b78b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -208,6 +208,10 @@ pub use backend::{ SeccompCmpOp, SeccompCondition, SeccompFilter, SeccompRule, TargetArch, }; +// Until https://github.com/rust-lang/libc/issues/3342 is fixed, define locally +// From +const SECCOMP_SET_MODE_FILTER: libc::c_int = 1; + // BPF structure definition for filter array. // See /usr/include/linux/filter.h . #[repr(C)] @@ -231,6 +235,11 @@ pub enum Error { EmptyFilter, /// System error related to calling `prctl`. Prctl(io::Error), + /// System error related to calling `seccomp` syscall. + Seccomp(io::Error), + /// Returned when calling `seccomp` with the thread sync flag (TSYNC) fails. Contains the pid + /// of the thread that caused the failure. + ThreadSync(libc::c_long), /// Json Frontend Error. #[cfg(feature = "json")] JsonFrontend(JsonFrontendError), @@ -243,6 +252,8 @@ impl std::error::Error for Error { match self { Backend(error) => Some(error), Prctl(error) => Some(error), + Seccomp(error) => Some(error), + ThreadSync(_) => None, #[cfg(feature = "json")] JsonFrontend(error) => Some(error), _ => None, @@ -264,6 +275,16 @@ impl Display for Error { Prctl(errno) => { write!(f, "Error calling `prctl`: {}", errno) } + Seccomp(errno) => { + write!(f, "Error calling `seccomp`: {}", errno) + } + ThreadSync(pid) => { + write!( + f, + "Seccomp filter synchronization failed in thread `{}`", + pid + ) + } #[cfg(feature = "json")] JsonFrontend(error) => { write!(f, "Json Frontend error: {}", error) @@ -292,6 +313,30 @@ impl From for Error { /// /// [`BpfProgram`]: type.BpfProgram.html pub fn apply_filter(bpf_filter: BpfProgramRef) -> Result<()> { + apply_filter_with_flags(bpf_filter, 0) +} + +/// Apply a BPF filter to the all threads in the process via the TSYNC feature. Please read the +/// man page for seccomp (`man 2 seccomp`) for more information. +/// +/// # 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, libc::SECCOMP_FILTER_FLAG_TSYNC) +} + +/// 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 seccomp's flags parameter. +/// +/// [`BpfProgram`]: type.BpfProgram.html +fn apply_filter_with_flags(bpf_filter: BpfProgramRef, flags: libc::c_ulong) -> Result<()> { // If the program is empty, don't install the filter. if bpf_filter.is_empty() { return Err(Error::EmptyFilter); @@ -314,14 +359,21 @@ 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, + SECCOMP_SET_MODE_FILTER, + flags, bpf_prog_ptr, ) }; - if rc != 0 { - return Err(Error::Prctl(io::Error::last_os_error())); + + #[allow(clippy::comparison_chain)] + // Per manpage, if TSYNC fails, retcode is >0 and equals the pid of the thread that caused the + // failure. Otherwise, error code is -1 and errno is set. + if rc < 0 { + return Err(Error::Seccomp(io::Error::last_os_error())); + } else if rc > 0 { + return Err(Error::ThreadSync(rc)); } Ok(()) diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index 6f51f453..167f8c52 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -714,7 +714,7 @@ fn test_filter_apply() { // Apply seccomp filter. assert!(matches!( apply_filter(&filter).unwrap_err(), - Error::Prctl(_) + Error::Seccomp(_) )); }) .join() @@ -756,7 +756,7 @@ fn test_filter_apply() { assert!(matches!( apply_filter(&filter).unwrap_err(), - Error::Prctl(_) + Error::Seccomp(_) )); // test that seccomp level remains 0 on failure. diff --git a/tests/multi_thread.rs b/tests/multi_thread.rs new file mode 100644 index 00000000..97e32ca6 --- /dev/null +++ b/tests/multi_thread.rs @@ -0,0 +1,97 @@ +#![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; + +fn check_getpid_fails() { + 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"); +} + +#[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); + + // first check getpid is working + let pid = unsafe { libc::getpid() }; + let errno = std::io::Error::last_os_error().raw_os_error().unwrap(); + + assert!(pid > 0, "getpid should return the actual pid"); + assert_eq!(errno, 0, "there should be no errors"); + + // create two threads, one which applies the filter to all threads and another which tries + // to call getpid. + 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(); + + // Verify seccomp is working in this thread + check_getpid_fails(); + + // 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(); + + // Verify seccomp is working in this thread after disallowing it in other thread + check_getpid_fails(); + + // 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() + ); + + // Verify seccomp is working in the parent thread as well + check_getpid_fails(); +}