Skip to content

Commit

Permalink
feat(aya): Add iterator program type
Browse files Browse the repository at this point in the history
BPF iterators[0] are a way to dump kernel data into user-space and an
alternative to `/proc` filesystem.

This change adds support for BPF iterators on the user-space side. It
provides a possibility to retrieve the outputs of BPF iterator programs
both from sync and async Rust code.

[0] https://docs.kernel.org/bpf/bpf_iterators.html
  • Loading branch information
vadorovsky committed Nov 18, 2024
1 parent 2791bad commit a06e293
Show file tree
Hide file tree
Showing 12 changed files with 332 additions and 8 deletions.
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ edition = "2021"
[workspace.dependencies]
anyhow = { version = "1", default-features = false }
assert_matches = { version = "1.5.0", default-features = false }
async-io = { version = "2.0", default-features = false }
async-fs = { version = "2.1", default-features = false }
async-io = { version = "2.4", default-features = false }
bindgen = { version = "0.70", default-features = false }
bitflags = { version = "2.2.1", default-features = false }
bytes = { version = "1", default-features = false }
Expand Down
5 changes: 5 additions & 0 deletions aya-obj/src/obj.rs
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,9 @@ pub enum ProgramSection {
attach_type: CgroupSockAttachType,
},
CgroupDevice,
Iter {
sleepable: bool,
},
}

