From 3787b4be44dcc243865bfc0ac954316775712ae2 Mon Sep 17 00:00:00 2001 From: Altug Bozkurt Date: Thu, 9 Jan 2025 16:39:43 +0300 Subject: [PATCH] lsm-cgroup: attachment type support --- Cargo.toml | 2 +- aya-ebpf-macros/src/lsm.rs | 100 ++++++++++++++++++------- aya-obj/src/obj.rs | 36 ++++++++- aya-obj/src/programs/lsm.rs | 21 ++++++ aya-obj/src/programs/mod.rs | 2 + aya/src/bpf.rs | 9 ++- aya/src/programs/lsm.rs | 76 +++++++++++++++++-- aya/src/programs/mod.rs | 1 - test/integration-ebpf/src/test.rs | 9 ++- test/integration-test/Cargo.toml | 1 + test/integration-test/src/tests.rs | 1 + test/integration-test/src/tests/lsm.rs | 54 +++++++++++++ 12 files changed, 268 insertions(+), 44 deletions(-) create mode 100644 aya-obj/src/programs/lsm.rs create mode 100644 test/integration-test/src/tests/lsm.rs diff --git a/Cargo.toml b/Cargo.toml index b49d2652..baa6b920 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -78,7 +78,7 @@ indoc = { version = "2.0", default-features = false } libc = { version = "0.2.105", default-features = false } log = { version = "0.4", default-features = false } netns-rs = { version = "0.1", default-features = false } -nix = { version = "0.29.0", default-features = false } +nix = { version = "0.29.0", default-features = true } num_enum = { version = "0.7", default-features = false } object = { version = "0.36", default-features = false } octorust = { version = "0.9.0", default-features = false } diff --git a/aya-ebpf-macros/src/lsm.rs b/aya-ebpf-macros/src/lsm.rs index 2eb53dcd..2691ad5c 100644 --- a/aya-ebpf-macros/src/lsm.rs +++ b/aya-ebpf-macros/src/lsm.rs @@ -9,6 +9,7 @@ use crate::args::{err_on_unknown_args, pop_bool_arg, pop_string_arg}; pub(crate) struct Lsm { item: ItemFn, hook: Option, + cgroup: bool, sleepable: bool, } @@ -18,46 +19,64 @@ impl Lsm { let mut args = syn::parse2(attrs)?; let hook = pop_string_arg(&mut args, "hook"); let sleepable = pop_bool_arg(&mut args, "sleepable"); + let cgroup = pop_bool_arg(&mut args, "cgroup"); err_on_unknown_args(&args)?; + Ok(Self { item, hook, + cgroup, sleepable, }) } pub(crate) fn expand(&self) -> TokenStream { - let Self { - item, - hook, - sleepable, - } = self; - let ItemFn { - attrs: _, - vis, - sig, - block: _, - } = item; - let section_prefix = if *sleepable { "lsm.s" } else { "lsm" }; - let section_name: Cow<'_, _> = if let Some(hook) = hook { - format!("{}/{}", section_prefix, hook).into() + if self.cgroup{ + let section_name = if let Some(name) = &self.hook{ + format!("lsm_cgroup/{}", name) + } else { + ("lsm_cgroup").to_owned() + }; + + let fn_name = &self.item.sig.ident; + let item = &self.item; + + quote! { + #[no_mangle] + #[link_section = #section_name] + fn #fn_name(ctx: *mut ::core::ffi::c_void) -> i32 { + return #fn_name(::aya_ebpf::programs::LsmContext::new(ctx)); + + #item + } + } } else { - section_prefix.into() - }; - // 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] - #vis fn #fn_name(ctx: *mut ::core::ffi::c_void) -> i32 { - return #fn_name(::aya_ebpf::programs::LsmContext::new(ctx)); + let section_prefix = if self.sleepable { "lsm.s" } else { "lsm" }; + let section_name: Cow<'_, _> = if let Some(hook) = &self.hook { + format!("{}/{}", section_prefix, hook).into() + } else { + section_prefix.into() + }; + + let fn_vis = &self.item.vis; + let fn_name = self.item.sig.ident.clone(); + let item = &self.item; - #item + quote! { + #[no_mangle] + #[link_section = #section_name] + #fn_vis fn #fn_name(ctx: *mut ::core::ffi::c_void) -> i32 { + return #fn_name(::aya_ebpf::programs::LsmContext::new(ctx)); + + #item + } } } } + + // 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. } #[cfg(test)] @@ -122,4 +141,33 @@ mod tests { }; assert_eq!(expected.to_string(), expanded.to_string()); } + + #[test] + fn test_lsm_cgroup() { + let prog = Lsm::parse( + parse_quote! { + hook = "bprm_committed_creds", + cgroup + }, + 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()); + } } diff --git a/aya-obj/src/obj.rs b/aya-obj/src/obj.rs index 4f115024..056381d1 100644 --- a/aya-obj/src/obj.rs +++ b/aya-obj/src/obj.rs @@ -27,7 +27,7 @@ use crate::{ }, maps::{bpf_map_def, BtfMap, BtfMapDef, LegacyMap, Map, PinningType, MINIMUM_MAP_SIZE}, programs::{ - CgroupSockAddrAttachType, CgroupSockAttachType, CgroupSockoptAttachType, XdpAttachType, + CgroupSockAddrAttachType, CgroupSockAttachType, CgroupSockoptAttachType, LsmAttachType, XdpAttachType }, relocation::*, util::HashMap, @@ -274,6 +274,7 @@ pub enum ProgramSection { RawTracePoint, Lsm { sleepable: bool, + attach_type: LsmAttachType, }, BtfTracePoint, FEntry { @@ -434,8 +435,9 @@ impl FromStr for ProgramSection { "lirc_mode2" => LircMode2, "perf_event" => PerfEvent, "raw_tp" | "raw_tracepoint" => RawTracePoint, - "lsm" => Lsm { sleepable: false }, - "lsm.s" => Lsm { sleepable: true }, + "lsm" => Lsm { sleepable: false, attach_type: LsmAttachType::Mac}, + "lsm.s" => Lsm { sleepable: true, attach_type: LsmAttachType::Mac }, + "lsm_cgroup" => Lsm { sleepable: false, attach_type: LsmAttachType::Cgroup }, "fentry" => FEntry { sleepable: false }, "fentry.s" => FEntry { sleepable: true }, "fexit" => FExit { sleepable: false }, @@ -2190,7 +2192,7 @@ mod tests { Some(Program { section: ProgramSection::Lsm { sleepable: false, - .. + attach_type: LsmAttachType::Mac }, .. }) @@ -2223,6 +2225,32 @@ 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::Lsm { + sleepable: false, + attach_type: LsmAttachType::Cgroup + }, + .. + }) + ); + } + #[test] fn test_parse_section_btf_tracepoint() { let mut obj = fake_obj(); diff --git a/aya-obj/src/programs/lsm.rs b/aya-obj/src/programs/lsm.rs new file mode 100644 index 00000000..9379375c --- /dev/null +++ b/aya-obj/src/programs/lsm.rs @@ -0,0 +1,21 @@ +//! XDP programs. + +use crate::generated::bpf_attach_type; + +/// Defines where to attach an `XDP` program. +#[derive(Copy, Clone, Debug)] +pub enum LsmAttachType { + /// Cgroup based LSM program + Cgroup, + /// MAC based LSM program + Mac, +} + +impl From for bpf_attach_type { + fn from(value: LsmAttachType) -> Self { + match value { + LsmAttachType::Cgroup => bpf_attach_type::BPF_LSM_CGROUP, + LsmAttachType::Mac => bpf_attach_type::BPF_LSM_MAC, + } + } +} diff --git a/aya-obj/src/programs/mod.rs b/aya-obj/src/programs/mod.rs index b6e91e7a..ee00aa8d 100644 --- a/aya-obj/src/programs/mod.rs +++ b/aya-obj/src/programs/mod.rs @@ -5,8 +5,10 @@ pub mod cgroup_sock_addr; pub mod cgroup_sockopt; mod types; pub mod xdp; +pub mod lsm; pub use cgroup_sock::CgroupSockAttachType; pub use cgroup_sock_addr::CgroupSockAddrAttachType; pub use cgroup_sockopt::CgroupSockoptAttachType; pub use xdp::XdpAttachType; +pub use lsm::LsmAttachType; diff --git a/aya/src/bpf.rs b/aya/src/bpf.rs index 940c8f07..1c2a0563 100644 --- a/aya/src/bpf.rs +++ b/aya/src/bpf.rs @@ -411,7 +411,7 @@ impl<'a> EbpfLoader<'a> { ProgramSection::Extension | ProgramSection::FEntry { sleepable: _ } | ProgramSection::FExit { sleepable: _ } - | ProgramSection::Lsm { sleepable: _ } + | ProgramSection::Lsm { sleepable: _, attach_type: _ } | ProgramSection::BtfTracePoint | ProgramSection::Iter { sleepable: _ } => { return Err(EbpfError::BtfError(err)) @@ -649,13 +649,16 @@ impl<'a> EbpfLoader<'a> { ProgramSection::RawTracePoint => Program::RawTracePoint(RawTracePoint { data: ProgramData::new(prog_name, obj, btf_fd, *verifier_log_level), }), - ProgramSection::Lsm { sleepable } => { + ProgramSection::Lsm { sleepable , attach_type} => { let mut data = ProgramData::new(prog_name, obj, btf_fd, *verifier_log_level); if *sleepable { data.flags = BPF_F_SLEEPABLE; } - Program::Lsm(Lsm { data }) + Program::Lsm(Lsm { + data, + attach_type: *attach_type, + }) } ProgramSection::BtfTracePoint => Program::BtfTracePoint(BtfTracePoint { data: ProgramData::new(prog_name, obj, btf_fd, *verifier_log_level), diff --git a/aya/src/programs/lsm.rs b/aya/src/programs/lsm.rs index bcf9d052..86de53a5 100644 --- a/aya/src/programs/lsm.rs +++ b/aya/src/programs/lsm.rs @@ -1,12 +1,16 @@ //! LSM probes. +use std::os::fd::AsFd; + +use aya_obj::programs::LsmAttachType; + use crate::{ - generated::{bpf_attach_type::BPF_LSM_MAC, bpf_prog_type::BPF_PROG_TYPE_LSM}, + generated::bpf_prog_type::BPF_PROG_TYPE_LSM, obj::btf::{Btf, BtfKind}, programs::{ define_link_wrapper, load_program, utils::attach_raw_tracepoint, FdLink, FdLinkId, ProgramData, ProgramError, - }, + }, sys::{bpf_link_create, LinkTarget, SyscallError}, }; /// A program that attaches to Linux LSM hooks. Used to implement security policy and @@ -24,7 +28,7 @@ use crate::{ /// The minimum kernel version required to use this feature is 5.7. /// /// # Examples -/// +/// LSM with MAC attachment type /// ```no_run /// # #[derive(thiserror::Error, Debug)] /// # enum LsmError { @@ -44,12 +48,36 @@ use crate::{ /// program.attach()?; /// # Ok::<(), LsmError>(()) /// ``` +/// +/// LSM with cgroup attachment type +/// ```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::Lsm, BtfError, Btf}; +/// +/// let btf = Btf::from_sys_fs()?; +/// let file = File::open("/sys/fs/cgroup/unified")?; +/// let program: &mut Lsm = bpf.program_mut("lsm_prog").unwrap().try_into()?; +/// program.load("security_bprm_exec", &btf)?; +/// program.attach(Some(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 Lsm { pub(crate) data: ProgramData, + pub(crate) attach_type: LsmAttachType, } impl Lsm { @@ -60,7 +88,7 @@ impl Lsm { /// * `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_MAC); + self.data.expected_attach_type = Some(self.attach_type.into()); 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)?); @@ -70,11 +98,47 @@ impl Lsm { /// Attaches the program. /// /// The returned value can be used to detach, see [Lsm::detach]. - pub fn attach(&mut self) -> Result { - attach_raw_tracepoint(&mut self.data, None) + pub fn attach(&mut self, cgroup: Option) -> Result { + match self.attach_type{ + LsmAttachType::Cgroup => { + if let Some(cgroup) = cgroup{ + 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; + + let link_fd = bpf_link_create( + prog_fd, + LinkTarget::Fd(cgroup_fd), + attach_type, + btf_id, + 0, + None, + ) + .map_err(|(_, io_error)| SyscallError { + call: "bpf_link_create", + io_error, + })?; + + self.data + .links + .insert(LsmLink::new(FdLink::new( + link_fd, + ))) + }else { + return Err(ProgramError::UnexpectedProgramType); + } + }, + LsmAttachType::Mac => { + attach_raw_tracepoint(&mut self.data, None) + } + } + } } + define_link_wrapper!( /// The link used by [Lsm] programs. LsmLink, diff --git a/aya/src/programs/mod.rs b/aya/src/programs/mod.rs index f9fbeec8..69156001 100644 --- a/aya/src/programs/mod.rs +++ b/aya/src/programs/mod.rs @@ -963,7 +963,6 @@ impl_from_pin!( CgroupSysctl, LircMode2, PerfEvent, - Lsm, RawTracePoint, BtfTracePoint, FEntry, diff --git a/test/integration-ebpf/src/test.rs b/test/integration-ebpf/src/test.rs index 88f01e89..50325ca2 100644 --- a/test/integration-ebpf/src/test.rs +++ b/test/integration-ebpf/src/test.rs @@ -2,9 +2,7 @@ #![no_main] use aya_ebpf::{ - bindings::xdp_action, - macros::{kprobe, kretprobe, tracepoint, uprobe, uretprobe, xdp}, - programs::{ProbeContext, RetProbeContext, TracePointContext, XdpContext}, + bindings::xdp_action, helpers::{bpf_get_current_cgroup_id, bpf_get_current_pid_tgid}, macros::{kprobe, kretprobe, lsm, tracepoint, uprobe, uretprobe, xdp}, programs::{LsmContext, ProbeContext, RetProbeContext, TracePointContext, XdpContext} }; #[xdp] @@ -44,6 +42,11 @@ pub fn test_uretprobe(_ctx: RetProbeContext) -> u32 { 0 } +#[lsm(hook="socket_bind", cgroup)] +pub fn test_lsmcgroup(_ctx: LsmContext) -> i32 { + 0 +} + #[cfg(not(test))] #[panic_handler] fn panic(_info: &core::panic::PanicInfo) -> ! { diff --git a/test/integration-test/Cargo.toml b/test/integration-test/Cargo.toml index 4cdeaa28..8980b18c 100644 --- a/test/integration-test/Cargo.toml +++ b/test/integration-test/Cargo.toml @@ -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 } diff --git a/test/integration-test/src/tests.rs b/test/integration-test/src/tests.rs index c278e3fd..10a127b1 100644 --- a/test/integration-test/src/tests.rs +++ b/test/integration-test/src/tests.rs @@ -13,3 +13,4 @@ mod smoke; mod strncmp; mod tcx; mod xdp; +mod lsm; diff --git a/test/integration-test/src/tests/lsm.rs b/test/integration-test/src/tests/lsm.rs new file mode 100644 index 00000000..e9208365 --- /dev/null +++ b/test/integration-test/src/tests/lsm.rs @@ -0,0 +1,54 @@ +use std::{fs::File, io::{ErrorKind, Write}, path::Path}; +use aya::{programs::Lsm, util::KernelVersion, Btf, Ebpf}; +use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4, TcpListener}; +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 Lsm = bpf.program_mut("test_lsmcgroup").unwrap().try_into().unwrap(); + let btf = Btf::from_sys_fs().expect("could not get btf from sys"); + if let Err(err) = prog.load("socket_bind", &btf) { + panic!("{err}"); + } + + let cgroup_path = Path::new(".").join("/sys/fs/cgroup/").join("lsm_cgroup_test"); + + let _ = std::fs::create_dir_all( cgroup_path.clone()).expect("could not create the cgroup dir"); + + let p = prog.attach( + Some(File::open(cgroup_path.clone()).unwrap()), + ) + .unwrap(); + + unsafe { + match 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))) + ); + } + } + } +}