Skip to content

Commit de60de6

Browse files
Altug Bozkurtaltugbozkurt07
Altug Bozkurt
authored andcommitted
lsm: cgroup attachment type support
1 parent 5942fe3 commit de60de6

File tree

19 files changed

+713
-129
lines changed

19 files changed

+713
-129
lines changed

aya-ebpf-macros/src/lib.rs

+45
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ mod fentry;
1010
mod fexit;
1111
mod kprobe;
1212
mod lsm;
13+
mod lsm_cgroup;
1314
mod map;
1415
mod perf_event;
1516
mod raw_tracepoint;
@@ -34,6 +35,7 @@ use fentry::FEntry;
3435
use fexit::FExit;
3536
use kprobe::{KProbe, KProbeKind};
3637
use lsm::Lsm;
38+
use lsm_cgroup::LsmCgroup;
3739
use map::Map;
3840
use perf_event::PerfEvent;
3941
use proc_macro::TokenStream;
@@ -326,6 +328,49 @@ pub fn lsm(attrs: TokenStream, item: TokenStream) -> TokenStream {
326328
.into()
327329
}
328330

331+
/// Marks a function as an LSM program that can be attached to cgroups.
332+
/// This program will only trigger for workloads in the attached cgroups.
333+
/// Used to implement security policy and audit logging.
334+
///
335+
/// The hook name is the first and only argument to the macro.
336+
///
337+
/// LSM probes can be attached to the kernel's security hooks to implement mandatory
338+
/// access control policy and security auditing.
339+
///
340+
/// LSM probes require a kernel compiled with `CONFIG_BPF_LSM=y` and `CONFIG_DEBUG_INFO_BTF=y`.
341+
/// In order for the probes to fire, you also need the BPF LSM to be enabled through your
342+
/// kernel's boot paramters (like `lsm=lockdown,yama,bpf`).
343+
///
344+
/// # Minimum kernel version
345+
///
346+
/// The minimum kernel version required to use this feature is 6.0.
347+
///
348+
/// # Examples
349+
///
350+
/// ```no_run
351+
/// use aya_ebpf::{macros::lsm_cgroup, programs::LsmContext};
352+
///
353+
/// #[lsm_cgroup(hook = "file_open")]
354+
/// pub fn file_open(ctx: LsmContext) -> i32 {
355+
/// match unsafe { try_file_open(ctx) } {
356+
/// Ok(ret) => ret,
357+
/// Err(ret) => ret,
358+
/// }
359+
/// }
360+
///
361+
/// unsafe fn try_file_open(_ctx: LsmContext) -> Result<i32, i32> {
362+
/// Err(0)
363+
/// }
364+
/// ```
365+
#[proc_macro_attribute]
366+
pub fn lsm_cgroup(attrs: TokenStream, item: TokenStream) -> TokenStream {
367+
match LsmCgroup::parse(attrs.into(), item.into()) {
368+
Ok(prog) => prog.expand(),
369+
Err(err) => err.into_compile_error(),
370+
}
371+
.into()
372+
}
373+
329374
/// Marks a function as a [BTF-enabled raw tracepoint][1] eBPF program that can be attached at
330375
/// a pre-defined kernel trace point.
331376
///

