#[cfg(target_os = "linux")]
use aya::Ebpf;
use crate::error::EbpfError;
pub struct KprobeManager {
target_pid: Option<i32>,
#[cfg(target_os = "linux")]
links: Vec<Box<dyn std::any::Any>>,
}
impl std::fmt::Debug for KprobeManager {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("KprobeManager")
.field("target_pid", &self.target_pid)
.finish()
}
}
impl KprobeManager {
#[cfg(target_os = "linux")]
pub fn attach(bpf: &mut Ebpf, target_pid: Option<i32>) -> Result<Self, EbpfError> {
if let Some(pid) = target_pid {
let mut pid_filter: aya::maps::HashMap<_, u32, u8> = aya::maps::HashMap::try_from(
bpf.map_mut("PID_FILTER")
.ok_or_else(|| EbpfError::ProbeAttach("PID_FILTER map not found".into()))?,
)
.map_err(|e| EbpfError::ProbeAttach(e.to_string()))?;
pid_filter
.insert(pid as u32, 1, 0)
.map_err(|e| EbpfError::ProbeAttach(e.to_string()))?;
}
let probes: &[(&str, &str)] = &[
("aa_sys_openat", "__x64_sys_openat"),
("aa_sys_openat_ret", "__x64_sys_openat"),
("aa_sys_read", "__x64_sys_read"),
("aa_sys_write", "__x64_sys_write"),
("aa_sys_unlink", "__x64_sys_unlinkat"),
("aa_sys_unlink_legacy", "__x64_sys_unlink"),
("aa_sys_rename", "__x64_sys_renameat2"),
("aa_sys_rename_legacy", "__x64_sys_rename"),
];
let mut links: Vec<Box<dyn std::any::Any>> = Vec::with_capacity(probes.len());
for (prog_name, fn_name) in probes {
let program: &mut aya::programs::KProbe = bpf
.program_mut(prog_name)
.ok_or_else(|| EbpfError::ProbeAttach(format!("{prog_name} program not found")))?
.try_into()
.map_err(|e: aya::programs::ProgramError| EbpfError::ProbeAttach(e.to_string()))?;
program
.load()
.map_err(|e| EbpfError::ProbeAttach(format!("{prog_name} load failed: {e}")))?;
let link = program
.attach(fn_name, 0)
.map_err(|e| EbpfError::ProbeAttach(format!("{prog_name} attach to {fn_name} failed: {e}")))?;
links.push(Box::new(link));
tracing::info!(program = prog_name, function = fn_name, "kprobe attached");
}
Ok(Self { target_pid, links })
}
#[cfg(not(target_os = "linux"))]
pub fn attach(_bpf: &mut (), _target_pid: Option<i32>) -> Result<Self, EbpfError> {
Err(EbpfError::ProbeAttach("kprobe attachment requires Linux".into()))
}
#[cfg(target_os = "linux")]
pub fn detach(&mut self) {
let count = self.links.len();
self.links.clear();
if count > 0 {
tracing::info!(probes = count, "kprobes detached");
}
}
#[cfg(not(target_os = "linux"))]
pub fn detach(&mut self) {}
#[cfg(target_os = "linux")]
pub fn is_attached(&self) -> bool {
!self.links.is_empty()
}
#[cfg(not(target_os = "linux"))]
pub fn is_attached(&self) -> bool {
false
}
pub const KPROBE_TARGETS: &[(&str, &str)] = &[
("aa_sys_openat", "__x64_sys_openat"),
("aa_sys_openat_ret", "__x64_sys_openat"),
("aa_sys_read", "__x64_sys_read"),
("aa_sys_write", "__x64_sys_write"),
("aa_sys_unlink", "__x64_sys_unlinkat"),
("aa_sys_unlink_legacy", "__x64_sys_unlink"),
("aa_sys_rename", "__x64_sys_renameat2"),
("aa_sys_rename_legacy", "__x64_sys_rename"),
];
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[cfg(not(target_os = "linux"))]
fn attach_returns_error_on_non_linux() {
let err = KprobeManager::attach(&mut (), Some(1234)).unwrap_err();
assert!(matches!(err, EbpfError::ProbeAttach(_)));
assert!(err.to_string().contains("requires Linux"));
}
#[test]
#[cfg(not(target_os = "linux"))]
fn attach_returns_error_on_non_linux_system_wide() {
let err = KprobeManager::attach(&mut (), None).unwrap_err();
assert!(matches!(err, EbpfError::ProbeAttach(_)));
}
#[test]
fn kprobe_targets_covers_all_file_io_syscalls() {
let targets = KprobeManager::KPROBE_TARGETS;
assert_eq!(targets.len(), 8);
let prog_names: Vec<&str> = targets.iter().map(|(p, _)| *p).collect();
assert!(prog_names.contains(&"aa_sys_openat"));
assert!(prog_names.contains(&"aa_sys_openat_ret"));
assert!(prog_names.contains(&"aa_sys_read"));
assert!(prog_names.contains(&"aa_sys_write"));
assert!(prog_names.contains(&"aa_sys_unlink"));
assert!(prog_names.contains(&"aa_sys_unlink_legacy"));
assert!(prog_names.contains(&"aa_sys_rename"));
assert!(prog_names.contains(&"aa_sys_rename_legacy"));
}
#[test]
#[cfg(not(target_os = "linux"))]
fn detach_is_noop_on_non_linux() {
let mut mgr = KprobeManager { target_pid: None };
assert!(!mgr.is_attached());
mgr.detach(); assert!(!mgr.is_attached());
}
#[test]
fn kprobe_targets_kernel_functions_are_prefixed() {
for (_, fn_name) in KprobeManager::KPROBE_TARGETS {
assert!(
fn_name.starts_with("__x64_sys_"),
"kernel function {fn_name} should use __x64_sys_ prefix"
);
}
}
}