use std::ffi::CStr;
use std::path::{Path, PathBuf};
use anyhow::{Context, Result, anyhow};
#[derive(Debug, Clone)]
#[allow(dead_code)] pub struct LiveHostKernelEnv {
pub release: String,
pub btf_path: PathBuf,
pub vmlinux_elf_path: Option<PathBuf>,
pub kallsyms_path: PathBuf,
}
impl LiveHostKernelEnv {
#[allow(dead_code)]
pub fn discover() -> Result<Self> {
let release = uname_release().context("uname(2) failed")?;
let btf_path = locate_btf(&release).ok_or_else(|| {
anyhow!("no BTF found (looked in /sys/kernel/btf/vmlinux and ELF paths)")
})?;
let vmlinux_elf_path = locate_vmlinux_elf(&release);
Ok(Self {
release,
btf_path,
vmlinux_elf_path,
kallsyms_path: PathBuf::from("/proc/kallsyms"),
})
}
}
#[allow(dead_code)]
pub fn uname_release() -> Result<String> {
let mut uts: libc::utsname = unsafe { std::mem::zeroed() };
let ret = unsafe { libc::uname(&mut uts as *mut libc::utsname) };
if ret != 0 {
return Err(anyhow!(
"uname(2) failed: {}",
std::io::Error::last_os_error()
));
}
let release = unsafe { CStr::from_ptr(uts.release.as_ptr()) }
.to_str()
.context("uname.release was not valid UTF-8")?
.to_string();
Ok(release)
}
fn locate_btf(release: &str) -> Option<PathBuf> {
let sysfs = Path::new("/sys/kernel/btf/vmlinux");
if sysfs.is_file() {
return Some(sysfs.to_path_buf());
}
locate_vmlinux_elf(release)
}
fn locate_vmlinux_elf(release: &str) -> Option<PathBuf> {
let candidates = [
format!("/lib/modules/{release}/build/vmlinux"),
format!("/usr/lib/debug/boot/vmlinux-{release}"),
format!("/usr/lib/debug/lib/modules/{release}/vmlinux"),
];
for cand in &candidates {
let p = Path::new(cand);
if p.is_file() {
return Some(p.to_path_buf());
}
}
None
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct KallsymsTable {
by_name: std::collections::HashMap<String, u64>,
}
impl KallsymsTable {
#[allow(dead_code)]
pub fn load_from(env: &LiveHostKernelEnv) -> Result<Self> {
Self::load_from_path(&env.kallsyms_path)
}
#[allow(dead_code)]
pub fn load_from_path(path: &Path) -> Result<Self> {
let raw =
std::fs::read_to_string(path).with_context(|| format!("read {}", path.display()))?;
Ok(Self::parse(&raw))
}
pub fn parse(raw: &str) -> Self {
let mut by_name = std::collections::HashMap::new();
for line in raw.lines() {
let mut parts = line.split_whitespace();
let Some(addr) = parts.next() else { continue };
let _ty = parts.next();
let Some(sym) = parts.next() else { continue };
let Ok(addr) = u64::from_str_radix(addr, 16) else {
continue;
};
if addr == 0 {
continue;
}
by_name.insert(sym.to_string(), addr);
}
Self { by_name }
}
#[allow(dead_code)]
pub fn resolve(&self, name: &str) -> Option<u64> {
self.by_name.get(name).copied()
}
#[allow(dead_code)]
pub fn len(&self) -> usize {
self.by_name.len()
}
#[allow(dead_code)]
pub fn is_empty(&self) -> bool {
self.by_name.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn uname_release_returns_nonempty() {
let release = uname_release().expect("uname succeeds on Linux");
assert!(!release.is_empty());
assert!(
release.contains('.'),
"release {release:?} should look like X.Y or X.Y.Z"
);
}
#[test]
fn kallsyms_parse_basic() {
let raw = "\
ffffffff80100000 T _stext
ffffffff80101234 T scx_disable_workfn
ffffffff80105678 t local_static_function
ffffffff8000abcd D ext_sched_class
";
let table = KallsymsTable::parse(raw);
assert_eq!(table.resolve("_stext"), Some(0xffffffff80100000));
assert_eq!(
table.resolve("scx_disable_workfn"),
Some(0xffffffff80101234)
);
assert_eq!(
table.resolve("local_static_function"),
Some(0xffffffff80105678)
);
assert_eq!(table.resolve("ext_sched_class"), Some(0xffffffff8000abcd));
assert_eq!(table.len(), 4);
assert!(!table.is_empty());
}
#[test]
fn kallsyms_parse_skips_zero_addresses() {
let raw = "\
0000000000000000 T _stext
0000000000000000 T scx_disable_workfn
";
let table = KallsymsTable::parse(raw);
assert!(table.is_empty());
assert_eq!(table.resolve("_stext"), None);
}
#[test]
fn kallsyms_parse_skips_malformed_lines() {
let raw = "\
ffffffff80100000 T _stext
not-a-hex-address T garbage
short_line
ffffffff80105678 T good_symbol
";
let table = KallsymsTable::parse(raw);
assert_eq!(table.resolve("_stext"), Some(0xffffffff80100000));
assert_eq!(table.resolve("good_symbol"), Some(0xffffffff80105678));
assert_eq!(table.resolve("garbage"), None);
assert_eq!(table.len(), 2);
}
#[test]
fn kallsyms_load_from_path() {
use std::io::Write;
let tmp = tempfile::NamedTempFile::new().unwrap();
let mut f = tmp.reopen().unwrap();
writeln!(f, "ffffffff80100000 T _stext").unwrap();
writeln!(f, "ffffffff80101234 T target_symbol").unwrap();
drop(f);
let table = KallsymsTable::load_from_path(tmp.path()).unwrap();
assert_eq!(table.resolve("target_symbol"), Some(0xffffffff80101234));
}
#[test]
fn live_host_kernel_env_discover_smoke() {
if !Path::new("/sys/kernel/btf/vmlinux").is_file() {
return;
}
let env = LiveHostKernelEnv::discover().expect("BTF present, discover should succeed");
assert!(!env.release.is_empty());
assert!(env.btf_path.exists());
assert_eq!(env.kallsyms_path, Path::new("/proc/kallsyms"));
}
#[test]
fn locate_btf_no_real_release_returns_none_or_sysfs() {
let result = locate_btf("definitely-not-a-kernel-release-9.99");
if let Some(p) = result {
assert_eq!(p, Path::new("/sys/kernel/btf/vmlinux"))
}
}
}