-
Notifications
You must be signed in to change notification settings - Fork 16
Add support for seccomp thread sync feature #58
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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" | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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)] | ||
|
@@ -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, | ||
alindima marked this conversation as resolved.
Show resolved
Hide resolved
|
||
#[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<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<()> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ideally There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
|
@@ -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(()) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
#![allow(clippy::undocumented_unsafe_blocks)] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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(); | ||
} |
Uh oh!
There was an error while loading. Please reload this page.