From d88f9cdd0e4b91b47a12f1abe2230bd6b2defc2e Mon Sep 17 00:00:00 2001 From: Tyrone Wu Date: Sat, 19 Oct 2024 18:39:16 +0000 Subject: [PATCH] aya,aya-obj: add feature probing program type Adds API that probes whether kernel supports a program type. --- Cargo.toml | 1 + aya/src/sys/bpf.rs | 47 +++++-- aya/src/sys/feature_probe.rs | 111 +++++++++++++++ aya/src/sys/mod.rs | 2 + test/integration-test/Cargo.toml | 1 + test/integration-test/src/tests.rs | 1 + .../src/tests/feature_probe.rs | 132 ++++++++++++++++++ test/integration-test/src/tests/load.rs | 2 +- xtask/public-api/aya.txt | 1 + 9 files changed, 289 insertions(+), 9 deletions(-) create mode 100644 aya/src/sys/feature_probe.rs create mode 100644 test/integration-test/src/tests/feature_probe.rs diff --git a/Cargo.toml b/Cargo.toml index da46f9a8..1e67c411 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -89,6 +89,7 @@ object = { version = "0.36", default-features = false } once_cell = { version = "1.20.1", default-features = false } proc-macro2 = { version = "1", default-features = false } proc-macro2-diagnostics = { version = "0.10.1", default-features = false } +procfs = { version = "0.17.0", default-features = false } public-api = { version = "0.47.0", default-features = false } quote = { version = "1", default-features = false } rand = { version = "0.9", default-features = false } diff --git a/aya/src/sys/bpf.rs b/aya/src/sys/bpf.rs index 79ef4629..d490d2b7 100644 --- a/aya/src/sys/bpf.rs +++ b/aya/src/sys/bpf.rs @@ -27,7 +27,7 @@ use libc::{ENOENT, ENOSPC}; use crate::{ Btf, FEATURES, Pod, VerifierLogLevel, maps::{MapData, PerCpuValues}, - programs::links::LinkRef, + programs::{ProgramType, links::LinkRef}, sys::{Syscall, SyscallError, syscall}, util::KernelVersion, }; @@ -706,7 +706,7 @@ pub(crate) fn bpf_btf_get_fd_by_id(id: u32) -> Result bool { - with_trivial_prog(|attr| { + with_trivial_prog(ProgramType::TracePoint, |attr| { let u = unsafe { &mut attr.__bindgen_anon_3 }; let name = c"aya_name_check"; let name_bytes = name.to_bytes(); @@ -727,7 +727,7 @@ fn new_insn(code: u8, dst_reg: u8, src_reg: u8, offset: i16, imm: i32) -> bpf_in insn } -fn with_trivial_prog(op: F) -> T +pub(super) fn with_trivial_prog(program_type: ProgramType, op: F) -> T where F: FnOnce(&mut bpf_attr) -> T, { @@ -743,14 +743,45 @@ where u.insn_cnt = insns.len() as u32; u.insns = insns.as_ptr() as u64; - u.prog_type = bpf_prog_type::BPF_PROG_TYPE_TRACEPOINT as u32; + + // Setting `expected_attach_type` for cgroup_sock produces `E2BIG` in versions 4.16 and below. + // `bpf_prog_load_fixup_attach_type()` https://elixir.bootlin.com/linux/v4.17/source/kernel/bpf/syscall.c#L1195 + // takes care of it for us in v4.17 and onwards. + let expected_attach_type = match program_type { + ProgramType::CgroupSkb => Some(bpf_attach_type::BPF_CGROUP_INET_INGRESS), + ProgramType::CgroupSockAddr => Some(bpf_attach_type::BPF_CGROUP_INET4_BIND), + ProgramType::LircMode2 => Some(bpf_attach_type::BPF_LIRC_MODE2), + ProgramType::CgroupSockopt => Some(bpf_attach_type::BPF_CGROUP_GETSOCKOPT), + ProgramType::Tracing => Some(bpf_attach_type::BPF_TRACE_FENTRY), + ProgramType::Lsm => Some(bpf_attach_type::BPF_LSM_MAC), + ProgramType::SkLookup => Some(bpf_attach_type::BPF_SK_LOOKUP), + ProgramType::SkReuseport => Some(bpf_attach_type::BPF_SK_REUSEPORT_SELECT), + ProgramType::Netfilter => Some(bpf_attach_type::BPF_NETFILTER), + _ => None, + }; + + match program_type { + ProgramType::KProbe => { + if let Ok(current_version) = KernelVersion::current() { + u.kern_version = current_version.code(); + } + } + // syscall required to be sleepable: https://elixir.bootlin.com/linux/v5.14/source/kernel/bpf/verifier.c#L13240 + ProgramType::Syscall => u.prog_flags = aya_obj::generated::BPF_F_SLEEPABLE, + _ => {} + } + + u.prog_type = program_type as u32; + if let Some(expected_attach_type) = expected_attach_type { + u.expected_attach_type = expected_attach_type as u32; + } op(&mut attr) } /// Tests whether `nr_map_ids` & `map_ids` fields in `bpf_prog_info` is available. pub(crate) fn is_info_map_ids_supported() -> bool { - with_trivial_prog(|attr| { + with_trivial_prog(ProgramType::TracePoint, |attr| { let prog_fd = match bpf_prog_load(attr) { Ok(fd) => fd, Err(_) => return false, @@ -764,7 +795,7 @@ pub(crate) fn is_info_map_ids_supported() -> bool { /// Tests whether `gpl_compatible` field in `bpf_prog_info` is available. pub(crate) fn is_info_gpl_compatible_supported() -> bool { - with_trivial_prog(|attr| { + with_trivial_prog(ProgramType::TracePoint, |attr| { let prog_fd = match bpf_prog_load(attr) { Ok(fd) => fd, Err(_) => return false, @@ -805,7 +836,7 @@ pub(crate) fn is_probe_read_kernel_supported() -> bool { } pub(crate) fn is_perf_link_supported() -> bool { - with_trivial_prog(|attr| { + with_trivial_prog(ProgramType::TracePoint, |attr| { if let Ok(fd) = bpf_prog_load(attr) { let fd = fd.as_fd(); // Uses an invalid target FD so we get EBADF if supported. @@ -1073,7 +1104,7 @@ pub(crate) fn is_btf_type_tag_supported() -> bool { bpf_load_btf(btf_bytes.as_slice(), &mut [], Default::default()).is_ok() } -fn bpf_prog_load(attr: &mut bpf_attr) -> io::Result { +pub(super) fn bpf_prog_load(attr: &mut bpf_attr) -> io::Result { // SAFETY: BPF_PROG_LOAD returns a new file descriptor. unsafe { fd_sys_bpf(bpf_cmd::BPF_PROG_LOAD, attr) } } diff --git a/aya/src/sys/feature_probe.rs b/aya/src/sys/feature_probe.rs new file mode 100644 index 00000000..78cf6ace --- /dev/null +++ b/aya/src/sys/feature_probe.rs @@ -0,0 +1,111 @@ +//! Probes and identifies available eBPF features supported by the host kernel. + +use aya_obj::btf::{Btf, BtfKind}; +use libc::{E2BIG, EINVAL}; + +use super::{SyscallError, bpf_prog_load, with_trivial_prog}; +use crate::programs::{ProgramError, ProgramType}; + +/// Whether the host kernel supports the [`ProgramType`]. +/// +/// # Examples +/// +/// ```no_run +/// # use aya::{programs::ProgramType, sys::is_program_supported}; +/// # +/// match is_program_supported(ProgramType::Xdp) { +/// Ok(true) => println!("XDP supported :)"), +/// Ok(false) => println!("XDP not supported :("), +/// Err(err) => println!("Uh oh! Unexpected error: {:?}", err), +/// } +/// ``` +/// +/// # Errors +/// +/// Returns [`ProgramError::SyscallError`] if a syscall fails with an unexpected +/// error, or [`ProgramError::Btf`] for BTF related errors. +/// +/// Certain errors are expected and handled internally; only unanticipated +/// failures during probing will result in these errors. +pub fn is_program_supported(program_type: ProgramType) -> Result { + if program_type == ProgramType::Unspecified { + return Ok(false); + } + + let mut verifier_log = [0_u8; 136]; + // First aim for a valid bpf_prog_load using these funcs for tracing & lsm. + // If symbols can't be retrieved from BTF, then leave unset and defer to verifier logs. + let attach_btf_id = match program_type { + // `bpf_fentry_test1` symbol from https://elixir.bootlin.com/linux/v5.5/source/net/bpf/test_run.c#L112 + ProgramType::Tracing => Some("bpf_fentry_test1"), + // `bpf_lsm_bpf` symbol from https://elixir.bootlin.com/linux/v5.7/source/include/linux/lsm_hook_defs.h#L364 + // or https://elixir.bootlin.com/linux/v5.11/source/kernel/bpf/bpf_lsm.c#L135 on later versions + ProgramType::Lsm => Some("bpf_lsm_bpf"), + _ => None, + } + .map(|func_name| { + Btf::from_sys_fs() + .and_then(|btf| btf.id_by_type_name_kind(func_name, BtfKind::Func)) + .unwrap_or(0) + }); + + let error = match with_trivial_prog(program_type, |attr| { + // SAFETY: union access + let u = unsafe { &mut attr.__bindgen_anon_3 }; + + if let Some(attach_btf_id) = attach_btf_id { + u.attach_btf_id = attach_btf_id; + } + match program_type { + // Use verifier log to detect support if loading fails. + // Loading *may* fail for tracing & lsm if func symbols cannot be found in BTF. + // Loading for extension is intentionally expected to fail. + ProgramType::Tracing | ProgramType::Extension | ProgramType::Lsm => { + u.log_buf = verifier_log.as_mut_ptr() as u64; + u.log_level = 1; + u.log_size = verifier_log.len() as u32; + } + _ => {} + } + + bpf_prog_load(attr).err().map(|io_error| { + ProgramError::SyscallError(SyscallError { + call: "bpf_prog_load", + io_error, + }) + }) + }) { + Some(err) => err, + None => return Ok(true), + }; + + // Loading may fail for some types (namely tracing, extension, lsm, & struct_ops), so we + // perform additional examination on the OS error and/or verifier logs. + match &error { + ProgramError::SyscallError(err) => { + match err.io_error.raw_os_error() { + // For most types, `EINVAL` typically indicates it is not supported. + // However, further examination is required for tracing, extension, and lsm. + Some(EINVAL) => { + // When `attach_btf_id` is unset for types that require it, the following + // message is written to logs. + // Message comes from `check_attach_btf_id()` https://elixir.bootlin.com/linux/v5.5/source/kernel/bpf/verifier.c#L9535, + // or `bpf_check_attach_target()` https://elixir.bootlin.com/linux/v5.9/source/kernel/bpf/verifier.c#L10849 on later versions + let supported = matches!( + program_type, + ProgramType::Tracing | ProgramType::Extension | ProgramType::Lsm + if verifier_log.starts_with(b"Tracing programs must provide btf_id")); + Ok(supported) + } + Some(E2BIG) => Ok(false), + // `ENOTSUPP` from `check_struct_ops_btf_id()` https://elixir.bootlin.com/linux/v5.6/source/kernel/bpf/verifier.c#L9740 + // indicates that it reached the verifier section, meaning the kernel is at least + // aware of the type's existence. Otherwise, it will produce `EINVAL`, meaning the + // type is immediately rejected and does not exist. + Some(524) if program_type == ProgramType::StructOps => Ok(true), + _ => Err(error), + } + } + _ => Err(error), + } +} diff --git a/aya/src/sys/mod.rs b/aya/src/sys/mod.rs index f8c8944b..f2cffdf4 100644 --- a/aya/src/sys/mod.rs +++ b/aya/src/sys/mod.rs @@ -1,6 +1,7 @@ //! A collection of system calls for performing eBPF related operations. mod bpf; +mod feature_probe; mod netlink; mod perf_event; @@ -17,6 +18,7 @@ use aya_obj::generated::{bpf_attr, bpf_cmd, perf_event_attr}; pub(crate) use bpf::*; #[cfg(test)] pub(crate) use fake::*; +pub use feature_probe::is_program_supported; #[doc(hidden)] pub use netlink::netlink_set_link_up; pub(crate) use netlink::*; diff --git a/test/integration-test/Cargo.toml b/test/integration-test/Cargo.toml index 162fc82c..f908c4ea 100644 --- a/test/integration-test/Cargo.toml +++ b/test/integration-test/Cargo.toml @@ -27,6 +27,7 @@ libc = { workspace = true } log = { workspace = true } netns-rs = { workspace = true } object = { workspace = true, features = ["elf", "read_core", "std"] } +procfs = { workspace = true, features = ["flate2"] } rand = { workspace = true, features = ["thread_rng"] } rbpf = { workspace = true } scopeguard = { workspace = true } diff --git a/test/integration-test/src/tests.rs b/test/integration-test/src/tests.rs index 9ca83669..43894366 100644 --- a/test/integration-test/src/tests.rs +++ b/test/integration-test/src/tests.rs @@ -1,6 +1,7 @@ mod bpf_probe_read; mod btf_relocations; mod elf; +mod feature_probe; mod info; mod iter; mod load; diff --git a/test/integration-test/src/tests/feature_probe.rs b/test/integration-test/src/tests/feature_probe.rs new file mode 100644 index 00000000..8fff1a4e --- /dev/null +++ b/test/integration-test/src/tests/feature_probe.rs @@ -0,0 +1,132 @@ +//! Test feature probing against kernel version. + +use aya::{Btf, programs::ProgramType, sys::is_program_supported, util::KernelVersion}; +use procfs::kernel_config; + +use crate::utils::kernel_assert; + +#[test] +fn probe_supported_programs() { + let mut kern_version: KernelVersion; + let kernel_config = kernel_config().unwrap_or_default(); + macro_rules! is_supported { + ($prog_type:expr) => { + is_program_supported($prog_type).unwrap() + }; + } + + kern_version = KernelVersion::new(3, 19, 0); + kernel_assert!(is_supported!(ProgramType::SocketFilter), kern_version); + + kern_version = KernelVersion::new(4, 1, 0); + kernel_assert!(is_supported!(ProgramType::KProbe), kern_version); + kernel_assert!(is_supported!(ProgramType::SchedClassifier), kern_version); + kernel_assert!(is_supported!(ProgramType::SchedAction), kern_version); + + kern_version = KernelVersion::new(4, 7, 0); + kernel_assert!(is_supported!(ProgramType::TracePoint), kern_version); + + kern_version = KernelVersion::new(4, 8, 0); + kernel_assert!(is_supported!(ProgramType::Xdp), kern_version); + + kern_version = KernelVersion::new(4, 9, 0); + kernel_assert!(is_supported!(ProgramType::PerfEvent), kern_version); + + kern_version = KernelVersion::new(4, 10, 0); + kernel_assert!(is_supported!(ProgramType::CgroupSkb), kern_version); + kernel_assert!(is_supported!(ProgramType::CgroupSock), kern_version); + kernel_assert!(is_supported!(ProgramType::LwtInput), kern_version); + kernel_assert!(is_supported!(ProgramType::LwtOutput), kern_version); + kernel_assert!(is_supported!(ProgramType::LwtXmit), kern_version); + + kern_version = KernelVersion::new(4, 13, 0); + kernel_assert!(is_supported!(ProgramType::SockOps), kern_version); + + kern_version = KernelVersion::new(4, 14, 0); + kernel_assert!(is_supported!(ProgramType::SkSkb), kern_version); + + kern_version = KernelVersion::new(4, 15, 0); + kernel_assert!(is_supported!(ProgramType::CgroupDevice), kern_version); + + kern_version = KernelVersion::new(4, 17, 0); + kernel_assert!(is_supported!(ProgramType::SkMsg), kern_version); + kernel_assert!(is_supported!(ProgramType::RawTracePoint), kern_version); + kernel_assert!(is_supported!(ProgramType::CgroupSockAddr), kern_version); + + kern_version = KernelVersion::new(4, 18, 0); + kernel_assert!(is_supported!(ProgramType::LwtSeg6local), kern_version); + + // `lirc_mode2` requires CONFIG_BPF_LIRC_MODE2=y + let lirc_mode2_config = matches!( + kernel_config.get("CONFIG_BPF_LIRC_MODE2"), + Some(procfs::ConfigSetting::Yes) + ); + let lirc_mode2 = is_supported!(ProgramType::LircMode2); + kernel_assert!( + if aya::util::KernelVersion::current().unwrap() >= kern_version { + lirc_mode2 == lirc_mode2_config + } else { + lirc_mode2 + }, + kern_version + ); + if !lirc_mode2_config { + eprintln!("CONFIG_BPF_LIRC_MODE2 required for lirc_mode2 program type"); + } + + kern_version = KernelVersion::new(4, 19, 0); + kernel_assert!(is_supported!(ProgramType::SkReuseport), kern_version); + + kern_version = KernelVersion::new(4, 20, 0); + kernel_assert!(is_supported!(ProgramType::FlowDissector), kern_version); + + kern_version = KernelVersion::new(5, 2, 0); + kernel_assert!(is_supported!(ProgramType::CgroupSysctl), kern_version); + kernel_assert!( + is_supported!(ProgramType::RawTracePointWritable), + kern_version + ); + + kern_version = KernelVersion::new(5, 3, 0); + kernel_assert!(is_supported!(ProgramType::CgroupSockopt), kern_version); + + kern_version = KernelVersion::new(5, 5, 0); + kernel_assert!(is_supported!(ProgramType::Tracing), kern_version); // Requires `CONFIG_DEBUG_INFO_BTF=y` + + kern_version = KernelVersion::new(5, 6, 0); + kernel_assert!(is_supported!(ProgramType::StructOps), kern_version); + kernel_assert!(is_supported!(ProgramType::Extension), kern_version); + + kern_version = KernelVersion::new(5, 7, 0); + // `lsm` requires `CONFIG_DEBUG_INFO_BTF=y` & `CONFIG_BPF_LSM=y` + // Ways to check if `CONFIG_BPF_LSM` is enabled: + // - kernel config has `CONFIG_BPF_LSM=y`, but config is not always exposed. + // - an LSM hooks is present in BTF, e.g. `bpf_lsm_bpf`. hooks are found in `bpf_lsm.c` + let lsm_enabled = matches!( + kernel_config.get("CONFIG_BPF_LSM"), + Some(procfs::ConfigSetting::Yes) + ) || Btf::from_sys_fs() + .and_then(|btf| btf.id_by_type_name_kind("bpf_lsm_bpf", aya_obj::btf::BtfKind::Func)) + .is_ok(); + let lsm = is_supported!(ProgramType::Lsm); + kernel_assert!( + if aya::util::KernelVersion::current().unwrap() >= kern_version { + lsm == lsm_enabled + } else { + lsm + }, + kern_version + ); + if !lsm_enabled { + eprintln!("CONFIG_BPF_LSM required for lsm program type"); + } + + kern_version = KernelVersion::new(5, 9, 0); + kernel_assert!(is_supported!(ProgramType::SkLookup), kern_version); + + kern_version = KernelVersion::new(5, 14, 0); + kernel_assert!(is_supported!(ProgramType::Syscall), kern_version); + + kern_version = KernelVersion::new(6, 4, 0); + kernel_assert!(is_supported!(ProgramType::Netfilter), kern_version); +} diff --git a/test/integration-test/src/tests/load.rs b/test/integration-test/src/tests/load.rs index a93e6f16..85268ab9 100644 --- a/test/integration-test/src/tests/load.rs +++ b/test/integration-test/src/tests/load.rs @@ -14,7 +14,7 @@ use aya_obj::programs::XdpAttachType; use test_log::test; const MAX_RETRIES: usize = 100; -const RETRY_DURATION: Duration = Duration::from_millis(10); +pub(crate) const RETRY_DURATION: Duration = Duration::from_millis(10); #[test] fn long_name() { diff --git a/xtask/public-api/aya.txt b/xtask/public-api/aya.txt index 94a70d3c..2d62d08f 100644 --- a/xtask/public-api/aya.txt +++ b/xtask/public-api/aya.txt @@ -10076,6 +10076,7 @@ pub fn aya::sys::SyscallError::borrow_mut(&mut self) -> &mut T impl core::convert::From for aya::sys::SyscallError pub fn aya::sys::SyscallError::from(t: T) -> T pub fn aya::sys::enable_stats(stats_type: aya::sys::Stats) -> core::result::Result +pub fn aya::sys::is_program_supported(program_type: aya::programs::ProgramType) -> core::result::Result pub mod aya::util pub struct aya::util::KernelVersion impl aya::util::KernelVersion