diff --git a/aya-log/Cargo.toml b/aya-log/Cargo.toml
index 43fc762b..9ff57b7b 100644
--- a/aya-log/Cargo.toml
+++ b/aya-log/Cargo.toml
@@ -13,6 +13,7 @@ edition.workspace = true
 
 [dependencies]
 aya = { path = "../aya", version = "^0.12.0", features = ["async_tokio"] }
+aya-obj = { path = "../aya-obj", version = "^0.1.0" }
 aya-log-common = { path = "../aya-log-common", version = "^0.1.14", default-features = false }
 bytes = { workspace = true }
 log = { workspace = true }
diff --git a/aya-log/src/lib.rs b/aya-log/src/lib.rs
index dbb02d2f..c57324b2 100644
--- a/aya-log/src/lib.rs
+++ b/aya-log/src/lib.rs
@@ -72,6 +72,7 @@ use aya::{
 use aya_log_common::{
     Argument, DisplayHint, Level, LogValueLength, RecordField, LOG_BUF_CAPACITY, LOG_FIELDS,
 };
+use aya_obj::Features;
 use bytes::BytesMut;
 use log::{error, Log, Record};
 use thiserror::Error;
@@ -121,8 +122,8 @@ impl EbpfLogger {
     ///
     /// Attaches to the logs produced by `program_id`. Can be used to read logs generated by a
     /// pinned program. The log records will be written to the default logger. See [log::logger].
-    pub fn init_from_id(program_id: u32) -> Result<EbpfLogger, Error> {
-        Self::init_from_id_with_logger(program_id, log::logger())
+    pub fn init_from_id(program_id: u32, features: Features) -> Result<EbpfLogger, Error> {
+        Self::init_from_id_with_logger(program_id, log::logger(), features)
     }
 
     /// Attaches to an existing `aya-log-ebpf` instance and logs with the given logger.
@@ -132,6 +133,7 @@ impl EbpfLogger {
     pub fn init_from_id_with_logger<T: Log + 'static>(
         program_id: u32,
         logger: T,
+        features: Features,
     ) -> Result<EbpfLogger, Error> {
         let program_info = loaded_programs()
             .filter_map(|info| info.ok())
@@ -147,7 +149,7 @@ impl EbpfLogger {
                 None => false,
             })
             .ok_or(Error::MapNotFound)?;
-        let map = MapData::from_id(map.id()).map_err(Error::MapError)?;
+        let map = MapData::from_id(map.id(), features.clone()).map_err(Error::MapError)?;
 
         Self::read_logs_async(Map::PerfEventArray(map), logger)?;
 
diff --git a/aya-obj/src/attach.rs b/aya-obj/src/attach.rs
new file mode 100644
index 00000000..82f27294
--- /dev/null
+++ b/aya-obj/src/attach.rs
@@ -0,0 +1,185 @@
+//! Link types for BPFFS Permissions
+
+use crate::generated::bpf_attach_type;
+
+/// The type of BPF link
+#[derive(Copy, Clone, Debug)]
+pub enum BpfAttachType {
+    /// Cgroup Inet Ingress
+    CgroupInetIngress,
+    /// Cgroup Inet Egress
+    CgroupInetEgress,
+    /// Cgroup Inet Sock Create
+    CgroupInetSockCreate,
+    /// Cgroup Sock Ops
+    CgroupSockOps,
+    /// Sk Skb Stream Parser
+    SkSkbStreamParser,
+    /// Sk Skb Stream Verdict
+    SkSkbStreamVerdict,
+    /// Cgroup Device
+    CgroupDevice,
+    /// Sk Msg Verdict
+    SkMsgVerdict,
+    /// Cgroup Inet4 Bind
+    CgroupInet4Bind,
+    /// Cgroup Inet6 Bind
+    CgroupInet6Bind,
+    /// Cgroup Inet4 Connect
+    CgroupInet4Connect,
+    /// Cgroup Inet6 Connect
+    CgroupInet6Connect,
+    /// Cgroup Inet4 Post Bind
+    CgroupInet4PostBind,
+    /// Cgroup Inet6 Post Bind
+    CgroupInet6PostBind,
+    /// Cgroup Udp4 Sendmsg
+    CgroupUdp4Sendmsg,
+    /// Cgroup Udp6 Sendmsg
+    CgroupUdp6Sendmsg,
+    /// Lirc Mode2
+    LircMode2,
+    /// Flow Dissector
+    FlowDissector,
+    /// Cgroup Sysctl
+    CgroupSysctl,
+    /// Cgroup Udp4 Recvmsg
+    CgroupUdp4Recvmsg,
+    /// Cgroup Udp6 Recvmsg
+    CgroupUdp6Recvmsg,
+    /// Cgroup Getsockopt
+    CgroupGetsockopt,
+    /// Cgroup Setsockopt
+    CgroupSetsockopt,
+    /// Trace Raw Tp
+    TraceRawTp,
+    /// Trace Fentry
+    TraceFentry,
+    /// Trace Fexit
+    TraceFexit,
+    /// Modify Return
+    ModifyReturn,
+    /// Lsm Mac
+    LsmMac,
+    /// Trace Iter
+    TraceIter,
+    /// Cgroup Inet4 Getpeername
+    CgroupInet4Getpeername,
+    /// Cgroup Inet6 Getpeername
+    CgroupInet6Getpeername,
+    /// Cgroup Inet4 Getsockname
+    CgroupInet4Getsockname,
+    /// Cgroup Inet6 Getsockname
+    CgroupInet6Getsockname,
+    /// Xdp Devmap
+    XdpDevmap,
+    /// Cgroup Inet Sock Release
+    CgroupInetSockRelease,
+    /// Xdp Cpumap
+    XdpCpumap,
+    /// Sk Lookup
+    SkLookup,
+    /// Xdp
+    Xdp,
+    /// Sk Skb Verdict
+    SkSkbVerdict,
+    /// Sk Reuseport Select
+    SkReuseportSelect,
+    /// Sk Reuseport Select Or Migrate
+    SkReuseportSelectOrMigrate,
+    /// Perf Event
+    PerfEvent,
+    /// Trace Kprobe Multi
+    TraceKprobeMulti,
+    /// Lsm Cgroup
+    LsmCgroup,
+    /// Struct Ops
+    StructOps,
+    /// Netfilter
+    Netfilter,
+    /// Tcx Ingress
+    TcxIngress,
+    /// Tcx Egress
+    TcxEgress,
+    /// Trace Uprobe Multi
+    TraceUprobeMulti,
+    /// Cgroup Unix Connect
+    CgroupUnixConnect,
+    /// Cgroup Unix Sendmsg
+    CgroupUnixSendmsg,
+    /// Cgroup Unix Recvmsg
+    CgroupUnixRecvmsg,
+    /// Cgroup Unix Getpeername
+    CgroupUnixGetpeername,
+    /// Cgroup Unix Getsockname
+    CgroupUnixGetsockname,
+    /// Netkit Primary
+    NetkitPrimary,
+    /// Netkit Peer
+    NetkitPeer,
+}
+
+impl From<BpfAttachType> for bpf_attach_type {
+    fn from(attach_type: BpfAttachType) -> Self {
+        match attach_type {
+            BpfAttachType::CgroupInetIngress => bpf_attach_type::BPF_CGROUP_INET_INGRESS,
+            BpfAttachType::CgroupInetEgress => bpf_attach_type::BPF_CGROUP_INET_EGRESS,
+            BpfAttachType::CgroupInetSockCreate => bpf_attach_type::BPF_CGROUP_INET_SOCK_CREATE,
+            BpfAttachType::CgroupSockOps => bpf_attach_type::BPF_CGROUP_SOCK_OPS,
+            BpfAttachType::SkSkbStreamParser => bpf_attach_type::BPF_SK_SKB_STREAM_PARSER,
+            BpfAttachType::SkSkbStreamVerdict => bpf_attach_type::BPF_SK_SKB_STREAM_VERDICT,
+            BpfAttachType::CgroupDevice => bpf_attach_type::BPF_CGROUP_DEVICE,
+            BpfAttachType::SkMsgVerdict => bpf_attach_type::BPF_SK_MSG_VERDICT,
+            BpfAttachType::CgroupInet4Bind => bpf_attach_type::BPF_CGROUP_INET4_BIND,
+            BpfAttachType::CgroupInet6Bind => bpf_attach_type::BPF_CGROUP_INET6_BIND,
+            BpfAttachType::CgroupInet4Connect => bpf_attach_type::BPF_CGROUP_INET4_CONNECT,
+            BpfAttachType::CgroupInet6Connect => bpf_attach_type::BPF_CGROUP_INET6_CONNECT,
+            BpfAttachType::CgroupInet4PostBind => bpf_attach_type::BPF_CGROUP_INET4_POST_BIND,
+            BpfAttachType::CgroupInet6PostBind => bpf_attach_type::BPF_CGROUP_INET6_POST_BIND,
+            BpfAttachType::CgroupUdp4Sendmsg => bpf_attach_type::BPF_CGROUP_UDP4_SENDMSG,
+            BpfAttachType::CgroupUdp6Sendmsg => bpf_attach_type::BPF_CGROUP_UDP6_SENDMSG,
+            BpfAttachType::LircMode2 => bpf_attach_type::BPF_LIRC_MODE2,
+            BpfAttachType::FlowDissector => bpf_attach_type::BPF_FLOW_DISSECTOR,
+            BpfAttachType::CgroupSysctl => bpf_attach_type::BPF_CGROUP_SYSCTL,
+            BpfAttachType::CgroupUdp4Recvmsg => bpf_attach_type::BPF_CGROUP_UDP4_RECVMSG,
+            BpfAttachType::CgroupUdp6Recvmsg => bpf_attach_type::BPF_CGROUP_UDP6_RECVMSG,
+            BpfAttachType::CgroupGetsockopt => bpf_attach_type::BPF_CGROUP_GETSOCKOPT,
+            BpfAttachType::CgroupSetsockopt => bpf_attach_type::BPF_CGROUP_SETSOCKOPT,
+            BpfAttachType::TraceRawTp => bpf_attach_type::BPF_TRACE_RAW_TP,
+            BpfAttachType::TraceFentry => bpf_attach_type::BPF_TRACE_FENTRY,
+            BpfAttachType::TraceFexit => bpf_attach_type::BPF_TRACE_FEXIT,
+            BpfAttachType::ModifyReturn => bpf_attach_type::BPF_MODIFY_RETURN,
+            BpfAttachType::LsmMac => bpf_attach_type::BPF_LSM_MAC,
+            BpfAttachType::TraceIter => bpf_attach_type::BPF_TRACE_ITER,
+            BpfAttachType::CgroupInet4Getpeername => bpf_attach_type::BPF_CGROUP_INET4_GETPEERNAME,
+            BpfAttachType::CgroupInet6Getpeername => bpf_attach_type::BPF_CGROUP_INET6_GETPEERNAME,
+            BpfAttachType::CgroupInet4Getsockname => bpf_attach_type::BPF_CGROUP_INET4_GETSOCKNAME,
+            BpfAttachType::CgroupInet6Getsockname => bpf_attach_type::BPF_CGROUP_INET6_GETSOCKNAME,
+            BpfAttachType::XdpDevmap => bpf_attach_type::BPF_XDP_DEVMAP,
+            BpfAttachType::CgroupInetSockRelease => bpf_attach_type::BPF_CGROUP_INET_SOCK_RELEASE,
+            BpfAttachType::XdpCpumap => bpf_attach_type::BPF_XDP_CPUMAP,
+            BpfAttachType::SkLookup => bpf_attach_type::BPF_SK_LOOKUP,
+            BpfAttachType::Xdp => bpf_attach_type::BPF_XDP,
+            BpfAttachType::SkSkbVerdict => bpf_attach_type::BPF_SK_SKB_VERDICT,
+            BpfAttachType::SkReuseportSelect => bpf_attach_type::BPF_SK_REUSEPORT_SELECT,
+            BpfAttachType::SkReuseportSelectOrMigrate => {
+                bpf_attach_type::BPF_SK_REUSEPORT_SELECT_OR_MIGRATE
+            }
+            BpfAttachType::PerfEvent => bpf_attach_type::BPF_PERF_EVENT,
+            BpfAttachType::TraceKprobeMulti => bpf_attach_type::BPF_TRACE_KPROBE_MULTI,
+            BpfAttachType::LsmCgroup => bpf_attach_type::BPF_LSM_CGROUP,
+            BpfAttachType::StructOps => bpf_attach_type::BPF_STRUCT_OPS,
+            BpfAttachType::Netfilter => bpf_attach_type::BPF_NETFILTER,
+            BpfAttachType::TcxIngress => bpf_attach_type::BPF_TCX_INGRESS,
+            BpfAttachType::TcxEgress => bpf_attach_type::BPF_TCX_EGRESS,
+            BpfAttachType::TraceUprobeMulti => bpf_attach_type::BPF_TRACE_UPROBE_MULTI,
+            BpfAttachType::CgroupUnixConnect => bpf_attach_type::BPF_CGROUP_UNIX_CONNECT,
+            BpfAttachType::CgroupUnixSendmsg => bpf_attach_type::BPF_CGROUP_UNIX_SENDMSG,
+            BpfAttachType::CgroupUnixRecvmsg => bpf_attach_type::BPF_CGROUP_UNIX_RECVMSG,
+            BpfAttachType::CgroupUnixGetpeername => bpf_attach_type::BPF_CGROUP_UNIX_GETPEERNAME,
+            BpfAttachType::CgroupUnixGetsockname => bpf_attach_type::BPF_CGROUP_UNIX_GETSOCKNAME,
+            BpfAttachType::NetkitPrimary => bpf_attach_type::BPF_NETKIT_PRIMARY,
+            BpfAttachType::NetkitPeer => bpf_attach_type::BPF_NETKIT_PEER,
+        }
+    }
+}
diff --git a/aya-obj/src/btf/btf.rs b/aya-obj/src/btf/btf.rs
index ef7217ad..5b2cde50 100644
--- a/aya-obj/src/btf/btf.rs
+++ b/aya-obj/src/btf/btf.rs
@@ -160,7 +160,7 @@ pub enum BtfError {
 }
 
 /// Available BTF features
-#[derive(Default, Debug)]
+#[derive(Default, Debug, Clone)]
 #[allow(missing_docs)]
 pub struct BtfFeatures {
     btf_func: bool,
diff --git a/aya-obj/src/cmd.rs b/aya-obj/src/cmd.rs
new file mode 100644
index 00000000..003bb62b
--- /dev/null
+++ b/aya-obj/src/cmd.rs
@@ -0,0 +1,126 @@
+//! Command types for BPFFS Permissions
+
+use crate::generated::bpf_cmd;
+
+/// The type of BPF link
+#[derive(Copy, Clone, Debug)]
+pub enum BpfCommand {
+    /// Map Create
+    MapCreate,
+    /// Map Lookup Element
+    MapLookupElem,
+    /// Map Update Element
+    MapUpdateElem,
+    /// Map Delete Element
+    MapDeleteElem,
+    /// Map Get Next Key
+    MapGetNextKey,
+    /// Program Load
+    ProgLoad,
+    /// Object Pin
+    ObjPin,
+    /// Object Get
+    ObjGet,
+    /// Program Attach
+    ProgAttach,
+    /// Program Detach
+    ProgDetach,
+    /// Program Test Run
+    ProgTestRun,
+    /// Program Get Next Id
+    ProgGetNextId,
+    /// Map Get Next Id
+    MapGetNextId,
+    /// Program Get FD By Id
+    ProgGetFdById,
+    /// Map Get FD By Id
+    MapGetFdById,
+    /// Object Get Info By FD
+    ObjGetInfoByFd,
+    /// Program Query
+    ProgQuery,
+    /// Raw Tracepoint Open
+    RawTracepointOpen,
+    /// BTF Load
+    BtfLoad,
+    /// BTF Get FD By Id
+    BtfGetFdById,
+    /// Task FD Query
+    TaskFdQuery,
+    /// Map Lookup And Delete Element
+    MapLookupAndDeleteElem,
+    /// Map Freeze
+    MapFreeze,
+    /// BTF Get Next Id
+    BtfGetNextId,
+    /// Map Lookup Batch
+    MapLookupBatch,
+    /// Map Lookup And Delete Batch
+    MapLookupAndDeleteBatch,
+    /// Map Update Batch
+    MapUpdateBatch,
+    /// Map Delete Batch
+    MapDeleteBatch,
+    /// Link Create
+    LinkCreate,
+    /// Link Update
+    LinkUpdate,
+    /// Link Get FD By Id
+    LinkGetFdById,
+    /// Link Get Next Id
+    LinkGetNextId,
+    /// Enable Stats
+    EnableStats,
+    /// Iter Create
+    IterCreate,
+    /// Link Detach
+    LinkDetach,
+    /// Program Bind Map
+    ProgBindMap,
+    /// Token Create
+    TokenCreate,
+}
+
+impl From<BpfCommand> for bpf_cmd {
+    fn from(value: BpfCommand) -> Self {
+        match value {
+            BpfCommand::MapCreate => bpf_cmd::BPF_MAP_CREATE,
+            BpfCommand::MapLookupElem => bpf_cmd::BPF_MAP_LOOKUP_ELEM,
+            BpfCommand::MapUpdateElem => bpf_cmd::BPF_MAP_UPDATE_ELEM,
+            BpfCommand::MapDeleteElem => bpf_cmd::BPF_MAP_DELETE_ELEM,
+            BpfCommand::MapGetNextKey => bpf_cmd::BPF_MAP_GET_NEXT_KEY,
+            BpfCommand::ProgLoad => bpf_cmd::BPF_PROG_LOAD,
+            BpfCommand::ObjPin => bpf_cmd::BPF_OBJ_PIN,
+            BpfCommand::ObjGet => bpf_cmd::BPF_OBJ_GET,
+            BpfCommand::ProgAttach => bpf_cmd::BPF_PROG_ATTACH,
+            BpfCommand::ProgDetach => bpf_cmd::BPF_PROG_DETACH,
+            BpfCommand::ProgTestRun => bpf_cmd::BPF_PROG_TEST_RUN,
+            BpfCommand::ProgGetNextId => bpf_cmd::BPF_PROG_GET_NEXT_ID,
+            BpfCommand::MapGetNextId => bpf_cmd::BPF_MAP_GET_NEXT_ID,
+            BpfCommand::ProgGetFdById => bpf_cmd::BPF_PROG_GET_FD_BY_ID,
+            BpfCommand::MapGetFdById => bpf_cmd::BPF_MAP_GET_FD_BY_ID,
+            BpfCommand::ObjGetInfoByFd => bpf_cmd::BPF_OBJ_GET_INFO_BY_FD,
+            BpfCommand::ProgQuery => bpf_cmd::BPF_PROG_QUERY,
+            BpfCommand::RawTracepointOpen => bpf_cmd::BPF_RAW_TRACEPOINT_OPEN,
+            BpfCommand::BtfLoad => bpf_cmd::BPF_BTF_LOAD,
+            BpfCommand::BtfGetFdById => bpf_cmd::BPF_BTF_GET_FD_BY_ID,
+            BpfCommand::TaskFdQuery => bpf_cmd::BPF_TASK_FD_QUERY,
+            BpfCommand::MapLookupAndDeleteElem => bpf_cmd::BPF_MAP_LOOKUP_AND_DELETE_ELEM,
+            BpfCommand::MapFreeze => bpf_cmd::BPF_MAP_FREEZE,
+            BpfCommand::BtfGetNextId => bpf_cmd::BPF_BTF_GET_NEXT_ID,
+            BpfCommand::MapLookupBatch => bpf_cmd::BPF_MAP_LOOKUP_BATCH,
+            BpfCommand::MapLookupAndDeleteBatch => bpf_cmd::BPF_MAP_LOOKUP_AND_DELETE_BATCH,
+            BpfCommand::MapUpdateBatch => bpf_cmd::BPF_MAP_UPDATE_BATCH,
+            BpfCommand::MapDeleteBatch => bpf_cmd::BPF_MAP_DELETE_BATCH,
+            BpfCommand::LinkCreate => bpf_cmd::BPF_LINK_CREATE,
+            BpfCommand::LinkUpdate => bpf_cmd::BPF_LINK_UPDATE,
+            BpfCommand::LinkGetFdById => bpf_cmd::BPF_LINK_GET_FD_BY_ID,
+            BpfCommand::LinkGetNextId => bpf_cmd::BPF_LINK_GET_NEXT_ID,
+            BpfCommand::EnableStats => bpf_cmd::BPF_ENABLE_STATS,
+            BpfCommand::IterCreate => bpf_cmd::BPF_ITER_CREATE,
+            BpfCommand::LinkDetach => bpf_cmd::BPF_LINK_DETACH,
+            BpfCommand::ProgBindMap => bpf_cmd::BPF_PROG_BIND_MAP,
+            BpfCommand::TokenCreate => bpf_cmd::BPF_TOKEN_CREATE,
+        }
+    }
+}
diff --git a/aya-obj/src/lib.rs b/aya-obj/src/lib.rs
index ea0e5670..b83e4f37 100644
--- a/aya-obj/src/lib.rs
+++ b/aya-obj/src/lib.rs
@@ -85,8 +85,11 @@ mod std {
     }
 }
 
+pub mod attach;
 pub mod btf;
+pub mod cmd;
 pub mod generated;
+pub mod links;
 pub mod maps;
 pub mod obj;
 pub mod programs;
diff --git a/aya-obj/src/links.rs b/aya-obj/src/links.rs
new file mode 100644
index 00000000..79ab7f66
--- /dev/null
+++ b/aya-obj/src/links.rs
@@ -0,0 +1,57 @@
+//! Link types for BPFFS Permissions
+
+use crate::generated::bpf_link_type;
+
+/// The type of BPF link
+#[derive(Copy, Clone, Debug)]
+pub enum BpfLinkType {
+    /// Not Specified
+    Unspecified,
+    /// Raw Tracepoint
+    RawTracepoint,
+    /// Tracing
+    Tracing,
+    /// Cgroup
+    Cgroup,
+    /// Iter
+    Iter,
+    /// Netns
+    Netns,
+    /// Xdp
+    Xdp,
+    /// Perf Event
+    PerfEvent,
+    /// Kprobe Multi
+    KprobeMulti,
+    /// Struct Ops
+    StructOps,
+    /// Netfilter
+    Netfilter,
+    /// Tcx
+    Tcx,
+    /// Uprobe Multi
+    UprobeMulti,
+    /// Netkit
+    Netkit,
+}
+
+impl From<BpfLinkType> for bpf_link_type {
+    fn from(value: BpfLinkType) -> Self {
+        match value {
+            BpfLinkType::Unspecified => bpf_link_type::BPF_LINK_TYPE_UNSPEC,
+            BpfLinkType::RawTracepoint => bpf_link_type::BPF_LINK_TYPE_RAW_TRACEPOINT,
+            BpfLinkType::Tracing => bpf_link_type::BPF_LINK_TYPE_TRACING,
+            BpfLinkType::Cgroup => bpf_link_type::BPF_LINK_TYPE_CGROUP,
+            BpfLinkType::Iter => bpf_link_type::BPF_LINK_TYPE_ITER,
+            BpfLinkType::Netns => bpf_link_type::BPF_LINK_TYPE_NETNS,
+            BpfLinkType::Xdp => bpf_link_type::BPF_LINK_TYPE_XDP,
+            BpfLinkType::PerfEvent => bpf_link_type::BPF_LINK_TYPE_PERF_EVENT,
+            BpfLinkType::KprobeMulti => bpf_link_type::BPF_LINK_TYPE_KPROBE_MULTI,
+            BpfLinkType::StructOps => bpf_link_type::BPF_LINK_TYPE_STRUCT_OPS,
+            BpfLinkType::Netfilter => bpf_link_type::BPF_LINK_TYPE_NETFILTER,
+            BpfLinkType::Tcx => bpf_link_type::BPF_LINK_TYPE_TCX,
+            BpfLinkType::UprobeMulti => bpf_link_type::BPF_LINK_TYPE_UPROBE_MULTI,
+            BpfLinkType::Netkit => bpf_link_type::BPF_LINK_TYPE_NETKIT,
+        }
+    }
+}
diff --git a/aya-obj/src/maps.rs b/aya-obj/src/maps.rs
index ca85bba7..553881a6 100644
--- a/aya-obj/src/maps.rs
+++ b/aya-obj/src/maps.rs
@@ -5,7 +5,7 @@ use core::mem;
 
 #[cfg(not(feature = "std"))]
 use crate::std;
-use crate::EbpfSectionKind;
+use crate::{generated::bpf_map_type, EbpfSectionKind};
 
 /// Invalid map type encontered
 pub struct InvalidMapTypeError {
@@ -13,6 +13,124 @@ pub struct InvalidMapTypeError {
     pub map_type: u32,
 }
 
+/// The type of BPF Map
+#[derive(Copy, Clone, Debug)]
+pub enum BpfMapType {
+    /// Not Specified
+    Unspecified,
+    /// Hash
+    Hash,
+    /// Array
+    Array,
+    /// Prog Array
+    ProgArray,
+    /// Perf Event Array
+    PerfEventArray,
+    /// Per-CPU Hash
+    PerCpuHash,
+    /// Per-CPU Array
+    PerCpuArray,
+    /// Stack Trace
+    StackTrace,
+    /// Cgroup Array
+    CgroupArray,
+    /// LRU Hash
+    LruHash,
+    /// LRU Per-CPU Hash
+    LruPerCpuHash,
+    /// LPM Trie
+    LpmTrie,
+    /// Array of Maps
+    ArrayOfMaps,
+    /// Hash of Maps
+    HashOfMaps,
+    /// Devmap
+    Devmap,
+    /// Sockmap
+    Sockmap,
+    /// Cpumap
+    Cpumap,
+    /// Xskmap
+    Xskmap,
+    /// Sockhash
+    Sockhash,
+    /// Cgroup Storage (deprecated)
+    CgroupStorageDeprecated,
+    /// Reuseport Sockarray
+    ReuseportSockarray,
+    /// Per-CPU Cgroup Storage (deprecated)
+    PerCpuCgroupStorageDeprecated,
+    /// Queue
+    Queue,
+    /// Stack
+    Stack,
+    /// Sk Storage
+    SkStorage,
+    /// Devmap Hash
+    DevmapHash,
+    /// Struct Ops
+    StructOps,
+    /// Ringbuf
+    Ringbuf,
+    /// Inode Storage
+    InodeStorage,
+    /// Task Storage
+    TaskStorage,
+    /// Bloom Filter
+    BloomFilter,
+    /// User Ringbuf
+    UserRingbuf,
+    /// Cgroup Storage
+    CgroupStorage,
+    /// Arena
+    Arena,
+}
+
+impl From<BpfMapType> for bpf_map_type {
+    fn from(value: BpfMapType) -> Self {
+        match value {
+            BpfMapType::Unspecified => bpf_map_type::BPF_MAP_TYPE_UNSPEC,
+            BpfMapType::Hash => bpf_map_type::BPF_MAP_TYPE_HASH,
+            BpfMapType::Array => bpf_map_type::BPF_MAP_TYPE_ARRAY,
+            BpfMapType::ProgArray => bpf_map_type::BPF_MAP_TYPE_PROG_ARRAY,
+            BpfMapType::PerfEventArray => bpf_map_type::BPF_MAP_TYPE_PERF_EVENT_ARRAY,
+            BpfMapType::PerCpuHash => bpf_map_type::BPF_MAP_TYPE_PERCPU_HASH,
+            BpfMapType::PerCpuArray => bpf_map_type::BPF_MAP_TYPE_PERCPU_ARRAY,
+            BpfMapType::StackTrace => bpf_map_type::BPF_MAP_TYPE_STACK_TRACE,
+            BpfMapType::CgroupArray => bpf_map_type::BPF_MAP_TYPE_CGROUP_ARRAY,
+            BpfMapType::LruHash => bpf_map_type::BPF_MAP_TYPE_LRU_HASH,
+            BpfMapType::LruPerCpuHash => bpf_map_type::BPF_MAP_TYPE_LRU_PERCPU_HASH,
+            BpfMapType::LpmTrie => bpf_map_type::BPF_MAP_TYPE_LPM_TRIE,
+            BpfMapType::ArrayOfMaps => bpf_map_type::BPF_MAP_TYPE_ARRAY_OF_MAPS,
+            BpfMapType::HashOfMaps => bpf_map_type::BPF_MAP_TYPE_HASH_OF_MAPS,
+            BpfMapType::Devmap => bpf_map_type::BPF_MAP_TYPE_DEVMAP,
+            BpfMapType::Sockmap => bpf_map_type::BPF_MAP_TYPE_SOCKMAP,
+            BpfMapType::Cpumap => bpf_map_type::BPF_MAP_TYPE_CPUMAP,
+            BpfMapType::Xskmap => bpf_map_type::BPF_MAP_TYPE_XSKMAP,
+            BpfMapType::Sockhash => bpf_map_type::BPF_MAP_TYPE_SOCKHASH,
+            BpfMapType::CgroupStorageDeprecated => {
+                bpf_map_type::BPF_MAP_TYPE_CGROUP_STORAGE_DEPRECATED
+            }
+            BpfMapType::ReuseportSockarray => bpf_map_type::BPF_MAP_TYPE_REUSEPORT_SOCKARRAY,
+            BpfMapType::PerCpuCgroupStorageDeprecated => {
+                bpf_map_type::BPF_MAP_TYPE_PERCPU_CGROUP_STORAGE_DEPRECATED
+            }
+            BpfMapType::Queue => bpf_map_type::BPF_MAP_TYPE_QUEUE,
+            BpfMapType::Stack => bpf_map_type::BPF_MAP_TYPE_STACK,
+            BpfMapType::SkStorage => bpf_map_type::BPF_MAP_TYPE_SK_STORAGE,
+            BpfMapType::DevmapHash => bpf_map_type::BPF_MAP_TYPE_DEVMAP_HASH,
+            BpfMapType::StructOps => bpf_map_type::BPF_MAP_TYPE_STRUCT_OPS,
+            BpfMapType::Ringbuf => bpf_map_type::BPF_MAP_TYPE_RINGBUF,
+            BpfMapType::InodeStorage => bpf_map_type::BPF_MAP_TYPE_INODE_STORAGE,
+            BpfMapType::TaskStorage => bpf_map_type::BPF_MAP_TYPE_TASK_STORAGE,
+            BpfMapType::BloomFilter => bpf_map_type::BPF_MAP_TYPE_BLOOM_FILTER,
+            BpfMapType::UserRingbuf => bpf_map_type::BPF_MAP_TYPE_USER_RINGBUF,
+            BpfMapType::CgroupStorage => bpf_map_type::BPF_MAP_TYPE_CGRP_STORAGE,
+            BpfMapType::Arena => bpf_map_type::BPF_MAP_TYPE_ARENA,
+        }
+    }
+}
+
 impl TryFrom<u32> for crate::generated::bpf_map_type {
     type Error = InvalidMapTypeError;
 
diff --git a/aya-obj/src/obj.rs b/aya-obj/src/obj.rs
index f6f4fc8b..652cb710 100644
--- a/aya-obj/src/obj.rs
+++ b/aya-obj/src/obj.rs
@@ -37,7 +37,7 @@ use crate::{
 const KERNEL_VERSION_ANY: u32 = 0xFFFF_FFFE;
 
 /// Features implements BPF and BTF feature detection
-#[derive(Default, Debug)]
+#[derive(Default, Debug, Clone)]
 #[allow(missing_docs)]
 pub struct Features {
     bpf_name: bool,
diff --git a/aya-obj/src/programs/mod.rs b/aya-obj/src/programs/mod.rs
index 6b66b005..fc5477b1 100644
--- a/aya-obj/src/programs/mod.rs
+++ b/aya-obj/src/programs/mod.rs
@@ -1,5 +1,7 @@
 //! Program struct and type bindings.
 
+use crate::generated::bpf_prog_type;
+
 pub mod cgroup_sock;
 pub mod cgroup_sock_addr;
 pub mod cgroup_sockopt;
@@ -9,3 +11,116 @@ pub use cgroup_sock::CgroupSockAttachType;
 pub use cgroup_sock_addr::CgroupSockAddrAttachType;
 pub use cgroup_sockopt::CgroupSockoptAttachType;
 pub use xdp::XdpAttachType;
+
+/// The type of BPF statistic to enable.
+#[derive(Copy, Clone, Debug)]
+pub enum BpfProgType {
+    /// Not Specified
+    Unspecified,
+    /// Socket Filter
+    SocketFilter,
+    /// Kprobe
+    Kprobe,
+    /// Sched Cls
+    SchedCls,
+    /// Sched Act
+    SchedAct,
+    /// Tracepoint
+    Tracepoint,
+    /// XDP
+    Xdp,
+    /// Perf Event
+    PerfEvent,
+    /// Cgroup Skb
+    CgroupSkb,
+    /// Cgroup Sock
+    CgroupSock,
+    /// Lwt In
+    LwtIn,
+    /// Lwt Out
+    LwtOut,
+    /// Lwt Xmit
+    LwtXmit,
+    /// Sock Ops
+    SockOps,
+    /// Sk Skb
+    SkSkb,
+    /// Cgroup Device
+    CgroupDevice,
+    /// Sk Msg
+    SkMsg,
+    /// Raw Tracepoint
+    RawTracepoint,
+    /// Cgroup Sock Addr
+    CgroupSockAddr,
+    /// Lwt Seg6 Local
+    LwtSeg6Local,
+    /// Lirc Mode2
+    LircMode2,
+    /// Sk Reuseport
+    SkReuseport,
+    /// Flow Dissector
+    FlowDissector,
+    /// Cgroup Sysctl
+    CgroupSysctl,
+    /// Raw Tracepoint Writable
+    RawTracepointWritable,
+    /// Cgroup Sockopt
+    CgroupSockopt,
+    /// Tracing
+    Tracing,
+    /// Struct Ops
+    StructOps,
+    /// Ext
+    Ext,
+    /// Lsm
+    Lsm,
+    /// Sk Lookup
+    SkLookup,
+    /// Syscall
+    Syscall,
+    /// Netfilter
+    Netfilter,
+}
+
+impl From<BpfProgType> for bpf_prog_type {
+    fn from(value: BpfProgType) -> Self {
+        match value {
+            BpfProgType::Unspecified => bpf_prog_type::BPF_PROG_TYPE_UNSPEC,
+            BpfProgType::SocketFilter => bpf_prog_type::BPF_PROG_TYPE_SOCKET_FILTER,
+            BpfProgType::Kprobe => bpf_prog_type::BPF_PROG_TYPE_KPROBE,
+            BpfProgType::SchedCls => bpf_prog_type::BPF_PROG_TYPE_SCHED_CLS,
+            BpfProgType::SchedAct => bpf_prog_type::BPF_PROG_TYPE_SCHED_ACT,
+            BpfProgType::Tracepoint => bpf_prog_type::BPF_PROG_TYPE_TRACEPOINT,
+            BpfProgType::Xdp => bpf_prog_type::BPF_PROG_TYPE_XDP,
+            BpfProgType::PerfEvent => bpf_prog_type::BPF_PROG_TYPE_PERF_EVENT,
+            BpfProgType::CgroupSkb => bpf_prog_type::BPF_PROG_TYPE_CGROUP_SKB,
+            BpfProgType::CgroupSock => bpf_prog_type::BPF_PROG_TYPE_CGROUP_SOCK,
+            BpfProgType::LwtIn => bpf_prog_type::BPF_PROG_TYPE_LWT_IN,
+            BpfProgType::LwtOut => bpf_prog_type::BPF_PROG_TYPE_LWT_OUT,
+            BpfProgType::LwtXmit => bpf_prog_type::BPF_PROG_TYPE_LWT_XMIT,
+            BpfProgType::SockOps => bpf_prog_type::BPF_PROG_TYPE_SOCK_OPS,
+            BpfProgType::SkSkb => bpf_prog_type::BPF_PROG_TYPE_SK_SKB,
+            BpfProgType::CgroupDevice => bpf_prog_type::BPF_PROG_TYPE_CGROUP_DEVICE,
+            BpfProgType::SkMsg => bpf_prog_type::BPF_PROG_TYPE_SK_MSG,
+            BpfProgType::RawTracepoint => bpf_prog_type::BPF_PROG_TYPE_RAW_TRACEPOINT,
+            BpfProgType::CgroupSockAddr => bpf_prog_type::BPF_PROG_TYPE_CGROUP_SOCK_ADDR,
+            BpfProgType::LwtSeg6Local => bpf_prog_type::BPF_PROG_TYPE_LWT_SEG6LOCAL,
+            BpfProgType::LircMode2 => bpf_prog_type::BPF_PROG_TYPE_LIRC_MODE2,
+            BpfProgType::SkReuseport => bpf_prog_type::BPF_PROG_TYPE_SK_REUSEPORT,
+            BpfProgType::FlowDissector => bpf_prog_type::BPF_PROG_TYPE_FLOW_DISSECTOR,
+            BpfProgType::CgroupSysctl => bpf_prog_type::BPF_PROG_TYPE_CGROUP_SYSCTL,
+            BpfProgType::RawTracepointWritable => {
+                bpf_prog_type::BPF_PROG_TYPE_RAW_TRACEPOINT_WRITABLE
+            }
+            BpfProgType::CgroupSockopt => bpf_prog_type::BPF_PROG_TYPE_CGROUP_SOCKOPT,
+            BpfProgType::Tracing => bpf_prog_type::BPF_PROG_TYPE_TRACING,
+            BpfProgType::StructOps => bpf_prog_type::BPF_PROG_TYPE_STRUCT_OPS,
+            BpfProgType::Ext => bpf_prog_type::BPF_PROG_TYPE_EXT,
+            BpfProgType::Lsm => bpf_prog_type::BPF_PROG_TYPE_LSM,
+            BpfProgType::SkLookup => bpf_prog_type::BPF_PROG_TYPE_SK_LOOKUP,
+            BpfProgType::Syscall => bpf_prog_type::BPF_PROG_TYPE_SYSCALL,
+            BpfProgType::Netfilter => bpf_prog_type::BPF_PROG_TYPE_NETFILTER,
+        }
+    }
+}
diff --git a/aya/src/bpf.rs b/aya/src/bpf.rs
index 42a9a489..4faa3938 100644
--- a/aya/src/bpf.rs
+++ b/aya/src/bpf.rs
@@ -1,10 +1,12 @@
 use std::{
     borrow::Cow,
     collections::{HashMap, HashSet},
+    ffi::CString,
     fs, io,
     os::{
-        fd::{AsFd as _, AsRawFd as _, OwnedFd},
+        fd::{AsFd as _, AsRawFd as _, BorrowedFd, FromRawFd, OwnedFd},
         raw::c_int,
+        unix::ffi::OsStrExt as _,
     },
     path::{Path, PathBuf},
     sync::Arc,
@@ -16,6 +18,7 @@ use aya_obj::{
     relocation::EbpfRelocationError,
     EbpfSectionKind, Features,
 };
+use libc::{O_CLOEXEC, O_DIRECTORY, O_RDWR};
 use log::{debug, warn};
 use thiserror::Error;
 
@@ -36,7 +39,7 @@ use crate::{
         SkMsg, SkSkb, SkSkbKind, SockOps, SocketFilter, TracePoint, UProbe, Xdp,
     },
     sys::{
-        bpf_load_btf, is_bpf_cookie_supported, is_bpf_global_data_supported,
+        bpf_load_btf, bpf_token_create, 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_perf_link_supported,
@@ -70,32 +73,28 @@ unsafe impl<T: Pod, const N: usize> Pod for [T; N] {}
 
 pub use aya_obj::maps::{bpf_map_def, PinningType};
 
-lazy_static::lazy_static! {
-    pub(crate) static ref FEATURES: Features = detect_features();
-}
-
-fn detect_features() -> Features {
-    let btf = if is_btf_supported() {
+fn detect_features(token_fd: Option<BorrowedFd<'_>>) -> Features {
+    let btf = if is_btf_supported(token_fd) {
         Some(BtfFeatures::new(
-            is_btf_func_supported(),
-            is_btf_func_global_supported(),
-            is_btf_datasec_supported(),
-            is_btf_float_supported(),
-            is_btf_decl_tag_supported(),
-            is_btf_type_tag_supported(),
-            is_btf_enum64_supported(),
+            is_btf_func_supported(token_fd),
+            is_btf_func_global_supported(token_fd),
+            is_btf_datasec_supported(token_fd),
+            is_btf_float_supported(token_fd),
+            is_btf_decl_tag_supported(token_fd),
+            is_btf_type_tag_supported(token_fd),
+            is_btf_enum64_supported(token_fd),
         ))
     } else {
         None
     };
     let f = Features::new(
-        is_prog_name_supported(),
-        is_probe_read_kernel_supported(),
-        is_perf_link_supported(),
-        is_bpf_global_data_supported(),
-        is_bpf_cookie_supported(),
-        is_prog_id_supported(BPF_MAP_TYPE_CPUMAP),
-        is_prog_id_supported(BPF_MAP_TYPE_DEVMAP),
+        is_prog_name_supported(token_fd),
+        is_probe_read_kernel_supported(token_fd),
+        is_perf_link_supported(token_fd),
+        is_bpf_global_data_supported(token_fd),
+        is_bpf_cookie_supported(token_fd),
+        is_prog_id_supported(BPF_MAP_TYPE_CPUMAP, token_fd),
+        is_prog_id_supported(BPF_MAP_TYPE_DEVMAP, token_fd),
         btf,
     );
     debug!("BPF Feature Detection: {:#?}", f);
@@ -103,8 +102,8 @@ fn detect_features() -> Features {
 }
 
 /// Returns a reference to the detected BPF features.
-pub fn features() -> &'static Features {
-    &FEATURES
+pub fn features(token_fd: Option<BorrowedFd<'_>>) -> Features {
+    detect_features(token_fd)
 }
 
 /// Builder style API for advanced loading of eBPF programs.
@@ -132,6 +131,7 @@ pub fn features() -> &'static Features {
 pub struct EbpfLoader<'a> {
     btf: Option<Cow<'a, Btf>>,
     map_pin_path: Option<PathBuf>,
+    token_path: PathBuf,
     globals: HashMap<&'a str, (&'a [u8], bool)>,
     max_entries: HashMap<&'a str, u32>,
     extensions: HashSet<&'a str>,
@@ -170,6 +170,7 @@ impl<'a> EbpfLoader<'a> {
         Self {
             btf: Btf::from_sys_fs().ok().map(Cow::Owned),
             map_pin_path: None,
+            token_path: Path::new("/sys/fs/bpf").to_owned(),
             globals: HashMap::new(),
             max_entries: HashMap::new(),
             extensions: HashSet::new(),
@@ -355,6 +356,23 @@ impl<'a> EbpfLoader<'a> {
         self
     }
 
+    /// Sets the path of the BPFFS to use for obtaining a token.
+    ///
+    /// # Example
+    ///
+    /// ```no_run
+    /// use aya::EbpfLoader;
+    ///
+    /// let bpf = EbpfLoader::new()
+    ///    .token_path("/sys/fs/bpf")
+    ///   .load_file("file.o")?;
+    /// # Ok::<(), aya::EbpfError>(())
+    /// ```
+    pub fn token_path<P: AsRef<Path>>(&mut self, path: P) -> &mut Self {
+        self.token_path = path.as_ref().to_path_buf();
+        self
+    }
+
     /// Loads eBPF bytecode from a file.
     ///
     /// # Examples
@@ -389,18 +407,23 @@ impl<'a> EbpfLoader<'a> {
         let Self {
             btf,
             map_pin_path,
+            token_path,
             globals,
             max_entries,
             extensions,
             verifier_log_level,
             allow_unsupported_maps,
         } = self;
+
+        let token_fd = create_token(token_path).ok().map(Arc::new);
+        let borrow_token_fd = token_fd.as_ref().map(|x| x.as_fd());
+        let features = detect_features(borrow_token_fd);
         let mut obj = Object::parse(data)?;
         obj.patch_map_data(globals.clone())?;
 
-        let btf_fd = if let Some(features) = &FEATURES.btf() {
+        let btf_fd = if let Some(features) = features.btf() {
             if let Some(btf) = obj.fixup_and_sanitize_btf(features)? {
-                match load_btf(btf.to_bytes(), *verifier_log_level) {
+                match load_btf(btf.to_bytes(), *verifier_log_level, borrow_token_fd) {
                     Ok(btf_fd) => Some(Arc::new(btf_fd)),
                     // Only report an error here if the BTF is truly needed, otherwise proceed without.
                     Err(err) => {
@@ -461,7 +484,7 @@ impl<'a> EbpfLoader<'a> {
         let mut maps = HashMap::new();
         for (name, mut obj) in obj.maps.drain() {
             if let (false, EbpfSectionKind::Bss | EbpfSectionKind::Data | EbpfSectionKind::Rodata) =
-                (FEATURES.bpf_global_data(), obj.section_kind())
+                (features.bpf_global_data(), obj.section_kind())
             {
                 continue;
             }
@@ -485,16 +508,18 @@ impl<'a> EbpfLoader<'a> {
             }
             match obj.map_type().try_into() {
                 Ok(BPF_MAP_TYPE_CPUMAP) => {
-                    obj.set_value_size(if FEATURES.cpumap_prog_id() { 8 } else { 4 })
+                    obj.set_value_size(if features.cpumap_prog_id() { 8 } else { 4 })
                 }
                 Ok(BPF_MAP_TYPE_DEVMAP | BPF_MAP_TYPE_DEVMAP_HASH) => {
-                    obj.set_value_size(if FEATURES.devmap_prog_id() { 8 } else { 4 })
+                    obj.set_value_size(if features.devmap_prog_id() { 8 } else { 4 })
                 }
                 _ => (),
             }
             let btf_fd = btf_fd.as_deref().map(|fd| fd.as_fd());
             let mut map = match obj.pinning() {
-                PinningType::None => MapData::create(obj, &name, btf_fd)?,
+                PinningType::None => {
+                    MapData::create(obj, &name, btf_fd, token_fd.clone(), features.clone())?
+                }
                 PinningType::ByName => {
                     // pin maps in /sys/fs/bpf by default to align with libbpf
                     // behavior https://github.com/libbpf/libbpf/blob/v1.2.2/src/libbpf.c#L2161.
@@ -502,7 +527,14 @@ impl<'a> EbpfLoader<'a> {
                         .as_deref()
                         .unwrap_or_else(|| Path::new("/sys/fs/bpf"));
 
-                    MapData::create_pinned_by_name(path, obj, &name, btf_fd)?
+                    MapData::create_pinned_by_name(
+                        path,
+                        obj,
+                        &name,
+                        btf_fd,
+                        token_fd.clone(),
+                        features.clone(),
+                    )?
                 }
             };
             map.finalize()?;
@@ -521,7 +553,7 @@ impl<'a> EbpfLoader<'a> {
             &text_sections,
         )?;
         obj.relocate_calls(&text_sections)?;
-        obj.sanitize_functions(&FEATURES);
+        obj.sanitize_functions(&features);
 
         let programs = obj
             .programs
@@ -529,7 +561,7 @@ impl<'a> EbpfLoader<'a> {
             .map(|(name, prog_obj)| {
                 let function_obj = obj.functions.get(&prog_obj.function_key()).unwrap().clone();
 
-                let prog_name = if FEATURES.bpf_name() {
+                let prog_name = if features.bpf_name() {
                     Some(name.clone())
                 } else {
                     None
@@ -538,23 +570,51 @@ impl<'a> EbpfLoader<'a> {
                 let obj = (prog_obj, function_obj);
 
                 let btf_fd = btf_fd.clone();
+                let token_fd = token_fd.clone();
                 let program = if extensions.contains(name.as_str()) {
                     Program::Extension(Extension {
-                        data: ProgramData::new(prog_name, obj, btf_fd, *verifier_log_level),
+                        data: ProgramData::new(
+                            prog_name,
+                            obj,
+                            btf_fd,
+                            *verifier_log_level,
+                            token_fd,
+                            features.clone(),
+                        ),
                     })
                 } else {
                     match &section {
                         ProgramSection::KProbe => Program::KProbe(KProbe {
-                            data: ProgramData::new(prog_name, obj, btf_fd, *verifier_log_level),
+                            data: ProgramData::new(
+                                prog_name,
+                                obj,
+                                btf_fd,
+                                *verifier_log_level,
+                                token_fd,
+                                features.clone(),
+                            ),
                             kind: ProbeKind::KProbe,
                         }),
                         ProgramSection::KRetProbe => Program::KProbe(KProbe {
-                            data: ProgramData::new(prog_name, obj, btf_fd, *verifier_log_level),
+                            data: ProgramData::new(
+                                prog_name,
+                                obj,
+                                btf_fd,
+                                *verifier_log_level,
+                                token_fd,
+                                features.clone(),
+                            ),
                             kind: ProbeKind::KRetProbe,
                         }),
                         ProgramSection::UProbe { sleepable } => {
-                            let mut data =
-                                ProgramData::new(prog_name, obj, btf_fd, *verifier_log_level);
+                            let mut data = ProgramData::new(
+                                prog_name,
+                                obj,
+                                btf_fd,
+                                *verifier_log_level,
+                                token_fd,
+                                features.clone(),
+                            );
                             if *sleepable {
                                 data.flags = BPF_F_SLEEPABLE;
                             }
@@ -564,8 +624,14 @@ impl<'a> EbpfLoader<'a> {
                             })
                         }
                         ProgramSection::URetProbe { sleepable } => {
-                            let mut data =
-                                ProgramData::new(prog_name, obj, btf_fd, *verifier_log_level);
+                            let mut data = ProgramData::new(
+                                prog_name,
+                                obj,
+                                btf_fd,
+                                *verifier_log_level,
+                                token_fd,
+                                features.clone(),
+                            );
                             if *sleepable {
                                 data.flags = BPF_F_SLEEPABLE;
                             }
@@ -575,16 +641,36 @@ impl<'a> EbpfLoader<'a> {
                             })
                         }
                         ProgramSection::TracePoint => Program::TracePoint(TracePoint {
-                            data: ProgramData::new(prog_name, obj, btf_fd, *verifier_log_level),
+                            data: ProgramData::new(
+                                prog_name,
+                                obj,
+                                btf_fd,
+                                *verifier_log_level,
+                                token_fd,
+                                features.clone(),
+                            ),
                         }),
                         ProgramSection::SocketFilter => Program::SocketFilter(SocketFilter {
-                            data: ProgramData::new(prog_name, obj, btf_fd, *verifier_log_level),
+                            data: ProgramData::new(
+                                prog_name,
+                                obj,
+                                btf_fd,
+                                *verifier_log_level,
+                                token_fd,
+                                features.clone(),
+                            ),
                         }),
                         ProgramSection::Xdp {
                             frags, attach_type, ..
                         } => {
-                            let mut data =
-                                ProgramData::new(prog_name, obj, btf_fd, *verifier_log_level);
+                            let mut data = ProgramData::new(
+                                prog_name,
+                                obj,
+                                btf_fd,
+                                *verifier_log_level,
+                                token_fd,
+                                features.clone(),
+                            );
                             if *frags {
                                 data.flags = BPF_F_XDP_HAS_FRAGS;
                             }
@@ -594,101 +680,252 @@ impl<'a> EbpfLoader<'a> {
                             })
                         }
                         ProgramSection::SkMsg => Program::SkMsg(SkMsg {
-                            data: ProgramData::new(prog_name, obj, btf_fd, *verifier_log_level),
+                            data: ProgramData::new(
+                                prog_name,
+                                obj,
+                                btf_fd,
+                                *verifier_log_level,
+                                token_fd,
+                                features.clone(),
+                            ),
                         }),
                         ProgramSection::CgroupSysctl => Program::CgroupSysctl(CgroupSysctl {
-                            data: ProgramData::new(prog_name, obj, btf_fd, *verifier_log_level),
+                            data: ProgramData::new(
+                                prog_name,
+                                obj,
+                                btf_fd,
+                                *verifier_log_level,
+                                token_fd,
+                                features.clone(),
+                            ),
                         }),
                         ProgramSection::CgroupSockopt { attach_type, .. } => {
                             Program::CgroupSockopt(CgroupSockopt {
-                                data: ProgramData::new(prog_name, obj, btf_fd, *verifier_log_level),
+                                data: ProgramData::new(
+                                    prog_name,
+                                    obj,
+                                    btf_fd,
+                                    *verifier_log_level,
+                                    token_fd,
+                                    features.clone(),
+                                ),
                                 attach_type: *attach_type,
                             })
                         }
                         ProgramSection::SkSkbStreamParser => Program::SkSkb(SkSkb {
-                            data: ProgramData::new(prog_name, obj, btf_fd, *verifier_log_level),
+                            data: ProgramData::new(
+                                prog_name,
+                                obj,
+                                btf_fd,
+                                *verifier_log_level,
+                                token_fd,
+                                features.clone(),
+                            ),
                             kind: SkSkbKind::StreamParser,
                         }),
                         ProgramSection::SkSkbStreamVerdict => Program::SkSkb(SkSkb {
-                            data: ProgramData::new(prog_name, obj, btf_fd, *verifier_log_level),
+                            data: ProgramData::new(
+                                prog_name,
+                                obj,
+                                btf_fd,
+                                *verifier_log_level,
+                                token_fd,
+                                features.clone(),
+                            ),
                             kind: SkSkbKind::StreamVerdict,
                         }),
                         ProgramSection::SockOps => Program::SockOps(SockOps {
-                            data: ProgramData::new(prog_name, obj, btf_fd, *verifier_log_level),
+                            data: ProgramData::new(
+                                prog_name,
+                                obj,
+                                btf_fd,
+                                *verifier_log_level,
+                                token_fd,
+                                features.clone(),
+                            ),
                         }),
                         ProgramSection::SchedClassifier => {
                             Program::SchedClassifier(SchedClassifier {
-                                data: ProgramData::new(prog_name, obj, btf_fd, *verifier_log_level),
+                                data: ProgramData::new(
+                                    prog_name,
+                                    obj,
+                                    btf_fd,
+                                    *verifier_log_level,
+                                    token_fd,
+                                    features.clone(),
+                                ),
                             })
                         }
                         ProgramSection::CgroupSkb => Program::CgroupSkb(CgroupSkb {
-                            data: ProgramData::new(prog_name, obj, btf_fd, *verifier_log_level),
+                            data: ProgramData::new(
+                                prog_name,
+                                obj,
+                                btf_fd,
+                                *verifier_log_level,
+                                token_fd,
+                                features.clone(),
+                            ),
                             expected_attach_type: None,
                         }),
                         ProgramSection::CgroupSkbIngress => Program::CgroupSkb(CgroupSkb {
-                            data: ProgramData::new(prog_name, obj, btf_fd, *verifier_log_level),
+                            data: ProgramData::new(
+                                prog_name,
+                                obj,
+                                btf_fd,
+                                *verifier_log_level,
+                                token_fd,
+                                features.clone(),
+                            ),
                             expected_attach_type: Some(CgroupSkbAttachType::Ingress),
                         }),
                         ProgramSection::CgroupSkbEgress => Program::CgroupSkb(CgroupSkb {
-                            data: ProgramData::new(prog_name, obj, btf_fd, *verifier_log_level),
+                            data: ProgramData::new(
+                                prog_name,
+                                obj,
+                                btf_fd,
+                                *verifier_log_level,
+                                token_fd,
+                                features.clone(),
+                            ),
                             expected_attach_type: Some(CgroupSkbAttachType::Egress),
                         }),
                         ProgramSection::CgroupSockAddr { attach_type, .. } => {
                             Program::CgroupSockAddr(CgroupSockAddr {
-                                data: ProgramData::new(prog_name, obj, btf_fd, *verifier_log_level),
+                                data: ProgramData::new(
+                                    prog_name,
+                                    obj,
+                                    btf_fd,
+                                    *verifier_log_level,
+                                    token_fd,
+                                    features.clone(),
+                                ),
                                 attach_type: *attach_type,
                             })
                         }
                         ProgramSection::LircMode2 => Program::LircMode2(LircMode2 {
-                            data: ProgramData::new(prog_name, obj, btf_fd, *verifier_log_level),
+                            data: ProgramData::new(
+                                prog_name,
+                                obj,
+                                btf_fd,
+                                *verifier_log_level,
+                                token_fd,
+                                features.clone(),
+                            ),
                         }),
                         ProgramSection::PerfEvent => Program::PerfEvent(PerfEvent {
-                            data: ProgramData::new(prog_name, obj, btf_fd, *verifier_log_level),
+                            data: ProgramData::new(
+                                prog_name,
+                                obj,
+                                btf_fd,
+                                *verifier_log_level,
+                                token_fd,
+                                features.clone(),
+                            ),
                         }),
                         ProgramSection::RawTracePoint => Program::RawTracePoint(RawTracePoint {
-                            data: ProgramData::new(prog_name, obj, btf_fd, *verifier_log_level),
+                            data: ProgramData::new(
+                                prog_name,
+                                obj,
+                                btf_fd,
+                                *verifier_log_level,
+                                token_fd,
+                                features.clone(),
+                            ),
                         }),
                         ProgramSection::Lsm { sleepable } => {
-                            let mut data =
-                                ProgramData::new(prog_name, obj, btf_fd, *verifier_log_level);
+                            let mut data = ProgramData::new(
+                                prog_name,
+                                obj,
+                                btf_fd,
+                                *verifier_log_level,
+                                token_fd,
+                                features.clone(),
+                            );
                             if *sleepable {
                                 data.flags = BPF_F_SLEEPABLE;
                             }
                             Program::Lsm(Lsm { data })
                         }
                         ProgramSection::BtfTracePoint => Program::BtfTracePoint(BtfTracePoint {
-                            data: ProgramData::new(prog_name, obj, btf_fd, *verifier_log_level),
+                            data: ProgramData::new(
+                                prog_name,
+                                obj,
+                                btf_fd,
+                                *verifier_log_level,
+                                token_fd,
+                                features.clone(),
+                            ),
                         }),
                         ProgramSection::FEntry { sleepable } => {
-                            let mut data =
-                                ProgramData::new(prog_name, obj, btf_fd, *verifier_log_level);
+                            let mut data = ProgramData::new(
+                                prog_name,
+                                obj,
+                                btf_fd,
+                                *verifier_log_level,
+                                token_fd,
+                                features.clone(),
+                            );
                             if *sleepable {
                                 data.flags = BPF_F_SLEEPABLE;
                             }
                             Program::FEntry(FEntry { data })
                         }
                         ProgramSection::FExit { sleepable } => {
-                            let mut data =
-                                ProgramData::new(prog_name, obj, btf_fd, *verifier_log_level);
+                            let mut data = ProgramData::new(
+                                prog_name,
+                                obj,
+                                btf_fd,
+                                *verifier_log_level,
+                                token_fd,
+                                features.clone(),
+                            );
                             if *sleepable {
                                 data.flags = BPF_F_SLEEPABLE;
                             }
                             Program::FExit(FExit { data })
                         }
                         ProgramSection::Extension => Program::Extension(Extension {
-                            data: ProgramData::new(prog_name, obj, btf_fd, *verifier_log_level),
+                            data: ProgramData::new(
+                                prog_name,
+                                obj,
+                                btf_fd,
+                                *verifier_log_level,
+                                token_fd,
+                                features.clone(),
+                            ),
                         }),
                         ProgramSection::SkLookup => Program::SkLookup(SkLookup {
-                            data: ProgramData::new(prog_name, obj, btf_fd, *verifier_log_level),
+                            data: ProgramData::new(
+                                prog_name,
+                                obj,
+                                btf_fd,
+                                *verifier_log_level,
+                                token_fd,
+                                features.clone(),
+                            ),
                         }),
                         ProgramSection::CgroupSock { attach_type, .. } => {
                             Program::CgroupSock(CgroupSock {
-                                data: ProgramData::new(prog_name, obj, btf_fd, *verifier_log_level),
+                                data: ProgramData::new(
+                                    prog_name,
+                                    obj,
+                                    btf_fd,
+                                    *verifier_log_level,
+                                    token_fd,
+                                    features.clone(),
+                                ),
                                 attach_type: *attach_type,
                             })
                         }
                         ProgramSection::CgroupDevice => Program::CgroupDevice(CgroupDevice {
-                            data: ProgramData::new(prog_name, obj, btf_fd, *verifier_log_level),
+                            data: ProgramData::new(
+                                prog_name,
+                                obj,
+                                btf_fd,
+                                *verifier_log_level,
+                                token_fd,
+                                features.clone(),
+                            ),
                         }),
                     }
                 };
@@ -1073,6 +1310,17 @@ impl Ebpf {
 /// The error type returned by [`Ebpf::load_file`] and [`Ebpf::load`].
 #[derive(Debug, Error)]
 pub enum EbpfError {
+    /// An IOError occurred
+    #[error(transparent)]
+    IOError(#[from] io::Error),
+
+    #[error("{path} is not a valid path")]
+    /// Invalid path
+    InvalidPath {
+        /// The file path
+        path: PathBuf,
+    },
+
     /// Error loading file
     #[error("error loading {path}")]
     FileError {
@@ -1123,9 +1371,13 @@ pub enum EbpfError {
 #[deprecated(since = "0.13.0", note = "use `EbpfError` instead")]
 pub type BpfError = EbpfError;
 
-fn load_btf(raw_btf: Vec<u8>, verifier_log_level: VerifierLogLevel) -> Result<OwnedFd, BtfError> {
+fn load_btf(
+    raw_btf: Vec<u8>,
+    verifier_log_level: VerifierLogLevel,
+    token_fd: Option<BorrowedFd<'_>>,
+) -> Result<OwnedFd, BtfError> {
     let (ret, verifier_log) = retry_with_verifier_logs(10, |logger| {
-        bpf_load_btf(raw_btf.as_slice(), logger, verifier_log_level)
+        bpf_load_btf(raw_btf.as_slice(), logger, verifier_log_level, token_fd)
     });
     ret.map_err(|(_, io_error)| BtfError::LoadError {
         io_error,
@@ -1157,3 +1409,28 @@ impl<'a, T: Pod> From<&'a T> for GlobalData<'a> {
         }
     }
 }
+
+/// Creates an eBPF Token at the given path.
+///
+/// The token is used to control access to the eBPF syscalls
+/// from unprivileged userspace programs.
+#[allow(dead_code)]
+pub(crate) fn create_token<P: AsRef<Path>>(path: P) -> Result<OwnedFd, EbpfError> {
+    let path = path.as_ref();
+    let path_string = CString::new(path.as_os_str().as_bytes()).map_err(|_| {
+        EbpfError::IOError(io::Error::new(
+            io::ErrorKind::InvalidInput,
+            "path contains a null byte",
+        ))
+    })?;
+    let bpffs_fd: i32 = unsafe { libc::open(path_string.as_ptr(), O_DIRECTORY, O_RDWR, O_CLOEXEC) };
+    if bpffs_fd < 0 {
+        return Err(EbpfError::IOError(io::Error::last_os_error()));
+    }
+    let bpffs_fd = unsafe { OwnedFd::from_raw_fd(bpffs_fd) };
+
+    bpf_token_create(bpffs_fd.as_fd(), 0).map_err(|(_, error)| EbpfError::FileError {
+        path: path.to_owned(),
+        error,
+    })
+}
diff --git a/aya/src/lib.rs b/aya/src/lib.rs
index ccceb9e0..ee9a560d 100644
--- a/aya/src/lib.rs
+++ b/aya/src/lib.rs
@@ -94,6 +94,7 @@ pub use obj::btf::{Btf, BtfError};
 pub use object::Endianness;
 #[doc(hidden)]
 pub use sys::netlink_set_link_up;
+pub use sys::{create_bpf_filesystem, FilesystemPermissions, FilesystemPermissionsBuilder};
 
 // See https://github.com/rust-lang/rust/pull/124210; this structure exists to avoid crashing the
 // process when we try to close a fake file descriptor.
diff --git a/aya/src/maps/mod.rs b/aya/src/maps/mod.rs
index 82467aee..e4a14851 100644
--- a/aya/src/maps/mod.rs
+++ b/aya/src/maps/mod.rs
@@ -57,8 +57,10 @@ use std::{
     os::fd::{AsFd, BorrowedFd, OwnedFd},
     path::Path,
     ptr,
+    sync::Arc,
 };
 
+use aya_obj::Features;
 use libc::{getrlimit, rlim_t, rlimit, RLIMIT_MEMLOCK, RLIM_INFINITY};
 use log::warn;
 use obj::maps::InvalidMapTypeError;
@@ -546,6 +548,7 @@ pub(crate) fn check_v_size<V>(map: &MapData) -> Result<(), MapError> {
 pub struct MapData {
     obj: obj::Map,
     fd: MapFd,
+    features: Features,
 }
 
 impl MapData {
@@ -554,6 +557,8 @@ impl MapData {
         obj: obj::Map,
         name: &str,
         btf_fd: Option<BorrowedFd<'_>>,
+        token_fd: Option<Arc<OwnedFd>>,
+        features: Features,
     ) -> Result<Self, MapError> {
         let c_name = CString::new(name).map_err(|_| MapError::InvalidName { name: name.into() })?;
 
@@ -561,21 +566,28 @@ impl MapData {
         let kernel_version = KernelVersion::current().unwrap();
         #[cfg(test)]
         let kernel_version = KernelVersion::new(0xff, 0xff, 0xff);
-        let fd =
-            bpf_create_map(&c_name, &obj, btf_fd, kernel_version).map_err(|(code, io_error)| {
-                if kernel_version < KernelVersion::new(5, 11, 0) {
-                    maybe_warn_rlimit();
-                }
+        let fd = bpf_create_map(
+            &c_name,
+            &obj,
+            btf_fd,
+            kernel_version,
+            token_fd.as_ref().map(|fd| fd.as_fd()),
+        )
+        .map_err(|(code, io_error)| {
+            if kernel_version < KernelVersion::new(5, 11, 0) {
+                maybe_warn_rlimit();
+            }
 
-                MapError::CreateError {
-                    name: name.into(),
-                    code,
-                    io_error,
-                }
-            })?;
+            MapError::CreateError {
+                name: name.into(),
+                code,
+                io_error,
+            }
+        })?;
         Ok(Self {
             obj,
             fd: MapFd::from_fd(fd),
+            features,
         })
     }
 
@@ -584,6 +596,8 @@ impl MapData {
         obj: obj::Map,
         name: &str,
         btf_fd: Option<BorrowedFd<'_>>,
+        token_fd: Option<Arc<OwnedFd>>,
+        features: Features,
     ) -> Result<Self, MapError> {
         use std::os::unix::ffi::OsStrExt as _;
 
@@ -605,9 +619,10 @@ impl MapData {
             Ok(fd) => Ok(Self {
                 obj,
                 fd: MapFd::from_fd(fd),
+                features,
             }),
             Err(_) => {
-                let map = Self::create(obj, name, btf_fd)?;
+                let map = Self::create(obj, name, btf_fd, token_fd, features)?;
                 map.pin(&path).map_err(|error| MapError::PinError {
                     name: Some(name.into()),
                     error,
@@ -618,7 +633,11 @@ impl MapData {
     }
 
     pub(crate) fn finalize(&mut self) -> Result<(), MapError> {
-        let Self { obj, fd } = self;
+        let Self {
+            obj,
+            fd,
+            features: _,
+        } = self;
         if !obj.data().is_empty() && obj.section_kind() != EbpfSectionKind::Bss {
             bpf_map_update_elem_ptr(fd.as_fd(), &0 as *const _, obj.data_mut().as_mut_ptr(), 0)
                 .map_err(|(_, io_error)| SyscallError {
@@ -639,7 +658,7 @@ impl MapData {
     }
 
     /// Loads a map from a pinned path in bpffs.
-    pub fn from_pin<P: AsRef<Path>>(path: P) -> Result<Self, MapError> {
+    pub fn from_pin<P: AsRef<Path>>(path: P, features: Features) -> Result<Self, MapError> {
         use std::os::unix::ffi::OsStrExt as _;
 
         let path = path.as_ref();
@@ -657,13 +676,13 @@ impl MapData {
             io_error,
         })?;
 
-        Self::from_fd(fd)
+        Self::from_fd(fd, features)
     }
 
     /// Loads a map from a map id.
-    pub fn from_id(id: u32) -> Result<Self, MapError> {
+    pub fn from_id(id: u32, features: Features) -> Result<Self, MapError> {
         let fd = bpf_map_get_fd_by_id(id)?;
-        Self::from_fd(fd)
+        Self::from_fd(fd, features)
     }
 
     /// Loads a map from a file descriptor.
@@ -671,11 +690,12 @@ impl MapData {
     /// If loading from a BPF Filesystem (bpffs) you should use [`Map::from_pin`](crate::maps::MapData::from_pin).
     /// This API is intended for cases where you have received a valid BPF FD from some other means.
     /// For example, you received an FD over Unix Domain Socket.
-    pub fn from_fd(fd: OwnedFd) -> Result<Self, MapError> {
+    pub fn from_fd(fd: OwnedFd, features: Features) -> Result<Self, MapError> {
         let MapInfo(info) = MapInfo::new_from_fd(fd.as_fd())?;
         Ok(Self {
             obj: parse_map_info(info, PinningType::None),
             fd: MapFd::from_fd(fd),
+            features,
         })
     }
 
@@ -706,7 +726,11 @@ impl MapData {
     pub fn pin<P: AsRef<Path>>(&self, path: P) -> Result<(), PinError> {
         use std::os::unix::ffi::OsStrExt as _;
 
-        let Self { fd, obj: _ } = self;
+        let Self {
+            fd,
+            obj: _,
+            features: _,
+        } = self;
         let path = path.as_ref();
         let path_string = CString::new(path.as_os_str().as_bytes()).map_err(|error| {
             PinError::InvalidPinPath {
@@ -723,12 +747,20 @@ impl MapData {
 
     /// Returns the file descriptor of the map.
     pub fn fd(&self) -> &MapFd {
-        let Self { obj: _, fd } = self;
+        let Self {
+            obj: _,
+            fd,
+            features: _,
+        } = self;
         fd
     }
 
     pub(crate) fn obj(&self) -> &obj::Map {
-        let Self { obj, fd: _ } = self;
+        let Self {
+            obj,
+            fd: _,
+            features: _,
+        } = self;
         obj
     }
 
@@ -1039,6 +1071,8 @@ pub fn loaded_maps() -> impl Iterator<Item = Result<MapInfo, MapError>> {
 
 #[cfg(test)]
 mod test_utils {
+    use aya_obj::Features;
+
     use crate::{
         bpf_map_def,
         generated::{bpf_cmd, bpf_map_type},
@@ -1055,7 +1089,7 @@ mod test_utils {
             } => Ok(crate::MockableFd::mock_signed_fd().into()),
             call => panic!("unexpected syscall {:?}", call),
         });
-        MapData::create(obj, "foo", None).unwrap()
+        MapData::create(obj, "foo", None, None, Features::default()).unwrap()
     }
 
     pub(super) fn new_obj_map<K>(map_type: bpf_map_type) -> obj::Map {
@@ -1119,10 +1153,11 @@ mod tests {
         });
 
         assert_matches!(
-            MapData::from_id(1234),
+            MapData::from_id(1234, Features::default()),
             Ok(MapData {
                 obj: _,
                 fd,
+                features: _,
             }) => assert_eq!(fd.as_fd().as_raw_fd(), crate::MockableFd::mock_signed_fd())
         );
     }
@@ -1138,10 +1173,11 @@ mod tests {
         });
 
         assert_matches!(
-            MapData::create(new_obj_map(), "foo", None),
+            MapData::create(new_obj_map(), "foo", None, None, Features::default()),
             Ok(MapData {
                 obj: _,
                 fd,
+                features: _,
             }) => assert_eq!(fd.as_fd().as_raw_fd(), crate::MockableFd::mock_signed_fd())
         );
     }
@@ -1178,7 +1214,8 @@ mod tests {
             _ => Err((-1, io::Error::from_raw_os_error(EFAULT))),
         });
 
-        let map_data = MapData::create(new_obj_map(), TEST_NAME, None).unwrap();
+        let map_data =
+            MapData::create(new_obj_map(), TEST_NAME, None, None, Features::default()).unwrap();
         assert_eq!(TEST_NAME, map_data.info().unwrap().name_as_str().unwrap());
     }
 
@@ -1256,7 +1293,7 @@ mod tests {
         override_syscall(|_| Err((-42, io::Error::from_raw_os_error(EFAULT))));
 
         assert_matches!(
-            MapData::create(new_obj_map(), "foo", None),
+            MapData::create(new_obj_map(), "foo", None, None, Features::default()),
             Err(MapError::CreateError { name, code, io_error }) => {
                 assert_eq!(name, "foo");
                 assert_eq!(code, -42);
diff --git a/aya/src/maps/xdp/cpu_map.rs b/aya/src/maps/xdp/cpu_map.rs
index b5c0727c..963a7831 100644
--- a/aya/src/maps/xdp/cpu_map.rs
+++ b/aya/src/maps/xdp/cpu_map.rs
@@ -13,7 +13,7 @@ use crate::{
     maps::{check_bounds, check_kv_size, IterableMap, MapData, MapError},
     programs::ProgramFd,
     sys::{bpf_map_lookup_elem, bpf_map_update_elem, SyscallError},
-    Pod, FEATURES,
+    Pod,
 };
 
 /// An array of available CPUs.
@@ -56,8 +56,7 @@ pub struct CpuMap<T> {
 impl<T: Borrow<MapData>> CpuMap<T> {
     pub(crate) fn new(map: T) -> Result<Self, MapError> {
         let data = map.borrow();
-
-        if FEATURES.cpumap_prog_id() {
+        if data.features.cpumap_prog_id() {
             check_kv_size::<u32, bpf_cpumap_val>(data)?;
         } else {
             check_kv_size::<u32, u32>(data)?;
@@ -83,8 +82,7 @@ impl<T: Borrow<MapData>> CpuMap<T> {
         let data = self.inner.borrow();
         check_bounds(data, cpu_index)?;
         let fd = data.fd().as_fd();
-
-        let value = if FEATURES.cpumap_prog_id() {
+        let value = if data.features.cpumap_prog_id() {
             bpf_map_lookup_elem::<_, bpf_cpumap_val>(fd, &cpu_index, flags).map(|value| {
                 value.map(|value| CpuMapValue {
                     queue_size: value.qsize,
@@ -146,7 +144,7 @@ impl<T: BorrowMut<MapData>> CpuMap<T> {
         check_bounds(data, cpu_index)?;
         let fd = data.fd().as_fd();
 
-        let res = if FEATURES.cpumap_prog_id() {
+        let res = if data.features.cpumap_prog_id() {
             let mut value = unsafe { std::mem::zeroed::<bpf_cpumap_val>() };
             value.qsize = queue_size;
             // Default is valid as the kernel will only consider fd > 0:
diff --git a/aya/src/maps/xdp/dev_map.rs b/aya/src/maps/xdp/dev_map.rs
index 44062df5..e47270fd 100644
--- a/aya/src/maps/xdp/dev_map.rs
+++ b/aya/src/maps/xdp/dev_map.rs
@@ -13,7 +13,7 @@ use crate::{
     maps::{check_bounds, check_kv_size, IterableMap, MapData, MapError},
     programs::ProgramFd,
     sys::{bpf_map_lookup_elem, bpf_map_update_elem, SyscallError},
-    Pod, FEATURES,
+    Pod,
 };
 
 /// An array of network devices.
@@ -48,8 +48,7 @@ pub struct DevMap<T> {
 impl<T: Borrow<MapData>> DevMap<T> {
     pub(crate) fn new(map: T) -> Result<Self, MapError> {
         let data = map.borrow();
-
-        if FEATURES.devmap_prog_id() {
+        if data.features.devmap_prog_id() {
             check_kv_size::<u32, bpf_devmap_val>(data)?;
         } else {
             check_kv_size::<u32, u32>(data)?;
@@ -75,8 +74,7 @@ impl<T: Borrow<MapData>> DevMap<T> {
         let data = self.inner.borrow();
         check_bounds(data, index)?;
         let fd = data.fd().as_fd();
-
-        let value = if FEATURES.devmap_prog_id() {
+        let value = if data.features.devmap_prog_id() {
             bpf_map_lookup_elem::<_, bpf_devmap_val>(fd, &index, flags).map(|value| {
                 value.map(|value| DevMapValue {
                     if_index: value.ifindex,
@@ -136,8 +134,7 @@ impl<T: BorrowMut<MapData>> DevMap<T> {
         let data = self.inner.borrow_mut();
         check_bounds(data, index)?;
         let fd = data.fd().as_fd();
-
-        let res = if FEATURES.devmap_prog_id() {
+        let res = if data.features.devmap_prog_id() {
             let mut value = unsafe { std::mem::zeroed::<bpf_devmap_val>() };
             value.ifindex = target_if_index;
             // Default is valid as the kernel will only consider fd > 0:
diff --git a/aya/src/maps/xdp/dev_map_hash.rs b/aya/src/maps/xdp/dev_map_hash.rs
index 1b941bc4..e18d4026 100644
--- a/aya/src/maps/xdp/dev_map_hash.rs
+++ b/aya/src/maps/xdp/dev_map_hash.rs
@@ -13,7 +13,6 @@ use crate::{
     maps::{check_kv_size, hash_map, IterableMap, MapData, MapError, MapIter, MapKeys},
     programs::ProgramFd,
     sys::{bpf_map_lookup_elem, SyscallError},
-    FEATURES,
 };
 
 /// An hashmap of network devices.
@@ -49,7 +48,7 @@ impl<T: Borrow<MapData>> DevMapHash<T> {
     pub(crate) fn new(map: T) -> Result<Self, MapError> {
         let data = map.borrow();
 
-        if FEATURES.devmap_prog_id() {
+        if data.features.devmap_prog_id() {
             check_kv_size::<u32, bpf_devmap_val>(data)?;
         } else {
             check_kv_size::<u32, u32>(data)?;
@@ -65,8 +64,9 @@ impl<T: Borrow<MapData>> DevMapHash<T> {
     /// Returns [`MapError::SyscallError`] if `bpf_map_lookup_elem` fails.
     pub fn get(&self, key: u32, flags: u64) -> Result<DevMapValue, MapError> {
         let fd = self.inner.borrow().fd().as_fd();
+        let data = self.inner.borrow();
 
-        let value = if FEATURES.devmap_prog_id() {
+        let value = if data.features.devmap_prog_id() {
             bpf_map_lookup_elem::<_, bpf_devmap_val>(fd, &key, flags).map(|value| {
                 value.map(|value| DevMapValue {
                     if_index: value.ifindex,
@@ -127,7 +127,8 @@ impl<T: BorrowMut<MapData>> DevMapHash<T> {
         program: Option<&ProgramFd>,
         flags: u64,
     ) -> Result<(), XdpMapError> {
-        if FEATURES.devmap_prog_id() {
+        let data = self.inner.borrow();
+        if data.features.devmap_prog_id() {
             let mut value = unsafe { std::mem::zeroed::<bpf_devmap_val>() };
             value.ifindex = target_if_index;
             // Default is valid as the kernel will only consider fd > 0:
diff --git a/aya/src/programs/cgroup_skb.rs b/aya/src/programs/cgroup_skb.rs
index 84749423..0006db9a 100644
--- a/aya/src/programs/cgroup_skb.rs
+++ b/aya/src/programs/cgroup_skb.rs
@@ -1,6 +1,13 @@
 //! Cgroup skb programs.
 
-use std::{hash::Hash, os::fd::AsFd, path::Path};
+use std::{
+    hash::Hash,
+    os::fd::{AsFd, OwnedFd},
+    path::Path,
+    sync::Arc,
+};
+
+use aya_obj::Features;
 
 use crate::{
     generated::{
@@ -140,8 +147,11 @@ impl CgroupSkb {
     pub fn from_pin<P: AsRef<Path>>(
         path: P,
         expected_attach_type: CgroupSkbAttachType,
+        token_fd: Option<Arc<OwnedFd>>,
+        features: Features,
     ) -> Result<Self, ProgramError> {
-        let data = ProgramData::from_pinned_path(path, VerifierLogLevel::default())?;
+        let data =
+            ProgramData::from_pinned_path(path, VerifierLogLevel::default(), token_fd, features)?;
         Ok(Self {
             data,
             expected_attach_type: Some(expected_attach_type),
diff --git a/aya/src/programs/cgroup_sock.rs b/aya/src/programs/cgroup_sock.rs
index 24f07869..e8cd376b 100644
--- a/aya/src/programs/cgroup_sock.rs
+++ b/aya/src/programs/cgroup_sock.rs
@@ -1,8 +1,14 @@
 //! Cgroup socket programs.
 
-use std::{hash::Hash, os::fd::AsFd, path::Path};
+use std::{
+    hash::Hash,
+    os::fd::{AsFd, OwnedFd},
+    path::Path,
+    sync::Arc,
+};
 
 pub use aya_obj::programs::CgroupSockAttachType;
+use aya_obj::Features;
 
 use crate::{
     generated::bpf_prog_type::BPF_PROG_TYPE_CGROUP_SOCK,
@@ -115,8 +121,11 @@ impl CgroupSock {
     pub fn from_pin<P: AsRef<Path>>(
         path: P,
         attach_type: CgroupSockAttachType,
+        token_fd: Option<Arc<OwnedFd>>,
+        features: Features,
     ) -> Result<Self, ProgramError> {
-        let data = ProgramData::from_pinned_path(path, VerifierLogLevel::default())?;
+        let data =
+            ProgramData::from_pinned_path(path, VerifierLogLevel::default(), token_fd, features)?;
         Ok(Self { data, attach_type })
     }
 }
diff --git a/aya/src/programs/cgroup_sock_addr.rs b/aya/src/programs/cgroup_sock_addr.rs
index 79b607ca..eb44f2b7 100644
--- a/aya/src/programs/cgroup_sock_addr.rs
+++ b/aya/src/programs/cgroup_sock_addr.rs
@@ -1,8 +1,14 @@
 //! Cgroup socket address programs.
 
-use std::{hash::Hash, os::fd::AsFd, path::Path};
+use std::{
+    hash::Hash,
+    os::fd::{AsFd, OwnedFd},
+    path::Path,
+    sync::Arc,
+};
 
 pub use aya_obj::programs::CgroupSockAddrAttachType;
+use aya_obj::Features;
 
 use crate::{
     generated::bpf_prog_type::BPF_PROG_TYPE_CGROUP_SOCK_ADDR,
@@ -119,8 +125,11 @@ impl CgroupSockAddr {
     pub fn from_pin<P: AsRef<Path>>(
         path: P,
         attach_type: CgroupSockAddrAttachType,
+        token_fd: Option<Arc<OwnedFd>>,
+        features: Features,
     ) -> Result<Self, ProgramError> {
-        let data = ProgramData::from_pinned_path(path, VerifierLogLevel::default())?;
+        let data =
+            ProgramData::from_pinned_path(path, VerifierLogLevel::default(), token_fd, features)?;
         Ok(Self { data, attach_type })
     }
 }
diff --git a/aya/src/programs/cgroup_sockopt.rs b/aya/src/programs/cgroup_sockopt.rs
index 414fece6..c109804f 100644
--- a/aya/src/programs/cgroup_sockopt.rs
+++ b/aya/src/programs/cgroup_sockopt.rs
@@ -1,8 +1,14 @@
 //! Cgroup socket option programs.
 
-use std::{hash::Hash, os::fd::AsFd, path::Path};
+use std::{
+    hash::Hash,
+    os::fd::{AsFd, OwnedFd},
+    path::Path,
+    sync::Arc,
+};
 
 pub use aya_obj::programs::CgroupSockoptAttachType;
+use aya_obj::Features;
 
 use crate::{
     generated::bpf_prog_type::BPF_PROG_TYPE_CGROUP_SOCKOPT,
@@ -118,8 +124,11 @@ impl CgroupSockopt {
     pub fn from_pin<P: AsRef<Path>>(
         path: P,
         attach_type: CgroupSockoptAttachType,
+        token_fd: Option<Arc<OwnedFd>>,
+        features: Features,
     ) -> Result<Self, ProgramError> {
-        let data = ProgramData::from_pinned_path(path, VerifierLogLevel::default())?;
+        let data =
+            ProgramData::from_pinned_path(path, VerifierLogLevel::default(), token_fd, features)?;
         Ok(Self { data, attach_type })
     }
 }
diff --git a/aya/src/programs/kprobe.rs b/aya/src/programs/kprobe.rs
index 81e830e9..8c83846b 100644
--- a/aya/src/programs/kprobe.rs
+++ b/aya/src/programs/kprobe.rs
@@ -2,10 +2,12 @@
 use std::{
     ffi::OsStr,
     io,
-    os::fd::AsFd as _,
+    os::fd::{AsFd as _, OwnedFd},
     path::{Path, PathBuf},
+    sync::Arc,
 };
 
+use aya_obj::Features;
 use thiserror::Error;
 
 use crate::{
@@ -78,7 +80,15 @@ impl KProbe {
         fn_name: T,
         offset: u64,
     ) -> Result<KProbeLinkId, ProgramError> {
-        attach(&mut self.data, self.kind, fn_name.as_ref(), offset, None)
+        let features = self.data.features.clone();
+        attach(
+            &mut self.data,
+            self.kind,
+            fn_name.as_ref(),
+            offset,
+            None,
+            &features,
+        )
     }
 
     /// Detaches the program.
@@ -102,8 +112,14 @@ impl KProbe {
     ///
     /// On drop, any managed links are detached and the program is unloaded. This will not result in
     /// the program being unloaded from the kernel if it is still pinned.
-    pub fn from_pin<P: AsRef<Path>>(path: P, kind: ProbeKind) -> Result<Self, ProgramError> {
-        let data = ProgramData::from_pinned_path(path, VerifierLogLevel::default())?;
+    pub fn from_pin<P: AsRef<Path>>(
+        path: P,
+        kind: ProbeKind,
+        token_fd: Option<Arc<OwnedFd>>,
+        features: Features,
+    ) -> Result<Self, ProgramError> {
+        let data =
+            ProgramData::from_pinned_path(path, VerifierLogLevel::default(), token_fd, features)?;
         Ok(Self { data, kind })
     }
 }
diff --git a/aya/src/programs/mod.rs b/aya/src/programs/mod.rs
index 675e36f0..e3f4d056 100644
--- a/aya/src/programs/mod.rs
+++ b/aya/src/programs/mod.rs
@@ -78,6 +78,7 @@ use std::{
     time::{Duration, SystemTime},
 };
 
+use aya_obj::Features;
 use libc::ENOSPC;
 use thiserror::Error;
 
@@ -464,6 +465,8 @@ pub(crate) struct ProgramData<T: Link> {
     pub(crate) attach_btf_id: Option<u32>,
     pub(crate) attach_prog_fd: Option<ProgramFd>,
     pub(crate) btf_fd: Option<Arc<OwnedFd>>,
+    pub(crate) token_fd: Option<Arc<OwnedFd>>,
+    pub(crate) features: Features,
     pub(crate) verifier_log_level: VerifierLogLevel,
     pub(crate) path: Option<PathBuf>,
     pub(crate) flags: u32,
@@ -475,6 +478,8 @@ impl<T: Link> ProgramData<T> {
         obj: (obj::Program, obj::Function),
         btf_fd: Option<Arc<OwnedFd>>,
         verifier_log_level: VerifierLogLevel,
+        token_fd: Option<Arc<OwnedFd>>,
+        features: Features,
     ) -> Self {
         Self {
             name,
@@ -489,6 +494,8 @@ impl<T: Link> ProgramData<T> {
             verifier_log_level,
             path: None,
             flags: 0,
+            token_fd,
+            features,
         }
     }
 
@@ -498,6 +505,8 @@ impl<T: Link> ProgramData<T> {
         path: &Path,
         info: bpf_prog_info,
         verifier_log_level: VerifierLogLevel,
+        token_fd: Option<Arc<OwnedFd>>,
+        features: Features,
     ) -> Result<Self, ProgramError> {
         let attach_btf_id = if info.attach_btf_id > 0 {
             Some(info.attach_btf_id)
@@ -521,12 +530,16 @@ impl<T: Link> ProgramData<T> {
             verifier_log_level,
             path: Some(path.to_path_buf()),
             flags: 0,
+            token_fd,
+            features,
         })
     }
 
     pub(crate) fn from_pinned_path<P: AsRef<Path>>(
         path: P,
         verifier_log_level: VerifierLogLevel,
+        token_fd: Option<Arc<OwnedFd>>,
+        features: Features,
     ) -> Result<Self, ProgramError> {
         use std::os::unix::ffi::OsStrExt as _;
 
@@ -539,7 +552,15 @@ impl<T: Link> ProgramData<T> {
 
         let info = ProgramInfo::new_from_fd(fd.as_fd())?;
         let name = info.name_as_str().map(|s| s.to_string());
-        Self::from_bpf_prog_info(name, fd, path.as_ref(), info.0, verifier_log_level)
+        Self::from_bpf_prog_info(
+            name,
+            fd,
+            path.as_ref(),
+            info.0,
+            verifier_log_level,
+            token_fd,
+            features,
+        )
     }
 }
 
@@ -601,6 +622,8 @@ fn load_program<T: Link>(
         verifier_log_level,
         path: _,
         flags,
+        token_fd,
+        features: _,
     } = data;
     if fd.is_some() {
         return Err(ProgramError::AlreadyLoaded);
@@ -660,7 +683,12 @@ fn load_program<T: Link>(
     };
 
     let (ret, verifier_log) = retry_with_verifier_logs(10, |logger| {
-        bpf_load_program(&attr, logger, *verifier_log_level)
+        bpf_load_program(
+            &attr,
+            logger,
+            *verifier_log_level,
+            token_fd.as_ref().map(|fd| fd.as_fd()),
+        )
     });
 
     match ret {
@@ -869,8 +897,8 @@ macro_rules! impl_from_pin {
                 ///
                 /// On drop, any managed links are detached and the program is unloaded. This will not result in
                 /// the program being unloaded from the kernel if it is still pinned.
-                pub fn from_pin<P: AsRef<Path>>(path: P) -> Result<Self, ProgramError> {
-                    let data = ProgramData::from_pinned_path(path, VerifierLogLevel::default())?;
+                pub fn from_pin<P: AsRef<Path>>(path: P, token_fd: Option<Arc<OwnedFd>>, features: Features) -> Result<Self, ProgramError> {
+                    let data = ProgramData::from_pinned_path(path, VerifierLogLevel::default(), token_fd, features)?;
                     Ok(Self { data })
                 }
             }
diff --git a/aya/src/programs/perf_attach.rs b/aya/src/programs/perf_attach.rs
index 8cf04871..02925df7 100644
--- a/aya/src/programs/perf_attach.rs
+++ b/aya/src/programs/perf_attach.rs
@@ -1,6 +1,8 @@
 //! Perf attach links.
 use std::os::fd::{AsFd as _, AsRawFd as _, BorrowedFd, OwnedFd, RawFd};
 
+use aya_obj::Features;
+
 use crate::{
     generated::bpf_attach_type::BPF_PERF_EVENT,
     programs::{
@@ -8,7 +10,7 @@ use crate::{
         FdLink, Link, ProgramError,
     },
     sys::{bpf_link_create, perf_event_ioctl, LinkTarget, SysResult, SyscallError},
-    FEATURES, PERF_EVENT_IOC_DISABLE, PERF_EVENT_IOC_ENABLE, PERF_EVENT_IOC_SET_BPF,
+    PERF_EVENT_IOC_DISABLE, PERF_EVENT_IOC_ENABLE, PERF_EVENT_IOC_SET_BPF,
 };
 
 #[derive(Debug, Hash, Eq, PartialEq)]
@@ -73,8 +75,9 @@ impl Link for PerfLink {
 pub(crate) fn perf_attach(
     prog_fd: BorrowedFd<'_>,
     fd: OwnedFd,
+    features: &Features,
 ) -> Result<PerfLinkInner, ProgramError> {
-    if FEATURES.bpf_perf_link() {
+    if features.bpf_perf_link() {
         let link_fd = bpf_link_create(prog_fd, LinkTarget::Fd(fd.as_fd()), BPF_PERF_EVENT, None, 0)
             .map_err(|(_, io_error)| SyscallError {
                 call: "bpf_link_create",
diff --git a/aya/src/programs/perf_event.rs b/aya/src/programs/perf_event.rs
index 00ac4b38..53ab575e 100644
--- a/aya/src/programs/perf_event.rs
+++ b/aya/src/programs/perf_event.rs
@@ -180,7 +180,7 @@ impl PerfEvent {
             io_error,
         })?;
 
-        let link = perf_attach(prog_fd, fd)?;
+        let link = perf_attach(prog_fd, fd, &self.data.features)?;
         self.data.links.insert(PerfEventLink::new(link))
     }
 
diff --git a/aya/src/programs/probe.rs b/aya/src/programs/probe.rs
index 85be3b3c..303e5dd4 100644
--- a/aya/src/programs/probe.rs
+++ b/aya/src/programs/probe.rs
@@ -9,6 +9,7 @@ use std::{
     sync::atomic::{AtomicUsize, Ordering},
 };
 
+use aya_obj::Features;
 use libc::pid_t;
 
 use crate::{
@@ -112,6 +113,7 @@ pub(crate) fn attach<T: Link + From<PerfLinkInner>>(
     fn_name: &OsStr,
     offset: u64,
     pid: Option<pid_t>,
+    features: &Features,
 ) -> Result<T::Id, ProgramError> {
     // https://github.com/torvalds/linux/commit/e12f03d7031a977356e3d7b75a68c2185ff8d155
     // Use debugfs to create probe
@@ -122,7 +124,7 @@ pub(crate) fn attach<T: Link + From<PerfLinkInner>>(
         perf_attach_debugfs(prog_fd, fd, ProbeEvent { kind, event_alias })
     } else {
         let fd = create_as_probe(kind, fn_name, offset, pid)?;
-        perf_attach(prog_fd, fd)
+        perf_attach(prog_fd, fd, features)
     }?;
     program_data.links.insert(T::from(link))
 }
diff --git a/aya/src/programs/sk_skb.rs b/aya/src/programs/sk_skb.rs
index 4d571dcb..7fe09f84 100644
--- a/aya/src/programs/sk_skb.rs
+++ b/aya/src/programs/sk_skb.rs
@@ -1,6 +1,12 @@
 //! Skskb programs.
 
-use std::{os::fd::AsFd as _, path::Path};
+use std::{
+    os::fd::{AsFd as _, OwnedFd},
+    path::Path,
+    sync::Arc,
+};
+
+use aya_obj::Features;
 
 use crate::{
     generated::{
@@ -116,8 +122,14 @@ impl SkSkb {
     ///
     /// On drop, any managed links are detached and the program is unloaded. This will not result in
     /// the program being unloaded from the kernel if it is still pinned.
-    pub fn from_pin<P: AsRef<Path>>(path: P, kind: SkSkbKind) -> Result<Self, ProgramError> {
-        let data = ProgramData::from_pinned_path(path, VerifierLogLevel::default())?;
+    pub fn from_pin<P: AsRef<Path>>(
+        path: P,
+        kind: SkSkbKind,
+        token_fd: Option<Arc<OwnedFd>>,
+        features: Features,
+    ) -> Result<Self, ProgramError> {
+        let data =
+            ProgramData::from_pinned_path(path, VerifierLogLevel::default(), token_fd, features)?;
         Ok(Self { data, kind })
     }
 }
diff --git a/aya/src/programs/tc.rs b/aya/src/programs/tc.rs
index c2dd5a13..508f6ef3 100644
--- a/aya/src/programs/tc.rs
+++ b/aya/src/programs/tc.rs
@@ -2,10 +2,12 @@
 use std::{
     ffi::{CStr, CString},
     io,
-    os::fd::AsFd as _,
+    os::fd::{AsFd as _, OwnedFd},
     path::Path,
+    sync::Arc,
 };
 
+use aya_obj::Features;
 use thiserror::Error;
 
 use crate::{
@@ -230,8 +232,13 @@ impl SchedClassifier {
     ///
     /// On drop, any managed links are detached and the program is unloaded. This will not result in
     /// the program being unloaded from the kernel if it is still pinned.
-    pub fn from_pin<P: AsRef<Path>>(path: P) -> Result<Self, ProgramError> {
-        let data = ProgramData::from_pinned_path(path, VerifierLogLevel::default())?;
+    pub fn from_pin<P: AsRef<Path>>(
+        path: P,
+        token_fd: Option<Arc<OwnedFd>>,
+        features: Features,
+    ) -> Result<Self, ProgramError> {
+        let data =
+            ProgramData::from_pinned_path(path, VerifierLogLevel::default(), token_fd, features)?;
         Ok(Self { data })
     }
 }
diff --git a/aya/src/programs/trace_point.rs b/aya/src/programs/trace_point.rs
index 9d304204..c7c959bd 100644
--- a/aya/src/programs/trace_point.rs
+++ b/aya/src/programs/trace_point.rs
@@ -89,7 +89,7 @@ impl TracePoint {
                 io_error,
             })?;
 
-        let link = perf_attach(prog_fd, fd)?;
+        let link = perf_attach(prog_fd, fd, &self.data.features)?;
         self.data.links.insert(TracePointLink::new(link))
     }
 
diff --git a/aya/src/programs/uprobe.rs b/aya/src/programs/uprobe.rs
index 28d6749f..9042c68b 100644
--- a/aya/src/programs/uprobe.rs
+++ b/aya/src/programs/uprobe.rs
@@ -6,11 +6,16 @@ use std::{
     fs,
     io::{self, BufRead, Cursor, Read},
     mem,
-    os::{fd::AsFd as _, raw::c_char, unix::ffi::OsStrExt},
+    os::{
+        fd::{AsFd as _, OwnedFd},
+        raw::c_char,
+        unix::ffi::OsStrExt,
+    },
     path::{Path, PathBuf},
     sync::Arc,
 };
 
+use aya_obj::Features;
 use libc::pid_t;
 use object::{Object, ObjectSection, ObjectSymbol, Symbol};
 use thiserror::Error;
@@ -96,7 +101,15 @@ impl UProbe {
         };
 
         let path = path.as_os_str();
-        attach(&mut self.data, self.kind, path, sym_offset + offset, pid)
+        let features = self.data.features.clone();
+        attach(
+            &mut self.data,
+            self.kind,
+            path,
+            sym_offset + offset,
+            pid,
+            &features,
+        )
     }
 
     /// Detaches the program.
@@ -120,8 +133,14 @@ impl UProbe {
     ///
     /// On drop, any managed links are detached and the program is unloaded. This will not result in
     /// the program being unloaded from the kernel if it is still pinned.
-    pub fn from_pin<P: AsRef<Path>>(path: P, kind: ProbeKind) -> Result<Self, ProgramError> {
-        let data = ProgramData::from_pinned_path(path, VerifierLogLevel::default())?;
+    pub fn from_pin<P: AsRef<Path>>(
+        path: P,
+        kind: ProbeKind,
+        token_fd: Option<Arc<OwnedFd>>,
+        features: Features,
+    ) -> Result<Self, ProgramError> {
+        let data =
+            ProgramData::from_pinned_path(path, VerifierLogLevel::default(), token_fd, features)?;
         Ok(Self { data, kind })
     }
 }
diff --git a/aya/src/programs/xdp.rs b/aya/src/programs/xdp.rs
index 5d49f4f1..2b9a14a3 100644
--- a/aya/src/programs/xdp.rs
+++ b/aya/src/programs/xdp.rs
@@ -4,10 +4,12 @@ use std::{
     ffi::CString,
     hash::Hash,
     io,
-    os::fd::{AsFd as _, AsRawFd as _, BorrowedFd, RawFd},
+    os::fd::{AsFd as _, AsRawFd as _, BorrowedFd, OwnedFd, RawFd},
     path::Path,
+    sync::Arc,
 };
 
+use aya_obj::Features;
 use libc::if_nametoindex;
 use thiserror::Error;
 
@@ -183,8 +185,11 @@ impl Xdp {
     pub fn from_pin<P: AsRef<Path>>(
         path: P,
         attach_type: XdpAttachType,
+        token_fd: Option<Arc<OwnedFd>>,
+        features: Features,
     ) -> Result<Self, ProgramError> {
-        let mut data = ProgramData::from_pinned_path(path, VerifierLogLevel::default())?;
+        let mut data =
+            ProgramData::from_pinned_path(path, VerifierLogLevel::default(), token_fd, features)?;
         data.expected_attach_type = Some(attach_type.into());
         Ok(Self { data, attach_type })
     }
diff --git a/aya/src/sys/bpf.rs b/aya/src/sys/bpf.rs
index 1ae1dc95..5625493f 100644
--- a/aya/src/sys/bpf.rs
+++ b/aya/src/sys/bpf.rs
@@ -3,12 +3,21 @@ use std::{
     ffi::{c_char, CStr, CString},
     io, iter,
     mem::{self, MaybeUninit},
-    os::fd::{AsFd as _, AsRawFd as _, BorrowedFd, FromRawFd as _, OwnedFd, RawFd},
+    os::{
+        fd::{AsFd as _, AsRawFd as _, BorrowedFd, FromRawFd as _, OwnedFd, RawFd},
+        unix::ffi::OsStrExt as _,
+    },
+    path::Path,
+    ptr::null,
     slice,
 };
 
 use assert_matches::assert_matches;
-use libc::{ENOENT, ENOSPC};
+use aya_obj::{
+    attach::BpfAttachType, cmd::BpfCommand, generated::BPF_F_TOKEN_FD, maps::BpfMapType,
+    programs::BpfProgType, Features,
+};
+use libc::{c_void, AT_FDCWD, ENOENT, ENOSPC, MOVE_MOUNT_F_EMPTY_PATH};
 use obj::{
     btf::{BtfEnum64, Enum64},
     maps::{bpf_map_def, LegacyMap},
@@ -39,6 +48,7 @@ pub(crate) fn bpf_create_map(
     def: &obj::Map,
     btf_fd: Option<BorrowedFd<'_>>,
     kernel_version: KernelVersion,
+    token_fd: Option<BorrowedFd<'_>>,
 ) -> SysResult<OwnedFd> {
     let mut attr = unsafe { mem::zeroed::<bpf_attr>() };
 
@@ -48,6 +58,10 @@ pub(crate) fn bpf_create_map(
     u.value_size = def.value_size();
     u.max_entries = def.max_entries();
     u.map_flags = def.map_flags();
+    if let Some(v) = token_fd {
+        u.map_token_fd = v.as_raw_fd();
+        u.map_flags |= BPF_F_TOKEN_FD;
+    }
 
     if let obj::Map::Btf(m) = def {
         use bpf_map_type::*;
@@ -134,6 +148,7 @@ pub(crate) fn bpf_load_program(
     aya_attr: &EbpfLoadProgramAttrs<'_>,
     log_buf: &mut [u8],
     verifier_log_level: VerifierLogLevel,
+    token_fd: Option<BorrowedFd<'_>>,
 ) -> SysResult<OwnedFd> {
     let mut attr = unsafe { mem::zeroed::<bpf_attr>() };
 
@@ -158,6 +173,10 @@ pub(crate) fn bpf_load_program(
     u.insn_cnt = aya_attr.insns.len() as u32;
     u.license = aya_attr.license.as_ptr() as u64;
     u.kern_version = aya_attr.kernel_version;
+    if let Some(v) = token_fd {
+        u.prog_token_fd = v.as_raw_fd();
+        u.prog_flags |= BPF_F_TOKEN_FD;
+    }
 
     // these must be allocated here to ensure the slice outlives the pointer
     // so .as_ptr below won't point to garbage
@@ -617,6 +636,7 @@ pub(crate) fn bpf_load_btf(
     raw_btf: &[u8],
     log_buf: &mut [u8],
     verifier_log_level: VerifierLogLevel,
+    token_fd: Option<BorrowedFd<'_>>,
 ) -> SysResult<OwnedFd> {
     let mut attr = unsafe { mem::zeroed::<bpf_attr>() };
     let u = unsafe { &mut attr.__bindgen_anon_7 };
@@ -627,10 +647,21 @@ pub(crate) fn bpf_load_btf(
         u.btf_log_buf = log_buf.as_mut_ptr() as u64;
         u.btf_log_size = log_buf.len() as u32;
     }
+    if let Some(v) = token_fd {
+        u.btf_token_fd = v.as_raw_fd();
+        u.btf_flags |= BPF_F_TOKEN_FD;
+    }
     // SAFETY: `BPF_BTF_LOAD` returns a newly created fd.
     unsafe { fd_sys_bpf(bpf_cmd::BPF_BTF_LOAD, &mut attr) }
 }
 
+pub(crate) fn bpf_token_create(bpf_fs_fd: BorrowedFd<'_>, flags: u32) -> SysResult<OwnedFd> {
+    let mut attr = unsafe { mem::zeroed::<bpf_attr>() };
+    attr.token_create.bpffs_fd = bpf_fs_fd.as_raw_fd() as u32;
+    attr.token_create.flags = flags;
+    unsafe { fd_sys_bpf(bpf_cmd::BPF_TOKEN_CREATE, &mut attr) }
+}
+
 // 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) -> SysResult<OwnedFd> {
     let fd = sys_bpf(cmd, attr)?;
@@ -660,7 +691,7 @@ pub(crate) fn bpf_btf_get_fd_by_id(id: u32) -> Result<OwnedFd, SyscallError> {
     })
 }
 
-pub(crate) fn is_prog_name_supported() -> bool {
+pub(crate) fn is_prog_name_supported(token_fd: Option<BorrowedFd<'_>>) -> bool {
     let mut attr = unsafe { mem::zeroed::<bpf_attr>() };
     let u = unsafe { &mut attr.__bindgen_anon_3 };
     let mut name: [c_char; 16] = [0; 16];
@@ -684,11 +715,14 @@ pub(crate) fn is_prog_name_supported() -> bool {
     u.insn_cnt = insns.len() as u32;
     u.insns = insns.as_ptr() as u64;
     u.prog_type = bpf_prog_type::BPF_PROG_TYPE_SOCKET_FILTER as u32;
-
+    if let Some(v) = token_fd {
+        u.prog_token_fd = v.as_raw_fd();
+        u.prog_flags |= BPF_F_TOKEN_FD;
+    }
     bpf_prog_load(&mut attr).is_ok()
 }
 
-pub(crate) fn is_probe_read_kernel_supported() -> bool {
+pub(crate) fn is_probe_read_kernel_supported(token_fd: Option<BorrowedFd<'_>>) -> bool {
     let mut attr = unsafe { mem::zeroed::<bpf_attr>() };
     let u = unsafe { &mut attr.__bindgen_anon_3 };
 
@@ -708,11 +742,14 @@ pub(crate) fn is_probe_read_kernel_supported() -> bool {
     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;
-
+    if let Some(v) = token_fd {
+        u.prog_token_fd = v.as_raw_fd();
+        u.prog_flags |= BPF_F_TOKEN_FD;
+    }
     bpf_prog_load(&mut attr).is_ok()
 }
 
-pub(crate) fn is_perf_link_supported() -> bool {
+pub(crate) fn is_perf_link_supported(token_fd: Option<BorrowedFd<'_>>) -> bool {
     let mut attr = unsafe { mem::zeroed::<bpf_attr>() };
     let u = unsafe { &mut attr.__bindgen_anon_3 };
 
@@ -728,7 +765,10 @@ pub(crate) fn is_perf_link_supported() -> bool {
     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;
-
+    if let Some(v) = token_fd {
+        u.prog_token_fd = v.as_raw_fd();
+        u.prog_flags |= BPF_F_TOKEN_FD;
+    }
     if let Ok(fd) = bpf_prog_load(&mut attr) {
         let fd = crate::MockableFd::from_fd(fd);
         let fd = fd.as_fd();
@@ -743,7 +783,7 @@ pub(crate) fn is_perf_link_supported() -> bool {
     }
 }
 
-pub(crate) fn is_bpf_global_data_supported() -> bool {
+pub(crate) fn is_bpf_global_data_supported(token_fd: Option<BorrowedFd<'_>>) -> bool {
     let mut attr = unsafe { mem::zeroed::<bpf_attr>() };
     let u = unsafe { &mut attr.__bindgen_anon_3 };
 
@@ -773,6 +813,8 @@ pub(crate) fn is_bpf_global_data_supported() -> bool {
         }),
         "aya_global",
         None,
+        None,
+        Features::default(),
     );
 
     if let Ok(map) = map {
@@ -783,14 +825,17 @@ pub(crate) fn is_bpf_global_data_supported() -> bool {
         u.insn_cnt = insns.len() as u32;
         u.insns = insns.as_ptr() as u64;
         u.prog_type = bpf_prog_type::BPF_PROG_TYPE_SOCKET_FILTER as u32;
-
+        if let Some(v) = token_fd {
+            u.prog_token_fd = v.as_raw_fd();
+            u.prog_flags |= BPF_F_TOKEN_FD;
+        }
         bpf_prog_load(&mut attr).is_ok()
     } else {
         false
     }
 }
 
-pub(crate) fn is_bpf_cookie_supported() -> bool {
+pub(crate) fn is_bpf_cookie_supported(token_fd: Option<BorrowedFd<'_>>) -> bool {
     let mut attr = unsafe { mem::zeroed::<bpf_attr>() };
     let u = unsafe { &mut attr.__bindgen_anon_3 };
 
@@ -806,12 +851,18 @@ pub(crate) fn is_bpf_cookie_supported() -> bool {
     u.insn_cnt = insns.len() as u32;
     u.insns = insns.as_ptr() as u64;
     u.prog_type = bpf_prog_type::BPF_PROG_TYPE_KPROBE as u32;
-
+    if let Some(v) = token_fd {
+        u.prog_token_fd = v.as_raw_fd();
+        u.prog_flags |= BPF_F_TOKEN_FD;
+    }
     bpf_prog_load(&mut attr).is_ok()
 }
 
 /// Tests whether CpuMap, DevMap and DevMapHash support program ids
-pub(crate) fn is_prog_id_supported(map_type: bpf_map_type) -> bool {
+pub(crate) fn is_prog_id_supported(
+    map_type: bpf_map_type,
+    token_fd: Option<BorrowedFd<'_>>,
+) -> bool {
     assert_matches!(
         map_type,
         bpf_map_type::BPF_MAP_TYPE_CPUMAP
@@ -828,22 +879,27 @@ pub(crate) fn is_prog_id_supported(map_type: bpf_map_type) -> bool {
     u.max_entries = 1;
     u.map_flags = 0;
 
+    if let Some(v) = token_fd {
+        u.map_token_fd = v.as_raw_fd();
+        u.map_flags |= BPF_F_TOKEN_FD;
+    }
+
     // SAFETY: BPF_MAP_CREATE returns a new file descriptor.
     let fd = unsafe { fd_sys_bpf(bpf_cmd::BPF_MAP_CREATE, &mut attr) };
     let fd = fd.map(crate::MockableFd::from_fd);
     fd.is_ok()
 }
 
-pub(crate) fn is_btf_supported() -> bool {
+pub(crate) fn is_btf_supported(token_fd: Option<BorrowedFd<'_>>) -> bool {
     let mut btf = Btf::new();
     let name_offset = btf.add_string("int");
     let int_type = BtfType::Int(Int::new(name_offset, 4, IntEncoding::Signed, 0));
     btf.add_type(int_type);
     let btf_bytes = btf.to_bytes();
-    bpf_load_btf(btf_bytes.as_slice(), &mut [], Default::default()).is_ok()
+    bpf_load_btf(btf_bytes.as_slice(), &mut [], Default::default(), token_fd).is_ok()
 }
 
-pub(crate) fn is_btf_func_supported() -> bool {
+pub(crate) fn is_btf_func_supported(token_fd: Option<BorrowedFd<'_>>) -> bool {
     let mut btf = Btf::new();
     let name_offset = btf.add_string("int");
     let int_type = BtfType::Int(Int::new(name_offset, 4, IntEncoding::Signed, 0));
@@ -870,10 +926,10 @@ pub(crate) fn is_btf_func_supported() -> bool {
 
     let btf_bytes = btf.to_bytes();
 
-    bpf_load_btf(btf_bytes.as_slice(), &mut [], Default::default()).is_ok()
+    bpf_load_btf(btf_bytes.as_slice(), &mut [], Default::default(), token_fd).is_ok()
 }
 
-pub(crate) fn is_btf_func_global_supported() -> bool {
+pub(crate) fn is_btf_func_global_supported(token_fd: Option<BorrowedFd<'_>>) -> bool {
     let mut btf = Btf::new();
     let name_offset = btf.add_string("int");
     let int_type = BtfType::Int(Int::new(name_offset, 4, IntEncoding::Signed, 0));
@@ -900,10 +956,10 @@ pub(crate) fn is_btf_func_global_supported() -> bool {
 
     let btf_bytes = btf.to_bytes();
 
-    bpf_load_btf(btf_bytes.as_slice(), &mut [], Default::default()).is_ok()
+    bpf_load_btf(btf_bytes.as_slice(), &mut [], Default::default(), token_fd).is_ok()
 }
 
-pub(crate) fn is_btf_datasec_supported() -> bool {
+pub(crate) fn is_btf_datasec_supported(token_fd: Option<BorrowedFd<'_>>) -> bool {
     let mut btf = Btf::new();
     let name_offset = btf.add_string("int");
     let int_type = BtfType::Int(Int::new(name_offset, 4, IntEncoding::Signed, 0));
@@ -924,10 +980,10 @@ pub(crate) fn is_btf_datasec_supported() -> bool {
 
     let btf_bytes = btf.to_bytes();
 
-    bpf_load_btf(btf_bytes.as_slice(), &mut [], Default::default()).is_ok()
+    bpf_load_btf(btf_bytes.as_slice(), &mut [], Default::default(), token_fd).is_ok()
 }
 
-pub(crate) fn is_btf_enum64_supported() -> bool {
+pub(crate) fn is_btf_enum64_supported(token_fd: Option<BorrowedFd<'_>>) -> bool {
     let mut btf = Btf::new();
     let name_offset = btf.add_string("enum64");
 
@@ -940,10 +996,10 @@ pub(crate) fn is_btf_enum64_supported() -> bool {
 
     let btf_bytes = btf.to_bytes();
 
-    bpf_load_btf(btf_bytes.as_slice(), &mut [], Default::default()).is_ok()
+    bpf_load_btf(btf_bytes.as_slice(), &mut [], Default::default(), token_fd).is_ok()
 }
 
-pub(crate) fn is_btf_float_supported() -> bool {
+pub(crate) fn is_btf_float_supported(token_fd: Option<BorrowedFd<'_>>) -> bool {
     let mut btf = Btf::new();
     let name_offset = btf.add_string("float");
     let float_type = BtfType::Float(Float::new(name_offset, 16));
@@ -951,10 +1007,10 @@ pub(crate) fn is_btf_float_supported() -> bool {
 
     let btf_bytes = btf.to_bytes();
 
-    bpf_load_btf(btf_bytes.as_slice(), &mut [], Default::default()).is_ok()
+    bpf_load_btf(btf_bytes.as_slice(), &mut [], Default::default(), token_fd).is_ok()
 }
 
-pub(crate) fn is_btf_decl_tag_supported() -> bool {
+pub(crate) fn is_btf_decl_tag_supported(token_fd: Option<BorrowedFd<'_>>) -> bool {
     let mut btf = Btf::new();
     let name_offset = btf.add_string("int");
     let int_type = BtfType::Int(Int::new(name_offset, 4, IntEncoding::Signed, 0));
@@ -970,10 +1026,10 @@ pub(crate) fn is_btf_decl_tag_supported() -> bool {
 
     let btf_bytes = btf.to_bytes();
 
-    bpf_load_btf(btf_bytes.as_slice(), &mut [], Default::default()).is_ok()
+    bpf_load_btf(btf_bytes.as_slice(), &mut [], Default::default(), token_fd).is_ok()
 }
 
-pub(crate) fn is_btf_type_tag_supported() -> bool {
+pub(crate) fn is_btf_type_tag_supported(token_fd: Option<BorrowedFd<'_>>) -> bool {
     let mut btf = Btf::new();
 
     let int_type = BtfType::Int(Int::new(0, 4, IntEncoding::Signed, 0));
@@ -987,7 +1043,7 @@ pub(crate) fn is_btf_type_tag_supported() -> bool {
 
     let btf_bytes = btf.to_bytes();
 
-    bpf_load_btf(btf_bytes.as_slice(), &mut [], Default::default()).is_ok()
+    bpf_load_btf(btf_bytes.as_slice(), &mut [], Default::default(), token_fd).is_ok()
 }
 
 fn bpf_prog_load(attr: &mut bpf_attr) -> SysResult<OwnedFd> {
@@ -999,6 +1055,274 @@ fn sys_bpf(cmd: bpf_cmd, attr: &mut bpf_attr) -> SysResult<i64> {
     syscall(Syscall::Ebpf { cmd, attr })
 }
 
+/// The permissions of an eBPF filesystem.
+///
+/// By default, all commands, programs, maps and attach types are allowed.
+/// You can restrict the permissions by adding specific commands, programs, maps
+/// and attach types.
+#[derive(Debug, Clone)]
+pub struct FilesystemPermissions {
+    cmds: CString,
+    progs: CString,
+    maps: CString,
+    attach: CString,
+}
+
+impl Default for FilesystemPermissions {
+    fn default() -> Self {
+        Self {
+            cmds: CString::new("any").unwrap(),
+            progs: CString::new("any").unwrap(),
+            maps: CString::new("any").unwrap(),
+            attach: CString::new("any").unwrap(),
+        }
+    }
+}
+
+impl FilesystemPermissions {
+    /// Create a new set of permissions using a `[`FilesystemPermissionsBuilder`]`.
+    pub fn builder() -> FilesystemPermissionsBuilder {
+        FilesystemPermissionsBuilder::default()
+    }
+}
+
+/// Builder for [`FilesystemPermissions`].
+#[derive(Debug, Clone, Default)]
+pub struct FilesystemPermissionsBuilder {
+    cmds: Vec<BpfCommand>,
+    progs: Vec<BpfProgType>,
+    maps: Vec<BpfMapType>,
+    attach: Vec<BpfAttachType>,
+}
+
+impl FilesystemPermissionsBuilder {
+    /// Add a command to the permissions.
+    pub fn cmd(&mut self, cmd: BpfCommand) -> &mut Self {
+        self.cmds.push(cmd);
+        self
+    }
+
+    /// Add a program type to the permissions.
+    pub fn prog(&mut self, prog: BpfProgType) -> &mut Self {
+        self.progs.push(prog);
+        self
+    }
+
+    /// Add a map type to the permissions.
+    pub fn map(&mut self, map: BpfMapType) -> &mut Self {
+        self.maps.push(map);
+        self
+    }
+
+    /// Add an attach type to the permissions.
+    pub fn attach(&mut self, attach: BpfAttachType) -> &mut Self {
+        self.attach.push(attach);
+        self
+    }
+
+    /// Build the permissions.
+    pub fn build(&mut self) -> FilesystemPermissions {
+        let cmds_mask = if self.cmds.is_empty() {
+            CString::new("any").unwrap()
+        } else {
+            CString::new(format!(
+                "0x{:#x}",
+                self.cmds
+                    .iter()
+                    .map(|cmd| 1u64 << bpf_cmd::from(*cmd) as u64)
+                    .fold(0, |acc, cmd| acc | cmd)
+            ))
+            .unwrap()
+        };
+        let progs_mask = if self.progs.is_empty() {
+            CString::new("any").unwrap()
+        } else {
+            CString::new(format!(
+                "0x{:#x}",
+                self.progs
+                    .iter()
+                    .map(|prog| 1u64 << bpf_prog_type::from(*prog) as u64)
+                    .fold(0, |acc, prog| acc | prog)
+            ))
+            .unwrap()
+        };
+        let maps_mask = if self.maps.is_empty() {
+            CString::new("any").unwrap()
+        } else {
+            CString::new(format!(
+                "0x{:#x}",
+                self.maps
+                    .iter()
+                    .map(|map| 1u64 << bpf_map_type::from(*map) as u64)
+                    .fold(0, |acc, map| acc | map)
+            ))
+            .unwrap()
+        };
+        let attach_mask = if self.attach.is_empty() {
+            CString::new("any").unwrap()
+        } else {
+            CString::new(format!(
+                "0x{:#x}",
+                self.attach
+                    .iter()
+                    .map(|attach| 1u64 << bpf_attach_type::from(*attach) as u64)
+                    .fold(0, |acc, attach| acc | attach)
+            ))
+            .unwrap()
+        };
+        FilesystemPermissions {
+            cmds: cmds_mask,
+            progs: progs_mask,
+            maps: maps_mask,
+            attach: attach_mask,
+        }
+    }
+}
+
+const FSCONFIG_SET_STRING: u32 = 1;
+const FSCONFIG_CMD_CREATE: u32 = 6;
+
+/// Create a new eBPF filesystem at the given path.
+///
+/// # Example
+///
+/// ```no_run
+/// use aya_obj::{cmd::BpfCommand, maps::BpfMapType, programs::BpfProgType};
+/// use aya::{FilesystemPermissionsBuilder, create_bpf_filesystem};
+///
+/// let path = "/sys/fs/bpf";
+/// let perms = FilesystemPermissionsBuilder::default()
+///     .cmd(BpfCommand::ProgLoad)
+///     .prog(BpfProgType::SocketFilter)
+///     .map(BpfMapType::Array)
+///     .build();
+/// create_bpf_filesystem(path, perms).unwrap();
+/// ```
+pub fn create_bpf_filesystem<P: AsRef<Path>>(
+    path: P,
+    perms: FilesystemPermissions,
+) -> Result<(), SyscallError> {
+    let path = path.as_ref();
+
+    let path_string = CString::new(path.as_os_str().as_bytes()).map_err(|_| SyscallError {
+        call: "create_bpffs",
+        io_error: io::Error::new(io::ErrorKind::InvalidInput, "path contains a null byte"),
+    })?;
+
+    let fd = syscall(Syscall::FsOpen {
+        fsname: c"bpf".as_ptr(),
+        flags: 0,
+    })
+    .map_err(|(_, io_error)| SyscallError {
+        call: "fsopen",
+        io_error,
+    })?;
+
+    let fd = fd.try_into().map_err(|_| SyscallError {
+        call: "fsopen",
+        io_error: io::Error::new(
+            io::ErrorKind::InvalidData,
+            format!("create_bpffs: invalid fd returned: {fd}"),
+        ),
+    })?;
+
+    let fd = unsafe { OwnedFd::from_raw_fd(fd) };
+
+    let _ = syscall(Syscall::FsConfig {
+        fd: fd.as_fd(),
+        cmd: FSCONFIG_SET_STRING,
+        key: c"delegate_cmds".as_ptr(),
+        value: perms.cmds.as_ptr() as *const c_void,
+        aux: 0,
+    })
+    .map_err(|(_, io_error)| SyscallError {
+        call: "fsconfig",
+        io_error,
+    })?;
+
+    let _ = syscall(Syscall::FsConfig {
+        fd: fd.as_fd(),
+        cmd: FSCONFIG_SET_STRING,
+        key: c"delegate_maps".as_ptr(),
+        value: perms.maps.as_ptr() as *const c_void,
+        aux: 0,
+    })
+    .map_err(|(_, io_error)| SyscallError {
+        call: "fsconfig",
+        io_error,
+    })?;
+
+    let _ = syscall(Syscall::FsConfig {
+        fd: fd.as_fd(),
+        cmd: FSCONFIG_SET_STRING,
+        key: c"delegate_progs".as_ptr(),
+        value: perms.progs.as_ptr() as *const c_void,
+        aux: 0,
+    })
+    .map_err(|(_, io_error)| SyscallError {
+        call: "fsconfig",
+        io_error,
+    })?;
+
+    let _ = syscall(Syscall::FsConfig {
+        fd: fd.as_fd(),
+        cmd: FSCONFIG_SET_STRING,
+        key: c"delegate_attachs".as_ptr(),
+        value: perms.attach.as_ptr() as *const c_void,
+        aux: 0,
+    })
+    .map_err(|(_, io_error)| SyscallError {
+        call: "fsconfig",
+        io_error,
+    })?;
+
+    let _ = syscall(Syscall::FsConfig {
+        fd: fd.as_fd(),
+        cmd: FSCONFIG_CMD_CREATE,
+        key: null(),
+        value: null(),
+        aux: 0,
+    })
+    .map_err(|(_, io_error)| SyscallError {
+        call: "fsconfig",
+        io_error,
+    })?;
+
+    let mount_fd = syscall(Syscall::FsMount {
+        fd: fd.as_fd(),
+        flags: 0,
+        mount_attrs: 0,
+    })
+    .map_err(|(_, io_error)| SyscallError {
+        call: "fsmount",
+        io_error,
+    })?;
+
+    let mount_fd = mount_fd.try_into().map_err(|_| SyscallError {
+        call: "fsopen",
+        io_error: io::Error::new(
+            io::ErrorKind::InvalidData,
+            format!("create_bpffs: invalid fd returned: {mount_fd}"),
+        ),
+    })?;
+
+    let mount_fd = unsafe { OwnedFd::from_raw_fd(mount_fd) };
+
+    let _ = syscall(Syscall::MoveMount {
+        from_dirfd: mount_fd.as_fd(),
+        from_pathname: c"".as_ptr(),
+        to_dirfd: unsafe { BorrowedFd::borrow_raw(AT_FDCWD) },
+        to_pathname: path_string.as_ptr(),
+        flags: MOVE_MOUNT_F_EMPTY_PATH,
+    })
+    .map_err(|(_, io_error)| SyscallError {
+        call: "fsmount",
+        io_error,
+    })?;
+
+    Ok(())
+}
+
 fn bpf_obj_get_next_id(
     id: u32,
     cmd: bpf_cmd,
@@ -1105,7 +1429,7 @@ mod tests {
             } => Err((-1, io::Error::from_raw_os_error(EBADF))),
             _ => Ok(crate::MockableFd::mock_signed_fd().into()),
         });
-        let supported = is_perf_link_supported();
+        let supported = is_perf_link_supported(None);
         assert!(supported);
 
         override_syscall(|call| match call {
@@ -1115,7 +1439,7 @@ mod tests {
             } => Err((-1, io::Error::from_raw_os_error(EINVAL))),
             _ => Ok(crate::MockableFd::mock_signed_fd().into()),
         });
-        let supported = is_perf_link_supported();
+        let supported = is_perf_link_supported(None);
         assert!(!supported);
     }
 
@@ -1124,15 +1448,15 @@ mod tests {
         override_syscall(|_call| Ok(crate::MockableFd::mock_signed_fd().into()));
 
         // Ensure that the three map types we can check are accepted
-        let supported = is_prog_id_supported(bpf_map_type::BPF_MAP_TYPE_CPUMAP);
+        let supported = is_prog_id_supported(bpf_map_type::BPF_MAP_TYPE_CPUMAP, None);
         assert!(supported);
-        let supported = is_prog_id_supported(bpf_map_type::BPF_MAP_TYPE_DEVMAP);
+        let supported = is_prog_id_supported(bpf_map_type::BPF_MAP_TYPE_DEVMAP, None);
         assert!(supported);
-        let supported = is_prog_id_supported(bpf_map_type::BPF_MAP_TYPE_DEVMAP_HASH);
+        let supported = is_prog_id_supported(bpf_map_type::BPF_MAP_TYPE_DEVMAP_HASH, None);
         assert!(supported);
 
         override_syscall(|_call| Err((-1, io::Error::from_raw_os_error(EINVAL))));
-        let supported = is_prog_id_supported(bpf_map_type::BPF_MAP_TYPE_CPUMAP);
+        let supported = is_prog_id_supported(bpf_map_type::BPF_MAP_TYPE_CPUMAP, None);
         assert!(!supported);
     }
 
@@ -1140,6 +1464,27 @@ mod tests {
     #[should_panic = "assertion failed: `BPF_MAP_TYPE_HASH` does not match `bpf_map_type::BPF_MAP_TYPE_CPUMAP | bpf_map_type::BPF_MAP_TYPE_DEVMAP |
 bpf_map_type::BPF_MAP_TYPE_DEVMAP_HASH`"]
     fn test_prog_id_supported_reject_types() {
-        is_prog_id_supported(bpf_map_type::BPF_MAP_TYPE_HASH);
+        is_prog_id_supported(bpf_map_type::BPF_MAP_TYPE_HASH, None);
+    }
+
+    #[test]
+    fn test_bpf_create_filesystem() {
+        override_syscall(|call| match call {
+            Syscall::FsOpen { .. } => Ok(crate::MockableFd::mock_signed_fd().into()),
+            Syscall::FsConfig { .. } => Ok(0),
+            Syscall::FsMount {
+                flags, mount_attrs, ..
+            } => {
+                assert_eq!(flags, 0);
+                assert_eq!(mount_attrs, 0);
+                Ok(crate::MockableFd::mock_signed_fd().into())
+            }
+            Syscall::MoveMount { flags, .. } => {
+                assert_eq!(flags, MOVE_MOUNT_F_EMPTY_PATH);
+                Ok(0)
+            }
+            _ => Ok(crate::MockableFd::mock_signed_fd().into()),
+        });
+        create_bpf_filesystem("/foo", FilesystemPermissions::default()).unwrap();
     }
 }
diff --git a/aya/src/sys/mod.rs b/aya/src/sys/mod.rs
index ab486195..a0dd7076 100644
--- a/aya/src/sys/mod.rs
+++ b/aya/src/sys/mod.rs
@@ -6,15 +6,19 @@ mod perf_event;
 mod fake;
 
 use std::{
-    ffi::{c_int, c_void},
+    ffi::{c_int, c_uint, c_void},
     io, mem,
     os::fd::{AsRawFd as _, BorrowedFd},
 };
 
 pub(crate) use bpf::*;
+pub use bpf::{create_bpf_filesystem, FilesystemPermissions, FilesystemPermissionsBuilder};
 #[cfg(test)]
 pub(crate) use fake::*;
-use libc::{pid_t, SYS_bpf, SYS_perf_event_open};
+use libc::{
+    c_char, pid_t, SYS_bpf, SYS_fsconfig, SYS_fsmount, SYS_fsopen, SYS_move_mount,
+    SYS_perf_event_open,
+};
 #[doc(hidden)]
 pub use netlink::netlink_set_link_up;
 pub(crate) use netlink::*;
@@ -42,6 +46,29 @@ pub(crate) enum Syscall<'a> {
         request: c_int,
         arg: c_int,
     },
+    FsOpen {
+        fsname: *const c_char,
+        flags: u32,
+    },
+    FsMount {
+        fd: BorrowedFd<'a>,
+        flags: c_uint,
+        mount_attrs: c_uint,
+    },
+    FsConfig {
+        fd: BorrowedFd<'a>,
+        cmd: c_uint,
+        key: *const c_char,
+        value: *const c_void,
+        aux: c_int,
+    },
+    MoveMount {
+        from_dirfd: BorrowedFd<'a>,
+        from_pathname: *const c_char,
+        to_dirfd: BorrowedFd<'a>,
+        to_pathname: *const c_char,
+        flags: u32,
+    },
 }
 
 #[derive(Debug, Error)]
@@ -82,6 +109,49 @@ impl std::fmt::Debug for Syscall<'_> {
                 .field("request", request)
                 .field("arg", arg)
                 .finish(),
+            Self::FsOpen { fsname, flags } => f
+                .debug_struct("Syscall::FsOpen")
+                .field("fsname", fsname)
+                .field("flags", flags)
+                .finish(),
+            Self::FsMount {
+                fd,
+                flags,
+                mount_attrs,
+            } => f
+                .debug_struct("Syscall::FsMount")
+                .field("fd", fd)
+                .field("flags", flags)
+                .field("mount_attrs", mount_attrs)
+                .finish(),
+            Self::FsConfig {
+                fd,
+                cmd,
+                key,
+                value,
+                aux,
+            } => f
+                .debug_struct("Syscall::FsConfig")
+                .field("fd", fd)
+                .field("cmd", cmd)
+                .field("key", key)
+                .field("value", value)
+                .field("aux", aux)
+                .finish(),
+            Self::MoveMount {
+                from_dirfd,
+                from_pathname,
+                to_dirfd,
+                to_pathname,
+                flags,
+            } => f
+                .debug_struct("Syscall::MoveMount")
+                .field("from_dirfd", from_dirfd)
+                .field("from_pathname", from_pathname)
+                .field("to_dirfd", to_dirfd)
+                .field("to_pathname", to_pathname)
+                .field("flags", flags)
+                .finish(),
         }
     }
 }
@@ -110,6 +180,33 @@ fn syscall(call: Syscall<'_>) -> SysResult<i64> {
                     #[allow(clippy::useless_conversion)]
                     ret.into()
                 }
+                Syscall::FsOpen { fsname, flags } => libc::syscall(SYS_fsopen, fsname, flags),
+                Syscall::FsMount {
+                    fd,
+                    flags,
+                    mount_attrs,
+                } => libc::syscall(SYS_fsmount, fd.as_raw_fd(), flags, mount_attrs),
+                Syscall::FsConfig {
+                    fd,
+                    cmd,
+                    key,
+                    value,
+                    aux,
+                } => libc::syscall(SYS_fsconfig, fd.as_raw_fd(), cmd, key, value, aux),
+                Syscall::MoveMount {
+                    from_dirfd,
+                    from_pathname,
+                    to_dirfd,
+                    to_pathname,
+                    flags,
+                } => libc::syscall(
+                    SYS_move_mount,
+                    from_dirfd.as_raw_fd(),
+                    from_pathname,
+                    to_dirfd.as_raw_fd(),
+                    to_pathname,
+                    flags,
+                ),
             }
         };
 
diff --git a/test/integration-test/src/tests/load.rs b/test/integration-test/src/tests/load.rs
index 8ff13757..bce623e5 100644
--- a/test/integration-test/src/tests/load.rs
+++ b/test/integration-test/src/tests/load.rs
@@ -7,6 +7,7 @@ use std::{
 };
 
 use aya::{
+    features,
     maps::Array,
     programs::{
         links::{FdLink, PinnedLink},
@@ -385,6 +386,7 @@ fn pin_link() {
 
 #[test]
 fn pin_lifecycle() {
+    let features = features(None);
     let kernel_version = KernelVersion::current().unwrap();
     if kernel_version < KernelVersion::new(5, 18, 0) {
         eprintln!("skipping test on kernel {kernel_version:?}, support for BPF_F_XDP_HAS_FRAGS was added in 5.18.0; see https://github.com/torvalds/linux/commit/c2f2cdb");
@@ -404,7 +406,13 @@ fn pin_lifecycle() {
 
     // 2. Load program from bpffs but don't attach it
     {
-        let _ = Xdp::from_pin("/sys/fs/bpf/aya-xdp-test-prog", XdpAttachType::Interface).unwrap();
+        let _ = Xdp::from_pin(
+            "/sys/fs/bpf/aya-xdp-test-prog",
+            XdpAttachType::Interface,
+            None,
+            features.clone(),
+        )
+        .unwrap();
     }
 
     // should still be loaded since prog was pinned
@@ -412,8 +420,13 @@ fn pin_lifecycle() {
 
     // 3. Load program from bpffs and attach
     {
-        let mut prog =
-            Xdp::from_pin("/sys/fs/bpf/aya-xdp-test-prog", XdpAttachType::Interface).unwrap();
+        let mut prog = Xdp::from_pin(
+            "/sys/fs/bpf/aya-xdp-test-prog",
+            XdpAttachType::Interface,
+            None,
+            features.clone(),
+        )
+        .unwrap();
         let link_id = prog.attach("lo", XdpFlags::default()).unwrap();
         let link = prog.take_link(link_id).unwrap();
         let fd_link: FdLink = link.try_into().unwrap();
@@ -446,6 +459,7 @@ fn pin_lifecycle() {
 
 #[test]
 fn pin_lifecycle_tracepoint() {
+    let features = features(None);
     // 1. Load Program and Pin
     {
         let mut bpf = Ebpf::load(crate::TEST).unwrap();
@@ -463,7 +477,12 @@ fn pin_lifecycle_tracepoint() {
 
     // 2. Load program from bpffs but don't attach it
     {
-        let _ = TracePoint::from_pin("/sys/fs/bpf/aya-tracepoint-test-prog").unwrap();
+        let _ = TracePoint::from_pin(
+            "/sys/fs/bpf/aya-tracepoint-test-prog",
+            None,
+            features.clone(),
+        )
+        .unwrap();
     }
 
     // should still be loaded since prog was pinned
@@ -471,7 +490,8 @@ fn pin_lifecycle_tracepoint() {
 
     // 3. Load program from bpffs and attach
     {
-        let mut prog = TracePoint::from_pin("/sys/fs/bpf/aya-tracepoint-test-prog").unwrap();
+        let mut prog =
+            TracePoint::from_pin("/sys/fs/bpf/aya-tracepoint-test-prog", None, features).unwrap();
         let link_id = prog.attach("syscalls", "sys_enter_kill").unwrap();
         let link = prog.take_link(link_id).unwrap();
         let fd_link: FdLink = link.try_into().unwrap();
@@ -500,6 +520,7 @@ fn pin_lifecycle_tracepoint() {
 
 #[test]
 fn pin_lifecycle_kprobe() {
+    let features = features(None);
     // 1. Load Program and Pin
     {
         let mut bpf = Ebpf::load(crate::TEST).unwrap();
@@ -516,6 +537,8 @@ fn pin_lifecycle_kprobe() {
         let _ = KProbe::from_pin(
             "/sys/fs/bpf/aya-kprobe-test-prog",
             aya::programs::ProbeKind::KProbe,
+            None,
+            features.clone(),
         )
         .unwrap();
     }
@@ -528,6 +551,8 @@ fn pin_lifecycle_kprobe() {
         let mut prog = KProbe::from_pin(
             "/sys/fs/bpf/aya-kprobe-test-prog",
             aya::programs::ProbeKind::KProbe,
+            None,
+            features.clone(),
         )
         .unwrap();
         let link_id = prog.attach("try_to_wake_up", 0).unwrap();
@@ -564,6 +589,7 @@ extern "C" fn uprobe_function() {
 
 #[test]
 fn pin_lifecycle_uprobe() {
+    let features = features(None);
     const FIRST_PIN_PATH: &str = "/sys/fs/bpf/aya-uprobe-test-prog-1";
     const SECOND_PIN_PATH: &str = "/sys/fs/bpf/aya-uprobe-test-prog-2";
 
@@ -580,7 +606,13 @@ fn pin_lifecycle_uprobe() {
 
     // 2. Load program from bpffs but don't attach it
     {
-        let _ = UProbe::from_pin(FIRST_PIN_PATH, aya::programs::ProbeKind::UProbe).unwrap();
+        let _ = UProbe::from_pin(
+            FIRST_PIN_PATH,
+            aya::programs::ProbeKind::UProbe,
+            None,
+            features.clone(),
+        )
+        .unwrap();
     }
 
     // should still be loaded since prog was pinned
@@ -588,7 +620,13 @@ fn pin_lifecycle_uprobe() {
 
     // 3. Load program from bpffs and attach
     {
-        let mut prog = UProbe::from_pin(FIRST_PIN_PATH, aya::programs::ProbeKind::UProbe).unwrap();
+        let mut prog = UProbe::from_pin(
+            FIRST_PIN_PATH,
+            aya::programs::ProbeKind::UProbe,
+            None,
+            features.clone(),
+        )
+        .unwrap();
         let link_id = prog
             .attach(Some("uprobe_function"), 0, "/proc/self/exe", None)
             .unwrap();