mirror of https://github.com/aya-rs/aya
lsm :: cgroup attachment type support
parent
29b821376e
commit
97a52dd30f
@ -0,0 +1,87 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::quote;
|
||||
use syn::{ItemFn, Result};
|
||||
|
||||
use crate::args::{err_on_unknown_args, pop_string_arg};
|
||||
|
||||
pub(crate) struct LsmCgroup {
|
||||
item: ItemFn,
|
||||
hook: Option<String>,
|
||||
}
|
||||
|
||||
impl LsmCgroup {
|
||||
pub(crate) fn parse(attrs: TokenStream, item: TokenStream) -> Result<Self> {
|
||||
let item = syn::parse2(item)?;
|
||||
let mut args = syn::parse2(attrs)?;
|
||||
let hook = pop_string_arg(&mut args, "hook");
|
||||
err_on_unknown_args(&args)?;
|
||||
|
||||
Ok(Self { item, hook })
|
||||
}
|
||||
|
||||
pub(crate) fn expand(&self) -> TokenStream {
|
||||
let Self { item, hook } = self;
|
||||
let ItemFn {
|
||||
attrs: _,
|
||||
vis,
|
||||
sig,
|
||||
block: _,
|
||||
} = item;
|
||||
let section_prefix = "lsm_cgroup";
|
||||
let section_name: Cow<'_, _> = if let Some(name) = hook {
|
||||
format!("{}/{}", section_prefix, name).into()
|
||||
} else {
|
||||
section_prefix.into()
|
||||
};
|
||||
let fn_name = &sig.ident;
|
||||
// LSM probes need to return an integer corresponding to the correct
|
||||
// policy decision. Therefore we do not simply default to a return value
|
||||
// of 0 as in other program types.
|
||||
quote! {
|
||||
#[no_mangle]
|
||||
#[link_section = #section_name]
|
||||
#vis fn #fn_name(ctx: *mut ::core::ffi::c_void) -> i32 {
|
||||
return #fn_name(::aya_ebpf::programs::LsmContext::new(ctx));
|
||||
|
||||
#item
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use syn::parse_quote;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_lsm_cgroup() {
|
||||
let prog = LsmCgroup::parse(
|
||||
parse_quote! {
|
||||
hook = "bprm_committed_creds",
|
||||
},
|
||||
parse_quote! {
|
||||
fn bprm_committed_creds(ctx: &mut ::aya_ebpf::programs::LsmContext) -> i32 {
|
||||
0
|
||||
}
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let expanded = prog.expand();
|
||||
let expected = quote! {
|
||||
#[no_mangle]
|
||||
#[link_section = "lsm_cgroup/bprm_committed_creds"]
|
||||
fn bprm_committed_creds(ctx: *mut ::core::ffi::c_void) -> i32 {
|
||||
return bprm_committed_creds(::aya_ebpf::programs::LsmContext::new(ctx));
|
||||
|
||||
fn bprm_committed_creds(ctx: &mut ::aya_ebpf::programs::LsmContext) -> i32 {
|
||||
0
|
||||
}
|
||||
}
|
||||
};
|
||||
assert_eq!(expected.to_string(), expanded.to_string());
|
||||
}
|
||||
}
|
@ -0,0 +1,105 @@
|
||||
//! LSM probes.
|
||||
|
||||
use std::os::fd::AsFd;
|
||||
|
||||
use crate::{
|
||||
generated::{bpf_attach_type::BPF_LSM_CGROUP, bpf_prog_type::BPF_PROG_TYPE_LSM},
|
||||
obj::btf::{Btf, BtfKind},
|
||||
programs::{define_link_wrapper, load_program, FdLink, FdLinkId, ProgramData, ProgramError},
|
||||
sys::{bpf_link_create, BpfLinkCreateArgs, LinkTarget, SyscallError},
|
||||
};
|
||||
|
||||
/// A program that attaches to Linux LSM hooks with per-cgroup attachment type. Used to implement security policy and
|
||||
/// audit logging.
|
||||
///
|
||||
/// LSM probes can be attached to the kernel's [security hooks][1] to implement mandatory
|
||||
/// access control policy and security auditing.
|
||||
///
|
||||
/// LSM probes require a kernel compiled with `CONFIG_BPF_LSM=y` and `CONFIG_DEBUG_INFO_BTF=y`.
|
||||
/// In order for the probes to fire, you also need the BPF LSM to be enabled through your
|
||||
/// kernel's boot paramters (like `lsm=lockdown,yama,bpf`).
|
||||
///
|
||||
/// # Minimum kernel version
|
||||
///
|
||||
/// The minimum kernel version required to use this feature is 6.0.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// # #[derive(thiserror::Error, Debug)]
|
||||
/// # enum LsmError {
|
||||
/// # #[error(transparent)]
|
||||
/// # BtfError(#[from] aya::BtfError),
|
||||
/// # #[error(transparent)]
|
||||
/// # Program(#[from] aya::programs::ProgramError),
|
||||
/// # #[error(transparent)]
|
||||
/// # Ebpf(#[from] aya::EbpfError),
|
||||
/// # }
|
||||
/// # let mut bpf = Ebpf::load_file("ebpf_programs.o")?;
|
||||
/// use aya::{Ebpf, programs::LsmCgroup, BtfError, Btf};
|
||||
/// use std::fs::File;
|
||||
///
|
||||
/// let btf = Btf::from_sys_fs()?;
|
||||
/// let file = File::open("/sys/fs/cgroup/unified").unwrap();
|
||||
/// let program: &mut LsmCgroup = bpf.program_mut("lsm_prog").unwrap().try_into()?;
|
||||
/// program.load("security_bprm_exec", &btf)?;
|
||||
/// program.attach(file)?;
|
||||
/// # Ok::<(), LsmError>(())
|
||||
/// ```
|
||||
///
|
||||
/// [1]: https://elixir.bootlin.com/linux/latest/source/include/linux/lsm_hook_defs.h
|
||||
#[derive(Debug)]
|
||||
#[doc(alias = "BPF_PROG_TYPE_LSM")]
|
||||
pub struct LsmCgroup {
|
||||
pub(crate) data: ProgramData<LsmLink>,
|
||||
}
|
||||
|
||||
impl LsmCgroup {
|
||||
/// Loads the program inside the kernel.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `lsm_hook_name` - full name of the LSM hook that the program should
|
||||
/// be attached to
|
||||
pub fn load(&mut self, lsm_hook_name: &str, btf: &Btf) -> Result<(), ProgramError> {
|
||||
self.data.expected_attach_type = Some(BPF_LSM_CGROUP);
|
||||
let type_name = format!("bpf_lsm_{lsm_hook_name}");
|
||||
self.data.attach_btf_id =
|
||||
Some(btf.id_by_type_name_kind(type_name.as_str(), BtfKind::Func)?);
|
||||
load_program(BPF_PROG_TYPE_LSM, &mut self.data)
|
||||
}
|
||||
|
||||
/// Attaches the program.
|
||||
///
|
||||
/// The returned value can be used to detach, see [LsmCgroup::detach].
|
||||
pub fn attach<T: AsFd>(&mut self, cgroup: T) -> Result<LsmLinkId, ProgramError> {
|
||||
let prog_fd = self.fd()?;
|
||||
let prog_fd = prog_fd.as_fd();
|
||||
let cgroup_fd = cgroup.as_fd();
|
||||
let attach_type = self.data.expected_attach_type.unwrap();
|
||||
let btf_id = self.data.attach_btf_id.ok_or(ProgramError::NotLoaded)?;
|
||||
let link_fd = bpf_link_create(
|
||||
prog_fd,
|
||||
LinkTarget::Fd(cgroup_fd),
|
||||
attach_type,
|
||||
0,
|
||||
Some(BpfLinkCreateArgs::TargetBtfId(btf_id)),
|
||||
)
|
||||
.map_err(|(_, io_error)| SyscallError {
|
||||
call: "bpf_link_create",
|
||||
io_error,
|
||||
})?;
|
||||
|
||||
self.data.links.insert(LsmLink::new(FdLink::new(link_fd)))
|
||||
}
|
||||
}
|
||||
|
||||
define_link_wrapper!(
|
||||
/// The link used by [LsmCgroup] programs.
|
||||
LsmLink,
|
||||
/// The type returned by [LsmCgroup::attach]. Can be passed to [LsmCgroup::detach].
|
||||
LsmLinkId,
|
||||
FdLink,
|
||||
FdLinkId,
|
||||
LsmCgroup,
|
||||
);
|
@ -0,0 +1,83 @@
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{ErrorKind, Write},
|
||||
net::{Ipv4Addr, SocketAddr, SocketAddrV4, TcpListener},
|
||||
path::Path,
|
||||
};
|
||||
|
||||
use aya::{
|
||||
programs::{lsm_cgroup::LsmCgroup, Lsm},
|
||||
util::KernelVersion,
|
||||
Btf, Ebpf,
|
||||
};
|
||||
use nix::{
|
||||
sys::wait::waitpid,
|
||||
unistd::{fork, getpid, ForkResult},
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn lsm_cgroup() {
|
||||
let kernel_version = KernelVersion::current().unwrap();
|
||||
if kernel_version < KernelVersion::new(6, 0, 0) {
|
||||
eprintln!("skipping lsm_cgroup test on kernel {kernel_version:?}");
|
||||
return;
|
||||
}
|
||||
|
||||
let mut bpf: Ebpf = Ebpf::load(crate::TEST).unwrap();
|
||||
let prog: &mut LsmCgroup = bpf
|
||||
.program_mut("test_lsmcgroup")
|
||||
.unwrap()
|
||||
.try_into()
|
||||
.unwrap();
|
||||
let btf = Btf::from_sys_fs().expect("could not get btf from sys");
|
||||
prog.load("socket_bind", &btf).unwrap();
|
||||
|
||||
let cgroup_path = Path::new("/sys/fs/cgroup/lsm_cgroup_test");
|
||||
if !cgroup_path.exists() {
|
||||
std::fs::create_dir_all(cgroup_path).expect("could not create the cgroup dir");
|
||||
}
|
||||
|
||||
let _ = prog.attach(File::open(cgroup_path).unwrap()).unwrap();
|
||||
|
||||
match unsafe { fork().expect("Failed to fork process") } {
|
||||
ForkResult::Parent { child } => {
|
||||
waitpid(Some(child), None).unwrap();
|
||||
|
||||
let pid = getpid();
|
||||
|
||||
let mut f = File::create(cgroup_path.join("cgroup.procs"))
|
||||
.expect("could not open cgroup procs");
|
||||
f.write_fmt(format_args!("{}", pid.as_raw() as u64))
|
||||
.expect("could not write into procs file");
|
||||
|
||||
assert_matches::assert_matches!(TcpListener::bind("127.0.0.1:12345"), Err(e) => assert_eq!(
|
||||
e.kind(), ErrorKind::PermissionDenied)
|
||||
);
|
||||
}
|
||||
ForkResult::Child => {
|
||||
assert_matches::assert_matches!(TcpListener::bind("127.0.0.1:12345"), Ok(listener) => assert_eq!(
|
||||
listener.local_addr().unwrap(), SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 12345)))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lsm() {
|
||||
let kernel_version = KernelVersion::current().unwrap();
|
||||
if kernel_version < KernelVersion::new(5, 7, 0) {
|
||||
eprintln!("skipping lsm test on kernel {kernel_version:?}");
|
||||
return;
|
||||
}
|
||||
|
||||
let mut bpf: Ebpf = Ebpf::load(crate::TEST).unwrap();
|
||||
let prog: &mut Lsm = bpf.program_mut("test_lsm").unwrap().try_into().unwrap();
|
||||
let btf = Btf::from_sys_fs().expect("could not get btf from sys");
|
||||
prog.load("socket_bind", &btf).unwrap();
|
||||
|
||||
prog.attach().unwrap();
|
||||
|
||||
assert_matches::assert_matches!(TcpListener::bind("127.0.0.1:12345"), Err(e) => assert_eq!(
|
||||
e.kind(), ErrorKind::PermissionDenied)
|
||||
);
|
||||
}
|
Loading…
Reference in New Issue