diff --git a/crates/memtrack/src/ebpf/memtrack/mod.rs b/crates/memtrack/src/ebpf/memtrack/mod.rs index 8586872e..4c32a33a 100644 --- a/crates/memtrack/src/ebpf/memtrack/mod.rs +++ b/crates/memtrack/src/ebpf/memtrack/mod.rs @@ -136,6 +136,8 @@ impl MemtrackBpf { /// would detect. Either attaches given host privileges; the token only /// matters when `bpf()` is called from an unprivileged user namespace. pub fn with_variant(variant: BpfVariant, track_rmap: bool) -> Result { + crate::kernel::KernelBtf::ensure_available()?; + let page_shift = page_shift()?; let rmap = if track_rmap { RmapSupport::detect() diff --git a/crates/memtrack/src/kernel.rs b/crates/memtrack/src/kernel.rs index bafffa33..c99cf045 100644 --- a/crates/memtrack/src/kernel.rs +++ b/crates/memtrack/src/kernel.rs @@ -1,6 +1,15 @@ use crate::prelude::*; use std::fmt; +/// The running kernel's full release, e.g. `6.12.8+`. +fn kernel_release() -> Result { + const OSRELEASE_PATH: &str = "/proc/sys/kernel/osrelease"; + + std::fs::read_to_string(OSRELEASE_PATH) + .map(|release| release.trim().to_owned()) + .with_context(|| format!("Failed to read {OSRELEASE_PATH}")) +} + /// A kernel release, ordered by `(major, minor)`. The patch level is ignored: /// features are introduced in merge windows, never in a stable point release. #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] @@ -16,12 +25,8 @@ impl KernelVersion { /// The running kernel's release. pub fn current() -> Result { - const PATH: &str = "/proc/sys/kernel/osrelease"; - - let release = - std::fs::read_to_string(PATH).with_context(|| format!("Failed to read {PATH}"))?; - Self::parse(&release) - .with_context(|| format!("Failed to parse kernel release {:?}", release.trim())) + let release = kernel_release()?; + Self::parse(&release).with_context(|| format!("Failed to parse kernel release {release:?}")) } /// Parse the leading `.` of a release string, ignoring @@ -46,6 +51,40 @@ impl fmt::Display for KernelVersion { } } +/// Whether the running kernel exposes its own BTF. +/// +/// libbpf needs it to resolve CO-RE relocations, and the kernel resolves the +/// attach target of every `fentry`/`tp_btf` program against it, so a kernel +/// without BTF cannot load the programs at all. Detecting it up front replaces +/// libbpf's bare `-ESRCH` with something the reader can act on. +/// +/// Minimal kernels built for fast boot — microVM images in particular — commonly +/// drop `CONFIG_DEBUG_INFO_BTF`, so the message has to name the option. +pub struct KernelBtf; + +impl KernelBtf { + /// Present only on a kernel built with `CONFIG_DEBUG_INFO_BTF`. + const PATH: &'static str = "/sys/kernel/btf/vmlinux"; + + pub fn is_available() -> bool { + std::path::Path::new(Self::PATH).exists() + } + + pub fn ensure_available() -> Result<()> { + if Self::is_available() { + return Ok(()); + } + + let release = kernel_release().unwrap_or_else(|_| "unknown".to_owned()); + bail!( + "Memory profiling is not supported on this runner: its kernel ({release}) \ + was built without BTF, so {} does not exist. Use a runner whose kernel is \ + built with CONFIG_DEBUG_INFO_BTF=y.", + Self::PATH + ); + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/memtrack/src/lib.rs b/crates/memtrack/src/lib.rs index 1d3278d9..ccd93399 100644 --- a/crates/memtrack/src/lib.rs +++ b/crates/memtrack/src/lib.rs @@ -14,7 +14,7 @@ pub use ipc::{ IpcCommand as MemtrackIpcCommand, IpcMessage as MemtrackIpcMessage, IpcResponse as MemtrackIpcResponse, MemtrackIpcClient, MemtrackIpcServer, }; -pub use kernel::KernelVersion; +pub use kernel::{KernelBtf, KernelVersion}; #[cfg(feature = "ebpf")] pub use ebpf::*;