diff --git a/aya-obj/src/obj.rs b/aya-obj/src/obj.rs index f0168950..c6ea882d 100644 --- a/aya-obj/src/obj.rs +++ b/aya-obj/src/obj.rs @@ -23,8 +23,8 @@ use crate::{ Array, Btf, BtfError, BtfExt, BtfFeatures, BtfType, DataSecEntry, FuncSecInfo, LineSecInfo, }, generated::{ - bpf_insn, bpf_map_info, bpf_map_type::BPF_MAP_TYPE_ARRAY, BPF_CALL, BPF_F_RDONLY_PROG, - BPF_JMP, BPF_K, + __BindgenBitfieldUnit, bpf_insn, bpf_map_info, bpf_map_type::BPF_MAP_TYPE_ARRAY, BPF_CALL, + BPF_F_RDONLY_PROG, BPF_JMP, BPF_K, }, maps::{bpf_map_def, BtfMap, BtfMapDef, LegacyMap, Map, PinningType, MINIMUM_MAP_SIZE}, programs::{ @@ -135,6 +135,42 @@ impl Features { } } +impl bpf_insn { + /// Creates a [BPF instruction](bpf_insn). + /// + /// The arguments will be converted to the host's endianness. + pub const fn new(code: u8, dst_reg: u8, src_reg: u8, off: i16, imm: i32) -> Self { + if dst_reg > 10 || src_reg > 10 { + panic!("invalid register number"); + } + + let registers; + let offset; + let immediate; + + #[cfg(target_endian = "little")] + { + registers = (src_reg << 4) | dst_reg; + offset = off.swap_bytes(); + immediate = imm.swap_bytes(); + } + #[cfg(target_endian = "big")] + { + registers = (dst_reg << 4) | src_reg; + offset = off; + immediate = imm; + } + + bpf_insn { + code, + _bitfield_align_1: [], + _bitfield_1: __BindgenBitfieldUnit::new([registers]), + off: offset, + imm: immediate, + } + } +} + /// The loaded object file representation #[derive(Clone, Debug)] pub struct Object { diff --git a/aya/src/sys/bpf.rs b/aya/src/sys/bpf.rs index 2d7e35ba..26d000d8 100644 --- a/aya/src/sys/bpf.rs +++ b/aya/src/sys/bpf.rs @@ -1139,7 +1139,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) -> SysResult { +pub(super) fn bpf_prog_load(attr: &mut bpf_attr) -> SysResult { // 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..43501ebe --- /dev/null +++ b/aya/src/sys/feature_probe.rs @@ -0,0 +1,109 @@ +//! Probes and identifies available eBPF features supported by the host kernel. + +use std::mem; + +use aya_obj::generated::{bpf_attach_type, bpf_attr, bpf_insn, BPF_F_SLEEPABLE}; +use libc::{E2BIG, EINVAL}; + +use super::{bpf_prog_load, SyscallError}; +use crate::{programs::ProgramType, util::KernelVersion}; + +const RETURN_ZERO_INSNS: &[bpf_insn] = &[ + bpf_insn::new(0xb7, 0, 0, 0, 0), // mov64 r0 = 0 + bpf_insn::new(0x95, 0, 0, 0, 0), // exit +]; +const GPL_COMPATIBLE: &[u8; 4] = b"GPL\0"; + +/// Whether the host kernel supports the [`ProgramType`]. +/// +/// # Examples +/// +/// ```no_run +/// # use aya::{ +/// # programs::ProgramType, +/// # sys::feature_probe::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 [`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_program_supported(program_type: ProgramType) -> Result { + if program_type == ProgramType::Unspecified { + return Ok(false); + } + + // 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_3 }; + + // `bpf_prog_load_fixup_attach_type()` sets this for us for cgroup_sock and + // and sk_reuseport. + 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::CgroupSockopt => Some(bpf_attach_type::BPF_CGROUP_GETSOCKOPT), + ProgramType::SkLookup => Some(bpf_attach_type::BPF_SK_LOOKUP), + ProgramType::Netfilter => Some(bpf_attach_type::BPF_NETFILTER), + _ => None, + }; + + // Intentionally trigger `EINVAL` for some prog types, and use verifier + // logs to help confirm whether the variant actually exists. + let mut verifier_log = [0_u8; libc::PATH_MAX as usize]; + + match program_type { + ProgramType::KProbe => u.kern_version = KernelVersion::current().unwrap().code(), + ProgramType::Tracing | ProgramType::Extension | ProgramType::Lsm => { + u.log_buf = verifier_log.as_mut_ptr() as _; + u.log_size = libc::PATH_MAX as _; + u.log_level = 1; + } + ProgramType::Syscall => u.prog_flags = BPF_F_SLEEPABLE, + _ => {} + } + + u.prog_type = program_type as u32; + u.insn_cnt = 2; + u.insns = RETURN_ZERO_INSNS.as_ptr() as u64; + u.license = GPL_COMPATIBLE.as_ptr() as u64; + if let Some(expected_attach_type) = expected_attach_type { + u.expected_attach_type = expected_attach_type as u32; + } + + let io_error = match bpf_prog_load(&mut attr) { + Ok(_) => return Ok(true), + Err((_, io_error)) => io_error, + }; + match io_error.raw_os_error() { + Some(EINVAL) => { + // verifier/`bpf_check_attach_target()` produces same log message + // for these types (due to `attach_btf_id` unset) + 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) + } + // `E2BIG` when accessing/using fields that are not available + // e.g. `expected_attach_type` + Some(E2BIG) => Ok(false), + // `ENOTSUPP` from verifier/`check_struct_ops_btf_id()` for struct_ops + Some(524) if program_type == ProgramType::StructOps => Ok(true), + _ => Err(SyscallError { + call: "bpf_prog_load", + io_error, + }), + } +} diff --git a/aya/src/sys/mod.rs b/aya/src/sys/mod.rs index 5bc72d95..59cc4f90 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; +pub mod feature_probe; mod netlink; mod perf_event; diff --git a/test/integration-test/src/tests.rs b/test/integration-test/src/tests.rs index 61f490cf..6d585486 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 load; mod log; 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..e5be55f1 --- /dev/null +++ b/test/integration-test/src/tests/feature_probe.rs @@ -0,0 +1,207 @@ +//! Test feature probing against kernel version. + +use assert_matches::assert_matches; +use aya::{programs::ProgramType, sys::feature_probe::*, util::KernelVersion}; + +// TODO: Enable certain CONFIG_* options when compiling the image for VM tests. +#[test] +fn probe_supported_programs() { + let current = KernelVersion::current().unwrap(); + + let socket_filter = retry(3, || is_program_supported(ProgramType::SocketFilter)); + if current >= KernelVersion::new(3, 19, 0) { + assert_matches!(socket_filter, Ok(true)); + } else { + assert_matches!(socket_filter, Ok(false)); + } + + let kprobe = retry(3, || is_program_supported(ProgramType::KProbe)); + let sched_cls = retry(3, || is_program_supported(ProgramType::SchedClassifier)); + let sched_act = retry(3, || is_program_supported(ProgramType::SchedAction)); + if current >= KernelVersion::new(4, 1, 0) { + assert_matches!(kprobe, Ok(true)); + assert_matches!(sched_cls, Ok(true)); + assert_matches!(sched_act, Ok(true)); + } else { + assert_matches!(kprobe, Ok(false)); + assert_matches!(sched_cls, Ok(false)); + assert_matches!(sched_act, Ok(false)); + } + + let tracepoint = retry(3, || is_program_supported(ProgramType::TracePoint)); + if current >= KernelVersion::new(4, 7, 0) { + assert_matches!(tracepoint, Ok(true)); + } else { + assert_matches!(tracepoint, Ok(false)); + } + + let xdp = retry(3, || is_program_supported(ProgramType::Xdp)); + if current >= KernelVersion::new(4, 8, 0) { + assert_matches!(xdp, Ok(true)); + } else { + assert_matches!(xdp, Ok(false)); + } + + let perf_event = retry(3, || is_program_supported(ProgramType::PerfEvent)); + if current >= KernelVersion::new(4, 9, 0) { + assert_matches!(perf_event, Ok(true)); + } else { + assert_matches!(perf_event, Ok(false)); + } + + let cgroup_skb = retry(3, || is_program_supported(ProgramType::CgroupSkb)); + let cgroup_sock = retry(3, || is_program_supported(ProgramType::CgroupSock)); + let lwt_in = retry(3, || is_program_supported(ProgramType::LwtInput)); + let lwt_out = retry(3, || is_program_supported(ProgramType::LwtOutput)); + let lwt_xmit = retry(3, || is_program_supported(ProgramType::LwtXmit)); + if current >= KernelVersion::new(4, 10, 0) { + assert_matches!(cgroup_skb, Ok(true)); + assert_matches!(cgroup_sock, Ok(true)); + assert_matches!(lwt_in, Ok(true)); + assert_matches!(lwt_out, Ok(true)); + assert_matches!(lwt_xmit, Ok(true)); + } else { + assert_matches!(cgroup_skb, Ok(false)); + assert_matches!(cgroup_sock, Ok(false)); + assert_matches!(lwt_in, Ok(false)); + assert_matches!(lwt_out, Ok(false)); + assert_matches!(lwt_xmit, Ok(false)); + } + + let sock_ops = retry(3, || is_program_supported(ProgramType::SockOps)); + if current >= KernelVersion::new(4, 13, 0) { + assert_matches!(sock_ops, Ok(true)); + } else { + assert_matches!(sock_ops, Ok(false)); + } + + let sk_skb = retry(3, || is_program_supported(ProgramType::SkSkb)); + if current >= KernelVersion::new(4, 14, 0) { + assert_matches!(sk_skb, Ok(true)); + } else { + assert_matches!(sk_skb, Ok(false)); + } + + let cgroup_device = retry(3, || is_program_supported(ProgramType::CgroupDevice)); + if current >= KernelVersion::new(4, 15, 0) { + assert_matches!(cgroup_device, Ok(true)); + } else { + assert_matches!(cgroup_device, Ok(false)); + } + + let sk_msg = retry(3, || is_program_supported(ProgramType::SkMsg)); + let raw_tp = retry(3, || is_program_supported(ProgramType::RawTracePoint)); + let cgroup_sock_addr = retry(3, || is_program_supported(ProgramType::CgroupSockAddr)); + if current >= KernelVersion::new(4, 17, 0) { + assert_matches!(sk_msg, Ok(true)); + assert_matches!(raw_tp, Ok(true)); + assert_matches!(cgroup_sock_addr, Ok(true)); + } else { + assert_matches!(sk_msg, Ok(false)); + assert_matches!(raw_tp, Ok(false)); + assert_matches!(cgroup_sock_addr, Ok(false)); + } + + let lwt_seg6local = retry(3, || is_program_supported(ProgramType::LwtSeg6local)); + // Requires `CONFIG_BPF_LIRC_MODE2=y`. + // let lirc_mode2 = is_program_supported(ProgramType::LircMode2); + if current >= KernelVersion::new(4, 18, 0) { + assert_matches!(lwt_seg6local, Ok(true)); + // assert_matches!(lirc_mode2, Ok(true)); + } else { + assert_matches!(lwt_seg6local, Ok(false)); + // assert_matches!(lirc_mode2, Ok(false)); + } + + let sk_reuseport = retry(3, || is_program_supported(ProgramType::SkReuseport)); + if current >= KernelVersion::new(4, 19, 0) { + assert_matches!(sk_reuseport, Ok(true)); + } else { + assert_matches!(sk_reuseport, Ok(false)); + } + + let flow_dissector = retry(3, || is_program_supported(ProgramType::FlowDissector)); + if current >= KernelVersion::new(4, 20, 0) { + assert_matches!(flow_dissector, Ok(true)); + } else { + assert_matches!(flow_dissector, Ok(false)); + } + + let cgroup_sysctl = retry(3, || is_program_supported(ProgramType::CgroupSysctl)); + let raw_tp_writable = retry(3, || { + is_program_supported(ProgramType::RawTracePointWritable) + }); + if current >= KernelVersion::new(5, 2, 0) { + assert_matches!(cgroup_sysctl, Ok(true)); + assert_matches!(raw_tp_writable, Ok(true)); + } else { + assert_matches!(cgroup_sysctl, Ok(false)); + assert_matches!(raw_tp_writable, Ok(false)); + } + + let cgroup_sockopt = retry(3, || is_program_supported(ProgramType::CgroupSockopt)); + if current >= KernelVersion::new(5, 3, 0) { + assert_matches!(cgroup_sockopt, Ok(true)); + } else { + assert_matches!(cgroup_sockopt, Ok(false)); + } + + let tracing = retry(3, || is_program_supported(ProgramType::Tracing)); + if current >= KernelVersion::new(5, 5, 0) { + assert_matches!(tracing, Ok(true)); + } else { + assert_matches!(tracing, Ok(false)); + } + + let struct_ops = retry(3, || is_program_supported(ProgramType::StructOps)); + let extension = retry(3, || is_program_supported(ProgramType::Extension)); + if current >= KernelVersion::new(5, 6, 0) { + assert_matches!(struct_ops, Ok(true)); + assert_matches!(extension, Ok(true)); + } else { + assert_matches!(struct_ops, Ok(false)); + assert_matches!(extension, Ok(false)); + } + + // Requires `CONFIG_BPF_LSM=y` + // let lsm = retry(3, || is_program_supported(ProgramType::Lsm)); + // if current >= KernelVersion::new(5, 7, 0) { + // assert_matches!(lsm, Ok(true)); + // } else { + // assert_matches!(lsm, Ok(false)); + // } + + let sk_lookup = retry(3, || is_program_supported(ProgramType::SkLookup)); + if current >= KernelVersion::new(5, 9, 0) { + assert_matches!(sk_lookup, Ok(true)); + } else { + assert_matches!(sk_lookup, Ok(false)); + } + + let syscall = retry(3, || is_program_supported(ProgramType::Syscall)); + if current >= KernelVersion::new(5, 14, 0) { + assert_matches!(syscall, Ok(true)); + } else { + assert_matches!(syscall, Ok(false)); + } + + let netfilter = retry(3, || is_program_supported(ProgramType::Netfilter)); + if current >= KernelVersion::new(6, 4, 0) { + assert_matches!(netfilter, Ok(true)); + } else { + assert_matches!(netfilter, Ok(false)); + } +} + +// Back-to-back calls can be flaky and return `EPERM`. +fn retry(max_retries: u64, try_func: impl Fn() -> Result) -> Result { + let mut res = try_func(); + for i in 1..max_retries { + if res.is_ok() { + return res; + } + std::thread::sleep(std::time::Duration::from_millis(i * 10)); + res = try_func(); + } + res +} diff --git a/xtask/public-api/aya-obj.txt b/xtask/public-api/aya-obj.txt index 8d51e904..c3597242 100644 --- a/xtask/public-api/aya-obj.txt +++ b/xtask/public-api/aya-obj.txt @@ -4772,6 +4772,8 @@ pub fn aya_obj::generated::bpf_insn::new_bitfield_1(dst_reg: aya_obj::generated: pub fn aya_obj::generated::bpf_insn::set_dst_reg(&mut self, val: aya_obj::generated::__u8) pub fn aya_obj::generated::bpf_insn::set_src_reg(&mut self, val: aya_obj::generated::__u8) pub fn aya_obj::generated::bpf_insn::src_reg(&self) -> aya_obj::generated::__u8 +impl aya_obj::generated::bpf_insn +pub const fn aya_obj::generated::bpf_insn::new(code: u8, dst_reg: u8, src_reg: u8, off: i16, imm: i32) -> Self impl core::clone::Clone for aya_obj::generated::bpf_insn pub fn aya_obj::generated::bpf_insn::clone(&self) -> aya_obj::generated::bpf_insn impl core::fmt::Debug for aya_obj::generated::bpf_insn diff --git a/xtask/public-api/aya.txt b/xtask/public-api/aya.txt index 3d5c5874..3d90f6c9 100644 --- a/xtask/public-api/aya.txt +++ b/xtask/public-api/aya.txt @@ -9005,6 +9005,8 @@ impl aya::programs::MultiProgram for aya::programs::tc::SchedClassifier pub fn aya::programs::tc::SchedClassifier::fd(&self) -> core::result::Result, aya::programs::ProgramError> pub fn aya::programs::loaded_programs() -> impl core::iter::traits::iterator::Iterator> pub mod aya::sys +pub mod aya::sys::feature_probe +pub fn aya::sys::feature_probe::is_program_supported(program_type: aya::programs::ProgramType) -> core::result::Result #[non_exhaustive] pub enum aya::sys::Stats pub aya::sys::Stats::RunTime impl core::clone::Clone for aya::sys::Stats