diff --git a/aya-obj/src/btf/btf.rs b/aya-obj/src/btf/btf.rs
index 0d160a7d..91ac7e6d 100644
--- a/aya-obj/src/btf/btf.rs
+++ b/aya-obj/src/btf/btf.rs
@@ -18,11 +18,12 @@ use crate::{
         info::{FuncSecInfo, LineSecInfo},
         relocation::Relocation,
         Array, BtfEnum, BtfKind, BtfMember, BtfType, Const, Enum, FuncInfo, FuncLinkage, Int,
-        IntEncoding, LineInfo, Struct, Typedef, Union, VarLinkage,
+        IntEncoding, LineInfo, Struct, Typedef, Union, Var, VarLinkage,
     },
-    generated::{btf_ext_header, btf_header},
+    generated::{bpf_map_type, btf_ext_header, btf_header, BPF_F_RDONLY_PROG},
+    maps::{bpf_map_def, LegacyMap},
     util::{bytes_of, HashMap},
-    Object,
+    EbpfSectionKind, Map, Object,
 };
 
 pub(crate) const MAX_RESOLVE_DEPTH: u8 = 32;
@@ -157,6 +158,20 @@ pub enum BtfError {
     /// unable to get symbol name
     #[error("Unable to get symbol name")]
     InvalidSymbolName,
+
+    /// external symbol is invalid
+    #[error("Invalid extern symbol `{symbol_name}`")]
+    InvalidExternalSymbol {
+        /// name of the symbol
+        symbol_name: String,
+    },
+
+    /// external symbol not found
+    #[error("Extern symbol not found `{symbol_name}`")]
+    ExternalSymbolNotFound {
+        /// name of the symbol
+        symbol_name: String,
+    },
 }
 
 /// Available BTF features
@@ -463,6 +478,57 @@ impl Btf {
         })
     }
 
+    pub(crate) fn type_align(&self, root_type_id: u32) -> Result<usize, BtfError> {
+        let mut type_id = root_type_id;
+        for _ in 0..MAX_RESOLVE_DEPTH {
+            let ty = self.types.type_by_id(type_id)?;
+            let size = match ty {
+                BtfType::Array(Array { array, .. }) => {
+                    type_id = array.element_type;
+                    continue;
+                }
+                BtfType::Struct(Struct { size, members, .. })
+                | BtfType::Union(Union { size, members, .. }) => {
+                    let mut max_align = 1;
+
+                    for m in members {
+                        let align = self.type_align(m.btf_type)?;
+                        max_align = usize::max(align, max_align);
+
+                        if ty.member_bit_field_size(m).unwrap() == 0
+                            || m.offset % (8 * align as u32) != 0
+                        {
+                            return Ok(1);
+                        }
+                    }
+
+                    if size % max_align as u32 != 0 {
+                        return Ok(1);
+                    }
+
+                    return Ok(max_align);
+                }
+
+                other => {
+                    if let Some(size) = other.size() {
+                        u32::min(BtfType::ptr_size(), size)
+                    } else if let Some(next) = other.btf_type() {
+                        type_id = next;
+                        continue;
+                    } else {
+                        return Err(BtfError::UnexpectedBtfType { type_id });
+                    }
+                }
+            };
+
+            return Ok(size as usize);
+        }
+
+        Err(BtfError::MaximumTypeDepthReached {
+            type_id: root_type_id,
+        })
+    }
+
     /// Encodes the metadata as BTF format
     pub fn to_bytes(&self) -> Vec<u8> {
         // Safety: btf_header is POD
@@ -473,6 +539,37 @@ impl Btf {
         buf
     }
 
+    pub(crate) fn get_extern_data_sec_entry_info(
+        &self,
+        target_var_name: &str,
+    ) -> Result<(String, Var), BtfError> {
+        for t in &self.types.types {
+            if let BtfType::DataSec(d) = t {
+                let sec_name = self.string_at(d.name_offset)?;
+
+                for d in &d.entries {
+                    if let BtfType::Var(var) = self.types.type_by_id(d.btf_type)? {
+                        let var_name = self.string_at(var.name_offset)?;
+
+                        if target_var_name == var_name {
+                            if var.linkage != VarLinkage::Extern {
+                                return Err(BtfError::InvalidExternalSymbol {
+                                    symbol_name: var_name.into(),
+                                });
+                            }
+
+                            return Ok((sec_name.into(), var.clone()));
+                        }
+                    }
+                }
+            }
+        }
+
+        Err(BtfError::ExternalSymbolNotFound {
+            symbol_name: target_var_name.into(),
+        })
+    }
+
     // This follows the same logic as libbpf's bpf_object__sanitize_btf() function.
     // https://github.com/libbpf/libbpf/blob/05f94ddbb837f5f4b3161e341eed21be307eaa04/src/libbpf.c#L2701
     //
@@ -610,6 +707,14 @@ impl Btf {
                                     }
                                 };
                                 e.offset = *offset as u32;
