aya: split aya::programs::probe into ::kprobe and ::uprobe & add docs

pull/1/head
Alessandro Decina 4 years ago
parent d9634ae945
commit ae863bc663

@ -19,8 +19,8 @@ use crate::{
Object, ParseError, ProgramKind,
},
programs::{
probe::ProbeKind, KProbe, Program, ProgramData, ProgramError, SocketFilter, TracePoint,
UProbe, Xdp,
KProbe, ProbeKind, Program, ProgramData, ProgramError, SocketFilter, TracePoint, UProbe,
Xdp,
},
sys::bpf_map_update_elem_ptr,
util::{possible_cpus, POSSIBLE_CPUS},
@ -55,7 +55,7 @@ pub(crate) struct bpf_map_def {
pub(crate) map_flags: u32,
}
/// Used to work with eBPF programs and maps.
/// The main entry point into the library, used to work with eBPF programs and maps.
#[derive(Debug)]
pub struct Bpf {
maps: HashMap<String, MapLock>,

@ -69,6 +69,14 @@ pub enum BtfError {
MaximumTypeDepthReached { type_id: u32 },
}
/// Bpf Type Format (BTF) metadata.
///
/// BTF is a kind of debug metadata that allows eBPF programs compiled against one kernel version
/// to be loaded into different kernel versions.
///
/// Aya automatically loads BTF metadata if you use [`Bpf::load_file`](crate::Bpf::load_file). You
/// only need to explicitly use this type if you want to load BTF from a non-standard
/// location or if you are using [`Bpf::load`](crate::Bpf::load).
#[derive(Clone, Debug)]
pub struct Btf {
header: btf_header,
@ -78,10 +86,12 @@ pub struct Btf {
}
impl Btf {
/// Loads BTF metadata from `/sys/kernel/btf/vmlinux`.
pub fn from_sys_fs() -> Result<Btf, BtfError> {
Btf::parse_file("/sys/kernel/btf/vmlinux", Endianness::default())
}
/// Loads BTF metadata from the given `path`.
pub fn parse_file<P: AsRef<Path>>(path: P, endianness: Endianness) -> Result<Btf, BtfError> {
let path = path.as_ref();
Btf::parse(

@ -0,0 +1,54 @@
//! Kernel space probes.
//!
//! Kernel probes are eBPF programs that can be attached to almost any function inside the kernel.
use libc::pid_t;
use std::io;
use thiserror::Error;
use crate::{
generated::bpf_prog_type::BPF_PROG_TYPE_KPROBE,
programs::{
load_program,
probe::{attach, ProbeKind},
LinkRef, ProgramData, ProgramError,
},
};
/// A `kprobe` program.
#[derive(Debug)]
pub struct KProbe {
pub(crate) data: ProgramData,
pub(crate) kind: ProbeKind,
}
impl KProbe {
/// Loads the program inside the kernel.
///
/// See also [`Program::load`](crate::programs::Program::load).
pub fn load(&mut self) -> Result<(), ProgramError> {
load_program(BPF_PROG_TYPE_KPROBE, &mut self.data)
}
pub fn name(&self) -> String {
self.data.name.to_string()
}
pub fn attach(
&mut self,
fn_name: &str,
offset: u64,
pid: Option<pid_t>,
) -> Result<LinkRef, ProgramError> {
attach(&mut self.data, self.kind, fn_name, offset, pid)
}
}
#[derive(Debug, Error)]
pub enum KProbeError {
#[error("`{filename}`")]
FileError {
filename: String,
#[source]
io_error: io::Error,
},
}

@ -1,18 +1,68 @@
//! eBPF program types.
//!
//! eBPF programs are loaded inside the kernel and attached to one or more hook points. Whenever
//! the kernel or an application reaches those hook points, the programs are executed.
//!
//! # Loading programs
//!
//! When you call [`Bpf::load_file`] or [`Bpf::load`], all the programs present in the code are
//! parsed and can be retrieved using the [`Bpf::program`] and [`Bpf::program_mut`] methods. In
//! order to load a program, you need to get a handle to it and call the `load()` method, for
//! example:
//!
//! ```no_run
//! use aya::{Bpf, programs::KProbe};
//! use std::convert::TryInto;
//!
//! let mut bpf = Bpf::load_file("ebpf_programs.o")?;
//! // intercept_wakeups is the name of the program we want to load
//! let program: &mut KProbe = bpf.program_mut("intercept_wakeups")?.try_into()?;
//! program.load()?;
//! # Ok::<(), aya::BpfError>(())
//! ```
//!
//! # Attaching programs
//!
//! After being loaded, programs must be attached to their target hook points to be executed. The
//! eBPF platform supports many different program types, with each type providing different
//! attachment options. For example when attaching a [`KProbe`], you must provide the name of the
//! kernel function you want instrument; when loading an [`Xdp`] program, you need to specify the
//! network card name you want to hook into, and so forth.
//!
//! Currently aya supports [`KProbe`], [`UProbe`], [`SocketFilter`], [`TracePoint`] and [`Xdp`]
//! programs. To see how to attach them, see the documentation of the respective `attach()` method.
//!
//! # Interacting with programs
//!
//! eBPF programs are event-driven and execute when the hook points they are attached to are hit.
//! To communicate with user-space, programs use data structures provided by the eBPF platform,
//! which can be found in the [maps] module.
//!
//! [`Bpf::load_file`]: crate::Bpf::load_file
//! [`Bpf::load`]: crate::Bpf::load
//! [`Bpf::programs`]: crate::Bpf::programs
//! [`Bpf::program`]: crate::Bpf::program
//! [`Bpf::program_mut`]: crate::Bpf::program_mut
//! [maps]: crate::maps
mod kprobe;
mod perf_attach;
pub mod probe;
pub mod socket_filter;
pub mod trace_point;
pub mod xdp;
mod probe;
mod socket_filter;
mod trace_point;
mod uprobe;
mod xdp;
use libc::{close, ENOSPC};
use std::{cell::RefCell, cmp, convert::TryFrom, ffi::CStr, io, os::unix::io::RawFd, rc::Rc};
use thiserror::Error;
pub use kprobe::{KProbe, KProbeError};
use perf_attach::*;
pub use probe::{KProbe, KProbeError, UProbe, UProbeError};
pub use probe::ProbeKind;
pub use socket_filter::{SocketFilter, SocketFilterError};
pub use trace_point::{TracePoint, TracePointError};
pub use xdp::{Xdp, XdpError};
pub use uprobe::{UProbe, UProbeError};
pub use xdp::{Xdp, XdpError, XdpFlags};
use crate::{
generated::bpf_prog_type,
@ -87,6 +137,7 @@ pub trait ProgramFd {
fn fd(&self) -> Option<RawFd>;
}
/// eBPF program type.
#[derive(Debug)]
pub enum Program {
KProbe(KProbe),
@ -97,10 +148,21 @@ pub enum Program {
}
impl Program {
/// Loads the program in the kernel.
///
/// # Errors
///
/// If the load operation fails, the method returns
/// [`ProgramError::LoadError`] and the error's `verifier_log` field
/// contains the output from the kernel verifier.
///
/// If the program is already loaded, [`ProgramError::AlreadyLoaded`] is
/// returned.
pub fn load(&mut self) -> Result<(), ProgramError> {
load_program(self.prog_type(), self.data_mut())
}
/// Returns the low level program type.
pub fn prog_type(&self) -> bpf_prog_type {
use crate::generated::bpf_prog_type::*;
match self {
@ -112,7 +174,12 @@ impl Program {
}
}
pub(crate) fn data(&self) -> &ProgramData {
/// Returns the name of the program.
pub fn name(&self) -> &str {
&self.data().name
}
fn data(&self) -> &ProgramData {
match self {
Program::KProbe(p) => &p.data,
Program::UProbe(p) => &p.data,
@ -131,10 +198,6 @@ impl Program {
Program::Xdp(p) => &mut p.data,
}
}
pub fn name(&self) -> &str {
&self.data().name
}
}
#[derive(Debug)]

@ -1,167 +1,22 @@
use libc::pid_t;
use object::{Object, ObjectSymbol};
use std::{
error::Error,
ffi::CStr,
fs,
io::{self, BufRead, Cursor, Read},
mem,
os::raw::c_char,
path::{Path, PathBuf},
sync::Arc,
};
use thiserror::Error;
use std::{fs, io};
use crate::{
generated::bpf_prog_type::BPF_PROG_TYPE_KPROBE,
programs::{load_program, perf_attach, LinkRef, ProgramData, ProgramError},
sys::perf_event_open_probe,
};
const LD_SO_CACHE_FILE: &str = "/etc/ld.so.cache";
lazy_static! {
static ref LD_SO_CACHE: Result<LdSoCache, Arc<io::Error>> =
LdSoCache::load(LD_SO_CACHE_FILE).map_err(Arc::new);
}
const LD_SO_CACHE_HEADER: &str = "glibc-ld.so.cache1.1";
#[derive(Debug, Error)]
pub enum KProbeError {
#[error("`{filename}`")]
FileError {
filename: String,
#[source]
io_error: io::Error,
},
}
#[derive(Debug, Error)]
pub enum UProbeError {
#[error("error reading `{}` file", LD_SO_CACHE_FILE)]
InvalidLdSoCache {
#[source]
io_error: Arc<io::Error>,
programs::{
kprobe::KProbeError, perf_attach, uprobe::UProbeError, LinkRef, ProgramData, ProgramError,
},
#[error("could not resolve uprobe target `{path}`")]
InvalidTarget { path: PathBuf },
#[error("error resolving symbol")]
SymbolError {
symbol: String,
#[source]
error: Box<dyn Error + Send + Sync>,
},
#[error("`{filename}`")]
FileError {
filename: String,
#[source]
io_error: io::Error,
},
}
#[derive(Debug)]
pub struct KProbe {
pub(crate) data: ProgramData,
pub(crate) kind: ProbeKind,
}
#[derive(Debug)]
pub struct UProbe {
pub(crate) data: ProgramData,
pub(crate) kind: ProbeKind,
}
impl KProbe {
pub fn load(&mut self) -> Result<(), ProgramError> {
load_program(BPF_PROG_TYPE_KPROBE, &mut self.data)
}
pub fn name(&self) -> String {
self.data.name.to_string()
}
pub fn attach(
&mut self,
fn_name: &str,
offset: u64,
pid: Option<pid_t>,
) -> Result<LinkRef, ProgramError> {
attach(&mut self.data, self.kind, fn_name, offset, pid)
}
}
impl UProbe {
pub fn load(&mut self) -> Result<(), ProgramError> {
load_program(BPF_PROG_TYPE_KPROBE, &mut self.data)
}
pub fn name(&self) -> String {
self.data.name.to_string()
}
pub fn attach<T: AsRef<Path>>(
&mut self,
fn_name: Option<&str>,
offset: u64,
target: T,
pid: Option<pid_t>,
) -> Result<LinkRef, ProgramError> {
let target = target.as_ref();
let target_str = &*target.as_os_str().to_string_lossy();
let mut path = if let Some(pid) = pid {
find_lib_in_proc_maps(pid, &target_str).map_err(|io_error| UProbeError::FileError {
filename: format!("/proc/{}/maps", pid),
io_error,
})?
} else {
None
};
if path.is_none() {
path = if target.is_absolute() {
Some(target_str)
} else {
let cache =
LD_SO_CACHE
.as_ref()
.map_err(|error| UProbeError::InvalidLdSoCache {
io_error: error.clone(),
})?;
cache.resolve(target_str)
}
.map(String::from)
};
let path = path.ok_or(UProbeError::InvalidTarget {
path: target.to_owned(),
})?;
let sym_offset = if let Some(fn_name) = fn_name {
resolve_symbol(&path, fn_name).map_err(|error| UProbeError::SymbolError {
symbol: fn_name.to_string(),
error: Box::new(error),
})?
} else {
0
sys::perf_event_open_probe,
};
attach(&mut self.data, self.kind, &path, sym_offset + offset, pid)
}
}
#[derive(Debug, Copy, Clone)]
pub(crate) enum ProbeKind {
pub enum ProbeKind {
KProbe,
KRetProbe,
UProbe,
URetProbe,
}
fn attach(
pub(crate) fn attach(
program_data: &mut ProgramData,
kind: ProbeKind,
name: &str,
@ -196,149 +51,6 @@ fn attach(
perf_attach(program_data, fd)
}
fn proc_maps_libs(pid: pid_t) -> Result<Vec<(String, String)>, io::Error> {
let maps_file = format!("/proc/{}/maps", pid);
let data = fs::read_to_string(maps_file)?;
Ok(data
.lines()
.filter_map(|line| {
let line = line.split_whitespace().last()?;
if line.starts_with('/') {
let path = PathBuf::from(line);
let key = path.file_name().unwrap().to_string_lossy().into_owned();
Some((key, path.to_string_lossy().to_string()))
} else {
None
}
})
.collect())
}
fn find_lib_in_proc_maps(pid: pid_t, lib: &str) -> Result<Option<String>, io::Error> {
let libs = proc_maps_libs(pid)?;
let ret = if lib.contains(".so") {
libs.iter().find(|(k, _)| k.as_str().starts_with(lib))
} else {
let lib = lib.to_string();
let lib1 = lib.clone() + ".so";
let lib2 = lib + "-";
libs.iter()
.find(|(k, _)| k.starts_with(&lib1) || k.starts_with(&lib2))
};
Ok(ret.map(|(_, v)| v.clone()))
}
#[derive(Debug)]
pub(crate) struct CacheEntry {
key: String,
value: String,
flags: i32,
}
#[derive(Debug)]
pub(crate) struct LdSoCache {
entries: Vec<CacheEntry>,
}
impl LdSoCache {
pub fn load<T: AsRef<Path>>(path: T) -> Result<Self, io::Error> {
let data = fs::read(path)?;
Self::parse(&data)
}
fn parse(data: &[u8]) -> Result<Self, io::Error> {
let mut cursor = Cursor::new(data);
let read_u32 = |cursor: &mut Cursor<_>| -> Result<u32, io::Error> {
let mut buf = [0u8; mem::size_of::<u32>()];
cursor.read_exact(&mut buf)?;
Ok(u32::from_ne_bytes(buf))
};
let read_i32 = |cursor: &mut Cursor<_>| -> Result<i32, io::Error> {
let mut buf = [0u8; mem::size_of::<i32>()];
cursor.read_exact(&mut buf)?;
Ok(i32::from_ne_bytes(buf))
};
let mut buf = [0u8; LD_SO_CACHE_HEADER.len()];
cursor.read_exact(&mut buf)?;
let header = std::str::from_utf8(&buf).or(Err(io::Error::new(
io::ErrorKind::InvalidData,
"invalid ld.so.cache header",
)))?;
if header != LD_SO_CACHE_HEADER {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"invalid ld.so.cache header",
));
}
let num_entries = read_u32(&mut cursor)?;
let _str_tab_len = read_u32(&mut cursor)?;
cursor.consume(5 * mem::size_of::<u32>());
let mut entries = Vec::new();
for _ in 0..num_entries {
let flags = read_i32(&mut cursor)?;
let k_pos = read_u32(&mut cursor)? as usize;
let v_pos = read_u32(&mut cursor)? as usize;
cursor.consume(12);
let key =
unsafe { CStr::from_ptr(cursor.get_ref()[k_pos..].as_ptr() as *const c_char) }
.to_string_lossy()
.into_owned();
let value =
unsafe { CStr::from_ptr(cursor.get_ref()[v_pos..].as_ptr() as *const c_char) }
.to_string_lossy()
.into_owned();
entries.push(CacheEntry { key, value, flags });
}
Ok(LdSoCache { entries })
}
pub fn resolve(&self, lib: &str) -> Option<&str> {
let lib = if !lib.contains(".so") {
lib.to_string() + ".so"
} else {
lib.to_string()
};
self.entries
.iter()
.find(|entry| entry.key.starts_with(&lib))
.map(|entry| entry.value.as_str())
}
}
#[derive(Error, Debug)]
enum ResolveSymbolError {
#[error(transparent)]
Io(#[from] io::Error),
#[error("error parsing ELF")]
Object(#[from] object::Error),
#[error("unknown symbol `{0}`")]
Unknown(String),
}
fn resolve_symbol(path: &str, symbol: &str) -> Result<u64, ResolveSymbolError> {
let data = fs::read(path)?;
let obj = object::read::File::parse(&data)?;
obj.dynamic_symbols()
.chain(obj.symbols())
.find(|sym| sym.name().map(|name| name == symbol).unwrap_or(false))
.map(|s| s.address())
.ok_or_else(|| ResolveSymbolError::Unknown(symbol.to_string()))
}
fn read_sys_fs_perf_type(pmu: &str) -> Result<u32, (String, io::Error)> {
let file = format!("/sys/bus/event_source/devices/{}/type", pmu);

@ -0,0 +1,266 @@
//! User space probes.
use libc::pid_t;
use object::{Object, ObjectSymbol};
use std::{
error::Error,
ffi::CStr,
fs,
io::{self, BufRead, Cursor, Read},
mem,
os::raw::c_char,
path::{Path, PathBuf},
sync::Arc,
};
use thiserror::Error;
use crate::{
generated::bpf_prog_type::BPF_PROG_TYPE_KPROBE,
programs::{
load_program,
probe::{attach, ProbeKind},
LinkRef, ProgramData, ProgramError,
},
};
const LD_SO_CACHE_FILE: &str = "/etc/ld.so.cache";
lazy_static! {
static ref LD_SO_CACHE: Result<LdSoCache, Arc<io::Error>> =
LdSoCache::load(LD_SO_CACHE_FILE).map_err(Arc::new);
}
const LD_SO_CACHE_HEADER: &str = "glibc-ld.so.cache1.1";
#[derive(Debug)]
pub struct UProbe {
pub(crate) data: ProgramData,
pub(crate) kind: ProbeKind,
}
impl UProbe {
pub fn load(&mut self) -> Result<(), ProgramError> {
load_program(BPF_PROG_TYPE_KPROBE, &mut self.data)
}
pub fn name(&self) -> String {
self.data.name.to_string()
}
pub fn attach<T: AsRef<Path>>(
&mut self,
fn_name: Option<&str>,
offset: u64,
target: T,
pid: Option<pid_t>,
) -> Result<LinkRef, ProgramError> {
let target = target.as_ref();
let target_str = &*target.as_os_str().to_string_lossy();
let mut path = if let Some(pid) = pid {
find_lib_in_proc_maps(pid, &target_str).map_err(|io_error| UProbeError::FileError {
filename: format!("/proc/{}/maps", pid),
io_error,
})?
} else {
None
};
if path.is_none() {
path = if target.is_absolute() {
Some(target_str)
} else {
let cache =
LD_SO_CACHE
.as_ref()
.map_err(|error| UProbeError::InvalidLdSoCache {
io_error: error.clone(),
})?;
cache.resolve(target_str)
}
.map(String::from)
};
let path = path.ok_or(UProbeError::InvalidTarget {
path: target.to_owned(),
})?;
let sym_offset = if let Some(fn_name) = fn_name {
resolve_symbol(&path, fn_name).map_err(|error| UProbeError::SymbolError {
symbol: fn_name.to_string(),
error: Box::new(error),
})?
} else {
0
};
attach(&mut self.data, self.kind, &path, sym_offset + offset, pid)
}
}
#[derive(Debug, Error)]
pub enum UProbeError {
#[error("error reading `{}` file", LD_SO_CACHE_FILE)]
InvalidLdSoCache {
#[source]
io_error: Arc<io::Error>,
},
#[error("could not resolve uprobe target `{path}`")]
InvalidTarget { path: PathBuf },
#[error("error resolving symbol")]
SymbolError {
symbol: String,
#[source]
error: Box<dyn Error + Send + Sync>,
},
#[error("`{filename}`")]
FileError {
filename: String,
#[source]
io_error: io::Error,
},
}
fn proc_maps_libs(pid: pid_t) -> Result<Vec<(String, String)>, io::Error> {
let maps_file = format!("/proc/{}/maps", pid);
let data = fs::read_to_string(maps_file)?;
Ok(data
.lines()
.filter_map(|line| {
let line = line.split_whitespace().last()?;
if line.starts_with('/') {
let path = PathBuf::from(line);
let key = path.file_name().unwrap().to_string_lossy().into_owned();
Some((key, path.to_string_lossy().to_string()))
} else {
None
}
})
.collect())
}
fn find_lib_in_proc_maps(pid: pid_t, lib: &str) -> Result<Option<String>, io::Error> {
let libs = proc_maps_libs(pid)?;
let ret = if lib.contains(".so") {
libs.iter().find(|(k, _)| k.as_str().starts_with(lib))
} else {
let lib = lib.to_string();
let lib1 = lib.clone() + ".so";
let lib2 = lib + "-";
libs.iter()
.find(|(k, _)| k.starts_with(&lib1) || k.starts_with(&lib2))
};
Ok(ret.map(|(_, v)| v.clone()))
}
#[derive(Debug)]
pub(crate) struct CacheEntry {
key: String,
value: String,
flags: i32,
}
#[derive(Debug)]
pub(crate) struct LdSoCache {
entries: Vec<CacheEntry>,
}
impl LdSoCache {
pub fn load<T: AsRef<Path>>(path: T) -> Result<Self, io::Error> {
let data = fs::read(path)?;
Self::parse(&data)
}
fn parse(data: &[u8]) -> Result<Self, io::Error> {
let mut cursor = Cursor::new(data);
let read_u32 = |cursor: &mut Cursor<_>| -> Result<u32, io::Error> {
let mut buf = [0u8; mem::size_of::<u32>()];
cursor.read_exact(&mut buf)?;
Ok(u32::from_ne_bytes(buf))
};
let read_i32 = |cursor: &mut Cursor<_>| -> Result<i32, io::Error> {
let mut buf = [0u8; mem::size_of::<i32>()];
cursor.read_exact(&mut buf)?;
Ok(i32::from_ne_bytes(buf))
};
let mut buf = [0u8; LD_SO_CACHE_HEADER.len()];
cursor.read_exact(&mut buf)?;
let header = std::str::from_utf8(&buf).or(Err(io::Error::new(
io::ErrorKind::InvalidData,
"invalid ld.so.cache header",
)))?;
if header != LD_SO_CACHE_HEADER {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"invalid ld.so.cache header",
));
}
let num_entries = read_u32(&mut cursor)?;
let _str_tab_len = read_u32(&mut cursor)?;
cursor.consume(5 * mem::size_of::<u32>());
let mut entries = Vec::new();
for _ in 0..num_entries {
let flags = read_i32(&mut cursor)?;
let k_pos = read_u32(&mut cursor)? as usize;
let v_pos = read_u32(&mut cursor)? as usize;
cursor.consume(12);
let key =
unsafe { CStr::from_ptr(cursor.get_ref()[k_pos..].as_ptr() as *const c_char) }
.to_string_lossy()
.into_owned();
let value =
unsafe { CStr::from_ptr(cursor.get_ref()[v_pos..].as_ptr() as *const c_char) }
.to_string_lossy()
.into_owned();
entries.push(CacheEntry { key, value, flags });
}
Ok(LdSoCache { entries })
}
pub fn resolve(&self, lib: &str) -> Option<&str> {
let lib = if !lib.contains(".so") {
lib.to_string() + ".so"
} else {
lib.to_string()
};
self.entries
.iter()
.find(|entry| entry.key.starts_with(&lib))
.map(|entry| entry.value.as_str())
}
}
#[derive(Error, Debug)]
enum ResolveSymbolError {
#[error(transparent)]
Io(#[from] io::Error),
#[error("error parsing ELF")]
Object(#[from] object::Error),
#[error("unknown symbol `{0}`")]
Unknown(String),
}
fn resolve_symbol(path: &str, symbol: &str) -> Result<u64, ResolveSymbolError> {
let data = fs::read(path)?;
let obj = object::read::File::parse(&data)?;
obj.dynamic_symbols()
.chain(obj.symbols())
.find(|sym| sym.name().map(|name| name == symbol).unwrap_or(false))
.map(|s| s.address())
.ok_or_else(|| ResolveSymbolError::Unknown(symbol.to_string()))
}
Loading…
Cancel
Save