impl FromStr for ProgramSection {
Expand Down Expand Up @@ -441,6 +444,8 @@ impl FromStr for ProgramSection {
"fexit.s" => FExit { sleepable: true },
"freplace" => Extension,
"sk_lookup" => SkLookup,
"iter" => Iter { sleepable: false },
"iter.s" => Iter { sleepable: true },
_ => {
return Err(ParseError::InvalidProgramSection {
section: section.to_owned(),
Expand Down
5 changes: 3 additions & 2 deletions aya/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ edition.workspace = true

[dependencies]
assert_matches = { workspace = true }
async-fs = { workspace = true, optional = true }
async-io = { workspace = true, optional = true }
aya-obj = { path = "../aya-obj", version = "^0.2.1", features = ["std"] }
bitflags = { workspace = true }
Expand All @@ -30,8 +31,8 @@ tempfile = { workspace = true }

[features]
default = []
async_tokio = ["tokio/net"]
async_std = ["dep:async-io"]
async_tokio = ["tokio/fs", "tokio/io-util", "tokio/net"]
async_std = ["dep:async-fs", "dep:async-io"]

[package.metadata.docs.rs]
all-features = true
Expand Down
17 changes: 13 additions & 4 deletions aya/src/bpf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ use crate::{
},
programs::{
BtfTracePoint, CgroupDevice, CgroupSkb, CgroupSkbAttachType, CgroupSock, CgroupSockAddr,
CgroupSockopt, CgroupSysctl, Extension, FEntry, FExit, KProbe, LircMode2, Lsm, PerfEvent,
ProbeKind, Program, ProgramData, ProgramError, RawTracePoint, SchedClassifier, SkLookup,
SkMsg, SkSkb, SkSkbKind, SockOps, SocketFilter, TracePoint, UProbe, Xdp,
CgroupSockopt, CgroupSysctl, Extension, FEntry, FExit, Iter, KProbe, LircMode2, Lsm,
PerfEvent, ProbeKind, Program, ProgramData, ProgramError, RawTracePoint, SchedClassifier,
SkLookup, SkMsg, SkSkb, SkSkbKind, SockOps, SocketFilter, TracePoint, UProbe, Xdp,
},
sys::{
bpf_load_btf, is_bpf_cookie_supported, is_bpf_global_data_supported,
Expand Down Expand Up @@ -410,7 +410,8 @@ impl<'a> EbpfLoader<'a> {
| ProgramSection::FEntry { sleepable: _ }
| ProgramSection::FExit { sleepable: _ }
| ProgramSection::Lsm { sleepable: _ }
| ProgramSection::BtfTracePoint => {
| ProgramSection::BtfTracePoint
| ProgramSection::Iter { sleepable: _ } => {
return Err(EbpfError::BtfError(err))
}
ProgramSection::KRetProbe
Expand Down Expand Up @@ -688,6 +689,14 @@ impl<'a> EbpfLoader<'a> {
ProgramSection::CgroupDevice => Program::CgroupDevice(CgroupDevice {
data: ProgramData::new(prog_name, obj, btf_fd, *verifier_log_level),
}),
ProgramSection::Iter { sleepable } => {
let mut data =
ProgramData::new(prog_name, obj, btf_fd, *verifier_log_level);
if *sleepable {
data.flags = BPF_F_SLEEPABLE;
}
Program::Iter(Iter { data })
}
}
};
(name, program)
Expand Down
208 changes: 208 additions & 0 deletions aya/src/programs/iter.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
//! Iterators.
use std::{
mem::ManuallyDrop,
os::fd::{AsFd, AsRawFd, BorrowedFd, FromRawFd},
};

use crate::{
generated::{bpf_attach_type::BPF_TRACE_ITER, bpf_prog_type},
obj::{
btf::{Btf, BtfKind},
generated::bpf_link_type,
},
programs::{
define_link_wrapper, load_program, FdLink, LinkError, PerfLinkIdInner, PerfLinkInner,
ProgramData, ProgramError,
},
sys::{bpf_create_iter, bpf_link_create, bpf_link_get_info_by_fd, LinkTarget, SyscallError},
};

/// An eBPF iterator which allows to dump into user space.
///
/// It can be seen as an alternative to `/proc` filesystem, which offers more
/// flexibility about what information should be retrieved and how it should be
/// formatted.
///
/// # Minimum kernel version
///
/// The minimum kernel version required to use this feature is 5.8.
///
/// ```no_run
/// use aya::{programs::Iter, BtfError, Btf, Ebpf};
///
/// let program: &mut Iter = bpf.program_mut("iter_prog").unwrap().try_into()?;
/// program.load()?;
/// program.attach()?;
/// # Ok::<(), LsmError>(())
/// ```
#[derive(Debug)]
pub struct Iter {
pub(crate) data: ProgramData<IterLink>,
}

impl Iter {
/// Loads the program inside the kernel.
pub fn load(&mut self, iter_type: &str, btf: &Btf) -> Result<(), ProgramError> {
self.data.expected_attach_type = Some(BPF_TRACE_ITER);
let type_name = format!("bpf_iter_{iter_type}");
self.data.attach_btf_id =
Some(btf.id_by_type_name_kind(type_name.as_str(), BtfKind::Func)?);
load_program(bpf_prog_type::BPF_PROG_TYPE_TRACING, &mut self.data)
}

/// Attaches the program.
///
/// The returned value can be used to detach, see [Iter::detach].
pub fn attach(&mut self) -> Result<IterLinkId, ProgramError> {
let prog_fd = self.fd()?;
let prog_fd = prog_fd.as_fd();
let link_fd = bpf_link_create(prog_fd, LinkTarget::BtfId, BPF_TRACE_ITER, None, 0, None)
.map_err(|(_, io_error)| SyscallError {
call: "bpf_link_create",
io_error,
})?;

self.data
.links
.insert(IterLink::new(PerfLinkInner::FdLink(FdLink::new(link_fd))))
}

/// Detaches the program.
///
/// See [Iter::attach].
pub fn detach(&mut self, link_id: IterLinkId) -> Result<(), ProgramError> {
self.data.links.remove(link_id)
}

/// Takes ownership of the link referenced by the provided link_id.
///
/// The link will be detached on `Drop` and the caller is now responsible
/// for managing its lifetime.
pub fn take_link(&mut self, link_id: IterLinkId) -> Result<IterLink, ProgramError> {
self.data.take_link(link_id)
}
}

/// An iterator descriptor.
#[derive(Debug)]
pub struct IterFd {
fd: crate::MockableFd,
}

impl AsFd for IterFd {
fn as_fd(&self) -> BorrowedFd<'_> {
let Self { fd } = self;
fd.as_fd()
}
}

impl TryFrom<IterLink> for FdLink {
type Error = LinkError;

fn try_from(value: IterLink) -> Result<Self, Self::Error> {
if let PerfLinkInner::FdLink(fd) = value.into_inner() {
Ok(fd)
} else {
Err(LinkError::InvalidLink)
}
}
}

impl TryFrom<FdLink> for IterLink {
type Error = LinkError;

fn try_from(fd_link: FdLink) -> Result<Self, Self::Error> {
let info = bpf_link_get_info_by_fd(fd_link.fd.as_fd())?;
if info.type_ == (bpf_link_type::BPF_LINK_TYPE_ITER as u32) {
return Ok(Self::new(PerfLinkInner::FdLink(fd_link)));
}
Err(LinkError::InvalidLink)
}
}

define_link_wrapper!(
/// The link used by [Iter] programs.
IterLink,
/// The type returned by [Iter::attach]. Can be passed to [Iter::detach].
IterLinkId,
PerfLinkInner,
PerfLinkIdInner
);

impl IterLink {
/// Converts [IterLink] into a [std::fs::File]. That file can be used to
/// retrieve the outputs of the iterator program.
pub fn into_file(self) -> Result<std::fs::File, LinkError> {
if let PerfLinkInner::FdLink(fd) = self.into_inner() {
let fd = bpf_create_iter(fd.fd.as_fd()).map_err(|(_, error)| {
LinkError::SyscallError(SyscallError {
call: "bpf_iter_create",
io_error: error,
})
})?;
// We don't want to drop the descriptor when going out of the scope
// of this method. The lifecycle of the descriptor is going to be
// managed by the `File` created below.
let fd = ManuallyDrop::new(fd);
// SAFETY: We are sure that the file descriptor is valid. This
// `File` takes responsibility of closing it.
let file = unsafe { std::fs::File::from_raw_fd(fd.as_raw_fd()) };
Ok(file)
} else {
Err(LinkError::InvalidLink)
}
}

/// Converts [IterLink] into an [async_fd::File]. That file can be used to
/// retrieve the outputs of the iterator program.
#[cfg(feature = "async_std")]
pub fn into_async_io_file(self) -> Result<async_fs::File, LinkError> {
if let PerfLinkInner::FdLink(fd) = self.into_inner() {
let fd = bpf_create_iter(fd.fd.as_fd()).map_err(|(_, error)| {
LinkError::SyscallError(SyscallError {
call: "bpf_iter_create",
io_error: error,
})
})?;
// We don't want to drop the descriptor when going out of the scope
// of this method. The lifecycle of the descriptor is going to be
// managed by the `File` created below.
let fd = ManuallyDrop::new(fd);
// SAFETY: We are sure that the file descriptor is valid. This
// `File` takes responsibility of closing it.
//
// NOTE: Unfortunately, there is no `async_fs::File::from_raw_fd`
// method, so we have to work around that with creating
// `std::fs::File` and then converting it into `async_fs::File`.
let file = unsafe { std::fs::File::from_raw_fd(fd.as_raw_fd()) };
let file: async_fs::File = file.into();
Ok(file)
} else {
Err(LinkError::InvalidLink)
}
}

/// Converts [IterLink] into a [tokio::fs::File]. That file can be used to
/// retrieve the outputs of the iterator program.
#[cfg(feature = "async_tokio")]
pub fn into_tokio_file(self) -> Result<tokio::fs::File, LinkError> {
if let PerfLinkInner::FdLink(fd) = self.into_inner() {
let fd = bpf_create_iter(fd.fd.as_fd()).map_err(|(_, error)| {
LinkError::SyscallError(SyscallError {
call: "bpf_iter_create",
io_error: error,
})
})?;
// We don't want to drop the descriptor when going out of the scope
// of this method. The lifecycle of the descriptor is going to be
// managed by the `File` created below.
let fd = ManuallyDrop::new(fd);
// SAFETY: We are sure that the file descriptor is valid. This
// `File` takes responsibility of closing it.
let file = unsafe { tokio::fs::File::from_raw_fd(fd.as_raw_fd()) };
Ok(file)
} else {
Err(LinkError::InvalidLink)
}
}
}
Loading

0 comments on commit a06e293

Please sign in to comment.