Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for seccomp thread sync feature #58

Merged
merged 1 commit into from
Sep 12, 2023
Merged
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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
# Upcoming Release
alindima marked this conversation as resolved.
Show resolved Hide resolved

- 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
Expand Down
2 changes: 1 addition & 1 deletion coverage_config_x86_64.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"coverage_score": 93.6,
"coverage_score": 93.0,
"exclude_path": "tests/integration_tests.rs,tests/json.rs",
"crate_features": "json"
}
62 changes: 57 additions & 5 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 <linux/seccomp.h>
const SECCOMP_SET_MODE_FILTER: libc::c_int = 1;

// BPF structure definition for filter array.
// See /usr/include/linux/filter.h .
#[repr(C)]
Expand All @@ -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),
Expand All @@ -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,
alindima marked this conversation as resolved.
Show resolved Hide resolved
#[cfg(feature = "json")]
JsonFrontend(error) => Some(error),
_ => None,
Expand All @@ -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)
Expand Down Expand Up @@ -292,6 +313,30 @@ impl From<JsonFrontendError> 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<()> {
alindima marked this conversation as resolved.
Show resolved Hide resolved
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<()> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally flags should be similar to something like OFlags in nix: https://docs.rs/nix/latest/nix/fcntl/struct.OFlag.html, it's slightly more robust, but if apply_filter_with_flags remain just for internal purposes (ie. flags is not part of the public API), then leaving it like this for now is fine.

Copy link
Collaborator

@alindima alindima Sep 8, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was the initial approach in the PR but since different flags for the seccomp syscall results in different return types, I suggested to keep this function for internal use only, to keep things simple and hard to misuse

// If the program is empty, don't install the filter.
if bpf_filter.is_empty() {
return Err(Error::EmptyFilter);
Expand All @@ -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(())
Expand Down
4 changes: 2 additions & 2 deletions tests/integration_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -714,7 +714,7 @@ fn test_filter_apply() {
// Apply seccomp filter.
assert!(matches!(
apply_filter(&filter).unwrap_err(),
Error::Prctl(_)
Error::Seccomp(_)
));
})
.join()
Expand Down Expand Up @@ -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.
Expand Down
97 changes: 97 additions & 0 deletions tests/multi_thread.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
#![allow(clippy::undocumented_unsafe_blocks)]
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test was adapted from my tsync test in extrasafe. https://github.com/boustrophedon/extrasafe/blob/master/tests/thread_multi.rs


/// 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<i64, Vec<SeccompRule>> = 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();

alindima marked this conversation as resolved.
Show resolved Hide resolved
let filter: BpfProgram = filter.try_into().unwrap();
apply_filter_all_threads(&filter).unwrap();
boustrophedon marked this conversation as resolved.
Show resolved Hide resolved

// Verify seccomp is working in this thread
check_getpid_fails();

// seccomp setup done, let the other thread start
setup_tx.send(()).unwrap();

alindima marked this conversation as resolved.
Show resolved Hide resolved
// 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();
}