浏览代码

aya,aya-obj: add feature probing program type

Adds API that probes whether kernel supports a program type.
Tyrone Wu 7 月之前
父节点
当前提交
ab77decd9a

+ 1 - 0
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 }

+ 65 - 8
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<crate::MockableFd, Syscall
 }
 
 pub(crate) fn is_prog_name_supported() -> 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<T, F>(op: F) -> T
+pub(super) fn with_trivial_prog<T, F>(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<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) }
 }

+ 148 - 0
aya/src/sys/feature_probe.rs

@@ -0,0 +1,148 @@
+//! 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<bool, ProgramError> {
+    if program_type == ProgramType::Unspecified {
+        return Ok(false);
+    }
+
+    // Verifier log is used in tracing, extension, and lsm to detect support if loading fails due
+    // to unset `attach_btf_id`. A valid `attach_btf_id` is required for a successful load, but is
+    // left unset if the hook functions cannot be found in the BTF.
+    //
+    // If the program types are supported, but the field is unset, then the following message[0]
+    // is emitted to the verifier log:
+    // `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`
+    //
+    // Otherwise, if the program types are not supported, then the verifier log will be empty.
+    //
+    // [0] https://elixir.bootlin.com/linux/v5.5/source/kernel/bpf/verifier.c#L9535
+    let mut verifier_log = matches!(
+        program_type,
+        ProgramType::Tracing | ProgramType::Extension | ProgramType::Lsm
+    )
+    .then_some([0_u8; 256]);
+
+    // Both tracing and lsm types require a valid `attach_btf_id` to load successfully. However, if
+    // the symbols cannot be found in the BTF, then leave the field unset/0.
+    //
+    // The extension type also requires an `attach_btf_id`, but we intentionally leave it unset
+    // since a successful load requires additional setup with a separate BTF-backed program.
+    //
+    // 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;
+        }
+        // If loading fails for tracing, extension, and lsm types due to unset `attach_btf_id`,
+        // then we defer to verifier log to verify whether type is supported.
+        if let Some(verifier_log) = verifier_log.as_mut() {
+            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. The
+                    // message originated from `check_attach_btf_id()`[0] in v5.5 to v5.9, then
+                    // moved to `bpf_check_attach_target()`[1] in v5.10 and onward.
+                    //
+                    // 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 the verifier log is empty, then it was immediately rejected by the
+                    // kernel, meaning the types are not supported.
+                    //
+                    // [0] https://elixir.bootlin.com/linux/v5.5/source/kernel/bpf/verifier.c#L9535
+                    // [1] https://elixir.bootlin.com/linux/v5.9/source/kernel/bpf/verifier.c#L10849
+                    let supported = matches!(
+                        verifier_log,
+                        Some(verifier_log) if verifier_log.starts_with(b"Tracing programs must provide btf_id")
+                    );
+                    Ok(supported)
+                }
+                // `E2BIG` from `bpf_check_uarg_tail_zero()`[0] indicates that the kernel detected
+                // non-zero fields in `bpf_attr` that does not exist at its current version.
+                //
+                // [0] https://elixir.bootlin.com/linux/v4.18/source/kernel/bpf/syscall.c#L71
+                Some(E2BIG) => Ok(false),
+                // `ENOTSUPP` from `check_struct_ops_btf_id()`[0] 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.
+                //
+                // [0] https://elixir.bootlin.com/linux/v5.6/source/kernel/bpf/verifier.c#L9740
+                Some(524) if program_type == ProgramType::StructOps => Ok(true),
+                _ => Err(error),
+            }
+        }
+        _ => Err(error),
+    }
+}

+ 2 - 0
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::*;

+ 1 - 0
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 }

+ 1 - 0
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;

+ 131 - 0
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);
+}

+ 1 - 1
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() {

+ 1 - 0
xtask/public-api/aya.txt

@@ -10160,6 +10160,7 @@ pub fn aya::sys::SyscallError::borrow_mut(&mut self) -> &mut T
 impl<T> core::convert::From<T> 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<std::os::fd::owned::OwnedFd, aya::sys::SyscallError>
+pub fn aya::sys::is_program_supported(program_type: aya::programs::ProgramType) -> core::result::Result<bool, aya::programs::ProgramError>
 pub mod aya::util
 pub struct aya::util::KernelVersion
 impl aya::util::KernelVersion