lsm :: cgroup attachment type support

reviewable/pr1135/r3
Altug Bozkurt 1 week ago committed by altug bozkurt
parent 29b821376e
commit 97a52dd30f

@ -10,6 +10,7 @@ mod fentry;
mod fexit;
mod kprobe;
mod lsm;
mod lsm_cgroup;
mod map;
mod perf_event;
mod raw_tracepoint;
@ -34,6 +35,7 @@ use fentry::FEntry;
use fexit::FExit;
use kprobe::{KProbe, KProbeKind};
use lsm::Lsm;
use lsm_cgroup::LsmCgroup;
use map::Map;
use perf_event::PerfEvent;
use proc_macro::TokenStream;
@ -326,6 +328,49 @@ pub fn lsm(attrs: TokenStream, item: TokenStream) -> TokenStream {
.into()
}
/// Marks a function as an LSM program that can be attached to cgroups.
/// This program will only trigger for workloads in the attached cgroups.
/// Used to implement security policy and audit logging.
///
/// The hook name is the first and only argument to the macro.
///
/// LSM probes can be attached to the kernel's security hooks 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
/// use aya_ebpf::{macros::lsm_cgroup, programs::LsmContext};
///
/// #[lsm_cgroup(hook = "file_open")]
/// pub fn file_open(ctx: LsmContext) -> i32 {
/// match unsafe { try_file_open(ctx) } {
/// Ok(ret) => ret,
/// Err(ret) => ret,
/// }
/// }
///
/// unsafe fn try_file_open(_ctx: LsmContext) -> Result<i32, i32> {
/// Err(0)
/// }
/// ```
#[proc_macro_attribute]
pub fn lsm_cgroup(attrs: TokenStream, item: TokenStream) -> TokenStream {
match LsmCgroup::parse(attrs.into(), item.into()) {
Ok(prog) => prog.expand(),
Err(err) => err.into_compile_error(),
}
.into()
}
/// Marks a function as a [BTF-enabled raw tracepoint][1] eBPF program that can be attached at
/// a pre-defined kernel trace point.
///

@ -44,10 +44,10 @@ impl Lsm {
} 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.
let fn_name = &sig.ident;
quote! {
#[no_mangle]
#[link_section = #section_name]

@ -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());
}
}

