Skip to main content

ghostscope_loader/
kernel_caps.rs

1use aya::{
2    maps::MapType,
3    programs::ProgramType,
4    sys::{is_helper_supported, is_map_supported, BpfHelper},
5};
6use std::{fmt, sync::OnceLock};
7use tracing::{error, info, warn};
8
9/// Global cache for complete, hardware-backed kernel capability probes.
10static KERNEL_CAPS: KernelCapabilityCache = KernelCapabilityCache::new();
11
12#[derive(Debug)]
13struct KernelCapabilityCache {
14    full: OnceLock<KernelCapabilities>,
15}
16
17impl KernelCapabilityCache {
18    const fn new() -> Self {
19        Self {
20            full: OnceLock::new(),
21        }
22    }
23
24    fn get_or_detect<F>(&self, detect: F) -> Result<KernelCapabilities, KernelCapabilityError>
25    where
26        F: FnOnce() -> Result<KernelCapabilityDetection, KernelCapabilityError>,
27    {
28        if let Some(capabilities) = self.full.get() {
29            return Ok(*capabilities);
30        }
31
32        let detection = detect()?;
33        if detection.cacheable {
34            let _ = self.full.set(detection.capabilities);
35            if let Some(capabilities) = self.full.get() {
36                return Ok(*capabilities);
37            }
38        } else {
39            warn!("Kernel capability probe used fallback values; not caching this result");
40        }
41
42        Ok(detection.capabilities)
43    }
44}
45
46#[derive(Debug, Clone)]
47pub struct KernelCapabilityError {
48    message: String,
49}
50
51impl KernelCapabilityError {
52    fn new(message: impl Into<String>) -> Self {
53        Self {
54            message: message.into(),
55        }
56    }
57}
58
59impl fmt::Display for KernelCapabilityError {
60    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61        write!(f, "{}", self.message)
62    }
63}
64
65impl std::error::Error for KernelCapabilityError {}
66
67/// Kernel eBPF capabilities detection
68#[derive(Debug, Clone, Copy, PartialEq, Eq)]
69pub struct KernelCapabilities {
70    /// Whether the kernel supports BPF_MAP_TYPE_RINGBUF (requires >= 5.8)
71    pub supports_ringbuf: bool,
72    /// Whether the kernel supports BPF_MAP_TYPE_PERF_EVENT_ARRAY (requires >= 4.3)
73    pub supports_perf_event_array: bool,
74    /// Whether bpf_get_ns_current_pid_tgid helper is supported for kprobe/uprobe class programs.
75    pub supports_ns_current_pid_tgid_helper: bool,
76}
77
78impl KernelCapabilities {
79    /// Detect kernel capabilities for process startup, including startup-oriented logs and
80    /// user-facing error context.
81    pub fn detect_for_startup(force_perf_event_array: bool) -> Result<Self, KernelCapabilityError> {
82        detect_for_startup_with_detectors(force_perf_event_array, Self::get, Self::get_perf_only)
83    }
84
85    /// Get global kernel capabilities (detected once on first cacheable call)
86    /// Returns an error if neither RingBuf nor PerfEventArray support can be verified.
87    pub fn get() -> Result<Self, KernelCapabilityError> {
88        KERNEL_CAPS.get_or_detect(detect_full_capabilities)
89    }
90
91    /// Detect kernel capabilities with PerfEventArray-only startup semantics.
92    /// This intentionally bypasses the global cache because force-perf mode is a
93    /// runtime policy override, not the kernel's complete hardware capability set.
94    pub fn get_perf_only() -> Result<Self, KernelCapabilityError> {
95        detect_perf_only_capabilities()
96    }
97
98    /// Check if RingBuf is supported (convenience method)
99    pub fn ringbuf_supported() -> bool {
100        Self::get()
101            .map(|caps| caps.supports_ringbuf)
102            .unwrap_or(false)
103    }
104
105    /// Check if PerfEventArray is supported (convenience method)
106    pub fn perf_event_array_supported() -> bool {
107        Self::get()
108            .map(|caps| caps.supports_perf_event_array)
109            .unwrap_or(false)
110    }
111
112    /// Check if bpf_get_ns_current_pid_tgid helper is supported.
113    pub fn ns_current_pid_tgid_helper_supported() -> bool {
114        Self::get()
115            .map(|caps| caps.supports_ns_current_pid_tgid_helper)
116            .unwrap_or(false)
117    }
118}
119
120fn detect_for_startup_with_detectors<F, P>(
121    force_perf_event_array: bool,
122    detect_full: F,
123    detect_perf_only: P,
124) -> Result<KernelCapabilities, KernelCapabilityError>
125where
126    F: FnOnce() -> Result<KernelCapabilities, KernelCapabilityError>,
127    P: FnOnce() -> Result<KernelCapabilities, KernelCapabilityError>,
128{
129    let capabilities = if force_perf_event_array {
130        warn!("⚠️  TESTING MODE: force_perf_event_array=true - will use PerfEventArray");
131        detect_perf_only().map_err(|err| {
132            KernelCapabilityError::new(format!(
133                "{err}\nGhostScope requires Linux kernel >= 4.3 with PerfEventArray enabled."
134            ))
135        })?
136    } else {
137        detect_full().map_err(|err| {
138            KernelCapabilityError::new(format!(
139                "{err}\nHint: ensure CONFIG_BPF, CONFIG_BPF_SYSCALL and CONFIG_UPROBE_EVENTS are enabled in your kernel."
140            ))
141        })?
142    };
143
144    info!(
145        "Kernel eBPF startup summary: ringbuf_supported={} perf_event_array_supported={} helper_ns_current_pid_tgid={}",
146        capabilities.supports_ringbuf,
147        capabilities.supports_perf_event_array,
148        capabilities.supports_ns_current_pid_tgid_helper
149    );
150
151    Ok(capabilities)
152}
153
154#[derive(Debug, Clone, Copy)]
155struct KernelCapabilityDetection {
156    capabilities: KernelCapabilities,
157    cacheable: bool,
158}
159
160#[derive(Debug, Clone, Copy)]
161struct CapabilityProbe {
162    supported: bool,
163    cacheable: bool,
164}
165
166impl CapabilityProbe {
167    fn cacheable(supported: bool) -> Self {
168        Self {
169            supported,
170            cacheable: true,
171        }
172    }
173
174    fn uncacheable_unsupported() -> Self {
175        Self {
176            supported: false,
177            cacheable: false,
178        }
179    }
180}
181
182fn detect_full_capabilities() -> Result<KernelCapabilityDetection, KernelCapabilityError> {
183    let supports_ringbuf = detect_ringbuf_support();
184    let supports_perf_event_array = if !supports_ringbuf.supported {
185        detect_perf_event_array_support()
186    } else {
187        CapabilityProbe::cacheable(true)
188    };
189
190    if supports_ringbuf.supported {
191        info!("✓ Kernel supports RingBuf (>= 5.8)");
192    } else if supports_perf_event_array.supported {
193        warn!("⚠️  Kernel does not support RingBuf (< 5.8)");
194        warn!("⚠️  Will use PerfEventArray as fallback");
195        info!("✓ Kernel supports PerfEventArray (>= 4.3)");
196    } else {
197        if !supports_ringbuf.cacheable || !supports_perf_event_array.cacheable {
198            error!("❌ Unable to verify kernel eBPF event output support");
199            return Err(KernelCapabilityError::new(
200                "Unable to verify RingBuf or PerfEventArray support because one or more \
201                 eBPF capability probes failed. Check privileges and kernel BPF settings.",
202            ));
203        }
204
205        error!("❌ Kernel supports neither RingBuf nor PerfEventArray");
206        error!("❌ GhostScope requires kernel >= 4.3 for eBPF event output");
207        error!("❌ Current kernel appears to be older or eBPF is disabled");
208        return Err(KernelCapabilityError::new(
209            "Kernel lacks both RingBuf (>=5.8) and PerfEventArray (>=4.3) support. \
210             Please upgrade the kernel or enable eBPF features.",
211        ));
212    }
213
214    let supports_ns_current_pid_tgid_helper = detect_ns_current_pid_tgid_helper_support();
215    if supports_ns_current_pid_tgid_helper.supported {
216        info!("✓ Kernel supports helper bpf_get_ns_current_pid_tgid (id=120)");
217    } else {
218        warn!("⚠️  Kernel does not support helper bpf_get_ns_current_pid_tgid (id=120)");
219    }
220
221    Ok(KernelCapabilityDetection {
222        capabilities: KernelCapabilities {
223            supports_ringbuf: supports_ringbuf.supported,
224            supports_perf_event_array: supports_perf_event_array.supported,
225            supports_ns_current_pid_tgid_helper: supports_ns_current_pid_tgid_helper.supported,
226        },
227        cacheable: supports_ringbuf.cacheable
228            && supports_perf_event_array.cacheable
229            && supports_ns_current_pid_tgid_helper.cacheable,
230    })
231}
232
233fn detect_perf_only_capabilities() -> Result<KernelCapabilities, KernelCapabilityError> {
234    info!("Testing mode: Only detecting PerfEventArray support");
235    let supports_perf_event_array = detect_perf_event_array_support();
236
237    if !supports_perf_event_array.supported {
238        if !supports_perf_event_array.cacheable {
239            error!("❌ Unable to verify PerfEventArray support");
240            return Err(KernelCapabilityError::new(
241                "Unable to verify PerfEventArray support because the eBPF capability probe \
242                 failed. Check privileges and kernel BPF settings.",
243            ));
244        }
245
246        error!("❌ Kernel does not support PerfEventArray");
247        error!("❌ GhostScope requires kernel >= 4.3 for eBPF event output");
248        return Err(KernelCapabilityError::new(
249            "Kernel lacks PerfEventArray support (>=4.3 required). \
250             Please upgrade the kernel or enable eBPF features.",
251        ));
252    }
253
254    info!("✓ Kernel supports PerfEventArray (>= 4.3)");
255
256    let supports_ns_current_pid_tgid_helper = detect_ns_current_pid_tgid_helper_support();
257    if supports_ns_current_pid_tgid_helper.supported {
258        info!("✓ Kernel supports helper bpf_get_ns_current_pid_tgid (id=120)");
259    } else {
260        warn!("⚠️  Kernel does not support helper bpf_get_ns_current_pid_tgid (id=120)");
261    }
262
263    Ok(KernelCapabilities {
264        supports_ringbuf: false,
265        supports_perf_event_array: supports_perf_event_array.supported,
266        supports_ns_current_pid_tgid_helper: supports_ns_current_pid_tgid_helper.supported,
267    })
268}
269
270/// Detect RingBuf support by attempting to create a minimal map
271fn detect_ringbuf_support() -> CapabilityProbe {
272    detect_map_support(
273        MapType::RingBuf,
274        "RingBuf",
275        "this is normal on kernels < 5.8",
276    )
277}
278
279/// Detect PerfEventArray support by attempting to create a minimal map
280fn detect_perf_event_array_support() -> CapabilityProbe {
281    detect_map_support(
282        MapType::PerfEventArray,
283        "PerfEventArray",
284        "kernel may be older than 4.3",
285    )
286}
287
288fn detect_map_support(
289    map_type: MapType,
290    label: &str,
291    unsupported_context: &str,
292) -> CapabilityProbe {
293    info!("Probing kernel {label} support via aya::sys::is_map_supported...");
294
295    match is_map_supported(map_type) {
296        Ok(true) => {
297            info!("{label} map support probe succeeded - {label} is supported");
298            CapabilityProbe::cacheable(true)
299        }
300        Ok(false) => {
301            info!("{label} map support probe reported unsupported ({unsupported_context})");
302            CapabilityProbe::cacheable(false)
303        }
304        Err(err) => {
305            warn!("{label} map support probe failed unexpectedly: {err}");
306            CapabilityProbe::uncacheable_unsupported()
307        }
308    }
309}
310
311fn detect_ns_current_pid_tgid_helper_support() -> CapabilityProbe {
312    info!(
313        "Probing kernel bpf_get_ns_current_pid_tgid helper support via aya::sys::is_helper_supported..."
314    );
315
316    match is_helper_supported(
317        ProgramType::KProbe,
318        BpfHelper::BPF_FUNC_get_ns_current_pid_tgid,
319    ) {
320        Ok(true) => {
321            info!(
322                "bpf_get_ns_current_pid_tgid helper support probe succeeded - helper is supported"
323            );
324            CapabilityProbe::cacheable(true)
325        }
326        Ok(false) => {
327            info!("bpf_get_ns_current_pid_tgid helper support probe reported unsupported");
328            CapabilityProbe::cacheable(false)
329        }
330        Err(err) => {
331            warn!("bpf_get_ns_current_pid_tgid helper support probe failed unexpectedly: {err}");
332            CapabilityProbe::uncacheable_unsupported()
333        }
334    }
335}
336
337#[cfg(test)]
338mod tests {
339    use super::*;
340
341    fn caps(
342        supports_ringbuf: bool,
343        supports_perf_event_array: bool,
344        supports_ns_current_pid_tgid_helper: bool,
345    ) -> KernelCapabilities {
346        KernelCapabilities {
347            supports_ringbuf,
348            supports_perf_event_array,
349            supports_ns_current_pid_tgid_helper,
350        }
351    }
352
353    fn detection(capabilities: KernelCapabilities, cacheable: bool) -> KernelCapabilityDetection {
354        KernelCapabilityDetection {
355            capabilities,
356            cacheable,
357        }
358    }
359
360    #[test]
361    fn forced_perf_startup_does_not_populate_full_capabilities_cache() {
362        let cache = KernelCapabilityCache::new();
363        let perf_only_caps = caps(false, true, true);
364        let full_caps = caps(true, true, true);
365
366        let forced = detect_for_startup_with_detectors(
367            true,
368            || -> Result<KernelCapabilities, KernelCapabilityError> {
369                panic!("full detector should not run for forced perf startup")
370            },
371            || Ok(perf_only_caps),
372        )
373        .expect("forced perf startup detection");
374
375        assert_eq!(forced, perf_only_caps);
376
377        let normal = detect_for_startup_with_detectors(
378            false,
379            || cache.get_or_detect(|| Ok(detection(full_caps, true))),
380            || -> Result<KernelCapabilities, KernelCapabilityError> {
381                panic!("perf-only detector should not run for normal startup")
382            },
383        )
384        .expect("normal startup detection");
385
386        assert_eq!(normal, full_caps);
387        assert_eq!(
388            cache
389                .get_or_detect(|| {
390                    panic!("full detector should not rerun after cacheable detection")
391                })
392                .expect("cached full capabilities"),
393            full_caps
394        );
395    }
396
397    #[test]
398    fn uncacheable_full_probe_result_is_not_cached() {
399        let cache = KernelCapabilityCache::new();
400        let uncacheable_caps = caps(false, true, false);
401        let cacheable_caps = caps(true, true, true);
402
403        let first = cache
404            .get_or_detect(|| Ok(detection(uncacheable_caps, false)))
405            .expect("uncacheable startup result");
406        assert_eq!(first, uncacheable_caps);
407
408        let second = cache
409            .get_or_detect(|| Ok(detection(cacheable_caps, true)))
410            .expect("cacheable startup result");
411        assert_eq!(second, cacheable_caps);
412
413        assert_eq!(
414            cache
415                .get_or_detect(|| {
416                    panic!("full detector should not rerun after cacheable detection")
417                })
418                .expect("cached full capabilities"),
419            cacheable_caps
420        );
421    }
422}