mirror of https://github.com/aya-rs/aya
feat(aya): Add iterator program type
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.htmlreviewable/pr1088/r1
parent
e423fce58f
commit
88d20a0c99
@ -0,0 +1,225 @@
|
||||
//! 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.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ## Non-async Rust
|
||||
///
|
||||
/// ```no_run
|
||||
/// use std::io::{BufRead, BufReader};
|
||||
/// use aya::{programs::{Iter, ProgramError}, BtfError, Btf, Ebpf};
|
||||
/// # let mut ebpf = Ebpf::load_file("ebpf_programs.o")?;
|
||||
///
|
||||
/// let btf = Btf::from_sys_fs()?;
|
||||
/// let program: &mut Iter = ebpf.program_mut("iter_prog").unwrap().try_into()?;
|
||||
/// program.load("task", &btf)?;
|
||||
///
|
||||
/// let link_id = program.attach()?;
|
||||
/// let link = program.take_link(link_id)?;
|
||||
/// let file = link.into_file()?;
|
||||
/// let reader = BufReader::new(file);
|
||||
///
|
||||
/// let mut lines = reader.lines();
|
||||
/// for line in lines {
|
||||
/// let line = line?;
|
||||
/// println!("{line}");
|
||||
/// }
|
||||
/// # Ok::<(), Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
#[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::Iter, 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_fs::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)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
// clang-format off
|
||||
#include <vmlinux.h>
|
||||
#include <bpf/bpf_helpers.h>
|
||||
// clang-format on
|
||||
|
||||
char _license[] SEC("license") = "GPL";
|
||||
|
||||
SEC("iter/task")
|
||||
int iter_task(struct bpf_iter__task *ctx) {
|
||||
struct seq_file *seq = ctx->meta->seq;
|
||||
struct task_struct *task = ctx->task;
|
||||
if (task == NULL) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (ctx->meta->seq_num == 0) {
|
||||
BPF_SEQ_PRINTF(seq, "tgid pid name\n");
|
||||
}
|
||||
BPF_SEQ_PRINTF(seq, "%-8d %-8d %s\n", task->tgid, task->pid, task->comm);
|
||||
|
||||
return 0;
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
use std::io::BufRead;
|
||||
|
||||
use aya::{programs::Iter, Btf, Ebpf};
|
||||
use test_log::test;
|
||||
use tokio::io::AsyncBufReadExt;
|
||||
|
||||
#[test]
|
||||
fn iter_task() {
|
||||
let mut ebpf = Ebpf::load(crate::ITER_TASK).unwrap();
|
||||
let btf = Btf::from_sys_fs().unwrap();
|
||||
let prog: &mut Iter = ebpf.program_mut("iter_task").unwrap().try_into().unwrap();
|
||||
prog.load("task", &btf).unwrap();
|
||||
|
||||
let link_id = prog.attach().unwrap();
|
||||
let link = prog.take_link(link_id).unwrap();
|
||||
let file = link.into_file().unwrap();
|
||||
let reader = std::io::BufReader::new(file);
|
||||
|
||||
let mut lines = reader.lines();
|
||||
let line_title = lines.next().unwrap().unwrap();
|
||||
let line_init = lines.next().unwrap().unwrap();
|
||||
|
||||
assert_eq!(line_title, "tgid pid name");
|
||||
assert!(line_init == "1 1 init" || line_init == "1 1 systemd");
|
||||
}
|
||||
|
||||
#[test(tokio::test)]
|
||||
async fn iter_async_task() {
|
||||
let mut ebpf = Ebpf::load(crate::ITER_TASK).unwrap();
|
||||
let btf = Btf::from_sys_fs().unwrap();
|
||||
let prog: &mut Iter = ebpf.program_mut("iter_task").unwrap().try_into().unwrap();
|
||||
prog.load("task", &btf).unwrap();
|
||||
|
||||
let link_id = prog.attach().unwrap();
|
||||
let link = prog.take_link(link_id).unwrap();
|
||||
let file = link.into_tokio_file().unwrap();
|
||||
let reader = tokio::io::BufReader::new(file);
|
||||
|
||||
let mut lines = reader.lines();
|
||||
let line_title = lines.next_line().await.unwrap().unwrap();
|
||||
let line_init = lines.next_line().await.unwrap().unwrap();
|
||||
|
||||
assert_eq!(line_title, "tgid pid name");
|
||||
assert!(line_init == "1 1 init" || line_init == "1 1 systemd");
|
||||
}
|
Loading…
Reference in New Issue