From 69b42f4745a7b32a5c96dd6451794c25b698e1ab Mon Sep 17 00:00:00 2001
From: Tyrone Wu <wudevelops@gmail.com>
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.

Assertions for `LircMode2` and `Lsm` are disabled because they require
certain kernel configs to be enabled, which are not by default in VM
tests.
---
 aya/src/sys/bpf.rs                            |   4 +-
 aya/src/sys/feature_probe.rs                  | 135 +++++++++++
 aya/src/sys/mod.rs                            |   1 +
 test/integration-test/src/tests.rs            |   1 +
 .../src/tests/feature_probe.rs                | 210 ++++++++++++++++++
 xtask/public-api/aya.txt                      |   2 +
 6 files changed, 351 insertions(+), 2 deletions(-)
 create mode 100644 aya/src/sys/feature_probe.rs
 create mode 100644 test/integration-test/src/tests/feature_probe.rs

diff --git a/aya/src/sys/bpf.rs b/aya/src/sys/bpf.rs
index 2bc8c4c6..b968f748 100644
--- a/aya/src/sys/bpf.rs
+++ b/aya/src/sys/bpf.rs
@@ -729,7 +729,7 @@ fn new_insn(code: u8, dst_reg: u8, src_reg: u8, offset: i16, imm: i32) -> bpf_in
     insn
 }
 
-fn with_trivial_prog<T, F>(op: F) -> T
+pub(super) fn with_trivial_prog<T, F>(op: F) -> T
 where
     F: FnOnce(&mut bpf_attr) -> T,
 {
@@ -1075,7 +1075,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<crate::MockableFd> {
+pub(super) fn bpf_prog_load(attr: &mut bpf_attr) -> io::Result<crate::MockableFd> {
     // 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..91e5b949
--- /dev/null
+++ b/aya/src/sys/feature_probe.rs
@@ -0,0 +1,135 @@
+//! Probes and identifies available eBPF features supported by the host kernel.
+
+use std::io::ErrorKind;
+
+use aya_obj::{
+    btf::{Btf, BtfError, BtfKind},
+    generated::{bpf_attach_type, BPF_F_SLEEPABLE},
+};
+use libc::{E2BIG, EINVAL};
+
+use super::{bpf_prog_load, with_trivial_prog, SyscallError};
+use crate::{
+    programs::{ProgramError, ProgramType},
+    util::KernelVersion,
+};
+
+/// 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> {
+    with_trivial_prog(|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;
+        if let Some(expected_attach_type) = expected_attach_type {
+            u.expected_attach_type = expected_attach_type as u32;
+        }
+
+        bpf_prog_load(attr).map_err(|io_error| {
+            ProgramError::SyscallError(SyscallError {
+                call: "bpf_prog_load",
+                io_error,
+            })
+        })
+    })
+}
diff --git a/aya/src/sys/mod.rs b/aya/src/sys/mod.rs
index 5c7d173f..1f5818cb 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 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..c13af593
--- /dev/null
+++ b/test/integration-test/src/tests/feature_probe.rs
@@ -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
+}
diff --git a/xtask/public-api/aya.txt b/xtask/public-api/aya.txt
index 473968a3..f6ab86e4 100644
--- a/xtask/public-api/aya.txt
+++ b/xtask/public-api/aya.txt
@@ -9622,6 +9622,8 @@ impl aya::programs::MultiProgram for aya::programs::tc::SchedClassifier
 pub fn aya::programs::tc::SchedClassifier::fd(&self) -> core::result::Result<std::os::fd::owned::BorrowedFd<'_>, aya::programs::ProgramError>
 pub fn aya::programs::loaded_programs() -> impl core::iter::traits::iterator::Iterator<Item = core::result::Result<aya::programs::ProgramInfo, aya::programs::ProgramError>>
 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<bool, aya::programs::ProgramError>
 #[non_exhaustive] pub enum aya::sys::Stats
 pub aya::sys::Stats::RunTime
 impl core::clone::Clone for aya::sys::Stats