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-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<BtfFeatures>,
 }
 
@@ -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<BtfFeatures>,
     ) -> 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..ef6b5378 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,17 @@ impl ProgramInfo {
     ///
     /// Introduced in kernel v4.15.
     pub fn map_ids(&self) -> Result<Option<Vec<u32>>, 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<bool> = OnceLock::new();
+        CACHE
+            .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)?;
+                Ok(map_ids)
+            })
+            .transpose()
     }
 
     /// The name of the program as was provided when it was load. This is limited to 16 bytes.
@@ -140,8 +147,11 @@ impl ProgramInfo {
     ///
     /// Introduced in kernel v4.18.
     pub fn gpl_compatible(&self) -> Option<bool> {
-        FEATURES
-            .prog_info_gpl_compatible()
+        static CACHE: OnceLock<bool> = OnceLock::new();
+        CACHE
+            .get_or_init(|| {
+                self.0.gpl_compatible() != 0 || 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 79ef4629..b1df7b58 100644
--- a/aya/src/sys/bpf.rs
+++ b/aya/src/sys/bpf.rs
@@ -25,9 +25,9 @@ use aya_obj::{
 use libc::{ENOENT, ENOSPC};
 
 use crate::{
-    Btf, FEATURES, Pod, VerifierLogLevel,
+    Btf, Pod, VerifierLogLevel,
     maps::{MapData, PerCpuValues},
-    programs::links::LinkRef,
+    programs::{ProgramType, links::LinkRef},
     sys::{Syscall, SyscallError, syscall},
     util::KernelVersion,
 };
@@ -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 _;
         }
@@ -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<crate::MockableFd> {
+pub(super) unsafe fn fd_sys_bpf(
+    cmd: bpf_cmd,
+    attr: &mut bpf_attr,
+) -> io::Result<crate::MockableFd> {
     let fd = sys_bpf(cmd, attr)?;
     let fd = fd.try_into().map_err(|std::num::TryFromIntError { .. }| {
         io::Error::new(
@@ -706,7 +709,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 +730,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,37 +746,66 @@ 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;
-
-    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| {
-        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()
-    })
-}
+    // `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,
+    };
 
-/// Tests whether `gpl_compatible` field in `bpf_prog_info` is available.
-pub(crate) fn is_info_gpl_compatible_supported() -> bool {
-    with_trivial_prog(|attr| {
-        let prog_fd = match bpf_prog_load(attr) {
-            Ok(fd) => fd,
-            Err(_) => return false,
-        };
-        if let Ok::<bpf_prog_info, _>(info) = bpf_obj_get_info_by_fd(prog_fd.as_fd(), |_| {}) {
-            return info.gpl_compatible() != 0;
+    match program_type {
+        ProgramType::KProbe => {
+            if let Ok(current_version) = KernelVersion::current() {
+                u.kern_version = current_version.code();
+            }
         }
-        false
-    })
+        // 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)
 }
 
 pub(crate) fn is_probe_read_kernel_supported() -> bool {
@@ -805,7 +837,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 +1105,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) }
 }
@@ -1085,7 +1117,7 @@ fn sys_bpf(cmd: bpf_cmd, attr: &mut bpf_attr) -> io::Result<i64> {
     })
 }
 
-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
new file mode 100644
index 00000000..835653bf
--- /dev/null
+++ b/aya/src/sys/feature_probe.rs
@@ -0,0 +1,382 @@
+//! Probes and identifies available eBPF features supported by the host kernel.
+
+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, bpf_prog_info,
+    },
+};
+use libc::{E2BIG, EBADF, EINVAL};
+
+use super::{SyscallError, bpf_prog_load, fd_sys_bpf, unit_sys_bpf, with_trivial_prog};
+use crate::{
+    MockableFd,
+    maps::MapType,
+    programs::{ProgramError, ProgramType},
+    util::page_size,
+};
+
+/// 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 while probing: {:?}", 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 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),
+    }
+}
+
+/// 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 while probing: {:?}", 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<bool, SyscallError> {
+    // 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::<bpf_attr>() };
+    // 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::<bpf_attr>() };
+            // 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,
+        }),
+    }
+}
+
+/// Whether `nr_map_ids` & `map_ids` fields in `bpf_prog_info` are supported.
+pub(crate) fn is_prog_info_map_ids_supported() -> Result<bool, ProgramError> {
+    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::<bpf_prog_info>() };
+    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<bool, ProgramError> {
+    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::<bpf_prog_info>() };
+    info.set_gpl_compatible(1);
+
+    probe_bpf_info(fd, info).map_err(ProgramError::from)
+}
+
+/// Probes program and map info.
+fn probe_bpf_info<T>(fd: MockableFd, info: T) -> Result<bool, SyscallError> {
+    // SAFETY: all-zero byte-pattern valid for `bpf_attr`
+    let mut attr = unsafe { mem::zeroed::<bpf_attr>() };
+    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 f8c8944b..ee6a3af9 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(crate) 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_map_supported, 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..d0bb29e9
--- /dev/null
+++ b/test/integration-test/src/tests/feature_probe.rs
@@ -0,0 +1,225 @@
+//! Test feature probing against kernel version.
+
+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;
+
+#[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);
+}
+
+#[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/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/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-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
diff --git a/xtask/public-api/aya.txt b/xtask/public-api/aya.txt
index 94a70d3c..16e2a4ea 100644
--- a/xtask/public-api/aya.txt
+++ b/xtask/public-api/aya.txt
@@ -10076,6 +10076,8 @@ 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_map_supported(map_type: aya::maps::MapType) -> core::result::Result<bool, 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