From 0614367c3629f58bb90ad70f231117975a6a881d Mon Sep 17 00:00:00 2001 From: Tyrone Wu Date: Sat, 19 Oct 2024 18:39:16 +0000 Subject: [PATCH 1/4] 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 | 73 ++++++++-- aya/src/sys/feature_probe.rs | 130 +++++++++++++++++ aya/src/sys/mod.rs | 2 + test/integration-test/Cargo.toml | 1 + test/integration-test/src/tests.rs | 1 + .../src/tests/feature_probe.rs | 131 ++++++++++++++++++ test/integration-test/src/tests/load.rs | 2 +- xtask/public-api/aya.txt | 1 + 9 files changed, 333 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..21386e1d 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,71 @@ 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; + + // `expected_attach_type` field was added in v4.17 https://elixir.bootlin.com/linux/v4.17/source/include/uapi/linux/bpf.h#L310. + let expected_attach_type = match program_type { + ProgramType::SkMsg => Some(bpf_attach_type::BPF_SK_MSG_VERDICT), + ProgramType::CgroupSockAddr => Some(bpf_attach_type::BPF_CGROUP_INET4_BIND), + ProgramType::LircMode2 => Some(bpf_attach_type::BPF_LIRC_MODE2), + ProgramType::SkReuseport => Some(bpf_attach_type::BPF_SK_REUSEPORT_SELECT), + ProgramType::FlowDissector => Some(bpf_attach_type::BPF_FLOW_DISSECTOR), + ProgramType::CgroupSysctl => Some(bpf_attach_type::BPF_CGROUP_SYSCTL), + 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::Netfilter => Some(bpf_attach_type::BPF_NETFILTER), + // Program types below v4.17, or do not accept `expected_attach_type`, should leave the + // field unset. + // + // Types below v4.17: + ProgramType::Unspecified + | ProgramType::SocketFilter + | ProgramType::KProbe + | ProgramType::SchedClassifier + | ProgramType::SchedAction + | ProgramType::TracePoint + | ProgramType::Xdp + | ProgramType::PerfEvent + | ProgramType::CgroupSkb + | ProgramType::CgroupSock + | ProgramType::LwtInput + | ProgramType::LwtOutput + | ProgramType::LwtXmit + | ProgramType::SockOps + | ProgramType::SkSkb + | ProgramType::CgroupDevice + // Types that do not accept `expected_attach_type`: + | ProgramType::RawTracePoint + | ProgramType::LwtSeg6local + | ProgramType::RawTracePointWritable + | ProgramType::StructOps + | ProgramType::Extension + | ProgramType::Syscall => 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 +821,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 +862,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 +1130,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..f55b83fc --- /dev/null +++ b/aya/src/sys/feature_probe.rs @@ -0,0 +1,130 @@ +//! 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); + } + + // Verifier log is used in tracing, extension, and lsm to help detect support if loading fails. + // The expected target message for these types is: + // `Tracing programs must provide btf_id\nprocessed 0 insns (limit 1000000) max_states_per_insn 0 total_states 0 peak_states 0 mark_read 0\n\0` + // https://elixir.bootlin.com/linux/v5.5/source/kernel/bpf/verifier.c#L9535 + let mut verifier_log = [0_u8; 136]; + + // The tracing and lsm types require a valid `attach_btf_id` for a successful load. However, + // if the symbols cannot be found in the BTF, then leave the field unset/0. + // Although extension also requires `attach_btf_id`, we intentionally leave it unset since a + // successful load requires additional setup with another prog loaded with BTF. + // + // When `attach_btf_id` is unset, then loading will fail, and so we examine verifier log + // for the expected message. + 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 { + // If loading fails for these types due to unset `attach_btf_id`, then we defer to + // verifier log to verify whether type is supported. + 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) => { + // At this point for tracing, extension, and lsm, loading failed due to unset + // `attach_btf_id`, so we examine verifier log for the target message. + // Message originates in `check_attach_btf_id()` in v5.5 to v5.9 https://elixir.bootlin.com/linux/v5.5/source/kernel/bpf/verifier.c#L9535, + // then moved to `bpf_check_attach_target()` in 5.10 and onward https://elixir.bootlin.com/linux/v5.9/source/kernel/bpf/verifier.c#L10849. + // + // If target message is present in the logs, then loading process has reached + // up to the verifier section, which indicates that the kernel is at least + // aware of the program type variants. + // If logs is empty, then it was immediately rejected by the kernel, meaning + // the types are not supported. + let supported = matches!( + program_type, + ProgramType::Tracing | ProgramType::Extension | ProgramType::Lsm + ) && verifier_log + .starts_with(b"Tracing programs must provide btf_id"); + Ok(supported) + } + // `E2BIG` from `bpf_check_uarg_tail_zero()` https://elixir.bootlin.com/linux/v4.18/source/kernel/bpf/syscall.c#L71 + // indicates that the kernel detected non-zero fields in `bpf_attr` that does not + // exist at its current version. + 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..282f148b --- /dev/null +++ b/test/integration-test/src/tests/feature_probe.rs @@ -0,0 +1,131 @@ +//! 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 kernel_config = kernel_config().unwrap_or_default(); + macro_rules! is_supported { + ($prog_type:expr) => { + is_program_supported($prog_type).unwrap() + }; + } + + let kern_version = KernelVersion::new(3, 19, 0); + kernel_assert!(is_supported!(ProgramType::SocketFilter), kern_version); + + let 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); + + let kern_version = KernelVersion::new(4, 7, 0); + kernel_assert!(is_supported!(ProgramType::TracePoint), kern_version); + + let kern_version = KernelVersion::new(4, 8, 0); + kernel_assert!(is_supported!(ProgramType::Xdp), kern_version); + + let kern_version = KernelVersion::new(4, 9, 0); + kernel_assert!(is_supported!(ProgramType::PerfEvent), kern_version); + + let 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); + + let kern_version = KernelVersion::new(4, 13, 0); + kernel_assert!(is_supported!(ProgramType::SockOps), kern_version); + + let kern_version = KernelVersion::new(4, 14, 0); + kernel_assert!(is_supported!(ProgramType::SkSkb), kern_version); + + let kern_version = KernelVersion::new(4, 15, 0); + kernel_assert!(is_supported!(ProgramType::CgroupDevice), kern_version); + + let 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); + + let 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"); + } + + let kern_version = KernelVersion::new(4, 19, 0); + kernel_assert!(is_supported!(ProgramType::SkReuseport), kern_version); + + let kern_version = KernelVersion::new(4, 20, 0); + kernel_assert!(is_supported!(ProgramType::FlowDissector), kern_version); + + let kern_version = KernelVersion::new(5, 2, 0); + kernel_assert!(is_supported!(ProgramType::CgroupSysctl), kern_version); + kernel_assert!( + is_supported!(ProgramType::RawTracePointWritable), + kern_version + ); + + let kern_version = KernelVersion::new(5, 3, 0); + kernel_assert!(is_supported!(ProgramType::CgroupSockopt), kern_version); + + let kern_version = KernelVersion::new(5, 5, 0); + kernel_assert!(is_supported!(ProgramType::Tracing), kern_version); // Requires `CONFIG_DEBUG_INFO_BTF=y` + + let kern_version = KernelVersion::new(5, 6, 0); + kernel_assert!(is_supported!(ProgramType::StructOps), kern_version); + kernel_assert!(is_supported!(ProgramType::Extension), kern_version); + + let 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"); + } + + let kern_version = KernelVersion::new(5, 9, 0); + kernel_assert!(is_supported!(ProgramType::SkLookup), kern_version); + + let kern_version = KernelVersion::new(5, 14, 0); + kernel_assert!(is_supported!(ProgramType::Syscall), kern_version); + + let 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 From 1655df36c7e376d779476b7bf5ea21894f7acec6 Mon Sep 17 00:00:00 2001 From: Tyrone Wu Date: Mon, 28 Oct 2024 02:09:27 +0000 Subject: [PATCH 2/4] aya: add feature probing for map type Add API that probes whether kernel supports a map type. --- aya/src/sys/bpf.rs | 5 +- aya/src/sys/feature_probe.rs | 202 +++++++++++++++++- aya/src/sys/mod.rs | 2 +- .../src/tests/feature_probe.rs | 96 ++++++++- xtask/public-api/aya.txt | 1 + 5 files changed, 299 insertions(+), 7 deletions(-) diff --git a/aya/src/sys/bpf.rs b/aya/src/sys/bpf.rs index 21386e1d..4e80a71a 100644 --- a/aya/src/sys/bpf.rs +++ b/aya/src/sys/bpf.rs @@ -681,7 +681,10 @@ pub(crate) fn bpf_load_btf( } // SAFETY: only use for bpf_cmd that return a new file descriptor on success. -unsafe fn fd_sys_bpf(cmd: bpf_cmd, attr: &mut bpf_attr) -> io::Result { +pub(super) unsafe fn fd_sys_bpf( + cmd: bpf_cmd, + attr: &mut bpf_attr, +) -> io::Result { let fd = sys_bpf(cmd, attr)?; let fd = fd.try_into().map_err(|std::num::TryFromIntError { .. }| { io::Error::new( diff --git a/aya/src/sys/feature_probe.rs b/aya/src/sys/feature_probe.rs index f55b83fc..51eae01d 100644 --- a/aya/src/sys/feature_probe.rs +++ b/aya/src/sys/feature_probe.rs @@ -1,10 +1,20 @@ //! Probes and identifies available eBPF features supported by the host kernel. -use aya_obj::btf::{Btf, BtfKind}; -use libc::{E2BIG, EINVAL}; +use std::{mem, os::fd::AsRawFd as _}; -use super::{SyscallError, bpf_prog_load, with_trivial_prog}; -use crate::programs::{ProgramError, ProgramType}; +use aya_obj::{ + btf::{Btf, BtfKind}, + generated::{BPF_F_MMAPABLE, BPF_F_NO_PREALLOC, bpf_attr, bpf_cmd, bpf_map_type}, +}; +use libc::{E2BIG, EBADF, EINVAL}; + +use super::{SyscallError, bpf_prog_load, fd_sys_bpf, with_trivial_prog}; +use crate::{ + MockableFd, + maps::MapType, + programs::{ProgramError, ProgramType}, + util::page_size, +}; /// Whether the host kernel supports the [`ProgramType`]. /// @@ -128,3 +138,187 @@ pub fn is_program_supported(program_type: ProgramType) -> Result Err(error), } } + +/// Whether the host kernel supports the [`MapType`]. +/// +/// # Examples +/// +/// ```no_run +/// # use aya::{maps::MapType, sys::is_map_supported}; +/// # +/// match is_map_supported(MapType::HashOfMaps) { +/// Ok(true) => println!("hash_of_maps supported :)"), +/// Ok(false) => println!("hash_of_maps not supported :("), +/// Err(err) => println!("Uh oh! Unexpected error: {:?}", err), +/// } +/// ``` +/// +/// # Errors +/// +/// Returns [`SyscallError`] if kernel probing fails with an unexpected error. +/// +/// Note that certain errors are expected and handled internally; only +/// unanticipated failures during probing will result in this error. +pub fn is_map_supported(map_type: MapType) -> Result { + // Each `bpf_map_ops` struct contains their own `.map_alloc()` & `.map_alloc_check()` that does + // field validation on map_create. + let (key_size, value_size, max_entries) = match map_type { + MapType::Unspecified => return Ok(false), + MapType::Hash // https://elixir.bootlin.com/linux/v3.19/source/kernel/bpf/hashtab.c#L349 + | MapType::PerCpuHash // https://elixir.bootlin.com/linux/v4.6/source/kernel/bpf/hashtab.c#L726 + | MapType::LruHash // https://elixir.bootlin.com/linux/v4.10/source/kernel/bpf/hashtab.c#L1032 + | MapType::LruPerCpuHash // https://elixir.bootlin.com/linux/v4.10/source/kernel/bpf/hashtab.c#L1133 + => (1, 1, 1), + MapType::Array // https://elixir.bootlin.com/linux/v3.19/source/kernel/bpf/arraymap.c#L138 + | MapType::PerCpuArray // https://elixir.bootlin.com/linux/v4.6/source/kernel/bpf/arraymap.c#L283 + => (4, 1, 1), + MapType::ProgramArray // https://elixir.bootlin.com/linux/v4.2/source/kernel/bpf/arraymap.c#L239 + | MapType::PerfEventArray // https://elixir.bootlin.com/linux/v4.3/source/kernel/bpf/arraymap.c#L312 + | MapType::CgroupArray // https://elixir.bootlin.com/linux/v4.8/source/kernel/bpf/arraymap.c#L562 + | MapType::ArrayOfMaps // https://elixir.bootlin.com/linux/v4.12/source/kernel/bpf/arraymap.c#L595 + | MapType::DevMap // https://elixir.bootlin.com/linux/v4.14/source/kernel/bpf/devmap.c#L360 + | MapType::SockMap // https://elixir.bootlin.com/linux/v4.14/source/kernel/bpf/sockmap.c#L874 + | MapType::CpuMap // https://elixir.bootlin.com/linux/v4.15/source/kernel/bpf/cpumap.c#L589 + | MapType::XskMap // https://elixir.bootlin.com/linux/v4.18/source/kernel/bpf/xskmap.c#L224 + | MapType::ReuseportSockArray // https://elixir.bootlin.com/linux/v4.20/source/kernel/bpf/reuseport_array.c#L357 + | MapType::DevMapHash // https://elixir.bootlin.com/linux/v5.4/source/kernel/bpf/devmap.c#L713 + => (4, 4, 1), + MapType::StackTrace // https://elixir.bootlin.com/linux/v4.6/source/kernel/bpf/stackmap.c#L272 + => (4, 8, 1), + MapType::LpmTrie // https://elixir.bootlin.com/linux/v4.11/source/kernel/bpf/lpm_trie.c#L509 + => (8, 1, 1), + MapType::HashOfMaps // https://elixir.bootlin.com/linux/v4.12/source/kernel/bpf/hashtab.c#L1301 + | MapType::SockHash // https://elixir.bootlin.com/linux/v4.18/source/kernel/bpf/sockmap.c#L2507 + => (1, 4, 1), + MapType::CgroupStorage // https://elixir.bootlin.com/linux/v4.19/source/kernel/bpf/local_storage.c#L246 + | MapType::PerCpuCgroupStorage // https://elixir.bootlin.com/linux/v4.20/source/kernel/bpf/local_storage.c#L313 + => (16, 1, 0), + MapType::Queue // https://elixir.bootlin.com/linux/v4.20/source/kernel/bpf/queue_stack_maps.c#L267 + | MapType::Stack // https://elixir.bootlin.com/linux/v4.20/source/kernel/bpf/queue_stack_maps.c#L280 + | MapType::BloomFilter // https://elixir.bootlin.com/linux/v5.16/source/kernel/bpf/bloom_filter.c#L193 + => (0, 1, 1), + MapType::SkStorage // https://elixir.bootlin.com/linux/v5.2/source/net/core/bpf_sk_storage.c#L779 + | MapType::InodeStorage // https://elixir.bootlin.com/linux/v5.10/source/kernel/bpf/bpf_inode_storage.c#L239 + | MapType::TaskStorage // https://elixir.bootlin.com/linux/v5.11/source/kernel/bpf/bpf_task_storage.c#L285 + | MapType::CgrpStorage // https://elixir.bootlin.com/linux/v6.2/source/kernel/bpf/bpf_cgrp_storage.c#L216 + => (4, 1, 0), + MapType::StructOps // https://elixir.bootlin.com/linux/v5.6/source/kernel/bpf/bpf_struct_ops.c#L607 + => (4, 0, 1), + MapType::RingBuf // https://elixir.bootlin.com/linux/v5.8/source/kernel/bpf/ringbuf.c#L296 + | MapType::UserRingBuf // https://elixir.bootlin.com/linux/v6.1/source/kernel/bpf/ringbuf.c#L356 + // `max_entries` required to be multiple of kernel page size & power of 2: https://elixir.bootlin.com/linux/v5.8/source/kernel/bpf/ringbuf.c#L160 + => (0, 0, page_size() as u32), + MapType::Arena // https://elixir.bootlin.com/linux/v6.9/source/kernel/bpf/arena.c#L380 + => (0, 0, 1), + }; + + // SAFETY: all-zero byte-pattern valid for `bpf_attr` + let mut attr = unsafe { mem::zeroed::() }; + // SAFETY: union access + let u = unsafe { &mut attr.__bindgen_anon_1 }; + u.map_type = map_type as u32; + u.key_size = key_size; + u.value_size = value_size; + u.max_entries = max_entries; + + // Ensure that fd doesn't get dropped due to scoping for for *_of_maps type. + let inner_map_fd: MockableFd; + match map_type { + // lpm_trie required to not be pre-alloced: https://elixir.bootlin.com/linux/v4.11/source/kernel/bpf/lpm_trie.c#L419 + MapType::LpmTrie => u.map_flags = BPF_F_NO_PREALLOC, + // For these types, we aim to intentionally trigger `EBADF` by supplying invalid btf attach + // data to verify the map type's existance. Otherwise, negative support will produce + // `EINVAL` instead. + MapType::SkStorage + | MapType::InodeStorage + | MapType::TaskStorage + | MapType::CgrpStorage => { + // These types required to not be pre-alloced: + // - sk_storage: https://elixir.bootlin.com/linux/v5.2/source/net/core/bpf_sk_storage.c#L604 + // - inode_storage: https://elixir.bootlin.com/linux/v5.10/source/kernel/bpf/bpf_local_storage.c#L525 + // - task_storage: https://elixir.bootlin.com/linux/v5.11/source/kernel/bpf/bpf_local_storage.c#L527 + // - cgrp_storage: https://elixir.bootlin.com/linux/v6.2/source/kernel/bpf/bpf_local_storage.c#L539 + u.map_flags = BPF_F_NO_PREALLOC; + // Will trigger `EBADF` from `btf_get_by_fd()` https://elixir.bootlin.com/linux/v5.2/source/kernel/bpf/btf.c#L3428 + u.btf_fd = u32::MAX; + u.btf_key_type_id = 1; + u.btf_value_type_id = 1; + } + MapType::ArrayOfMaps | MapType::HashOfMaps => { + // SAFETY: all-zero byte-pattern valid for `bpf_attr` + let mut attr_map = unsafe { mem::zeroed::() }; + // SAFETY: union access + let u_map = unsafe { &mut attr_map.__bindgen_anon_1 }; + u_map.map_type = bpf_map_type::BPF_MAP_TYPE_HASH as u32; + u_map.key_size = 1; + u_map.value_size = 1; + u_map.max_entries = 1; + // SAFETY: BPF_MAP_CREATE returns a new file descriptor. + inner_map_fd = unsafe { fd_sys_bpf(bpf_cmd::BPF_MAP_CREATE, &mut attr_map) }.map_err( + |io_error| SyscallError { + call: "bpf_map_create", + io_error, + }, + )?; + + u.inner_map_fd = inner_map_fd.as_raw_fd() as u32; + } + // We aim to intentionally trigger `ENOTSUPP` by setting an invalid, non-zero + // `btf_vmlinux_value_type_id`. Negative support produce `EINVAL` instead. + MapType::StructOps => u.btf_vmlinux_value_type_id = 1, + // arena required to be mmapable: https://elixir.bootlin.com/linux/v6.9/source/kernel/bpf/arena.c#L103 + MapType::Arena => u.map_flags = BPF_F_MMAPABLE, + _ => {} + } + + // SAFETY: BPF_MAP_CREATE returns a new file descriptor. + let io_error = match unsafe { fd_sys_bpf(bpf_cmd::BPF_MAP_CREATE, &mut attr) } { + Ok(_) => return Ok(true), + Err(io_error) => io_error, + }; + + // sk_storage, struct_ops, inode_storage, task_storage, & cgrp_storage requires further + // examination to verify support. + match io_error.raw_os_error() { + Some(EINVAL) => Ok(false), + // These types use fields that may not exist at the kernel's current version. + // Supplying `bpf_attr` fields unknown to the kernel triggers `E2BIG` from + // `bpf_check_uarg_tail_zero()` https://elixir.bootlin.com/linux/v4.18/source/kernel/bpf/syscall.c#L71. + Some(E2BIG) + if matches!( + map_type, + MapType::SkStorage + | MapType::StructOps + | MapType::InodeStorage + | MapType::TaskStorage + | MapType::CgrpStorage + ) => + { + Ok(false) + } + // For these types, `EBADF` from `btf_get_by_fd()` https://elixir.bootlin.com/linux/v5.2/source/kernel/bpf/btf.c#L3428 + // indicates that map_create advanced far enough in the validation to recognize the type + // before being rejected. + // Otherwise, negative support produces `EINVAL`, meaning it was immediately rejected. + Some(EBADF) + if matches!( + map_type, + MapType::SkStorage + | MapType::InodeStorage + | MapType::TaskStorage + | MapType::CgrpStorage + ) => + { + Ok(true) + } + // `ENOTSUPP` from `bpf_struct_ops_map_alloc()` https://elixir.bootlin.com/linux/v5.6/source/kernel/bpf/bpf_struct_ops.c#L557 + // indicates that map_create advanced far enough in the validation to recognize the type + // before being rejected. + // Otherwise, negative support produces `EINVAL`, meaning it was immediately rejected. + Some(524) if map_type == MapType::StructOps => Ok(true), + _ => Err(SyscallError { + call: "bpf_map_create", + io_error, + }), + } +} diff --git a/aya/src/sys/mod.rs b/aya/src/sys/mod.rs index f2cffdf4..c0d2c59c 100644 --- a/aya/src/sys/mod.rs +++ b/aya/src/sys/mod.rs @@ -18,7 +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; +pub use feature_probe::{is_map_supported, is_program_supported}; #[doc(hidden)] pub use netlink::netlink_set_link_up; pub(crate) use netlink::*; diff --git a/test/integration-test/src/tests/feature_probe.rs b/test/integration-test/src/tests/feature_probe.rs index 282f148b..d0bb29e9 100644 --- a/test/integration-test/src/tests/feature_probe.rs +++ b/test/integration-test/src/tests/feature_probe.rs @@ -1,6 +1,12 @@ //! Test feature probing against kernel version. -use aya::{Btf, programs::ProgramType, sys::is_program_supported, util::KernelVersion}; +use aya::{ + Btf, + maps::MapType, + programs::ProgramType, + sys::{is_map_supported, is_program_supported}, + util::KernelVersion, +}; use procfs::kernel_config; use crate::utils::kernel_assert; @@ -129,3 +135,91 @@ fn probe_supported_programs() { let kern_version = KernelVersion::new(6, 4, 0); kernel_assert!(is_supported!(ProgramType::Netfilter), kern_version); } + +#[test] +fn probe_supported_maps() { + macro_rules! is_supported { + ($map_type:expr) => { + is_map_supported($map_type).unwrap() + }; + } + + let kern_version = KernelVersion::new(3, 19, 0); + kernel_assert!(is_supported!(MapType::Hash), kern_version); + kernel_assert!(is_supported!(MapType::Array), kern_version); + + let kern_version = KernelVersion::new(4, 2, 0); + kernel_assert!(is_supported!(MapType::ProgramArray), kern_version); + + let kern_version = KernelVersion::new(4, 3, 0); + kernel_assert!(is_supported!(MapType::PerfEventArray), kern_version); + + let kern_version = KernelVersion::new(4, 6, 0); + kernel_assert!(is_supported!(MapType::PerCpuHash), kern_version); + kernel_assert!(is_supported!(MapType::PerCpuArray), kern_version); + kernel_assert!(is_supported!(MapType::StackTrace), kern_version); + + let kern_version = KernelVersion::new(4, 8, 0); + kernel_assert!(is_supported!(MapType::CgroupArray), kern_version); + + let kern_version = KernelVersion::new(4, 10, 0); + kernel_assert!(is_supported!(MapType::LruHash), kern_version); + kernel_assert!(is_supported!(MapType::LruPerCpuHash), kern_version); + + let kern_version = KernelVersion::new(4, 11, 0); + kernel_assert!(is_supported!(MapType::LpmTrie), kern_version); + + let kern_version = KernelVersion::new(4, 12, 0); + kernel_assert!(is_supported!(MapType::ArrayOfMaps), kern_version); + kernel_assert!(is_supported!(MapType::HashOfMaps), kern_version); + + let kern_version = KernelVersion::new(4, 14, 0); + kernel_assert!(is_supported!(MapType::DevMap), kern_version); + kernel_assert!(is_supported!(MapType::SockMap), kern_version); + + let kern_version = KernelVersion::new(4, 15, 0); + kernel_assert!(is_supported!(MapType::CpuMap), kern_version); + + let kern_version = KernelVersion::new(4, 18, 0); + kernel_assert!(is_supported!(MapType::XskMap), kern_version); + kernel_assert!(is_supported!(MapType::SockHash), kern_version); + + let kern_version = KernelVersion::new(4, 19, 0); + kernel_assert!(is_supported!(MapType::CgroupStorage), kern_version); + kernel_assert!(is_supported!(MapType::ReuseportSockArray), kern_version); + + let kern_version = KernelVersion::new(4, 20, 0); + kernel_assert!(is_supported!(MapType::PerCpuCgroupStorage), kern_version); + kernel_assert!(is_supported!(MapType::Queue), kern_version); + kernel_assert!(is_supported!(MapType::Stack), kern_version); + + let kern_version = KernelVersion::new(5, 2, 0); + kernel_assert!(is_supported!(MapType::SkStorage), kern_version); + + let kern_version = KernelVersion::new(5, 4, 0); + kernel_assert!(is_supported!(MapType::DevMapHash), kern_version); + + let kern_version = KernelVersion::new(5, 6, 0); + kernel_assert!(is_supported!(MapType::StructOps), kern_version); + + let kern_version = KernelVersion::new(5, 8, 0); + kernel_assert!(is_supported!(MapType::RingBuf), kern_version); + + let kern_version = KernelVersion::new(5, 10, 0); + kernel_assert!(is_supported!(MapType::InodeStorage), kern_version); // Requires `CONFIG_BPF_LSM=y` + + let kern_version = KernelVersion::new(5, 11, 0); + kernel_assert!(is_supported!(MapType::TaskStorage), kern_version); + + let kern_version = KernelVersion::new(5, 16, 0); + kernel_assert!(is_supported!(MapType::BloomFilter), kern_version); + + let kern_version = KernelVersion::new(6, 1, 0); + kernel_assert!(is_supported!(MapType::UserRingBuf), kern_version); + + let kern_version = KernelVersion::new(6, 2, 0); + kernel_assert!(is_supported!(MapType::CgrpStorage), kern_version); + + let kern_version = KernelVersion::new(6, 9, 0); + kernel_assert!(is_supported!(MapType::Arena), kern_version); +} diff --git a/xtask/public-api/aya.txt b/xtask/public-api/aya.txt index 2d62d08f..16e2a4ea 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_map_supported(map_type: aya::maps::MapType) -> 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 From 63034cd204693e02520394fd4e151eb773d88a99 Mon Sep 17 00:00:00 2001 From: Tyrone Wu Date: Mon, 28 Oct 2024 02:12:54 +0000 Subject: [PATCH 3/4] aya,aya-obj: cache feat probed info fields Cached probed for ProgramInfo fields instead of exposing it through global FEATURE. Probing occurs on cache miss, which happens when first accessing the field, *and* if the field is 0. --- aya-obj/src/obj.rs | 16 ------ aya/src/bpf.rs | 8 ++- aya/src/programs/info.rs | 26 ++++++---- aya/src/sys/bpf.rs | 34 ++----------- aya/src/sys/feature_probe.rs | 66 +++++++++++++++++++++++-- aya/src/sys/mod.rs | 2 +- test/integration-test/src/tests/info.rs | 60 ++++++++++++++++------ xtask/public-api/aya-obj.txt | 4 -- 8 files changed, 131 insertions(+), 85 deletions(-) diff --git a/aya-obj/src/obj.rs b/aya-obj/src/obj.rs index 871e6937..f06270b5 100644 --- a/aya-obj/src/obj.rs +++ b/aya-obj/src/obj.rs @@ -45,8 +45,6 @@ pub struct Features { bpf_cookie: bool, cpumap_prog_id: bool, devmap_prog_id: bool, - prog_info_map_ids: bool, - prog_info_gpl_compatible: bool, btf: Option, } @@ -61,8 +59,6 @@ impl Features { bpf_cookie: bool, cpumap_prog_id: bool, devmap_prog_id: bool, - prog_info_map_ids: bool, - prog_info_gpl_compatible: bool, btf: Option, ) -> Self { Self { @@ -73,8 +69,6 @@ impl Features { bpf_cookie, cpumap_prog_id, devmap_prog_id, - prog_info_map_ids, - prog_info_gpl_compatible, btf, } } @@ -117,16 +111,6 @@ impl Features { self.devmap_prog_id } - /// Returns whether `bpf_prog_info` supports `nr_map_ids` & `map_ids` fields. - pub fn prog_info_map_ids(&self) -> bool { - self.prog_info_map_ids - } - - /// Returns whether `bpf_prog_info` supports `gpl_compatible` field. - pub fn prog_info_gpl_compatible(&self) -> bool { - self.prog_info_gpl_compatible - } - /// If BTF is supported, returns which BTF features are supported. pub fn btf(&self) -> Option<&BtfFeatures> { self.btf.as_ref() diff --git a/aya/src/bpf.rs b/aya/src/bpf.rs index 9c11defc..18fa60ea 100644 --- a/aya/src/bpf.rs +++ b/aya/src/bpf.rs @@ -32,9 +32,9 @@ use crate::{ bpf_load_btf, is_bpf_cookie_supported, is_bpf_global_data_supported, is_btf_datasec_supported, is_btf_decl_tag_supported, is_btf_enum64_supported, is_btf_float_supported, is_btf_func_global_supported, is_btf_func_supported, - is_btf_supported, is_btf_type_tag_supported, is_info_gpl_compatible_supported, - is_info_map_ids_supported, is_perf_link_supported, is_probe_read_kernel_supported, - is_prog_id_supported, is_prog_name_supported, retry_with_verifier_logs, + is_btf_supported, is_btf_type_tag_supported, is_perf_link_supported, + is_probe_read_kernel_supported, is_prog_id_supported, is_prog_name_supported, + retry_with_verifier_logs, }, util::{bytes_of, bytes_of_slice, nr_cpus, page_size}, }; @@ -82,8 +82,6 @@ fn detect_features() -> Features { is_bpf_cookie_supported(), is_prog_id_supported(BPF_MAP_TYPE_CPUMAP), is_prog_id_supported(BPF_MAP_TYPE_DEVMAP), - is_info_map_ids_supported(), - is_info_gpl_compatible_supported(), btf, ); debug!("BPF Feature Detection: {:#?}", f); diff --git a/aya/src/programs/info.rs b/aya/src/programs/info.rs index 876264d1..753d6054 100644 --- a/aya/src/programs/info.rs +++ b/aya/src/programs/info.rs @@ -4,6 +4,7 @@ use std::{ ffi::CString, os::fd::{AsFd as _, BorrowedFd}, path::Path, + sync::OnceLock, time::{Duration, SystemTime}, }; @@ -16,7 +17,9 @@ use super::{ use crate::{ FEATURES, sys::{ - SyscallError, bpf_get_object, bpf_prog_get_fd_by_id, bpf_prog_get_info_by_fd, iter_prog_ids, + SyscallError, bpf_get_object, bpf_prog_get_fd_by_id, bpf_prog_get_info_by_fd, + feature_probe::{is_prog_info_license_supported, is_prog_info_map_ids_supported}, + iter_prog_ids, }, util::bytes_of_bpf_name, }; @@ -108,13 +111,15 @@ impl ProgramInfo { /// /// Introduced in kernel v4.15. pub fn map_ids(&self) -> Result>, ProgramError> { - if FEATURES.prog_info_map_ids() { - let mut map_ids = vec![0u32; self.0.nr_map_ids as usize]; - bpf_prog_get_info_by_fd(self.fd()?.as_fd(), &mut map_ids)?; - Ok(Some(map_ids)) - } else { - Ok(None) - } + static CACHE: OnceLock = OnceLock::new(); + CACHE + .get_or_init(|| matches!(is_prog_info_map_ids_supported(), Ok(true))) + .then(|| { + let mut map_ids = vec![0u32; self.0.nr_map_ids as usize]; + bpf_prog_get_info_by_fd(self.fd()?.as_fd(), &mut map_ids)?; + Ok(map_ids) + }) + .transpose() } /// The name of the program as was provided when it was load. This is limited to 16 bytes. @@ -140,8 +145,9 @@ impl ProgramInfo { /// /// Introduced in kernel v4.18. pub fn gpl_compatible(&self) -> Option { - FEATURES - .prog_info_gpl_compatible() + static CACHE: OnceLock = OnceLock::new(); + CACHE + .get_or_init(|| matches!(is_prog_info_license_supported(), Ok(true))) .then_some(self.0.gpl_compatible() != 0) } diff --git a/aya/src/sys/bpf.rs b/aya/src/sys/bpf.rs index 4e80a71a..b1df7b58 100644 --- a/aya/src/sys/bpf.rs +++ b/aya/src/sys/bpf.rs @@ -25,7 +25,7 @@ use aya_obj::{ use libc::{ENOENT, ENOSPC}; use crate::{ - Btf, FEATURES, Pod, VerifierLogLevel, + Btf, Pod, VerifierLogLevel, maps::{MapData, PerCpuValues}, programs::{ProgramType, links::LinkRef}, sys::{Syscall, SyscallError, syscall}, @@ -593,7 +593,7 @@ pub(crate) fn bpf_prog_get_info_by_fd( // An `E2BIG` error can occur on kernels below v4.15 when handing over a large struct where the // extra space is not all-zero bytes. bpf_obj_get_info_by_fd(fd, |info: &mut bpf_prog_info| { - if FEATURES.prog_info_map_ids() { + if !map_ids.is_empty() { info.nr_map_ids = map_ids.len() as _; info.map_ids = map_ids.as_mut_ptr() as _; } @@ -808,34 +808,6 @@ where 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(ProgramType::TracePoint, |attr| { - let prog_fd = match bpf_prog_load(attr) { - Ok(fd) => fd, - Err(_) => return false, - }; - bpf_obj_get_info_by_fd(prog_fd.as_fd(), |info: &mut bpf_prog_info| { - info.nr_map_ids = 1 - }) - .is_ok() - }) -} - -/// Tests whether `gpl_compatible` field in `bpf_prog_info` is available. -pub(crate) fn is_info_gpl_compatible_supported() -> bool { - with_trivial_prog(ProgramType::TracePoint, |attr| { - let prog_fd = match bpf_prog_load(attr) { - Ok(fd) => fd, - Err(_) => return false, - }; - if let Ok::(info) = bpf_obj_get_info_by_fd(prog_fd.as_fd(), |_| {}) { - return info.gpl_compatible() != 0; - } - false - }) -} - pub(crate) fn is_probe_read_kernel_supported() -> bool { let mut attr = unsafe { mem::zeroed::() }; let u = unsafe { &mut attr.__bindgen_anon_3 }; @@ -1145,7 +1117,7 @@ fn sys_bpf(cmd: bpf_cmd, attr: &mut bpf_attr) -> io::Result { }) } -fn unit_sys_bpf(cmd: bpf_cmd, attr: &mut bpf_attr) -> io::Result<()> { +pub(super) fn unit_sys_bpf(cmd: bpf_cmd, attr: &mut bpf_attr) -> io::Result<()> { sys_bpf(cmd, attr).map(|code| assert_eq!(code, 0)) } diff --git a/aya/src/sys/feature_probe.rs b/aya/src/sys/feature_probe.rs index 51eae01d..835653bf 100644 --- a/aya/src/sys/feature_probe.rs +++ b/aya/src/sys/feature_probe.rs @@ -4,11 +4,13 @@ use std::{mem, os::fd::AsRawFd as _}; use aya_obj::{ btf::{Btf, BtfKind}, - generated::{BPF_F_MMAPABLE, BPF_F_NO_PREALLOC, bpf_attr, bpf_cmd, bpf_map_type}, + generated::{ + BPF_F_MMAPABLE, BPF_F_NO_PREALLOC, bpf_attr, bpf_cmd, bpf_map_type, bpf_prog_info, + }, }; use libc::{E2BIG, EBADF, EINVAL}; -use super::{SyscallError, bpf_prog_load, fd_sys_bpf, with_trivial_prog}; +use super::{SyscallError, bpf_prog_load, fd_sys_bpf, unit_sys_bpf, with_trivial_prog}; use crate::{ MockableFd, maps::MapType, @@ -26,7 +28,7 @@ use crate::{ /// 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), +/// Err(err) => println!("Uh oh! Unexpected error while probing: {:?}", err), /// } /// ``` /// @@ -149,7 +151,7 @@ pub fn is_program_supported(program_type: ProgramType) -> Result println!("hash_of_maps supported :)"), /// Ok(false) => println!("hash_of_maps not supported :("), -/// Err(err) => println!("Uh oh! Unexpected error: {:?}", err), +/// Err(err) => println!("Uh oh! Unexpected error while probing: {:?}", err), /// } /// ``` /// @@ -322,3 +324,59 @@ pub fn is_map_supported(map_type: MapType) -> Result { }), } } + +/// Whether `nr_map_ids` & `map_ids` fields in `bpf_prog_info` are supported. +pub(crate) fn is_prog_info_map_ids_supported() -> Result { + let fd = with_trivial_prog(ProgramType::SocketFilter, |attr| { + bpf_prog_load(attr).map_err(|io_error| { + ProgramError::SyscallError(SyscallError { + call: "bpf_prog_load", + io_error, + }) + }) + })?; + // SAFETY: all-zero byte-pattern valid for `bpf_prog_info` + let mut info = unsafe { mem::zeroed::() }; + info.nr_map_ids = 1; + + probe_bpf_info(fd, info).map_err(ProgramError::from) +} + +/// Tests whether `bpf_prog_info.gpl_compatible` field is supported. +pub(crate) fn is_prog_info_license_supported() -> Result { + let fd = with_trivial_prog(ProgramType::SocketFilter, |attr| { + bpf_prog_load(attr).map_err(|io_error| { + ProgramError::SyscallError(SyscallError { + call: "bpf_prog_load", + io_error, + }) + }) + })?; + // SAFETY: all-zero byte-pattern valid for `bpf_prog_info` + let mut info = unsafe { mem::zeroed::() }; + info.set_gpl_compatible(1); + + probe_bpf_info(fd, info).map_err(ProgramError::from) +} + +/// Probes program and map info. +fn probe_bpf_info(fd: MockableFd, info: T) -> Result { + // SAFETY: all-zero byte-pattern valid for `bpf_attr` + let mut attr = unsafe { mem::zeroed::() }; + attr.info.bpf_fd = fd.as_raw_fd() as u32; + attr.info.info_len = mem::size_of_val(&info) as u32; + attr.info.info = &info as *const _ as u64; + + let io_error = match unit_sys_bpf(bpf_cmd::BPF_OBJ_GET_INFO_BY_FD, &mut attr) { + Ok(()) => return Ok(true), + Err(io_error) => io_error, + }; + match io_error.raw_os_error() { + // `E2BIG` from `bpf_check_uarg_tail_zero()` + Some(E2BIG) => Ok(false), + _ => Err(SyscallError { + call: "bpf_obj_get_info_by_fd", + io_error, + }), + } +} diff --git a/aya/src/sys/mod.rs b/aya/src/sys/mod.rs index c0d2c59c..ee6a3af9 100644 --- a/aya/src/sys/mod.rs +++ b/aya/src/sys/mod.rs @@ -1,7 +1,7 @@ //! A collection of system calls for performing eBPF related operations. mod bpf; -mod feature_probe; +pub(crate) mod feature_probe; mod netlink; mod perf_event; diff --git a/test/integration-test/src/tests/info.rs b/test/integration-test/src/tests/info.rs index 99c7af46..cc9ea680 100644 --- a/test/integration-test/src/tests/info.rs +++ b/test/integration-test/src/tests/info.rs @@ -12,6 +12,7 @@ use aya::{ Ebpf, maps::{Array, HashMap, IterableMap as _, MapError, MapType, loaded_maps}, programs::{ProgramError, ProgramType, SocketFilter, TracePoint, UProbe, loaded_programs}, + sys::{is_map_supported, is_program_supported}, util::KernelVersion, }; use libc::EINVAL; @@ -20,7 +21,11 @@ use crate::utils::{kernel_assert, kernel_assert_eq}; #[test] fn test_loaded_programs() { - // Load a program. + if !is_program_supported(ProgramType::SocketFilter).unwrap() { + eprintln!("skipping test - socket_filter program not supported"); + return; + } + // Since we are only testing the programs for their metadata, there is no need to "attach" them. let mut bpf = Ebpf::load(crate::TEST).unwrap(); let prog: &mut UProbe = bpf.program_mut("test_uprobe").unwrap().try_into().unwrap(); @@ -33,9 +38,7 @@ fn test_loaded_programs() { if let ProgramError::SyscallError(err) = &err { // Skip entire test since feature not available if err.io_error.raw_os_error() == Some(EINVAL) { - eprintln!( - "ignoring test completely as `loaded_programs()` is not available on the host" - ); + eprintln!("skipping test - `loaded_programs()` not supported"); return; } } @@ -71,6 +74,11 @@ fn test_loaded_programs() { #[test] fn test_program_info() { + if !is_program_supported(ProgramType::SocketFilter).unwrap() { + eprintln!("skipping test - socket_filter program not supported"); + return; + } + // Kernels below v4.15 have been observed to have `bpf_jit_enable` disabled by default. let _guard = ensure_sysctl_enabled("/proc/sys/net/core/bpf_jit_enable"); @@ -129,6 +137,11 @@ fn test_program_info() { #[test] fn test_loaded_at() { + if !is_program_supported(ProgramType::SocketFilter).unwrap() { + eprintln!("skipping test - socket_filter program not supported"); + return; + } + let mut bpf: Ebpf = Ebpf::load(crate::SIMPLE_PROG).unwrap(); let prog: &mut SocketFilter = bpf.program_mut("simple_prog").unwrap().try_into().unwrap(); @@ -144,9 +157,7 @@ fn test_loaded_at() { let loaded_at = match prog.info().unwrap().loaded_at() { Some(time) => time, None => { - eprintln!( - "ignoring test completely as `load_time` field of `bpf_prog_info` is not available on the host" - ); + eprintln!("skipping test - `bpf_prog_info.load_time` field not supported"); return; } }; @@ -175,11 +186,12 @@ fn test_loaded_at() { #[test] fn test_prog_stats() { - // Test depends on whether trace point exists. + if !is_program_supported(ProgramType::TracePoint).unwrap() { + eprintln!("skipping test - tracepoint program not supported"); + return; + } if !Path::new("/sys/kernel/debug/tracing/events/syscalls/sys_enter_bpf").exists() { - eprintln!( - "ignoring test completely as `syscalls/sys_enter_bpf` is not available on the host" - ); + eprintln!("skipping test - `syscalls/sys_enter_bpf` not available"); return; } @@ -200,6 +212,17 @@ fn test_prog_stats() { #[test] fn list_loaded_maps() { + if !is_program_supported(ProgramType::SocketFilter).unwrap() { + eprintln!("skipping test - socket_filter program not supported"); + return; + } else if !is_map_supported(MapType::Hash).unwrap() { + eprintln!("skipping test - hash map not supported"); + return; + } else if !is_map_supported(MapType::Array).unwrap() { + eprintln!("skipping test - array map not supported"); + return; + } + // Load a program with maps. let mut bpf: Ebpf = Ebpf::load(crate::MAP_TEST).unwrap(); let prog: &mut SocketFilter = bpf.program_mut("simple_prog").unwrap().try_into().unwrap(); @@ -210,9 +233,7 @@ fn list_loaded_maps() { if let Err(err) = maps.peek().unwrap() { if let MapError::SyscallError(err) = &err { if err.io_error.raw_os_error() == Some(EINVAL) { - eprintln!( - "ignoring test completely as `loaded_maps()` is not available on the host" - ); + eprintln!("skipping test - `loaded_maps()` not supported"); return; } } @@ -250,6 +271,17 @@ fn list_loaded_maps() { #[test] fn test_map_info() { + if !is_program_supported(ProgramType::SocketFilter).unwrap() { + eprintln!("skipping test - socket_filter program not supported"); + return; + } else if !is_map_supported(MapType::Hash).unwrap() { + eprintln!("skipping test - hash map not supported"); + return; + } else if !is_map_supported(MapType::Array).unwrap() { + eprintln!("skipping test - array map not supported"); + return; + } + let mut bpf: Ebpf = Ebpf::load(crate::MAP_TEST).unwrap(); let prog: &mut SocketFilter = bpf.program_mut("simple_prog").unwrap().try_into().unwrap(); prog.load().unwrap(); diff --git a/xtask/public-api/aya-obj.txt b/xtask/public-api/aya-obj.txt index 9fce242e..59adfdcf 100644 --- a/xtask/public-api/aya-obj.txt +++ b/xtask/public-api/aya-obj.txt @@ -8678,8 +8678,6 @@ pub fn aya_obj::Features::bpf_probe_read_kernel(&self) -> bool pub fn aya_obj::Features::btf(&self) -> core::option::Option<&aya_obj::btf::BtfFeatures> pub fn aya_obj::Features::cpumap_prog_id(&self) -> bool pub fn aya_obj::Features::devmap_prog_id(&self) -> bool -pub fn aya_obj::Features::prog_info_gpl_compatible(&self) -> bool -pub fn aya_obj::Features::prog_info_map_ids(&self) -> bool impl core::default::Default for aya_obj::Features pub fn aya_obj::Features::default() -> aya_obj::Features impl core::fmt::Debug for aya_obj::Features @@ -9541,8 +9539,6 @@ pub fn aya_obj::Features::bpf_probe_read_kernel(&self) -> bool pub fn aya_obj::Features::btf(&self) -> core::option::Option<&aya_obj::btf::BtfFeatures> pub fn aya_obj::Features::cpumap_prog_id(&self) -> bool pub fn aya_obj::Features::devmap_prog_id(&self) -> bool -pub fn aya_obj::Features::prog_info_gpl_compatible(&self) -> bool -pub fn aya_obj::Features::prog_info_map_ids(&self) -> bool impl core::default::Default for aya_obj::Features pub fn aya_obj::Features::default() -> aya_obj::Features impl core::fmt::Debug for aya_obj::Features From e45bc32703f4c4308231c9aaea59cdcc4fb6f8a4 Mon Sep 17 00:00:00 2001 From: Tyrone Wu Date: Fri, 28 Mar 2025 22:03:48 +0000 Subject: [PATCH 4/4] aya: short-circuit info field if non-zero Short-circuits `CACHE` to true if the field is non-zero. This saves from executing the probing logic since the logic essentially checks if the field can process (or doesn't error) non-zero value. --- aya/src/programs/info.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/aya/src/programs/info.rs b/aya/src/programs/info.rs index 753d6054..ef6b5378 100644 --- a/aya/src/programs/info.rs +++ b/aya/src/programs/info.rs @@ -113,7 +113,9 @@ impl ProgramInfo { pub fn map_ids(&self) -> Result>, ProgramError> { static CACHE: OnceLock = OnceLock::new(); CACHE - .get_or_init(|| matches!(is_prog_info_map_ids_supported(), Ok(true))) + .get_or_init(|| { + self.0.nr_map_ids > 0 || matches!(is_prog_info_map_ids_supported(), Ok(true)) + }) .then(|| { let mut map_ids = vec![0u32; self.0.nr_map_ids as usize]; bpf_prog_get_info_by_fd(self.fd()?.as_fd(), &mut map_ids)?; @@ -147,7 +149,9 @@ impl ProgramInfo { pub fn gpl_compatible(&self) -> Option { static CACHE: OnceLock = OnceLock::new(); CACHE - .get_or_init(|| matches!(is_prog_info_license_supported(), Ok(true))) + .get_or_init(|| { + self.0.gpl_compatible() != 0 || matches!(is_prog_info_license_supported(), Ok(true)) + }) .then_some(self.0.gpl_compatible() != 0) }