@ -275,6 +275,7 @@ pub enum ProgramSection {
Lsm {
sleepable: bool,
},
LsmCgroup,
BtfTracePoint,
FEntry {
sleepable: bool,
@ -436,6 +437,7 @@ impl FromStr for ProgramSection {
"raw_tp" | "raw_tracepoint" => RawTracePoint,
"lsm" => Lsm { sleepable: false },
"lsm.s" => Lsm { sleepable: true },
"lsm_cgroup" => LsmCgroup,
"fentry" => FEntry { sleepable: false },
"fentry.s" => FEntry { sleepable: true },
"fexit" => FExit { sleepable: false },
@ -2188,10 +2190,7 @@ mod tests {
assert_matches!(
obj.programs.get("foo"),
Some(Program {
section: ProgramSection::Lsm {
sleepable: false,
..
},
section: ProgramSection::Lsm { sleepable: false },
..
})
);
@ -2223,6 +2222,29 @@ mod tests {
);
}
#[test]
fn test_parse_section_lsm_cgroup() {
let mut obj = fake_obj();
fake_sym(&mut obj, 0, 0, "foo", FAKE_INS_LEN);
assert_matches!(
obj.parse_section(fake_section(
EbpfSectionKind::Program,
"lsm_cgroup/foo",
bytes_of(&fake_ins()),
None
)),
Ok(())
);
assert_matches!(
obj.programs.get("foo"),
Some(Program {
section: ProgramSection::LsmCgroup { .. },
..
})
);
}
#[test]
fn test_parse_section_btf_tracepoint() {
let mut obj = fake_obj();

@ -30,8 +30,9 @@ use crate::{
programs::{
BtfTracePoint, CgroupDevice, CgroupSkb, CgroupSkbAttachType, CgroupSock, CgroupSockAddr,
CgroupSockopt, CgroupSysctl, Extension, FEntry, FExit, Iter, KProbe, LircMode2, Lsm,
PerfEvent, ProbeKind, Program, ProgramData, ProgramError, RawTracePoint, SchedClassifier,
SkLookup, SkMsg, SkSkb, SkSkbKind, SockOps, SocketFilter, TracePoint, UProbe, Xdp,
LsmCgroup, PerfEvent, ProbeKind, Program, ProgramData, ProgramError, RawTracePoint,
SchedClassifier, SkLookup, SkMsg, SkSkb, SkSkbKind, SockOps, SocketFilter, TracePoint,
UProbe, Xdp,
},
sys::{
bpf_load_btf, is_bpf_cookie_supported, is_bpf_global_data_supported,
@ -412,6 +413,7 @@ impl<'a> EbpfLoader<'a> {
| ProgramSection::FEntry { sleepable: _ }
| ProgramSection::FExit { sleepable: _ }
| ProgramSection::Lsm { sleepable: _ }
| ProgramSection::LsmCgroup
| ProgramSection::BtfTracePoint
| ProgramSection::Iter { sleepable: _ } => {
return Err(EbpfError::BtfError(err))
@ -657,6 +659,9 @@ impl<'a> EbpfLoader<'a> {
}
Program::Lsm(Lsm { data })
}
ProgramSection::LsmCgroup => Program::LsmCgroup(LsmCgroup {
data: ProgramData::new(prog_name, obj, btf_fd, *verifier_log_level),
}),
ProgramSection::BtfTracePoint => Program::BtfTracePoint(BtfTracePoint {
data: ProgramData::new(prog_name, obj, btf_fd, *verifier_log_level),
}),

@ -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,
);

@ -56,6 +56,7 @@ pub mod kprobe;
pub mod links;
pub mod lirc_mode2;
pub mod lsm;
pub mod lsm_cgroup;
pub mod perf_attach;
pub mod perf_event;
pub mod raw_trace_point;
@ -100,6 +101,7 @@ pub use crate::programs::{
links::{CgroupAttachMode, Link, LinkOrder},
lirc_mode2::LircMode2,
lsm::Lsm,
lsm_cgroup::LsmCgroup,
perf_event::{PerfEvent, PerfEventScope, PerfTypeId, SamplePolicy},
probe::ProbeKind,
raw_trace_point::RawTracePoint,
@ -295,6 +297,8 @@ pub enum Program {
RawTracePoint(RawTracePoint),
/// A [`Lsm`] program
Lsm(Lsm),
/// A [`LsmCgroup`] program
LsmCgroup(LsmCgroup),
/// A [`BtfTracePoint`] program
BtfTracePoint(BtfTracePoint),
/// A [`FEntry`] program
@ -332,6 +336,7 @@ impl Program {
Self::PerfEvent(_) => ProgramType::PerfEvent,
Self::RawTracePoint(_) => ProgramType::RawTracePoint,
Self::Lsm(_) => ProgramType::Lsm,
Self::LsmCgroup(_) => ProgramType::Lsm,
// The following program types are a subset of `TRACING` programs:
//
// - `BPF_TRACE_RAW_TP` (`BtfTracePoint`)
@ -371,6 +376,7 @@ impl Program {
Self::PerfEvent(p) => p.pin(path),
Self::RawTracePoint(p) => p.pin(path),
Self::Lsm(p) => p.pin(path),
Self::LsmCgroup(p) => p.pin(path),
Self::BtfTracePoint(p) => p.pin(path),
Self::FEntry(p) => p.pin(path),
Self::FExit(p) => p.pin(path),
@ -402,6 +408,7 @@ impl Program {
Self::PerfEvent(mut p) => p.unload(),
Self::RawTracePoint(mut p) => p.unload(),
Self::Lsm(mut p) => p.unload(),
Self::LsmCgroup(mut p) => p.unload(),
Self::BtfTracePoint(mut p) => p.unload(),
Self::FEntry(mut p) => p.unload(),
Self::FExit(mut p) => p.unload(),
@ -435,6 +442,7 @@ impl Program {
Self::PerfEvent(p) => p.fd(),
Self::RawTracePoint(p) => p.fd(),
Self::Lsm(p) => p.fd(),
Self::LsmCgroup(p) => p.fd(),
Self::BtfTracePoint(p) => p.fd(),
Self::FEntry(p) => p.fd(),
Self::FExit(p) => p.fd(),
@ -469,6 +477,7 @@ impl Program {
Self::PerfEvent(p) => p.info(),
Self::RawTracePoint(p) => p.info(),
Self::Lsm(p) => p.info(),
Self::LsmCgroup(p) => p.info(),
Self::BtfTracePoint(p) => p.info(),
Self::FEntry(p) => p.info(),
Self::FExit(p) => p.info(),
@ -780,6 +789,7 @@ impl_program_unload!(
LircMode2,
PerfEvent,
Lsm,
LsmCgroup,
RawTracePoint,
BtfTracePoint,
FEntry,
@ -821,6 +831,7 @@ impl_fd!(
LircMode2,
PerfEvent,
Lsm,
LsmCgroup,
RawTracePoint,
BtfTracePoint,
FEntry,
@ -927,6 +938,7 @@ impl_program_pin!(
LircMode2,
PerfEvent,
Lsm,
LsmCgroup,
RawTracePoint,
BtfTracePoint,
FEntry,
@ -966,8 +978,9 @@ impl_from_pin!(
SkMsg,
CgroupSysctl,
LircMode2,
PerfEvent,
Lsm,
LsmCgroup,
PerfEvent,
RawTracePoint,
BtfTracePoint,
FEntry,
@ -1023,6 +1036,7 @@ impl_try_from_program!(
LircMode2,
PerfEvent,
Lsm,
LsmCgroup,
RawTracePoint,
BtfTracePoint,
FEntry,
@ -1050,6 +1064,7 @@ impl_info!(
LircMode2,
PerfEvent,
Lsm,
LsmCgroup,
RawTracePoint,
BtfTracePoint,
FEntry,

@ -3,8 +3,8 @@
use aya_ebpf::{
bindings::xdp_action,
macros::{kprobe, kretprobe, tracepoint, uprobe, uretprobe, xdp},
programs::{ProbeContext, RetProbeContext, TracePointContext, XdpContext},
macros::{kprobe, kretprobe, lsm, lsm_cgroup, tracepoint, uprobe, uretprobe, xdp},
programs::{LsmContext, ProbeContext, RetProbeContext, TracePointContext, XdpContext},
};
#[xdp]
@ -44,6 +44,16 @@ pub fn test_uretprobe(_ctx: RetProbeContext) -> u32 {
0
}
#[lsm_cgroup(hook = "socket_bind")]
pub fn test_lsmcgroup(_ctx: LsmContext) -> i32 {
0
}
#[lsm(hook = "socket_bind")]
pub fn test_lsm(_ctx: LsmContext) -> i32 {
-1
}
#[cfg(not(test))]
#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {

@ -28,6 +28,7 @@ test-case = { workspace = true }
test-log = { workspace = true, features = ["log"] }
tokio = { workspace = true, features = ["macros", "rt-multi-thread", "time"] }
xdpilone = { workspace = true }
nix = { workspace = true, features = ["process"] }
[build-dependencies]
anyhow = { workspace = true }

@ -5,6 +5,7 @@ mod info;
mod iter;
mod load;
mod log;
mod lsm;
mod raw_tracepoint;
mod rbpf;
mod relocations;

@ -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…
Cancel
Save