use crate::sync::RwLockExt;
use std::path::{Path, PathBuf};
use std::sync::{Arc, OnceLock, RwLock};
static VMLINUX_BYTES_CACHE: OnceLock<RwLock<std::collections::HashMap<PathBuf, CachedEntry>>> =
OnceLock::new();
struct CachedEntry {
mtime: std::time::SystemTime,
bytes: Arc<Vec<u8>>,
}
pub(crate) fn cached_vmlinux_bytes(path: &Path) -> Option<Arc<Vec<u8>>> {
let canon = std::fs::canonicalize(path)
.ok()
.unwrap_or_else(|| path.to_path_buf());
let mtime = std::fs::metadata(&canon).and_then(|m| m.modified()).ok()?;
let slot = VMLINUX_BYTES_CACHE.get_or_init(|| RwLock::new(std::collections::HashMap::new()));
{
let read = slot.read_unpoisoned();
if let Some(entry) = read.get(&canon)
&& entry.mtime == mtime
{
return Some(Arc::clone(&entry.bytes));
}
}
let bytes = std::fs::read(&canon).ok()?;
let arc = Arc::new(bytes);
let mut write = slot.write_unpoisoned();
write.insert(
canon,
CachedEntry {
mtime,
bytes: Arc::clone(&arc),
},
);
Some(arc)
}
#[cfg(test)]
pub(crate) fn clear_vmlinux_cache_for_tests() {
if let Some(slot) = VMLINUX_BYTES_CACHE.get() {
slot.write_unpoisoned().clear();
}
}
pub(crate) fn find_vmlinux(kernel_path: &Path) -> Option<PathBuf> {
let dir = kernel_path.parent()?;
let candidate = dir.join("vmlinux");
if candidate.exists() {
return Some(candidate);
}
if let Ok(root) = dir.join("../../..").canonicalize() {
let candidate = root.join("vmlinux");
if candidate.exists() {
return Some(candidate);
}
}
if let Some(name) = kernel_path.file_name().and_then(|n| n.to_str()) {
let version = name.strip_prefix("vmlinuz-").unwrap_or(name);
for candidate in [
PathBuf::from(format!("/usr/lib/debug/boot/vmlinux-{version}")),
PathBuf::from(format!("/boot/vmlinux-{version}")),
PathBuf::from(format!("/lib/modules/{version}/build/vmlinux")),
] {
if candidate.exists() {
return Some(candidate);
}
}
}
if let Some(parent_name) = dir.file_name().and_then(|n| n.to_str()) {
for candidate in [
dir.join("build/vmlinux"),
PathBuf::from(format!("/boot/vmlinux-{parent_name}")),
] {
if candidate.exists() {
return Some(candidate);
}
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[cfg(target_arch = "x86_64")]
fn find_vmlinux_from_bzimage_path() {
let tmp = tempfile::TempDir::new().unwrap();
let boot_dir = tmp.path().join("arch/x86/boot");
std::fs::create_dir_all(&boot_dir).unwrap();
let vmlinux = tmp.path().join("vmlinux");
std::fs::write(&vmlinux, b"ELF").unwrap();
let bzimage = boot_dir.join("bzImage");
std::fs::write(&bzimage, b"kernel").unwrap();
let found = find_vmlinux(&bzimage);
assert_eq!(found, Some(vmlinux));
}
#[test]
fn find_vmlinux_sibling() {
let tmp = tempfile::TempDir::new().unwrap();
let vmlinux = tmp.path().join("vmlinux");
std::fs::write(&vmlinux, b"ELF").unwrap();
let kernel = tmp.path().join("bzImage");
std::fs::write(&kernel, b"kernel").unwrap();
let found = find_vmlinux(&kernel);
assert_eq!(found, Some(vmlinux));
}
#[test]
fn find_vmlinux_bare_filename() {
assert_eq!(find_vmlinux(Path::new("vmlinuz")), None);
}
#[test]
fn find_vmlinux_root_parent() {
let result = find_vmlinux(Path::new("/vmlinuz"));
if !Path::new("/vmlinux").exists() {
assert_eq!(result, None);
}
}
#[test]
fn find_vmlinux_missing_returns_none() {
let tmp = tempfile::TempDir::new().unwrap();
let kernel = tmp.path().join("bzImage");
std::fs::write(&kernel, b"kernel").unwrap();
assert_eq!(find_vmlinux(&kernel), None);
}
#[test]
fn cached_vmlinux_bytes_hits_on_second_call() {
let tmp = tempfile::TempDir::new().unwrap();
let vmlinux = tmp.path().join("vmlinux-test-cache");
std::fs::write(&vmlinux, b"FAKE_VMLINUX_BYTES").unwrap();
let first = cached_vmlinux_bytes(&vmlinux).expect("first read populates cache");
let second = cached_vmlinux_bytes(&vmlinux).expect("second read hits cache");
assert_eq!(first.as_slice(), b"FAKE_VMLINUX_BYTES");
assert!(
Arc::ptr_eq(&first, &second),
"cache hit must return the same Arc; got fresh allocations on each call"
);
}
#[test]
fn cached_vmlinux_bytes_missing_returns_none() {
let tmp = tempfile::TempDir::new().unwrap();
let nonexistent = tmp.path().join("missing-xyzzy");
assert!(cached_vmlinux_bytes(&nonexistent).is_none());
}
#[test]
#[cfg(unix)]
fn cached_vmlinux_bytes_dedups_symlinks_to_same_target() {
let tmp = tempfile::TempDir::new().unwrap();
let real = tmp.path().join("vmlinux-real");
std::fs::write(&real, b"SYMLINK_DEDUP_BYTES").unwrap();
let link_a = tmp.path().join("vmlinux-link-a");
let link_b = tmp.path().join("vmlinux-link-b");
std::os::unix::fs::symlink(&real, &link_a).unwrap();
std::os::unix::fs::symlink(&real, &link_b).unwrap();
let via_a = cached_vmlinux_bytes(&link_a).expect("read via symlink A");
let via_b = cached_vmlinux_bytes(&link_b).expect("read via symlink B");
assert!(
Arc::ptr_eq(&via_a, &via_b),
"two symlinks to the same target must canonicalize to the \
same cache key and return the same Arc; got fresh \
allocations, suggesting the canonicalize-then-key path \
regressed to keying on the raw symlink path."
);
}
#[test]
#[cfg(unix)]
fn cached_vmlinux_bytes_dangling_symlink_returns_none() {
let tmp = tempfile::TempDir::new().unwrap();
let target = tmp.path().join("vmlinux-gone");
let link = tmp.path().join("vmlinux-dangling");
std::fs::write(&target, b"ELF").unwrap();
std::os::unix::fs::symlink(&target, &link).unwrap();
std::fs::remove_file(&target).unwrap();
assert!(cached_vmlinux_bytes(&link).is_none());
}
#[test]
#[cfg(unix)]
fn cached_vmlinux_bytes_invalidates_on_mtime_change() {
let tmp = tempfile::TempDir::new().unwrap();
let vmlinux = tmp.path().join("vmlinux-mtime-test");
std::fs::write(&vmlinux, b"FIRST_BYTES").unwrap();
clear_vmlinux_cache_for_tests();
let first = cached_vmlinux_bytes(&vmlinux).expect("first read");
assert_eq!(first.as_slice(), b"FIRST_BYTES");
std::fs::write(&vmlinux, b"SECOND_BYTES_DIFFERENT").unwrap();
let path_c = std::ffi::CString::new(vmlinux.as_os_str().as_encoded_bytes()).unwrap();
let sentinel = libc::timeval {
tv_sec: 86_400,
tv_usec: 0,
};
let times = [sentinel, sentinel];
let rc = unsafe { libc::utimes(path_c.as_ptr(), times.as_ptr()) };
assert_eq!(rc, 0, "libc::utimes must succeed on the temp file");
let second = cached_vmlinux_bytes(&vmlinux).expect("second read");
assert_eq!(
second.as_slice(),
b"SECOND_BYTES_DIFFERENT",
"mtime change must invalidate cache and surface the rewritten bytes"
);
assert!(
!Arc::ptr_eq(&first, &second),
"post-rewrite second lookup must return a fresh Arc, \
not the stale cached one — Arc::ptr_eq returning true \
means the invalidation path didn't fire."
);
}
}