mirror of https://github.com/aya-rs/aya
aya,aya-obj: add feature probing program type
Adds API that probes whether kernel supports a program type. Additionally add `const fn bpf_insn::new()` so that common instructions are computed at compile-time instead of going through `copy_instructions()`. Assertions for `LircMode2` and `Lsm` are disabled because they require certain kernel configs to be enabled, which are not by default in VM tests.reviewable/pr1063/r1
parent
67a0595f87
commit
e1e775f83d
@ -0,0 +1,144 @@
|
||||
//! Probes and identifies available eBPF features supported by the host kernel.
|
||||
|
||||
use std::{io::ErrorKind, mem};
|
||||
|
||||
use aya_obj::{
|
||||
btf::{Btf, BtfError, BtfKind},
|
||||
generated::{bpf_attach_type, bpf_attr, bpf_insn, BPF_F_SLEEPABLE},
|
||||
};
|
||||
use libc::{E2BIG, EINVAL};
|
||||
|
||||
use super::{bpf_prog_load, SyscallError};
|
||||
use crate::{
|
||||
programs::{ProgramError, 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_LICENSE: &[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 [`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<bool, ProgramError> {
|
||||
if program_type == ProgramType::Unspecified {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let mut verifier_log = match program_type {
|
||||
ProgramType::Extension => vec![0_u8; libc::PATH_MAX as usize],
|
||||
_ => vec![],
|
||||
};
|
||||
let error = match create_minimal_program(program_type, &mut verifier_log) {
|
||||
Ok(_) => return Ok(true),
|
||||
Err(err) => err,
|
||||
};
|
||||
match error {
|
||||
ProgramError::SyscallError(err) if matches!(err.io_error.raw_os_error(), Some(EINVAL)) => {
|
||||
// verifier/`bpf_check_attach_target()` produces same log message
|
||||
// for these types due to unset `attach_btf_id`
|
||||
let supported = program_type == ProgramType::Extension
|
||||
&& verifier_log.starts_with(b"Tracing programs must provide btf_id");
|
||||
Ok(supported)
|
||||
}
|
||||
ProgramError::SyscallError(err) if matches!(err.io_error.raw_os_error(), Some(E2BIG)) => {
|
||||
Ok(false)
|
||||
}
|
||||
ProgramError::SyscallError(err)
|
||||
// `ENOTSUPP` from verifier/`check_struct_ops_btf_id()` for struct_ops
|
||||
if matches!(err.io_error.raw_os_error(), Some(524))
|
||||
&& program_type == ProgramType::StructOps =>
|
||||
{
|
||||
Ok(true)
|
||||
}
|
||||
ProgramError::Btf(BtfError::FileError { error, .. })
|
||||
if error.kind() == ErrorKind::NotFound =>
|
||||
{
|
||||
Ok(false)
|
||||
}
|
||||
_ => Err(error),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a minimal program with the specified type.
|
||||
/// Types not created for `Extension` and `StructOps`.
|
||||
fn create_minimal_program(
|
||||
program_type: ProgramType,
|
||||
verifier_log: &mut [u8],
|
||||
) -> Result<crate::MockableFd, ProgramError> {
|
||||
// SAFETY: all-zero byte-pattern valid for `bpf_attr`
|
||||
let mut attr = unsafe { mem::zeroed::<bpf_attr>() };
|
||||
// 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::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),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
match program_type {
|
||||
ProgramType::KProbe => u.kern_version = KernelVersion::current().unwrap().code(),
|
||||
ProgramType::Tracing | ProgramType::Lsm => {
|
||||
let btf = Btf::from_sys_fs()?;
|
||||
let func_name = match program_type {
|
||||
ProgramType::Tracing => "bpf_fentry_test1",
|
||||
_ => "bpf_lsm_bpf",
|
||||
};
|
||||
u.attach_btf_id = btf.id_by_type_name_kind(func_name, BtfKind::Func)?;
|
||||
}
|
||||
ProgramType::Extension => {
|
||||
u.log_buf = verifier_log.as_mut_ptr() as u64;
|
||||
u.log_level = 1;
|
||||
u.log_size = verifier_log.len() as u32;
|
||||
}
|
||||
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_LICENSE.as_ptr() as u64;
|
||||
if let Some(expected_attach_type) = expected_attach_type {
|
||||
u.expected_attach_type = expected_attach_type as u32;
|
||||
}
|
||||
|
||||
bpf_prog_load(&mut attr).map_err(|(_, io_error)| {
|
||||
ProgramError::SyscallError(SyscallError {
|
||||
call: "bpf_prog_load",
|
||||
io_error,
|
||||
})
|
||||
})
|
||||
}
|
@ -0,0 +1,210 @@
|
||||
//! Test feature probing against kernel version.
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
// Requires `CONFIG_DEBUG_INFO_BTF=y`
|
||||
let tracing = retry(3, || is_program_supported(ProgramType::Tracing));
|
||||
if current >= KernelVersion::new(5, 5, 0) && Path::new("/sys/kernel/btf").exists() {
|
||||
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` & `CONFIG_DEBUG_INFO_BTF=y`
|
||||
// let lsm = retry(3, || is_program_supported(ProgramType::Lsm));
|
||||
// if current >= KernelVersion::new(5, 7, 0) && Path::new("/sys/kernel/btf").exists() {
|
||||
// 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<T, E>(max_retries: u64, try_func: impl Fn() -> Result<T, E>) -> Result<T, E> {
|
||||
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
|
||||
}
|
Loading…
Reference in New Issue