#![allow(dead_code)]
mod blueprint;
mod bpf;
mod cpu_info;
mod debugfs;
mod error;
mod maps;
mod perf;
mod program_group;
mod program_version;
pub use blueprint::{ProgramBlueprint, SectionType};
pub use error::OxidebpfError;
pub use maps::{ArrayMap, BpfHashMap, RWMap};
pub use program_group::ProgramGroup;
pub use program_version::{PerfBufferSize, ProgramVersion};
use perf::syscall::{attach_kprobe, attach_kprobe_debugfs, attach_uprobe, attach_uprobe_debugfs};
use std::{
fmt::{self, Display, Formatter},
format,
os::unix::io::RawFd,
};
use lazy_static::lazy_static;
use libc::pid_t;
use slog::{info, o, Logger};
use slog_atomic::{AtomicSwitch, AtomicSwitchCtrl};
lazy_static! {
pub static ref LOGGER: (Logger, AtomicSwitchCtrl) = create_slogger_root();
}
fn create_slogger_root() -> (slog::Logger, AtomicSwitchCtrl) {
let drain = slog::Logger::root(slog::Discard, o!());
let drain = AtomicSwitch::new(drain);
(slog::Logger::root(drain.clone(), o!()), drain.ctrl())
}
#[cfg(target_arch = "aarch64")]
const ARCH_SYSCALL_PREFIX: &str = "__arm64_";
#[cfg(target_arch = "x86_64")]
const ARCH_SYSCALL_PREFIX: &str = "__x64_";
#[repr(C)]
#[derive(Debug, Default)]
pub struct CapUserHeader {
version: u32,
pid: i32,
}
#[repr(C)]
#[derive(Debug, Default)]
pub struct CapUserData {
effective: u32,
permitted: u32,
inheritable: u32,
}
#[derive(Debug)]
pub enum PerfChannelMessage {
Dropped(u64),
Event {
map_name: String,
cpuid: i32,
data: Vec<u8>,
},
}
#[derive(Clone)]
pub enum DebugfsMountOpts {
MountDisabled,
MountConventional,
MountCustom(String),
}
impl Default for DebugfsMountOpts {
fn default() -> Self {
DebugfsMountOpts::MountDisabled
}
}
impl From<&str> for DebugfsMountOpts {
fn from(value: &str) -> Self {
DebugfsMountOpts::MountCustom(value.to_string())
}
}
impl From<Option<&str>> for DebugfsMountOpts {
fn from(value: Option<&str>) -> DebugfsMountOpts {
match value {
Some(v) => v.into(),
None => DebugfsMountOpts::MountDisabled,
}
}
}
#[derive(Clone, Copy)]
pub enum SchedulingPolicy {
Other(i8),
Idle,
Batch(i8),
FIFO(u8),
RR(u8),
Deadline(u64, u64, u64),
}
impl From<SchedulingPolicy> for thread_priority::ThreadSchedulePolicy {
fn from(policy: SchedulingPolicy) -> Self {
match policy {
SchedulingPolicy::Other(_) => thread_priority::ThreadSchedulePolicy::Normal(
thread_priority::NormalThreadSchedulePolicy::Other,
),
SchedulingPolicy::Idle => thread_priority::ThreadSchedulePolicy::Normal(
thread_priority::NormalThreadSchedulePolicy::Idle,
),
SchedulingPolicy::Batch(_) => thread_priority::ThreadSchedulePolicy::Normal(
thread_priority::NormalThreadSchedulePolicy::Batch,
),
SchedulingPolicy::FIFO(_) => thread_priority::ThreadSchedulePolicy::Realtime(
thread_priority::RealtimeThreadSchedulePolicy::Fifo,
),
SchedulingPolicy::RR(_) => thread_priority::ThreadSchedulePolicy::Realtime(
thread_priority::RealtimeThreadSchedulePolicy::RoundRobin,
),
SchedulingPolicy::Deadline(_, _, _) => thread_priority::ThreadSchedulePolicy::Realtime(
thread_priority::RealtimeThreadSchedulePolicy::Deadline,
),
}
}
}
impl From<SchedulingPolicy> for thread_priority::ThreadPriority {
fn from(policy: SchedulingPolicy) -> Self {
match policy {
SchedulingPolicy::Other(_) | SchedulingPolicy::Idle | SchedulingPolicy::Batch(_) => {
thread_priority::ThreadPriority::from_posix(thread_priority::ScheduleParams {
sched_priority: 0,
})
}
SchedulingPolicy::FIFO(polling_priority) | SchedulingPolicy::RR(polling_priority) => {
thread_priority::ThreadPriority::from_posix(thread_priority::ScheduleParams {
sched_priority: polling_priority.clamp(1_u8, 99_u8) as i32,
})
}
SchedulingPolicy::Deadline(r, d, p) => {
thread_priority::ThreadPriority::Deadline(r, d, p)
}
}
}
}
#[derive(Clone, Default)]
struct TailCallMapping {
map: String,
index: u32,
}
#[derive(Clone, Default)]
pub struct Program<'a> {
kind: Option<ProgramType>,
name: &'a str,
attach_points: Vec<String>,
optional: bool,
loaded: bool,
is_syscall: bool,
fd: RawFd,
pid: Option<pid_t>,
tail_call_mapping: Option<TailCallMapping>,
debugfs_mount: DebugfsMountOpts,
}
impl<'a> Program<'a> {
pub fn new(name: &'a str, attach_points: &[&str]) -> Program<'a> {
Self {
kind: None,
name,
attach_points: attach_points.iter().map(|ap| ap.to_string()).collect(),
optional: false,
loaded: false,
is_syscall: false,
fd: -1,
pid: None,
tail_call_mapping: None,
debugfs_mount: DebugfsMountOpts::MountDisabled,
}
}
pub fn pid(mut self, pid: pid_t) -> Self {
self.pid = Some(pid);
self
}
pub fn optional(mut self, optional: bool) -> Self {
self.optional = optional;
self
}
pub fn syscall(mut self, syscall: bool) -> Self {
self.is_syscall = syscall;
self
}
pub fn tail_call_map_index(mut self, map_name: &str, tail_call_index: u32) -> Self {
self.tail_call_mapping = Some(TailCallMapping {
map: map_name.to_string(),
index: tail_call_index,
});
self
}
pub fn program_type(mut self, kind: ProgramType) -> Self {
self.kind = Some(kind);
self
}
fn set_debugfs_mount_point(&mut self, debugfs_mount: DebugfsMountOpts) {
self.debugfs_mount = debugfs_mount
}
fn mount_debugfs_if_missing(&self) {
let mount_point = match &self.debugfs_mount {
DebugfsMountOpts::MountDisabled => {
return;
}
DebugfsMountOpts::MountConventional => "/sys/kernel/debug",
DebugfsMountOpts::MountCustom(value) => value.as_str(),
};
if let Err(mount_err) = debugfs::mount_if_missing(mount_point) {
info!(LOGGER.0, "Failed to mount debugfs: {:?}", mount_err);
}
}
fn attach_kprobe(&self) -> Result<(Vec<String>, Vec<RawFd>), OxidebpfError> {
let is_return = self.kind == Some(ProgramType::Kretprobe);
self.attach_points
.iter()
.fold(Ok((vec![], vec![])), |mut result, attach_point| {
match attach_kprobe(self.fd, attach_point, is_return, None, 0) {
Ok(fd) => {
if let Ok((_, fds)) = &mut result {
fds.push(fd);
}
}
Err(e) => {
info!(LOGGER.0, "Program::attach_kprobe(); original error: {:?}", e);
self.mount_debugfs_if_missing();
match attach_kprobe_debugfs(self.fd, attach_point, is_return, None, 0) {
Ok((path, fd)) => {
if let Ok((paths, fds)) = &mut result {
paths.push(path);
fds.push(fd);
}
}
Err(s) => match &mut result {
Ok(_) => result = Err(vec![e, s]),
Err(errors) => {
info!(
LOGGER.0,
"Program::attach_kprobe(); multiple kprobe load errors: {:?}; {:?}", e, s
);
errors.extend(vec![e, s])
}
},
}
}
}
result
})
.map_err(OxidebpfError::MultipleErrors)
}
fn attach_uprobe(&self) -> Result<(Vec<String>, Vec<RawFd>), OxidebpfError> {
let is_return = self.kind == Some(ProgramType::Uretprobe);
let pid = self.pid.unwrap_or(-1);
cpu_info::online()?
.into_iter()
.flat_map(|cpu| {
self.attach_points
.iter()
.map(move |attach_point| (cpu, attach_point))
})
.fold(Ok((vec![], vec![])), |mut result, (cpu, attach_point)| {
match attach_uprobe(self.fd, attach_point, is_return, None, cpu, pid) {
Ok(fd) => {
if let Ok((_, fds)) = &mut result {
fds.push(fd);
}
}
Err(e) => {
self.mount_debugfs_if_missing();
match attach_uprobe_debugfs(
self.fd,
attach_point,
is_return,
None,
cpu,
pid,
) {
Ok((path, fd)) => {
if let Ok((paths, fds)) = &mut result {
paths.push(path);
fds.push(fd);
}
}
Err(s) => match &mut result {
Ok(_) => result = Err(vec![e, s]),
Err(errors) => {
info!(
LOGGER.0,
"Program::attach_uprobe(); multiple uprobe load errors: {:?}; {:?}", e, s
);
errors.extend(vec![e, s])
}
},
}
}
}
result
})
.map_err(OxidebpfError::MultipleErrors)
}
fn attach(&mut self) -> Result<(Vec<String>, Vec<RawFd>), OxidebpfError> {
match self.attach_probes() {
Ok(res) => Ok(res),
Err(e) => {
if self.is_syscall {
self.attach_points
.iter_mut()
.for_each(|ap| *ap = format!("{}{}", ARCH_SYSCALL_PREFIX, ap));
self.attach_probes()
} else {
info!(LOGGER.0, "Program::attach(); attach error: {:?}", e);
Err(e)
}
}
}
}
fn attach_probes(&self) -> Result<(Vec<String>, Vec<RawFd>), OxidebpfError> {
if !self.loaded {
info!(
LOGGER.0,
"Program::attach_probes(); attempting to attach probes while program not loaded"
);
return Err(OxidebpfError::ProgramNotLoaded);
}
match &self.kind {
Some(ProgramType::Kprobe | ProgramType::Kretprobe) => self.attach_kprobe(),
Some(ProgramType::Uprobe | ProgramType::Uretprobe) => self.attach_uprobe(),
Some(t) => {
info!(
LOGGER.0,
"Program::attach_probes(); attempting to load unsupported program type {:?}", t
);
Err(OxidebpfError::UnsupportedProgramType)
}
_ => {
info!(
LOGGER.0,
"Program::attach_probes(); attempting to load unsupported program type: unknown"
);
Err(OxidebpfError::UnsupportedProgramType)
}
}
}
pub(crate) fn loaded_as(&mut self, fd: RawFd) {
self.loaded = true;
self.fd = fd;
}
fn set_fd(&mut self, fd: RawFd) {
self.fd = fd
}
fn get_fd(&self) -> Result<RawFd, OxidebpfError> {
if self.loaded {
Ok(self.fd)
} else {
Err(OxidebpfError::ProgramNotLoaded)
}
}
}
pub fn set_memlock_limit(limit: usize) -> Result<(), OxidebpfError> {
unsafe {
let rlim = libc::rlimit {
rlim_cur: limit as u64,
rlim_max: limit as u64,
};
let ret = libc::setrlimit(libc::RLIMIT_MEMLOCK, &rlim as *const _);
if ret < 0 {
info!(
LOGGER.0,
"set_memlock_limit(); unable to set memlock limit, errno: {}",
nix::errno::errno()
);
Err(OxidebpfError::LinuxError(
"set_memlock_limit".to_string(),
nix::errno::Errno::from_i32(nix::errno::errno()),
))
} else {
Ok(())
}
}
}
pub fn get_capabilities() -> Result<(CapUserHeader, CapUserData), OxidebpfError> {
let mut hdrp = CapUserHeader {
version: 0x20080522, pid: 0, };
let mut datap = CapUserData::default();
let ret = unsafe {
libc::syscall(
libc::SYS_capget,
&mut hdrp as *mut _ as *mut libc::c_void,
&mut datap as *mut _ as *mut libc::c_void,
)
};
if ret < 0 {
Err(OxidebpfError::LinuxError(
"get_capabilities()".to_string(),
nix::errno::from_i32(nix::errno::errno()),
))
} else {
Ok((hdrp, datap))
}
}
pub fn get_memlock_limit() -> Result<usize, OxidebpfError> {
unsafe {
let mut rlim = libc::rlimit {
rlim_cur: 0,
rlim_max: 0,
};
let ret = libc::getrlimit(libc::RLIMIT_MEMLOCK, &mut rlim as *mut _);
if ret < 0 {
info!(
LOGGER.0,
"get_memlock_limit(); could not get memlock limit, errno: {}",
nix::errno::errno()
);
return Err(OxidebpfError::LinuxError(
"get_memlock_limit".to_string(),
nix::errno::Errno::from_i32(nix::errno::errno()),
));
}
Ok(rlim.rlim_cur as usize)
}
}
#[cfg(test)]
mod program_tests {
use super::*;
use std::path::PathBuf;
#[test]
fn test_program_group() {
let program = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("test")
.join(format!("test_program_{}", std::env::consts::ARCH));
let program_blueprint =
ProgramBlueprint::new(&std::fs::read(program).expect("Could not open file"), None)
.expect("Could not open test object file");
let mut program_group = ProgramGroup::new();
program_group
.load(
program_blueprint,
vec![ProgramVersion::new(vec![
Program::new("test_program_map_update", &["do_mount"]).syscall(true),
Program::new("test_program", &["do_mount"]).syscall(true),
])],
|| unreachable!(),
)
.expect("Could not load programs");
}
#[test]
fn test_memlock_limit() {
let program = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("test")
.join(format!("test_program_{}", std::env::consts::ARCH));
let program_blueprint =
ProgramBlueprint::new(&std::fs::read(program).expect("Could not open file"), None)
.expect("Could not open test object file");
let mut program_group = ProgramGroup::new().mem_limit(1234567);
let original_limit = get_memlock_limit().expect("could not get original limit");
program_group
.load(
program_blueprint,
vec![ProgramVersion::new(vec![
Program::new("test_program_map_update", &["do_mount"]).syscall(true),
Program::new("test_program", &["do_mount"]).syscall(true),
])],
|| unreachable!(),
)
.expect("Could not load programs");
let current_limit = get_memlock_limit().expect("could not get current limit");
assert_eq!(current_limit, 1234567);
assert_ne!(current_limit, original_limit);
set_memlock_limit(original_limit).expect("could not revert limit");
}
#[test]
fn test_program_group_array_maps() {
let program = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("test")
.join(format!("test_program_{}", std::env::consts::ARCH));
let program_blueprint =
ProgramBlueprint::new(&std::fs::read(program).expect("Could not open file"), None)
.expect("Could not open test object file");
let mut program_group = ProgramGroup::new();
program_group
.load(
program_blueprint,
vec![ProgramVersion::new(vec![
Program::new("test_program_map_update", &["sys_open", "sys_write"])
.syscall(true),
Program::new("test_program", &["do_mount"]).syscall(true),
])],
|| unreachable!(), )
.expect("Could not load programs");
match program_group.get_array_maps() {
Some(hash_map) => {
let array_map = match hash_map.get("__test_map") {
Some(map) => map,
None => {
panic!("There should have been a map with that name")
}
};
std::fs::write("/tmp/baz", "some data").expect("Unable to write file");
let val: u32 = unsafe { array_map.read(0).expect("Failed to read from map") };
assert_eq!(val, 1234);
let _ = unsafe {
array_map
.write(0, 0xAAAAAAAAu32)
.expect("Failed to write from map")
};
let val: u32 = unsafe { array_map.read(0).expect("Failed to read from map") };
assert_eq!(val, 0xAAAAAAAA);
}
None => {
panic!("Failed to get maps when they should have been present");
}
};
}
#[test]
fn test_program_group_tail_call() {
let program = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("test")
.join(format!("test_program_{}", std::env::consts::ARCH));
let program_blueprint =
ProgramBlueprint::new(&std::fs::read(program).expect("Could not open file"), None)
.expect("Could not open test object file");
let mut program_group = ProgramGroup::new();
program_group
.load(
program_blueprint,
vec![ProgramVersion::new(vec![
Program::new("test_program_tailcall", &["sys_open", "sys_write"]).syscall(true),
Program::new("test_program_tailcall_update_map", &[])
.tail_call_map_index("__test_tailcall_map", 0),
])],
|| unreachable!(),
)
.expect("Could not load programs");
match program_group.get_array_maps() {
Some(hash_map) => {
let array_map = match hash_map.get("__test_map") {
Some(map) => map,
None => {
panic!("There should have been a map with that name")
}
};
std::fs::write("/tmp/bar", "some data").expect("Unable to write file");
let val: u32 = unsafe { array_map.read(150).expect("Failed to read from map") };
assert_eq!(val, 111);
}
None => {
panic!("Failed to get maps when they should have been present");
}
};
}
#[test]
fn test_program_group_hash_maps() {
let program = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("test")
.join(format!("test_program_{}", std::env::consts::ARCH));
let program_blueprint =
ProgramBlueprint::new(&std::fs::read(program).expect("Could not open file"), None)
.expect("Could not open test object file");
let mut program_group = ProgramGroup::new();
program_group
.load(
program_blueprint,
vec![ProgramVersion::new(vec![
Program::new("test_program_map_update", &["sys_open", "sys_write"])
.syscall(true),
Program::new("test_program", &["do_mount"]).syscall(true),
])],
|| unreachable!(), )
.expect("Could not load programs");
match program_group.get_hash_maps() {
Some(map) => {
let hash_map = match map.get("__test_hash_map") {
Some(m) => m,
None => {
panic!("There should have been a map with that name")
}
};
std::fs::write("/tmp/foo", "some data").expect("Unable to write file");
let val: u64 =
unsafe { hash_map.read(0x12345u64).expect("Failed to read from map") };
assert_eq!(val, 1234);
let _ = unsafe {
hash_map
.write(std::process::id() as u64, 0xAAAAAAAAu64)
.expect("Failed to write from map")
};
let val: u64 = unsafe {
hash_map
.read(std::process::id() as u64)
.expect("Failed to read from map")
};
assert_eq!(val, 0xAAAAAAAA);
}
None => {
panic!("Failed to get maps when they should have been present");
}
};
}
}
#[cfg(test)]
mod scheduling_tests {
use thread_priority::ThreadPriority;
use super::*;
#[test]
fn idle_no_priority() {
let policy = SchedulingPolicy::Idle;
let converted: ThreadPriority = policy.into();
let expected = ThreadPriority::Crossplatform(0_u8.try_into().unwrap());
assert_eq!(converted, expected);
}
#[test]
fn other_no_priority() {
let policy = SchedulingPolicy::Other(10);
let converted: ThreadPriority = policy.into();
let expected = ThreadPriority::Crossplatform(0_u8.try_into().unwrap());
assert_eq!(converted, expected);
}
#[test]
fn batch_no_priority() {
let policy = SchedulingPolicy::Batch(39);
let converted: ThreadPriority = policy.into();
let expected = ThreadPriority::Crossplatform(0_u8.try_into().unwrap());
assert_eq!(converted, expected);
}
#[test]
fn fifo_clamped_priority() {
let policy = SchedulingPolicy::FIFO(0);
let converted: ThreadPriority = policy.into();
let expected = ThreadPriority::Crossplatform(1_u8.try_into().unwrap());
assert_eq!(converted, expected);
}
#[test]
fn sched_clamped_priority() {
let policy = SchedulingPolicy::RR(100);
let converted: ThreadPriority = policy.into();
let expected = ThreadPriority::Crossplatform(99_u8.try_into().unwrap());
assert_eq!(converted, expected);
}
#[test]
fn deadline_priority() {
let policy = SchedulingPolicy::Deadline(1, 2, 3);
let converted: ThreadPriority = policy.into();
let expected = ThreadPriority::Deadline(1, 2, 3);
assert_eq!(converted, expected);
}
}
#[derive(Debug, Clone, PartialEq, Copy)]
pub enum ProgramType {
Unspec,
Kprobe,
Kretprobe,
Uprobe,
Uretprobe,
Tracepoint,
RawTracepoint,
}
impl Default for ProgramType {
fn default() -> Self {
ProgramType::Unspec
}
}
impl From<&str> for ProgramType {
fn from(value: &str) -> ProgramType {
match value {
"kprobe" => ProgramType::Kprobe,
"kretprobe" => ProgramType::Kretprobe,
"uprobe" => ProgramType::Uprobe,
"uretprobe" => ProgramType::Uretprobe,
"tracepoint" => ProgramType::Tracepoint,
"rawtracepoint" => ProgramType::RawTracepoint,
_ => ProgramType::Unspec,
}
}
}
impl Display for ProgramType {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(
f,
"{}",
match self {
ProgramType::Unspec => "unspec",
ProgramType::Kprobe => "kprobe",
ProgramType::Kretprobe => "kretprobe",
ProgramType::Uprobe => "uprobe",
ProgramType::Uretprobe => "uretprobe",
ProgramType::Tracepoint => "tracepoint",
ProgramType::RawTracepoint => "rawtracepoint",
}
)
}
}