+
+                                if var.linkage == VarLinkage::Extern {
+                                    let mut var = var.clone();
+                                    var.linkage = VarLinkage::Global;
+
+                                    types.types[e.btf_type as usize] = BtfType::Var(var);
+                                }
+
                                 debug!(
                                     "{} {}: VAR {}: fixup offset {}",
                                     kind, name, var_name, offset
@@ -730,6 +835,107 @@ impl Default for Btf {
 }
 
 impl Object {
+    fn patch_extern_data_internal(
+        &mut self,
+        externs: &HashMap<String, Vec<u8>>,
+    ) -> Result<Option<(SectionIndex, Vec<u8>)>, BtfError> {
+        if let Some(ref mut obj_btf) = &mut self.btf {
+            if obj_btf.is_empty() {
+                return Ok(None);
+            }
+
+            let mut kconfig_map_index = 0;
+
+            for map in self.maps.values() {
+                if map.section_index() >= kconfig_map_index {
+                    kconfig_map_index = map.section_index() + 1;
+                }
+            }
+
+            let kconfig_map_index = self.maps.len();
+
+            let symbols = self
+                .symbol_table
+                .iter_mut()
+                .filter(|(_, s)| s.name.is_some() && s.section_index.is_none() && s.is_external)
+                .map(|(_, s)| (s.name.as_ref().unwrap().clone(), s));
+
+            let mut section_data = Vec::<u8>::new();
+            let mut offset = 0u64;
+            let mut has_extern_data = false;
+
+            for (name, symbol) in symbols {
+                let (datasec_name, var) = obj_btf.get_extern_data_sec_entry_info(&name)?;
+
+                if datasec_name == ".kconfig" {
+                    has_extern_data = true;
+
+                    let type_size = obj_btf.type_size(var.btf_type)?;
+                    let type_align = obj_btf.type_align(var.btf_type)? as u64;
+
+                    let mut external_value_opt = externs.get(&name);
+                    let empty_data = vec![0; type_size];
+
+                    if external_value_opt.is_none() && symbol.is_weak {
+                        external_value_opt = Some(&empty_data);
+                    }
+
+                    if let Some(data) = external_value_opt {
+                        symbol.address = (offset + (type_align - 1)) & !(type_align - 1);
+                        symbol.size = type_size as u64;
+                        symbol.section_index = Some(kconfig_map_index);
+
+                        section_data.resize((symbol.address - offset) as usize, 0);
+
+                        self.symbol_offset_by_name.insert(name, symbol.address);
+                        section_data.extend(data);
+                        offset = symbol.address + data.len() as u64;
+                    } else {
+                        return Err(BtfError::ExternalSymbolNotFound { symbol_name: name });
+                    }
+                }
+            }
+
+            if has_extern_data {
+                self.section_infos.insert(
+                    ".kconfig".into(),
+                    (SectionIndex(kconfig_map_index), section_data.len() as u64),
+                );
+
+                return Ok(Some((SectionIndex(kconfig_map_index), section_data)));
+            }
+        }
+        Ok(None)
+    }
+
+    /// Patches extern data
+    pub fn patch_extern_data(
+        &mut self,
+        externs: &HashMap<String, Vec<u8>>,
+    ) -> Result<(), BtfError> {
+        if let Some((section_index, data)) = self.patch_extern_data_internal(externs)? {
+            self.maps.insert(
+                ".kconfig".into(),
+                Map::Legacy(LegacyMap {
+                    def: bpf_map_def {
+                        map_type: bpf_map_type::BPF_MAP_TYPE_ARRAY as u32,
+                        key_size: mem::size_of::<u32>() as u32,
+                        value_size: data.len() as u32,
+                        max_entries: 1,
+                        map_flags: BPF_F_RDONLY_PROG,
+                        ..Default::default()
+                    },
+                    section_index: section_index.0,
+                    section_kind: EbpfSectionKind::Rodata,
+                    symbol_index: None,
+                    data,
+                }),
+            );
+        }
+
+        Ok(())
+    }
+
     /// Fixes up and sanitizes BTF data.
     ///
     /// Mostly, it removes unsupported types and works around LLVM behaviours.
diff --git a/aya-obj/src/btf/types.rs b/aya-obj/src/btf/types.rs
index 9e68b71c..3b75af76 100644
--- a/aya-obj/src/btf/types.rs
+++ b/aya-obj/src/btf/types.rs
@@ -1292,11 +1292,15 @@ impl BtfType {
             BtfType::Struct(t) => Some(t.size),
             BtfType::Union(t) => Some(t.size),
             BtfType::DataSec(t) => Some(t.size),
-            BtfType::Ptr(_) => Some(mem::size_of::<&()>() as u32),
+            BtfType::Ptr(_) => Some(Self::ptr_size()),
             _ => None,
         }
     }
 
+    pub(crate) fn ptr_size() -> u32 {
+        mem::size_of::<&()>() as u32
+    }
+
     pub(crate) fn btf_type(&self) -> Option<u32> {
         match self {
             BtfType::Const(t) => Some(t.btf_type),
diff --git a/aya-obj/src/obj.rs b/aya-obj/src/obj.rs
index e4be87da..4eb9f58e 100644
--- a/aya-obj/src/obj.rs
+++ b/aya-obj/src/obj.rs
@@ -49,6 +49,7 @@ pub struct Features {
     devmap_prog_id: bool,
     prog_info_map_ids: bool,
     prog_info_gpl_compatible: bool,
+    bpf_syscall_wrapper: bool,
     btf: Option<BtfFeatures>,
 }
 
@@ -65,6 +66,7 @@ impl Features {
         devmap_prog_id: bool,
         prog_info_map_ids: bool,
         prog_info_gpl_compatible: bool,
+        bpf_syscall_wrapper: bool,
         btf: Option<BtfFeatures>,
     ) -> Self {
         Self {
@@ -77,6 +79,7 @@ impl Features {
             devmap_prog_id,
             prog_info_map_ids,
             prog_info_gpl_compatible,
+            bpf_syscall_wrapper,
             btf,
         }
     }
@@ -118,6 +121,10 @@ impl Features {
     pub fn devmap_prog_id(&self) -> bool {
         self.devmap_prog_id
     }
+    /// Returns whether BPF syscall wrapper hooking is supported.
+    pub fn bpf_syscall_wrapper(&self) -> bool {
+        self.bpf_syscall_wrapper
+    }
 
     /// Returns whether `bpf_prog_info` supports `nr_map_ids` & `map_ids` fields.
     pub fn prog_info_map_ids(&self) -> bool {
@@ -483,6 +490,8 @@ impl Object {
                     address: symbol.address(),
                     size: symbol.size(),
                     is_definition: symbol.is_definition(),
+                    is_external: symbol.is_undefined() && (symbol.is_global() || symbol.is_weak()),
+                    is_weak: symbol.is_weak(),
                     kind: symbol.kind(),
                 };
                 bpf_obj.symbol_table.insert(symbol.index().0, sym);
@@ -1465,6 +1474,8 @@ mod tests {
                 size,
                 is_definition: false,
                 kind: SymbolKind::Text,
+                is_external: false,
+                is_weak: false,
             },
         );
         obj.symbols_by_section
@@ -2601,6 +2612,8 @@ mod tests {
                 address: 0,
                 size: 3,
                 is_definition: true,
+                is_external: false,
+                is_weak: false,
                 kind: SymbolKind::Data,
             },
         );
diff --git a/aya-obj/src/relocation.rs b/aya-obj/src/relocation.rs
index b05648ba..0c4e6643 100644
--- a/aya-obj/src/relocation.rs
+++ b/aya-obj/src/relocation.rs
@@ -99,6 +99,8 @@ pub(crate) struct Symbol {
     pub(crate) address: u64,
     pub(crate) size: u64,
     pub(crate) is_definition: bool,
+    pub(crate) is_external: bool,
+    pub(crate) is_weak: bool,
     pub(crate) kind: SymbolKind,
 }
 
@@ -218,7 +220,9 @@ fn relocate_maps<'a, I: Iterator<Item = &'a Relocation>>(
         };
 
         // calls and relocation to .text symbols are handled in a separate step
-        if insn_is_call(&instructions[ins_index]) || text_sections.contains(&section_index) {
+        if insn_is_call(&instructions[ins_index])
+            || (text_sections.contains(&section_index) && !sym.is_external)
+        {
             continue;
         }
 
@@ -367,10 +371,11 @@ impl<'a> FunctionLinker<'a> {
                     // only consider text relocations, data relocations are
                     // relocated in relocate_maps()
                     sym.kind == SymbolKind::Text
-                        || sym
-                            .section_index
-                            .map(|section_index| self.text_sections.contains(&section_index))
-                            .unwrap_or(false)
+                        || (!sym.is_external
+                            && sym
+                                .section_index
+                                .map(|section_index| self.text_sections.contains(&section_index))
+                                .unwrap_or(false))
                 });
 
             // not a call and not a text relocation, we don't need to do anything
@@ -510,6 +515,8 @@ mod test {
             address,
             size,
             is_definition: false,
+            is_external: false,
+            is_weak: false,
             kind: SymbolKind::Data,
         }
     }
diff --git a/aya/Cargo.toml b/aya/Cargo.toml
index 57e80c77..797fcbac 100644
--- a/aya/Cargo.toml
+++ b/aya/Cargo.toml
@@ -24,6 +24,7 @@ log = { workspace = true }
 object = { workspace = true, features = ["elf", "read_core", "std", "write"] }
 thiserror = { workspace = true }
 tokio = { workspace = true, features = ["rt"], optional = true }
+flate2 = "1.0"
 
 [dev-dependencies]
 tempfile = { workspace = true }
diff --git a/aya/src/bpf.rs b/aya/src/bpf.rs
index 8a189476..064d9182 100644
--- a/aya/src/bpf.rs
+++ b/aya/src/bpf.rs
@@ -1,7 +1,8 @@
 use std::{
     borrow::Cow,
     collections::{HashMap, HashSet},
-    fs, io,
+    fs::{self, File},
+    io::{self, Read},
     os::{
         fd::{AsFd as _, AsRawFd as _},
         raw::c_int,
@@ -16,6 +17,8 @@ use aya_obj::{
     relocation::EbpfRelocationError,
     EbpfSectionKind, Features,
 };
+use flate2::read::GzDecoder;
+use lazy_static::lazy_static;
 use log::{debug, warn};
 use thiserror::Error;
 
@@ -36,14 +39,15 @@ use crate::{
         SkMsg, SkSkb, SkSkbKind, SockOps, SocketFilter, TracePoint, UProbe, Xdp,
     },
     sys::{
-        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,
+        self, bpf_load_btf, is_bpf_cookie_supported, is_bpf_global_data_supported,
+        is_bpf_syscall_wrapper_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,
     },
-    util::{bytes_of, bytes_of_slice, page_size, possible_cpus, POSSIBLE_CPUS},
+    util::{bytes_of, bytes_of_slice, page_size, possible_cpus, KernelVersion, POSSIBLE_CPUS},
 };
 
 pub(crate) const BPF_OBJ_NAME_LEN: usize = 16;
@@ -98,6 +102,7 @@ fn detect_features() -> Features {
         is_prog_id_supported(BPF_MAP_TYPE_DEVMAP),
         is_info_map_ids_supported(),
         is_info_gpl_compatible_supported(),
+        is_bpf_syscall_wrapper_supported(),
         btf,
     );
     debug!("BPF Feature Detection: {:#?}", f);
@@ -109,6 +114,117 @@ pub fn features() -> &'static Features {
     &FEATURES
 }
 
+lazy_static! {
+    static ref KCONFIG_DEFINITION: HashMap<String, Vec<u8>> = compute_kconfig_definition(&FEATURES);
+}
+
+fn compute_kconfig_definition(features: &Features) -> HashMap<String, Vec<u8>> {
+    let mut result = HashMap::new();
+
+    if let Ok(KernelVersion {
+        major,
+        minor,
+        patch,
+    }) = KernelVersion::current()
+    {
+        result.insert(
+            "LINUX_KERNEL_VERSION".to_string(),
+            {
+                let value = (u64::from(major) << 16) + (u64::from(minor) << 8) + u64::from(patch);
+                value.to_ne_bytes()
+            }
+                .to_vec(),
+        );
+    }
+
+    let bpf_cookie = if features.bpf_cookie() { 1u64 } else { 0u64 };
+    let bpf_syscall_wrapper = if features.bpf_syscall_wrapper() {
+        1u64
+    } else {
+        0u64
+    };
+
+    result.insert(
+        "LINUX_HAS_BPF_COOKIE".to_string(),
+        bpf_cookie.to_ne_bytes().to_vec(),
+    );
+
+    result.insert(
+        "LINUX_HAS_SYSCALL_WRAPPER".to_string(),
+        bpf_syscall_wrapper.to_ne_bytes().to_vec(),
+    );
+
+    let mut raw_config_opt = None;
+
+    let proc_config_path = PathBuf::from("/proc/config.gz");
+
+    if proc_config_path.exists() {
+        if let Ok(file) = File::open(proc_config_path) {
+            let mut file = GzDecoder::new(file);
+            let mut output = String::new();
+            if file.read_to_string(&mut output).is_ok() {
+                raw_config_opt = Some(output);
+            }
+        }
+    }
+
+    if raw_config_opt.is_none() {
+        if let Ok(release) = sys::kernel_release() {
+            let config_path = PathBuf::from("/boot").join(format!("config-{}", release));
+
+            if config_path.exists() {
+                if let Ok(mut file) = File::open(config_path) {
+                    let mut output = String::new();
+                    if file.read_to_string(&mut output).is_ok() {
+                        raw_config_opt = Some(output);
+                    }
+                }
+            }
+        }
+    }
+
+    if let Some(raw_config) = raw_config_opt {
+        for line in raw_config.split('\n') {
+            if !line.starts_with("CONFIG_") {
+                continue;
+            }
+
+            let mut parts = line.split('=');
+            let (key, raw_value) = match (parts.next(), parts.next(), parts.count()) {
+                (Some(key), Some(value), 0) => (key, value),
+                _ => continue,
+            };
+
+            let value = match raw_value.chars().next() {
+                Some('n') => 0_u64.to_ne_bytes().to_vec(),
+                Some('y') => 1_u64.to_ne_bytes().to_vec(),
+                Some('m') => 2_u64.to_ne_bytes().to_vec(),
+                Some('"') => {
+                    if raw_value.len() > 2 || raw_value.ends_with('"') {
+                        continue;
+                    }
+
+                    let raw_value = &raw_value[1..raw_value.len() - 1];
+
+                    raw_value.as_bytes().to_vec()
+                }
+                Some(_) => {
+                    if let Ok(value) = raw_value.parse::<u64>() {
+                        value.to_ne_bytes().to_vec()
+                    } else {
+                        continue;
+                    }
+                }
+                None => continue,
+            };
+
+            result.insert(key.to_string(), value);
+        }
+    }
+
+    result
+}
+
 /// Builder style API for advanced loading of eBPF programs.
 ///
 /// Loading eBPF code involves a few steps, including loading maps and applying
@@ -399,6 +515,7 @@ impl<'a> EbpfLoader<'a> {
         } = self;
         let mut obj = Object::parse(data)?;
         obj.patch_map_data(globals.clone())?;
+        obj.patch_extern_data(&KCONFIG_DEFINITION)?;
 
         let btf_fd = if let Some(features) = &FEATURES.btf() {
             if let Some(btf) = obj.fixup_and_sanitize_btf(features)? {
diff --git a/aya/src/programs/mod.rs b/aya/src/programs/mod.rs
index fcca69fa..1eaa913c 100644
--- a/aya/src/programs/mod.rs
+++ b/aya/src/programs/mod.rs
@@ -38,7 +38,7 @@
 
 // modules we don't export
 mod info;
-mod probe;
+pub(crate) mod probe;
 mod utils;
 
 // modules we explicitly export so their pub items (Links etc) get exported too
diff --git a/aya/src/programs/probe.rs b/aya/src/programs/probe.rs
index 4d737e34..0e15f750 100644
--- a/aya/src/programs/probe.rs
+++ b/aya/src/programs/probe.rs
@@ -145,7 +145,7 @@ pub(crate) fn detach_debug_fs(event: ProbeEvent) -> Result<(), ProgramError> {
     })
 }
 
-fn create_as_probe(
+pub(crate) fn create_as_probe(
     kind: ProbeKind,
     fn_name: &OsStr,
     offset: u64,
diff --git a/aya/src/sys/bpf.rs b/aya/src/sys/bpf.rs
index 2d305246..42f16881 100644
--- a/aya/src/sys/bpf.rs
+++ b/aya/src/sys/bpf.rs
@@ -1,6 +1,6 @@
 use std::{
     cmp,
-    ffi::{c_char, CStr, CString},
+    ffi::{c_char, CStr, CString, OsStr},
     io, iter,
     mem::{self, MaybeUninit},
     os::fd::{AsFd as _, AsRawFd as _, BorrowedFd, FromRawFd as _, RawFd},
@@ -30,6 +30,7 @@ use crate::{
         },
         copy_instructions,
     },
+    programs::probe::create_as_probe,
     sys::{syscall, SysResult, Syscall, SyscallError},
     util::KernelVersion,
     Btf, Pod, VerifierLogLevel, BPF_OBJ_NAME_LEN, FEATURES,
@@ -950,6 +951,43 @@ pub(crate) fn is_prog_id_supported(map_type: bpf_map_type) -> bool {
     fd.is_ok()
 }
 
+fn arch_specific_syscall_prefix() -> Option<&'static str> {
+    if cfg!(target_arch = "aarch64") {
+        Some("arm64")
+    } else if cfg!(target_arch = "arm") {
+        Some("arm")
+    } else if cfg!(target_arch = "powerpc") {
+        Some("powerpc")
+    } else if cfg!(target_arch = "powerpc64") {
+        Some("powerpc64")
+    } else if cfg!(target_arch = "riscv32") || cfg!(target_arch = "riscv64") {
+        Some("riscv")
+    } else if cfg!(target_arch = "x86") {
+        Some("ia32")
+    } else if cfg!(target_arch = "x86_64") {
+        Some("x64")
+    } else if cfg!(target_arch = "s390x") {
+        Some("s390x")
+    } else if cfg!(target_arch = "mips") || cfg!(target_arch = "mips64") {
+        Some("mips")
+    } else {
+        None
+    }
+}
+
+pub(crate) fn is_bpf_syscall_wrapper_supported() -> bool {
+    let syscall_prefix_opt = arch_specific_syscall_prefix();
+
+    if let Some(syscall_prefix) = syscall_prefix_opt {
+        let syscall_name = format!("__{}_sys_bpf", syscall_prefix);
+        let syscall_name = OsStr::new(syscall_name.as_str());
+
+        return create_as_probe(crate::programs::ProbeKind::KProbe, syscall_name, 0, None).is_ok();
+    }
+
+    false
+}
+
 pub(crate) fn is_btf_supported() -> bool {
     let mut btf = Btf::new();
     let name_offset = btf.add_string("int");
diff --git a/aya/src/sys/mod.rs b/aya/src/sys/mod.rs
index 05f8b4dd..7dc4f6d0 100644
--- a/aya/src/sys/mod.rs
+++ b/aya/src/sys/mod.rs
@@ -191,3 +191,26 @@ impl From<Stats> for crate::generated::bpf_stats_type {
 pub fn enable_stats(stats_type: Stats) -> Result<OwnedFd, SyscallError> {
     bpf_enable_stats(stats_type.into()).map(|fd| fd.into_inner())
 }
+
+#[cfg(test)]
+pub(crate) fn kernel_release() -> Result<String, ()> {
+    Ok("unknown".to_string())
+}
+
+#[cfg(not(test))]
+pub(crate) fn kernel_release() -> Result<String, ()> {
+    use std::ffi::CStr;
+
+    use libc::utsname;
+
+    unsafe {
+        let mut v = mem::zeroed::<utsname>();
+        if libc::uname(&mut v as *mut _) != 0 {
+            return Err(());
+        }
+
+        let release = CStr::from_ptr(v.release.as_ptr());
+
+        Ok(release.to_string_lossy().into_owned())
+    }
+}
diff --git a/test/integration-test/bpf/kconfig.bpf.c b/test/integration-test/bpf/kconfig.bpf.c
new file mode 100644
index 00000000..0e8b1907
--- /dev/null
+++ b/test/integration-test/bpf/kconfig.bpf.c
@@ -0,0 +1,17 @@
+// clang-format off
+#include <vmlinux.h>
+#include <bpf/bpf_helpers.h>
+// clang-format on
+
+extern unsigned int CONFIG_BPF __kconfig;
+
+SEC("xdp")
+int kconfig(struct xdp_md *ctx) {
+  if (!CONFIG_BPF) {
+    return XDP_DROP;
+  }
+
+  return XDP_PASS;
+}
+
+char _license[] SEC("license") = "GPL";
diff --git a/test/integration-test/build.rs b/test/integration-test/build.rs
index 8e53b5a9..efbf430d 100644
--- a/test/integration-test/build.rs
+++ b/test/integration-test/build.rs
@@ -71,6 +71,7 @@ fn main() {
         ("multimap-btf.bpf.c", false),
         ("reloc.bpf.c", true),
         ("text_64_64_reloc.c", false),
+        ("kconfig.bpf.c", false),
     ];
 
     if build_integration_bpf {
diff --git a/test/integration-test/src/lib.rs b/test/integration-test/src/lib.rs
index fc31e6c2..e3258536 100644
--- a/test/integration-test/src/lib.rs
+++ b/test/integration-test/src/lib.rs
@@ -9,6 +9,7 @@ pub const RELOC_BTF: &[u8] =
     include_bytes_aligned!(concat!(env!("OUT_DIR"), "/reloc.bpf.target.o"));
 pub const TEXT_64_64_RELOC: &[u8] =
     include_bytes_aligned!(concat!(env!("OUT_DIR"), "/text_64_64_reloc.o"));
+pub const KCONFIG: &[u8] = include_bytes_aligned!(concat!(env!("OUT_DIR"), "/kconfig.bpf.o"));
 
 pub const LOG: &[u8] = include_bytes_aligned!(concat!(env!("OUT_DIR"), "/log"));
 pub const MAP_TEST: &[u8] = include_bytes_aligned!(concat!(env!("OUT_DIR"), "/map_test"));
diff --git a/test/integration-test/src/tests/smoke.rs b/test/integration-test/src/tests/smoke.rs
index 0d57aea9..03bbef2a 100644
--- a/test/integration-test/src/tests/smoke.rs
+++ b/test/integration-test/src/tests/smoke.rs
@@ -69,3 +69,16 @@ fn extension() {
         .load(pass.fd().unwrap().try_clone().unwrap(), "xdp_pass")
         .unwrap();
 }
+
+#[test]
+fn kconfig() {
+    let kernel_version = KernelVersion::current().unwrap();
+    if kernel_version < KernelVersion::new(5, 9, 0) {
+        eprintln!("skipping test on kernel {kernel_version:?}, XDP uses netlink");
+        return;
+    }
+    let mut bpf = Ebpf::load(crate::KCONFIG).unwrap();
+    let pass: &mut Xdp = bpf.program_mut("kconfig").unwrap().try_into().unwrap();
+    pass.load().unwrap();
+    pass.attach("lo", XdpFlags::default()).unwrap();
+}