aya-ebpf-macros/src/lsm.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,10 @@ impl Lsm {
4444
} else {
4545
section_prefix.into()
4646
};
47+
let fn_name = &sig.ident;
4748
// LSM probes need to return an integer corresponding to the correct
4849
// policy decision. Therefore we do not simply default to a return value
4950
// of 0 as in other program types.
50-
let fn_name = &sig.ident;
5151
quote! {
5252
#[no_mangle]
5353
#[link_section = #section_name]

aya-ebpf-macros/src/lsm_cgroup.rs

+87
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
use std::borrow::Cow;
2+
3+
use proc_macro2::TokenStream;
4+
use quote::quote;
5+
use syn::{ItemFn, Result};
6+
7+
use crate::args::{err_on_unknown_args, pop_string_arg};
8+
9+
pub(crate) struct LsmCgroup {
10+
item: ItemFn,
11+
hook: Option<String>,
12+
}
13+
14+
impl LsmCgroup {
15+
pub(crate) fn parse(attrs: TokenStream, item: TokenStream) -> Result<Self> {
16+
let item = syn::parse2(item)?;
17+
let mut args = syn::parse2(attrs)?;
18+
let hook = pop_string_arg(&mut args, "hook");
19+
err_on_unknown_args(&args)?;
20+
21+
Ok(Self { item, hook })
22+
}
23+
24+
pub(crate) fn expand(&self) -> TokenStream {
25+
let Self { item, hook } = self;
26+
let ItemFn {
27+
attrs: _,
28+
vis,
29+
sig,
30+
block: _,
31+
} = item;
32+
let section_prefix = "lsm_cgroup";
33+
let section_name: Cow<'_, _> = if let Some(name) = hook {
34+
format!("{}/{}", section_prefix, name).into()
35+
} else {
36+
section_prefix.into()
37+
};
38+
let fn_name = &sig.ident;
39+
// LSM probes need to return an integer corresponding to the correct
40+
// policy decision. Therefore we do not simply default to a return value
41+
// of 0 as in other program types.
42+
quote! {
43+
#[no_mangle]
44+
#[link_section = #section_name]
45+
#vis fn #fn_name(ctx: *mut ::core::ffi::c_void) -> i32 {
46+
return #fn_name(::aya_ebpf::programs::LsmContext::new(ctx));
47+
48+
#item
49+
}
50+
}
51+
}
52+
}
53+
54+
#[cfg(test)]
55+
mod tests {
56+
use syn::parse_quote;
57+
58+
use super::*;
59+
60+
#[test]
61+
fn test_lsm_cgroup() {
62+
let prog = LsmCgroup::parse(
63+
parse_quote! {
64+
hook = "bprm_committed_creds",
65+
},
66+
parse_quote! {
67+
fn bprm_committed_creds(ctx: &mut ::aya_ebpf::programs::LsmContext) -> i32 {
68+
0
69+
}
70+
},
71+
)
72+
.unwrap();
73+
let expanded = prog.expand();
74+
let expected = quote! {
75+
#[no_mangle]
76+
#[link_section = "lsm_cgroup/bprm_committed_creds"]
77+
fn bprm_committed_creds(ctx: *mut ::core::ffi::c_void) -> i32 {
78+
return bprm_committed_creds(::aya_ebpf::programs::LsmContext::new(ctx));
79+
80+
fn bprm_committed_creds(ctx: &mut ::aya_ebpf::programs::LsmContext) -> i32 {
81+
0
82+
}
83+
}
84+
};
85+
assert_eq!(expected.to_string(), expanded.to_string());
86+
}
87+
}

aya-obj/src/obj.rs

