From 7a0d51ef06eff3d5f89eeba25da908bf2362cd98 Mon Sep 17 00:00:00 2001 From: not-matthias Date: Mon, 31 Aug 2026 18:25:31 +0200 Subject: [PATCH 1/2] refactor(memtrack): extract kernel_release helper --- crates/memtrack/src/kernel.rs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/crates/memtrack/src/kernel.rs b/crates/memtrack/src/kernel.rs index bafffa33..9d249b68 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 From 94ace383f3b59990bcffc36826352704bd92f457 Mon Sep 17 00:00:00 2001 From: not-matthias Date: Mon, 31 Aug 2026 18:26:38 +0200 Subject: [PATCH 2/2] feat(memtrack): fail early when the kernel has no BTF libbpf resolves CO-RE relocations against the running kernel's own BTF, and the kernel resolves the attach target of every fentry/tp_btf program against it too, so a kernel without BTF cannot load the programs at all. libbpf reports this as a bare -ESRCH, which gives no hint about the cause. Check /sys/kernel/btf/vmlinux in MemtrackBpf::with_variant, where the load would otherwise fail, and report which kernel lacks it. COD-3417 --- crates/memtrack/src/ebpf/memtrack/mod.rs | 2 ++ crates/memtrack/src/kernel.rs | 34 ++++++++++++++++++++++++ crates/memtrack/src/lib.rs | 2 +- 3 files changed, 37 insertions(+), 1 deletion(-) 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 9d249b68..c99cf045 100644 --- a/crates/memtrack/src/kernel.rs +++ b/crates/memtrack/src/kernel.rs @@ -51,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::*;