+26-4
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,7 @@ pub enum ProgramSection {
275275
Lsm {
276276
sleepable: bool,
277277
},
278+
LsmCgroup,
278279
BtfTracePoint,
279280
FEntry {
280281
sleepable: bool,
@@ -436,6 +437,7 @@ impl FromStr for ProgramSection {
436437
"raw_tp" | "raw_tracepoint" => RawTracePoint,
437438
"lsm" => Lsm { sleepable: false },
438439
"lsm.s" => Lsm { sleepable: true },
440+
"lsm_cgroup" => LsmCgroup,
439441
"fentry" => FEntry { sleepable: false },
440442
"fentry.s" => FEntry { sleepable: true },
441443
"fexit" => FExit { sleepable: false },
@@ -2188,10 +2190,7 @@ mod tests {
21882190
assert_matches!(
21892191
obj.programs.get("foo"),
21902192
Some(Program {
2191-
section: ProgramSection::Lsm {
2192-
sleepable: false,
2193-
..
2194-
},
2193+
section: ProgramSection::Lsm { sleepable: false },
21952194
..
21962195
})
21972196
);
@@ -2223,6 +2222,29 @@ mod tests {
22232222
);
22242223
}
22252224

2225+
#[test]
2226+
fn test_parse_section_lsm_cgroup() {
2227+
let mut obj = fake_obj();
2228+
fake_sym(&mut obj, 0, 0, "foo", FAKE_INS_LEN);
2229+
2230+
assert_matches!(
2231+
obj.parse_section(fake_section(
2232+
EbpfSectionKind::Program,
2233+
"lsm_cgroup/foo",
2234+
bytes_of(&fake_ins()),
2235+
None
2236+
)),
2237+
Ok(())
2238+
);
2239+
assert_matches!(
2240+
obj.programs.get("foo"),
2241+
Some(Program {
2242+
section: ProgramSection::LsmCgroup { .. },
2243+
..
2244+
})
2245+
);
2246+
}
2247+
22262248
#[test]
22272249
fn test_parse_section_btf_tracepoint() {
22282250
let mut obj = fake_obj();

aya/src/bpf.rs

+7-2
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,9 @@ use crate::{
3030
programs::{
3131
BtfTracePoint, CgroupDevice, CgroupSkb, CgroupSkbAttachType, CgroupSock, CgroupSockAddr,
3232
CgroupSockopt, CgroupSysctl, Extension, FEntry, FExit, Iter, KProbe, LircMode2, Lsm,
33-
PerfEvent, ProbeKind, Program, ProgramData, ProgramError, RawTracePoint, SchedClassifier,
34-
SkLookup, SkMsg, SkSkb, SkSkbKind, SockOps, SocketFilter, TracePoint, UProbe, Xdp,
33+
LsmCgroup, PerfEvent, ProbeKind, Program, ProgramData, ProgramError, RawTracePoint,
34+
SchedClassifier, SkLookup, SkMsg, SkSkb, SkSkbKind, SockOps, SocketFilter, TracePoint,
35+
UProbe, Xdp,
3536
},
3637
sys::{
3738
bpf_load_btf, is_bpf_cookie_supported, is_bpf_global_data_supported,
@@ -412,6 +413,7 @@ impl<'a> EbpfLoader<'a> {
412413
| ProgramSection::FEntry { sleepable: _ }
413414
| ProgramSection::FExit { sleepable: _ }
414415
| ProgramSection::Lsm { sleepable: _ }
416+
| ProgramSection::LsmCgroup
415417
| ProgramSection::BtfTracePoint
416418
| ProgramSection::Iter { sleepable: _ } => {
417419
return Err(EbpfError::BtfError(err))
@@ -657,6 +659,9 @@ impl<'a> EbpfLoader<'a> {
657659
}
658660
Program::Lsm(Lsm { data })
659661
}
662+
ProgramSection::LsmCgroup => Program::LsmCgroup(LsmCgroup {
663+
data: ProgramData::new(prog_name, obj, btf_fd, *verifier_log_level),
664+
}),
660665
ProgramSection::BtfTracePoint => Program::BtfTracePoint(BtfTracePoint {
661666
data: ProgramData::new(prog_name, obj, btf_fd, *verifier_log_level),
662667
}),

aya/src/programs/lsm_cgroup.rs

+105
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
//! LSM probes.
2+
3+
use std::os::fd::AsFd;
4+
5+
use crate::{
6+
generated::{bpf_attach_type::BPF_LSM_CGROUP, bpf_prog_type::BPF_PROG_TYPE_LSM},
7+
obj::btf::{Btf, BtfKind},
8+
programs::{define_link_wrapper, load_program, FdLink, FdLinkId, ProgramData, ProgramError},
9+
sys::{bpf_link_create, BpfLinkCreateArgs, LinkTarget, SyscallError},
10+
};
11+
12+
/// A program that attaches to Linux LSM hooks with per-cgroup attachment type. Used to implement security policy and
13+
/// audit logging.
14+
///
15+
/// LSM probes can be attached to the kernel's [security hooks][1] to implement mandatory
16+
/// access control policy and security auditing.
17+
///
18+
/// LSM probes require a kernel compiled with `CONFIG_BPF_LSM=y` and `CONFIG_DEBUG_INFO_BTF=y`.
19+
/// In order for the probes to fire, you also need the BPF LSM to be enabled through your
20+
/// kernel's boot paramters (like `lsm=lockdown,yama,bpf`).
21+
///
22+
/// # Minimum kernel version
23+
///
24+
/// The minimum kernel version required to use this feature is 6.0.
25+
///
26+
/// # Examples
27+
///
28+
/// ```no_run
29+
/// # #[derive(thiserror::Error, Debug)]
30+
/// # enum LsmError {
31+
/// # #[error(transparent)]
32+
/// # BtfError(#[from] aya::BtfError),
33+
/// # #[error(transparent)]
34+
/// # Program(#[from] aya::programs::ProgramError),
35+
/// # #[error(transparent)]
36+
/// # Ebpf(#[from] aya::EbpfError),
37+
/// # }
38+
/// # let mut bpf = Ebpf::load_file("ebpf_programs.o")?;
39+
/// use aya::{Ebpf, programs::LsmCgroup, BtfError, Btf};
40+
/// use std::fs::File;
41+
///
42+
/// let btf = Btf::from_sys_fs()?;
43+
/// let file = File::open("/sys/fs/cgroup/unified").unwrap();
44+
/// let program: &mut LsmCgroup = bpf.program_mut("lsm_prog").unwrap().try_into()?;
45+
/// program.load("security_bprm_exec", &btf)?;
46+
/// program.attach(file)?;
47+
/// # Ok::<(), LsmError>(())
48+
/// ```
49+
///
50+
/// [1]: https://elixir.bootlin.com/linux/latest/source/include/linux/lsm_hook_defs.h
51+
#[derive(Debug)]
52+
#[doc(alias = "BPF_PROG_TYPE_LSM")]
53+
pub struct LsmCgroup {
54+
pub(crate) data: ProgramData<LsmLink>,
55+
}
56+
57+
impl LsmCgroup {
58+
/// Loads the program inside the kernel.
59+
///
60+
/// # Arguments
61+
///
62+
/// * `lsm_hook_name` - full name of the LSM hook that the program should
63+
/// be attached to
64+
pub fn load(&mut self, lsm_hook_name: &str, btf: &Btf) -> Result<(), ProgramError> {
65+
self.data.expected_attach_type = Some(BPF_LSM_CGROUP);
66+
let type_name = format!("bpf_lsm_{lsm_hook_name}");
67+
self.data.attach_btf_id =
68+
Some(btf.id_by_type_name_kind(type_name.as_str(), BtfKind::Func)?);
69+
load_program(BPF_PROG_TYPE_LSM, &mut self.data)
70+
}
71+
72+
/// Attaches the program.
73+
///
74+
/// The returned value can be used to detach, see [LsmCgroup::detach].
75+
pub fn attach<T: AsFd>(&mut self, cgroup: T) -> Result<LsmLinkId, ProgramError> {
76+
let prog_fd = self.fd()?;
77+
let prog_fd = prog_fd.as_fd();
78+
let cgroup_fd = cgroup.as_fd();
79+
let attach_type = self.data.expected_attach_type.unwrap();
80+
let btf_id = self.data.attach_btf_id.ok_or(ProgramError::NotLoaded)?;
81+
let link_fd = bpf_link_create(
82+
prog_fd,
83+
LinkTarget::Fd(cgroup_fd),
84+
attach_type,
85+
0,
86+
Some(BpfLinkCreateArgs::TargetBtfId(btf_id)),
87+
)
88+
.map_err(|(_, io_error)| SyscallError {
89+
call: "bpf_link_create",
90+
io_error,
91+
})?;
92+
93+
self.data.links.insert(LsmLink::new(FdLink::new(link_fd)))
94+
}
95+
}
96+
97+
define_link_wrapper!(
98+
/// The link used by [LsmCgroup] programs.
99+
LsmLink,
100+
/// The type returned by [LsmCgroup::attach]. Can be passed to [LsmCgroup::detach].
101+
LsmLinkId,
102+
FdLink,
103+
FdLinkId,
104+
LsmCgroup,
105+
);

0 commit comments

Comments
 (0)