Skip to main content

ghostscope_process/
pinned_bpf_maps.rs

1use crate::pid::{
2    host_pid_for_proc_pid, read_nspid_chain, read_pid_ns_inode, INITIAL_PID_NAMESPACE_INO,
3};
4use anyhow::Context;
5use aya::maps::{HashMap as AyaHashMap, Map, MapData, MapError};
6use aya_obj::maps::bpf_map_def;
7use aya_obj::{
8    generated::bpf_map_type::{BPF_MAP_TYPE_ARRAY, BPF_MAP_TYPE_HASH},
9    maps::LegacyMap,
10    EbpfSectionKind, Map as ObjMap,
11};
12use std::io;
13use std::path::{Path, PathBuf};
14use tracing::{info, warn};
15
16const BPFFS_MOUNT_POINT: &str = "/sys/fs/bpf";
17const BPFFS_ROOT: &str = "/sys/fs/bpf/ghostscope";
18const PROC_STAT_STARTTIME_INDEX: usize = 19;
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21struct CurrentProcessIdentity {
22    host_pid: u32,
23    host_pid_reliable: bool,
24    starttime: u64,
25    initial_pid_namespace: bool,
26}
27
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub enum BpffsPruneMode {
30    Stale,
31    Instance(String),
32    All,
33}
34
35#[derive(Debug, Clone, PartialEq, Eq)]
36pub struct BpffsPruneOptions {
37    pub mode: BpffsPruneMode,
38    pub dry_run: bool,
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub enum BpffsPruneStatus {
43    RemoveDir,
44    CleanKnownPins,
45    SkipLive,
46    Ignore,
47}
48
49#[derive(Debug, Clone, PartialEq, Eq)]
50pub struct BpffsPruneEntry {
51    pub directory: String,
52    pub status: BpffsPruneStatus,
53    pub reason: String,
54}
55
56#[derive(Debug, Clone, PartialEq, Eq)]
57pub struct BpffsPruneReport {
58    pub root: PathBuf,
59    pub dry_run: bool,
60    pub entries: Vec<BpffsPruneEntry>,
61}
62
63fn process_starttime(pid: u32) -> io::Result<u64> {
64    let stat = std::fs::read_to_string(format!("/proc/{pid}/stat"))?;
65    let (_, rest) = stat
66        .rsplit_once(") ")
67        .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "malformed /proc stat"))?;
68    let raw = rest
69        .split_whitespace()
70        .nth(PROC_STAT_STARTTIME_INDEX)
71        .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "missing starttime field"))?;
72    raw.parse::<u64>()
73        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
74}
75
76fn host_pid_mapping_from_chain(
77    chain: Option<&[u32]>,
78    allow_single_value_nspid: bool,
79) -> Option<u32> {
80    match chain {
81        Some([]) => None,
82        Some([only]) if allow_single_value_nspid => Some(*only),
83        Some([_only]) => None,
84        Some(values) => values.first().copied(),
85        None if allow_single_value_nspid => None,
86        None => None,
87    }
88}
89
90fn resolve_proc_pid_for_host_pid(host_pid: u32, allow_single_value_nspid: bool) -> Option<u32> {
91    let direct = Path::new("/proc").join(host_pid.to_string());
92    if direct.exists() {
93        let chain = read_nspid_chain(host_pid);
94        if host_pid_mapping_from_chain(chain.as_deref(), allow_single_value_nspid) == Some(host_pid)
95        {
96            return Some(host_pid);
97        }
98    }
99
100    let entries = std::fs::read_dir("/proc").ok()?;
101    for entry in entries.flatten() {
102        let Ok(proc_pid) = entry.file_name().to_string_lossy().parse::<u32>() else {
103            continue;
104        };
105        let chain = read_nspid_chain(proc_pid);
106        if host_pid_mapping_from_chain(chain.as_deref(), allow_single_value_nspid) == Some(host_pid)
107        {
108            return Some(proc_pid);
109        }
110    }
111
112    None
113}
114
115fn current_process_identity() -> anyhow::Result<CurrentProcessIdentity> {
116    let proc_pid = std::process::id();
117    let initial_pid_namespace = read_pid_ns_inode(proc_pid) == Some(INITIAL_PID_NAMESPACE_INO);
118    let nspid_chain = read_nspid_chain(proc_pid);
119    let host_pid_reliable =
120        initial_pid_namespace || nspid_chain.as_ref().is_some_and(|chain| chain.len() > 1);
121    Ok(CurrentProcessIdentity {
122        host_pid: host_pid_for_proc_pid(proc_pid),
123        host_pid_reliable,
124        starttime: process_starttime(proc_pid)?,
125        initial_pid_namespace,
126    })
127}
128
129fn current_process_dir_name() -> anyhow::Result<String> {
130    let identity = current_process_identity()?;
131    Ok(format!("{}-{}", identity.host_pid, identity.starttime))
132}
133
134fn parse_pin_dir_name(name: &str) -> Option<(u32, u64)> {
135    let (pid, starttime) = name.split_once('-')?;
136    let pid = pid.parse::<u32>().ok()?;
137    let starttime = starttime.parse::<u64>().ok()?;
138    Some((pid, starttime))
139}
140
141/// Compute the bpffs pin path for the proc_module_offsets map for current process
142/// Using per-process directory avoids conflicts across multiple GhostScope instances
143pub fn proc_offsets_pin_path() -> anyhow::Result<PathBuf> {
144    Ok(PathBuf::from(format!(
145        "{BPFFS_ROOT}/{}/proc_module_offsets",
146        current_process_dir_name()?
147    )))
148}
149
150/// Pin directory containing the per-process offsets map
151pub fn proc_offsets_pin_dir() -> anyhow::Result<PathBuf> {
152    proc_offsets_pin_path()?
153        .parent()
154        .map(|p| p.to_path_buf())
155        .ok_or_else(|| anyhow::anyhow!("bpffs root has no parent for proc offsets pin path"))
156}
157
158/// Map name as embedded in BPF object
159pub const PROC_OFFSETS_MAP_NAME: &str = "proc_module_offsets";
160pub const PROC_MODULE_RANGE_META_MAP_NAME: &str = "proc_module_range_meta";
161pub const PROC_MODULE_RANGES_MAP_NAME: &str = "proc_module_ranges";
162pub const ALLOWED_PIDS_MAP_NAME: &str = "allowed_pids";
163pub const PID_ALIASES_MAP_NAME: &str = "pid_aliases";
164pub const TARGET_EXEC_COMM_MAP_NAME: &str = "target_exec_comm";
165pub const SYSMON_MAP_CHANGE_UNFILTERED_MAP_NAME: &str = "sysmon_map_change_unfiltered";
166pub const BT_UNWIND_ROWS_MAP_NAME: &str = "bt_unwind_rows";
167pub const BT_MODULE_ROW_RANGES_MAP_NAME: &str = "bt_module_row_ranges";
168
169fn bpffs_is_mounted() -> bool {
170    let Ok(mountinfo) = std::fs::read_to_string("/proc/self/mountinfo") else {
171        return false;
172    };
173    mountinfo.lines().any(|line| {
174        let Some((left, right)) = line.split_once(" - ") else {
175            return false;
176        };
177        let mount_point = left.split_whitespace().nth(4);
178        let fs_type = right.split_whitespace().next();
179        mount_point == Some(BPFFS_MOUNT_POINT) && fs_type == Some("bpf")
180    })
181}
182
183fn bpffs_mount_hint_for_state(
184    pin_path: &Path,
185    bpffs_mount_point_exists: bool,
186    bpffs_mounted: bool,
187) -> Option<String> {
188    if !pin_path.starts_with(BPFFS_ROOT) {
189        return None;
190    }
191
192    if bpffs_mounted {
193        return None;
194    }
195
196    if !bpffs_mount_point_exists {
197        return Some(format!(
198            "GhostScope requires bpffs mounted at {BPFFS_MOUNT_POINT} to pin BPF maps under {BPFFS_ROOT}. That mount point does not exist. Try: `sudo mkdir -p {BPFFS_MOUNT_POINT} && sudo mount -t bpf bpf {BPFFS_MOUNT_POINT}`."
199        ));
200    }
201
202    Some(format!(
203        "GhostScope requires bpffs mounted at {BPFFS_MOUNT_POINT} to pin BPF maps under {BPFFS_ROOT}. Some systems, including WSL2 and minimal/container environments, do not mount it by default. Try: `sudo mount -t bpf bpf {BPFFS_MOUNT_POINT}` and verify with `mount | grep bpf`."
204    ))
205}
206
207pub fn bpffs_mount_hint_for_pin_path(pin_path: &Path) -> Option<String> {
208    bpffs_mount_hint_for_state(
209        pin_path,
210        Path::new(BPFFS_MOUNT_POINT).exists(),
211        bpffs_is_mounted(),
212    )
213}
214
215pub use ghostscope_protocol::{
216    PidAliasValue, ProcModuleKey, ProcModuleOffsetsValue, ProcModuleRangeKey, ProcModuleRangeMeta,
217    ProcModuleRangeValue,
218};
219
220fn proc_offsets_pin_layout_matches(map: &MapData) -> bool {
221    match map.info() {
222        Ok(info) => {
223            info.key_size() == ghostscope_protocol::PROC_MODULE_KEY_SIZE as u32
224                && info.value_size() == ghostscope_protocol::PROC_MODULE_OFFSETS_VALUE_SIZE as u32
225        }
226        Err(e) => {
227            warn!("Unable to inspect pinned proc_module_offsets map layout: {e}");
228            false
229        }
230    }
231}
232
233fn map_pin_layout_matches(
234    map: &MapData,
235    map_name: &str,
236    key_size: u32,
237    value_size: u32,
238    min_entries: Option<u32>,
239) -> bool {
240    match map.info() {
241        Ok(info) => {
242            info.key_size() == key_size
243                && info.value_size() == value_size
244                && min_entries.is_none_or(|entries| info.max_entries() >= entries)
245        }
246        Err(e) => {
247            warn!("Unable to inspect pinned {map_name} map layout: {e}");
248            false
249        }
250    }
251}
252
253fn create_and_pin_hash_map(
254    map_name: &str,
255    pin_path: &Path,
256    key_size: u32,
257    value_size: u32,
258    max_entries: u32,
259) -> anyhow::Result<()> {
260    ensure_pin_dir(pin_path).map_err(|e| {
261        let hint = bpffs_mount_hint_for_pin_path(pin_path)
262            .map(|hint| format!(" {hint}"))
263            .unwrap_or_default();
264        anyhow::anyhow!(
265            "Failed to create pin directory for {} at {}: {}.{}",
266            map_name,
267            pin_path.display(),
268            e,
269            hint
270        )
271    })?;
272
273    if pin_path.exists() {
274        match MapData::from_pin(pin_path) {
275            Ok(map)
276                if map_pin_layout_matches(
277                    &map,
278                    map_name,
279                    key_size,
280                    value_size,
281                    Some(max_entries),
282                ) =>
283            {
284                info!(
285                    "Reusing existing pinned map at {} (layout ok)",
286                    pin_path.display()
287                );
288                return Ok(());
289            }
290            Ok(_) => {
291                warn!(
292                    "Pinned {} at {} has stale ABI layout; recreating",
293                    map_name,
294                    pin_path.display()
295                );
296                let _ = std::fs::remove_file(pin_path);
297            }
298            Err(_) => {
299                let _ = std::fs::remove_file(pin_path);
300            }
301        }
302    }
303
304    let obj_map = ObjMap::Legacy(LegacyMap {
305        section_index: 0,
306        section_kind: EbpfSectionKind::Maps,
307        symbol_index: None,
308        def: bpf_map_def {
309            map_type: BPF_MAP_TYPE_HASH as u32,
310            key_size,
311            value_size,
312            max_entries,
313            map_flags: 0,
314            id: 0,
315            pinning: aya_obj::maps::PinningType::None,
316        },
317        inner_def: None,
318        data: Vec::new(),
319    });
320
321    let map = MapData::create(obj_map, map_name, None)?;
322    info!("Created {map_name} map with capacity {max_entries} entries");
323
324    match map.pin(pin_path) {
325        Ok(()) => {
326            info!("Pinned {} at {}", map_name, pin_path.display());
327            Ok(())
328        }
329        Err(e) => match MapData::from_pin(pin_path) {
330            Ok(map)
331                if map_pin_layout_matches(
332                    &map,
333                    map_name,
334                    key_size,
335                    value_size,
336                    Some(max_entries),
337                ) =>
338            {
339                info!(
340                    "Pin path {} already exists; reusing existing map ({}).",
341                    pin_path.display(),
342                    e
343                );
344                Ok(())
345            }
346            Ok(_) | Err(_) => {
347                let _ = std::fs::remove_file(pin_path);
348                let hint = bpffs_mount_hint_for_pin_path(pin_path)
349                    .map(|hint| format!(" {hint}"))
350                    .unwrap_or_default();
351                Err(anyhow::anyhow!(
352                    "Failed to pin {} at {}: {}",
353                    map_name,
354                    pin_path.display(),
355                    e
356                )
357                .context(format!("Unable to persist {map_name} in bpffs.{hint}")))
358            }
359        },
360    }
361}
362
363fn create_and_pin_array_map(
364    map_name: &str,
365    pin_path: &Path,
366    value_size: u32,
367    max_entries: u32,
368) -> anyhow::Result<bool> {
369    ensure_pin_dir(pin_path).map_err(|e| {
370        let hint = bpffs_mount_hint_for_pin_path(pin_path)
371            .map(|hint| format!(" {hint}"))
372            .unwrap_or_default();
373        anyhow::anyhow!(
374            "Failed to create pin directory for {} at {}: {}.{}",
375            map_name,
376            pin_path.display(),
377            e,
378            hint
379        )
380    })?;
381
382    if pin_path.exists() {
383        match MapData::from_pin(pin_path) {
384            Ok(map) if map_pin_layout_matches(&map, map_name, 4, value_size, Some(max_entries)) => {
385                info!(
386                    "Reusing existing pinned map at {} (layout ok)",
387                    pin_path.display()
388                );
389                return Ok(false);
390            }
391            Ok(_) => {
392                warn!(
393                    "Pinned {} at {} has stale ABI layout; recreating",
394                    map_name,
395                    pin_path.display()
396                );
397                let _ = std::fs::remove_file(pin_path);
398            }
399            Err(_) => {
400                let _ = std::fs::remove_file(pin_path);
401            }
402        }
403    }
404
405    let obj_map = ObjMap::Legacy(LegacyMap {
406        section_index: 0,
407        section_kind: EbpfSectionKind::Maps,
408        symbol_index: None,
409        def: bpf_map_def {
410            map_type: BPF_MAP_TYPE_ARRAY as u32,
411            key_size: 4,
412            value_size,
413            max_entries,
414            map_flags: 0,
415            id: 0,
416            pinning: aya_obj::maps::PinningType::None,
417        },
418        inner_def: None,
419        data: Vec::new(),
420    });
421
422    let map = MapData::create(obj_map, map_name, None)?;
423    info!("Created {map_name} map with capacity {max_entries} entries");
424
425    match map.pin(pin_path) {
426        Ok(()) => {
427            info!("Pinned {} at {}", map_name, pin_path.display());
428            Ok(true)
429        }
430        Err(e) => match MapData::from_pin(pin_path) {
431            Ok(map) if map_pin_layout_matches(&map, map_name, 4, value_size, Some(max_entries)) => {
432                info!(
433                    "Pin path {} already exists; reusing existing map ({}).",
434                    pin_path.display(),
435                    e
436                );
437                Ok(false)
438            }
439            Ok(_) | Err(_) => {
440                let _ = std::fs::remove_file(pin_path);
441                let hint = bpffs_mount_hint_for_pin_path(pin_path)
442                    .map(|hint| format!(" {hint}"))
443                    .unwrap_or_default();
444                Err(anyhow::anyhow!(
445                    "Failed to pin {} at {}: {}",
446                    map_name,
447                    pin_path.display(),
448                    e
449                )
450                .context(format!("Unable to persist {map_name} in bpffs.{hint}")))
451            }
452        },
453    }
454}
455
456fn ensure_pin_dir(path: &Path) -> std::io::Result<()> {
457    if let Some(dir) = path.parent() {
458        std::fs::create_dir_all(dir)
459    } else {
460        Ok(())
461    }
462}
463
464/// Ensure the pinned global proc_module_offsets map exists at the standard path.
465/// If not present, create and pin it with the specified capacity.
466pub fn ensure_pinned_proc_offsets_exists(max_entries: u32) -> anyhow::Result<()> {
467    let pin_path = proc_offsets_pin_path()?;
468    // Ensure parent dir exists
469    ensure_pin_dir(&pin_path).map_err(|e| {
470        let hint = bpffs_mount_hint_for_pin_path(&pin_path)
471            .map(|hint| format!(" {hint}"))
472            .unwrap_or_default();
473        anyhow::anyhow!(
474            "Failed to create pin directory for {} at {}: {}.{}",
475            PROC_OFFSETS_MAP_NAME,
476            pin_path.display(),
477            e,
478            hint
479        )
480    })?;
481
482    // If pinned file already exists, reuse it only when the ABI layout matches.
483    if pin_path.exists() {
484        match MapData::from_pin(&pin_path) {
485            Ok(map) if proc_offsets_pin_layout_matches(&map) => {
486                info!(
487                    "Reusing existing pinned map at {} (layout ok)",
488                    pin_path.display()
489                );
490                return Ok(());
491            }
492            Ok(_) => {
493                warn!(
494                    "Pinned {} at {} has stale ABI layout; recreating",
495                    PROC_OFFSETS_MAP_NAME,
496                    pin_path.display()
497                );
498                let _ = std::fs::remove_file(&pin_path);
499            }
500            Err(_) => {
501                // Stale/corrupted pin path, remove and recreate
502                let _ = std::fs::remove_file(&pin_path);
503            }
504        }
505    }
506
507    // Define the map as a legacy map (compatible with Aya expectations)
508    let obj_map = ObjMap::Legacy(LegacyMap {
509        section_index: 0,
510        section_kind: EbpfSectionKind::Maps,
511        symbol_index: None,
512        def: bpf_map_def {
513            map_type: BPF_MAP_TYPE_HASH as u32,
514            key_size: ghostscope_protocol::PROC_MODULE_KEY_SIZE as u32,
515            value_size: ghostscope_protocol::PROC_MODULE_OFFSETS_VALUE_SIZE as u32,
516            max_entries,
517            map_flags: 0,
518            id: 0,
519            pinning: aya_obj::maps::PinningType::None,
520        },
521        inner_def: None,
522        data: Vec::new(),
523    });
524
525    // Create the map in kernel
526    let map = MapData::create(obj_map, PROC_OFFSETS_MAP_NAME, None)?;
527    info!(
528        "Created {} map with capacity {} entries",
529        PROC_OFFSETS_MAP_NAME, max_entries
530    );
531
532    // Pin to bpffs for global reuse; handle races safely
533    match map.pin(&pin_path) {
534        Ok(()) => {
535            info!("Pinned {} at {}", PROC_OFFSETS_MAP_NAME, pin_path.display());
536            Ok(())
537        }
538        Err(e) => {
539            // If another thread/process pinned concurrently, reuse the existing pin
540            match MapData::from_pin(&pin_path) {
541                Ok(map) if proc_offsets_pin_layout_matches(&map) => {
542                    info!(
543                        "Pin path {} already exists; reusing existing map ({}).",
544                        pin_path.display(),
545                        e
546                    );
547                    Ok(())
548                }
549                Ok(_) | Err(_) => {
550                    // Best-effort cleanup and propagate error
551                    let _ = std::fs::remove_file(&pin_path);
552                    let hint = bpffs_mount_hint_for_pin_path(&pin_path)
553                        .map(|hint| format!(" {hint}"))
554                        .unwrap_or_default();
555                    Err(anyhow::anyhow!(
556                        "Failed to pin {} at {}: {}",
557                        PROC_OFFSETS_MAP_NAME,
558                        pin_path.display(),
559                        e
560                    )
561                    .context(format!(
562                        "Unable to persist {PROC_OFFSETS_MAP_NAME} in bpffs.{hint}"
563                    )))
564                }
565            }
566        }
567    }
568}
569
570fn open_pinned_hash_map<K, V>(path: PathBuf) -> anyhow::Result<AyaHashMap<MapData, K, V>>
571where
572    K: aya::Pod,
573    V: aya::Pod,
574{
575    let map_data = MapData::from_pin(path)?;
576    let map = Map::from_map_data(map_data)?;
577    Ok(AyaHashMap::try_from(map)?)
578}
579
580/// Compute the bpffs pin path for the allowed_pids map for current process
581pub fn allowed_pids_pin_path() -> anyhow::Result<PathBuf> {
582    Ok(PathBuf::from(format!(
583        "{BPFFS_ROOT}/{}/allowed_pids",
584        current_process_dir_name()?
585    )))
586}
587
588/// Compute the bpffs pin path for the pid_aliases map for current process.
589pub fn pid_aliases_pin_path() -> anyhow::Result<PathBuf> {
590    Ok(PathBuf::from(format!(
591        "{BPFFS_ROOT}/{}/pid_aliases",
592        current_process_dir_name()?
593    )))
594}
595
596/// Compute the bpffs pin path for the proc_module_range_meta map.
597pub fn proc_module_range_meta_pin_path() -> anyhow::Result<PathBuf> {
598    Ok(PathBuf::from(format!(
599        "{BPFFS_ROOT}/{}/proc_module_range_meta",
600        current_process_dir_name()?
601    )))
602}
603
604/// Compute the bpffs pin path for the proc_module_ranges map.
605pub fn proc_module_ranges_pin_path() -> anyhow::Result<PathBuf> {
606    Ok(PathBuf::from(format!(
607        "{BPFFS_ROOT}/{}/proc_module_ranges",
608        current_process_dir_name()?
609    )))
610}
611
612pub fn bt_unwind_rows_pin_path() -> anyhow::Result<PathBuf> {
613    Ok(PathBuf::from(format!(
614        "{BPFFS_ROOT}/{}/bt_unwind_rows",
615        current_process_dir_name()?
616    )))
617}
618
619pub fn bt_module_row_ranges_pin_path() -> anyhow::Result<PathBuf> {
620    Ok(PathBuf::from(format!(
621        "{BPFFS_ROOT}/{}/bt_module_row_ranges",
622        current_process_dir_name()?
623    )))
624}
625
626pub fn proc_module_ranges_max_entries(proc_offsets_max_entries: u32) -> u32 {
627    proc_offsets_max_entries.saturating_mul(2).max(1)
628}
629
630/// Ensure the pinned allowed_pids map exists under the per-process directory.
631pub fn ensure_pinned_allowed_pids_exists(max_entries: u32) -> anyhow::Result<()> {
632    let pin_path = allowed_pids_pin_path()?;
633    ensure_pin_dir(&pin_path).map_err(|e| {
634        let hint = bpffs_mount_hint_for_pin_path(&pin_path)
635            .map(|hint| format!(" {hint}"))
636            .unwrap_or_default();
637        anyhow::anyhow!(
638            "Failed to create pin directory for {} at {}: {}.{}",
639            ALLOWED_PIDS_MAP_NAME,
640            pin_path.display(),
641            e,
642            hint
643        )
644    })?;
645
646    if pin_path.exists() {
647        if MapData::from_pin(&pin_path).is_ok() {
648            info!("Reusing existing pinned map at {}", pin_path.display());
649            return Ok(());
650        } else {
651            let _ = std::fs::remove_file(&pin_path);
652        }
653    }
654
655    let obj_map = ObjMap::Legacy(LegacyMap {
656        section_index: 0,
657        section_kind: EbpfSectionKind::Maps,
658        symbol_index: None,
659        def: bpf_map_def {
660            map_type: BPF_MAP_TYPE_HASH as u32,
661            key_size: 4,
662            value_size: 1,
663            max_entries,
664            map_flags: 0,
665            id: 0,
666            pinning: aya_obj::maps::PinningType::None,
667        },
668        inner_def: None,
669        data: Vec::new(),
670    });
671
672    let map = MapData::create(obj_map, ALLOWED_PIDS_MAP_NAME, None)?;
673    info!(
674        "Created {} map with capacity {} entries",
675        ALLOWED_PIDS_MAP_NAME, max_entries
676    );
677
678    match map.pin(&pin_path) {
679        Ok(()) => {
680            info!("Pinned {} at {}", ALLOWED_PIDS_MAP_NAME, pin_path.display());
681            Ok(())
682        }
683        Err(e) => match MapData::from_pin(&pin_path) {
684            Ok(_) => {
685                info!(
686                    "Pin path {} already exists; reusing existing map ({}).",
687                    pin_path.display(),
688                    e
689                );
690                Ok(())
691            }
692            Err(_) => {
693                let _ = std::fs::remove_file(&pin_path);
694                let hint = bpffs_mount_hint_for_pin_path(&pin_path)
695                    .map(|hint| format!(" {hint}"))
696                    .unwrap_or_default();
697                Err(anyhow::anyhow!(
698                    "Failed to pin {} at {}: {}",
699                    ALLOWED_PIDS_MAP_NAME,
700                    pin_path.display(),
701                    e
702                )
703                .context(format!(
704                    "Unable to persist {ALLOWED_PIDS_MAP_NAME} in bpffs.{hint}"
705                )))
706            }
707        },
708    }
709}
710
711/// Ensure the pinned pid_aliases map exists under the per-process directory.
712pub fn ensure_pinned_pid_aliases_exists(max_entries: u32) -> anyhow::Result<()> {
713    let pin_path = pid_aliases_pin_path()?;
714    ensure_pin_dir(&pin_path).map_err(|e| {
715        let hint = bpffs_mount_hint_for_pin_path(&pin_path)
716            .map(|hint| format!(" {hint}"))
717            .unwrap_or_default();
718        anyhow::anyhow!(
719            "Failed to create pin directory for {} at {}: {}.{}",
720            PID_ALIASES_MAP_NAME,
721            pin_path.display(),
722            e,
723            hint
724        )
725    })?;
726
727    if pin_path.exists() {
728        if MapData::from_pin(&pin_path).is_ok() {
729            info!("Reusing existing pinned map at {}", pin_path.display());
730            return Ok(());
731        } else {
732            let _ = std::fs::remove_file(&pin_path);
733        }
734    }
735
736    let obj_map = ObjMap::Legacy(LegacyMap {
737        section_index: 0,
738        section_kind: EbpfSectionKind::Maps,
739        symbol_index: None,
740        def: bpf_map_def {
741            map_type: BPF_MAP_TYPE_HASH as u32,
742            key_size: std::mem::size_of::<u32>() as u32,
743            value_size: ghostscope_protocol::PID_ALIAS_VALUE_SIZE as u32,
744            max_entries,
745            map_flags: 0,
746            id: 0,
747            pinning: aya_obj::maps::PinningType::None,
748        },
749        inner_def: None,
750        data: Vec::new(),
751    });
752
753    let map = MapData::create(obj_map, PID_ALIASES_MAP_NAME, None)?;
754    info!(
755        "Created {} map with capacity {} entries",
756        PID_ALIASES_MAP_NAME, max_entries
757    );
758
759    match map.pin(&pin_path) {
760        Ok(()) => {
761            info!("Pinned {} at {}", PID_ALIASES_MAP_NAME, pin_path.display());
762            Ok(())
763        }
764        Err(e) => match MapData::from_pin(&pin_path) {
765            Ok(_) => {
766                info!(
767                    "Pin path {} already exists; reusing existing map ({}).",
768                    pin_path.display(),
769                    e
770                );
771                Ok(())
772            }
773            Err(_) => {
774                let _ = std::fs::remove_file(&pin_path);
775                let hint = bpffs_mount_hint_for_pin_path(&pin_path)
776                    .map(|hint| format!(" {hint}"))
777                    .unwrap_or_default();
778                Err(anyhow::anyhow!(
779                    "Failed to pin {} at {}: {}",
780                    PID_ALIASES_MAP_NAME,
781                    pin_path.display(),
782                    e
783                )
784                .context(format!(
785                    "Unable to persist {PID_ALIASES_MAP_NAME} in bpffs.{hint}"
786                )))
787            }
788        },
789    }
790}
791
792/// Ensure the pinned module range maps exist under the per-process directory.
793pub fn ensure_pinned_proc_module_ranges_exist(max_entries: u32) -> anyhow::Result<()> {
794    let meta_pin_path = proc_module_range_meta_pin_path()?;
795    create_and_pin_hash_map(
796        PROC_MODULE_RANGE_META_MAP_NAME,
797        &meta_pin_path,
798        std::mem::size_of::<u32>() as u32,
799        ghostscope_protocol::PROC_MODULE_RANGE_META_SIZE as u32,
800        max_entries.max(1),
801    )
802    .with_context(|| {
803        format!(
804            "Unable to prepare pinned {} map at {}",
805            PROC_MODULE_RANGE_META_MAP_NAME,
806            meta_pin_path.display()
807        )
808    })?;
809
810    let ranges_pin_path = proc_module_ranges_pin_path()?;
811    create_and_pin_hash_map(
812        PROC_MODULE_RANGES_MAP_NAME,
813        &ranges_pin_path,
814        ghostscope_protocol::PROC_MODULE_RANGE_KEY_SIZE as u32,
815        ghostscope_protocol::PROC_MODULE_RANGE_VALUE_SIZE as u32,
816        proc_module_ranges_max_entries(max_entries),
817    )
818    .with_context(|| {
819        format!(
820            "Unable to prepare pinned {} map at {}",
821            PROC_MODULE_RANGES_MAP_NAME,
822            ranges_pin_path.display()
823        )
824    })
825}
826
827/// Ensure the session-shared backtrace CFI maps exist under the per-process
828/// pin directory. These maps are shared by all trace loaders in one
829/// GhostScope process when backtrace rows are module-normalized.
830pub fn ensure_pinned_backtrace_cfi_maps_exist(
831    unwind_row_entries: u32,
832    module_entries: u32,
833) -> anyhow::Result<()> {
834    let rows_pin_path = bt_unwind_rows_pin_path()?;
835    let rows_created = create_and_pin_array_map(
836        BT_UNWIND_ROWS_MAP_NAME,
837        &rows_pin_path,
838        ghostscope_protocol::BACKTRACE_UNWIND_ROW_SIZE as u32,
839        unwind_row_entries.max(1),
840    )
841    .with_context(|| {
842        format!(
843            "Unable to prepare pinned {} map at {}",
844            BT_UNWIND_ROWS_MAP_NAME,
845            rows_pin_path.display()
846        )
847    })?;
848
849    let ranges_pin_path = bt_module_row_ranges_pin_path()?;
850    if rows_created && ranges_pin_path.exists() {
851        match std::fs::remove_file(&ranges_pin_path) {
852            Ok(()) => {}
853            Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
854            Err(error) => {
855                return Err(error).with_context(|| {
856                    format!(
857                        "Unable to remove stale {} map at {} after recreating {}",
858                        BT_MODULE_ROW_RANGES_MAP_NAME,
859                        ranges_pin_path.display(),
860                        BT_UNWIND_ROWS_MAP_NAME
861                    )
862                });
863            }
864        }
865        info!(
866            "Recreating {} because {} was recreated at {}",
867            BT_MODULE_ROW_RANGES_MAP_NAME,
868            BT_UNWIND_ROWS_MAP_NAME,
869            rows_pin_path.display()
870        );
871    }
872    create_and_pin_hash_map(
873        BT_MODULE_ROW_RANGES_MAP_NAME,
874        &ranges_pin_path,
875        std::mem::size_of::<u64>() as u32,
876        ghostscope_protocol::BACKTRACE_MODULE_ROW_RANGE_SIZE as u32,
877        module_entries.max(1),
878    )
879    .with_context(|| {
880        format!(
881            "Unable to prepare pinned {} map at {}",
882            BT_MODULE_ROW_RANGES_MAP_NAME,
883            ranges_pin_path.display()
884        )
885    })
886}
887
888/// Insert a PID into the allowed_pids pinned map.
889pub fn insert_allowed_pid(pid: u32) -> anyhow::Result<()> {
890    let mut map = open_pinned_hash_map::<u32, u8>(allowed_pids_pin_path()?)?;
891    map.insert(pid, 1, 0)
892        .map_err(|e| anyhow::anyhow!("allowed_pids update failed for {}: {}", pid, e))
893}
894
895/// Return whether a PID is already present in the allowed_pids pinned map.
896pub fn allowed_pid_exists(pid: u32) -> anyhow::Result<bool> {
897    let map = open_pinned_hash_map::<u32, u8>(allowed_pids_pin_path()?)?;
898    match map.get(&pid, 0) {
899        Ok(_) => Ok(true),
900        Err(MapError::KeyNotFound) => Ok(false),
901        Err(e) => Err(anyhow::anyhow!(
902            "allowed_pids lookup failed for {}: {}",
903            pid,
904            e
905        )),
906    }
907}
908
909/// Remove a PID from the allowed_pids pinned map.
910pub fn remove_allowed_pid(pid: u32) -> anyhow::Result<()> {
911    let mut map = open_pinned_hash_map::<u32, u8>(allowed_pids_pin_path()?)?;
912    map.remove(&pid)
913        .map_err(|e| anyhow::anyhow!("allowed_pids delete failed for {}: {}", pid, e))
914}
915
916/// Insert a runtime-pid -> proc-pid alias into the pinned pid_aliases map.
917pub fn insert_pid_alias(runtime_pid: u32, proc_pid: u32) -> anyhow::Result<()> {
918    let mut map = open_pinned_hash_map::<u32, PidAliasValue>(pid_aliases_pin_path()?)?;
919    let val = PidAliasValue { proc_pid };
920    map.insert(runtime_pid, val, 0).map_err(|e| {
921        anyhow::anyhow!(
922            "pid_aliases update failed for runtime_pid={} proc_pid={}: {}",
923            runtime_pid,
924            proc_pid,
925            e
926        )
927    })
928}
929
930/// Remove a runtime-pid alias from the pinned pid_aliases map.
931pub fn remove_pid_alias(runtime_pid: u32) -> anyhow::Result<()> {
932    let mut map = open_pinned_hash_map::<u32, PidAliasValue>(pid_aliases_pin_path()?)?;
933    map.remove(&runtime_pid).map_err(|e| {
934        anyhow::anyhow!(
935            "pid_aliases delete failed for runtime_pid={}: {}",
936            runtime_pid,
937            e
938        )
939    })
940}
941
942/// Purge all entries for a given pid in the pinned proc_module_offsets map.
943pub fn purge_offsets_for_pid(pid: u32) -> anyhow::Result<usize> {
944    let mut map =
945        open_pinned_hash_map::<ProcModuleKey, ProcModuleOffsetsValue>(proc_offsets_pin_path()?)?;
946    let mut deleted = 0usize;
947
948    let keys: Vec<ProcModuleKey> = map.keys().collect::<Result<_, _>>()?;
949    for key in keys {
950        if key.pid == pid {
951            map.remove(&key).map_err(|e| {
952                anyhow::anyhow!(
953                    "proc_module_offsets delete failed for pid={} cookie=0x{:08x}{:08x}: {}",
954                    pid,
955                    key.cookie_hi,
956                    key.cookie_lo,
957                    e
958                )
959            })?;
960            deleted += 1;
961        }
962    }
963
964    Ok(deleted)
965}
966
967fn purge_ranges_for_pid_slot(
968    map: &mut AyaHashMap<MapData, ProcModuleRangeKey, ProcModuleRangeValue>,
969    pid: u32,
970    slot: u32,
971) -> anyhow::Result<usize> {
972    let keys: Vec<ProcModuleRangeKey> = map.keys().collect::<Result<_, _>>()?;
973    let mut deleted = 0usize;
974    for key in keys {
975        if key.pid == pid && key.slot == slot {
976            map.remove(&key).map_err(|e| {
977                anyhow::anyhow!(
978                    "proc_module_ranges delete failed for pid={} slot={} index={}: {}",
979                    pid,
980                    key.slot,
981                    key.index,
982                    e
983                )
984            })?;
985            deleted += 1;
986        }
987    }
988    Ok(deleted)
989}
990
991/// Purge all module range index entries for a given pid.
992pub fn purge_ranges_for_pid(pid: u32) -> anyhow::Result<usize> {
993    let mut ranges = open_pinned_hash_map::<ProcModuleRangeKey, ProcModuleRangeValue>(
994        proc_module_ranges_pin_path()?,
995    )?;
996    let keys: Vec<ProcModuleRangeKey> = ranges.keys().collect::<Result<_, _>>()?;
997    let mut deleted = 0usize;
998    for key in keys {
999        if key.pid == pid {
1000            ranges.remove(&key).map_err(|e| {
1001                anyhow::anyhow!(
1002                    "proc_module_ranges delete failed for pid={} slot={} index={}: {}",
1003                    pid,
1004                    key.slot,
1005                    key.index,
1006                    e
1007                )
1008            })?;
1009            deleted += 1;
1010        }
1011    }
1012
1013    let mut meta =
1014        open_pinned_hash_map::<u32, ProcModuleRangeMeta>(proc_module_range_meta_pin_path()?)?;
1015    let _ = meta.remove(&pid);
1016    Ok(deleted)
1017}
1018
1019/// Open the pinned global proc_module_offsets map and insert entries.
1020pub fn insert_offsets_for_pid(
1021    pid: u32,
1022    items: &[(u64, ProcModuleOffsetsValue)],
1023) -> anyhow::Result<usize> {
1024    let mut map =
1025        open_pinned_hash_map::<ProcModuleKey, ProcModuleOffsetsValue>(proc_offsets_pin_path()?)?;
1026    let mut inserted = 0usize;
1027    for (cookie, off) in items {
1028        let key = ProcModuleKey {
1029            pid,
1030            pad: 0,
1031            cookie_lo: (*cookie & 0xffff_ffff) as u32,
1032            cookie_hi: (*cookie >> 32) as u32,
1033        };
1034        match map.insert(key, *off, 0) {
1035            Ok(()) => {
1036                tracing::debug!(
1037                    "proc_module_offsets insert ok: pid={} cookie=0x{:08x}{:08x} text=0x{:x} rodata=0x{:x} data=0x{:x} bss=0x{:x} base=0x{:x} size=0x{:x}",
1038                    pid, key.cookie_hi, key.cookie_lo, off.text, off.rodata, off.data, off.bss, off.base, off.size
1039                );
1040                inserted += 1
1041            }
1042            Err(e) => warn!(
1043                "proc_module_offsets insert failed for pid={} cookie=0x{:08x}{:08x}: {}",
1044                pid, key.cookie_hi, key.cookie_lo, e
1045            ),
1046        }
1047    }
1048    Ok(inserted)
1049}
1050
1051/// Replace the raw-address range index for a PID using a full `/proc/<pid>/maps`
1052/// snapshot. The inactive slot is rewritten first, then the PID meta entry is
1053/// flipped so eBPF readers either see the old complete snapshot or the new one.
1054pub fn replace_ranges_for_pid(
1055    pid: u32,
1056    items: &[(u64, ProcModuleOffsetsValue)],
1057) -> anyhow::Result<usize> {
1058    let mut meta =
1059        open_pinned_hash_map::<u32, ProcModuleRangeMeta>(proc_module_range_meta_pin_path()?)?;
1060    let active_slot = meta
1061        .get(&pid, 0)
1062        .map(|value| value.active_slot & 1)
1063        .unwrap_or(0);
1064    let inactive_slot = active_slot ^ 1;
1065
1066    let mut ranges = open_pinned_hash_map::<ProcModuleRangeKey, ProcModuleRangeValue>(
1067        proc_module_ranges_pin_path()?,
1068    )?;
1069    let purged = purge_ranges_for_pid_slot(&mut ranges, pid, inactive_slot)?;
1070    if purged > 0 {
1071        tracing::debug!(
1072            "proc_module_ranges purged {} inactive-slot entries for pid={} slot={}",
1073            purged,
1074            pid,
1075            inactive_slot
1076        );
1077    }
1078
1079    let mut values = items
1080        .iter()
1081        .filter_map(|(cookie, offsets)| {
1082            let end = offsets.base.checked_add(offsets.size)?;
1083            (offsets.base < end)
1084                .then(|| ProcModuleRangeValue::new(offsets.base, end, offsets.text, *cookie))
1085        })
1086        .collect::<Vec<_>>();
1087    values.sort_by_key(|value| (value.base, value.end, value.module_cookie()));
1088
1089    let mut inserted = 0usize;
1090    for (index, value) in values.iter().enumerate() {
1091        let key = ProcModuleRangeKey::new(pid, inactive_slot, index as u32);
1092        match ranges.insert(key, *value, 0) {
1093            Ok(()) => {
1094                tracing::debug!(
1095                    "proc_module_ranges insert ok: pid={} slot={} index={} base=0x{:x} end=0x{:x} text=0x{:x} cookie=0x{:08x}{:08x}",
1096                    pid,
1097                    inactive_slot,
1098                    index,
1099                    value.base,
1100                    value.end,
1101                    value.text,
1102                    value.cookie_hi,
1103                    value.cookie_lo
1104                );
1105                inserted += 1;
1106            }
1107            Err(e) => warn!(
1108                "proc_module_ranges insert failed for pid={} slot={} index={}: {}",
1109                pid, inactive_slot, index, e
1110            ),
1111        }
1112    }
1113
1114    meta.insert(
1115        pid,
1116        ProcModuleRangeMeta::new(inactive_slot, inserted as u32),
1117        0,
1118    )
1119    .map_err(|e| {
1120        anyhow::anyhow!(
1121            "proc_module_range_meta update failed for pid={} slot={} count={}: {}",
1122            pid,
1123            inactive_slot,
1124            inserted,
1125            e
1126        )
1127    })?;
1128
1129    Ok(inserted)
1130}
1131
1132#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1133enum DirCleanupOutcome {
1134    RemovedDir,
1135}
1136
1137fn cleanup_outcome_without_mutation(dir: &Path) -> anyhow::Result<DirCleanupOutcome> {
1138    if let Err(err) = std::fs::metadata(dir) {
1139        if err.kind() != io::ErrorKind::NotFound {
1140            return Err(err.into());
1141        }
1142    }
1143    Ok(DirCleanupOutcome::RemovedDir)
1144}
1145
1146fn cleanup_pinned_maps_in_dir(dir: &Path) -> anyhow::Result<DirCleanupOutcome> {
1147    match std::fs::remove_dir_all(dir) {
1148        Ok(()) => Ok(DirCleanupOutcome::RemovedDir),
1149        Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(DirCleanupOutcome::RemovedDir),
1150        Err(err) => Err(err.into()),
1151    }
1152}
1153
1154fn stale_reason_for_dir<R, S>(
1155    host_pid: u32,
1156    dir_starttime: u64,
1157    current: CurrentProcessIdentity,
1158    resolve_proc_pid: &R,
1159    read_starttime: &S,
1160) -> Option<&'static str>
1161where
1162    R: Fn(u32) -> Option<u32>,
1163    S: Fn(u32) -> io::Result<u64>,
1164{
1165    if current.host_pid_reliable && host_pid == current.host_pid {
1166        return (dir_starttime != current.starttime).then_some("starttime_mismatch");
1167    }
1168
1169    let Some(proc_pid) = resolve_proc_pid(host_pid) else {
1170        return current.initial_pid_namespace.then_some("pid_not_running");
1171    };
1172
1173    match read_starttime(proc_pid) {
1174        Ok(live_starttime) if live_starttime != dir_starttime => Some("starttime_mismatch"),
1175        Ok(_) => None,
1176        Err(_) => current.initial_pid_namespace.then_some("pid_not_running"),
1177    }
1178}
1179
1180fn prune_entry_for_cleanup(
1181    directory: String,
1182    reason: &str,
1183    outcome: DirCleanupOutcome,
1184) -> BpffsPruneEntry {
1185    BpffsPruneEntry {
1186        directory,
1187        status: match outcome {
1188            DirCleanupOutcome::RemovedDir => BpffsPruneStatus::RemoveDir,
1189        },
1190        reason: reason.to_string(),
1191    }
1192}
1193
1194fn prune_pinned_maps_under<R, S>(
1195    root: &Path,
1196    current: CurrentProcessIdentity,
1197    options: &BpffsPruneOptions,
1198    resolve_proc_pid: R,
1199    read_starttime: S,
1200) -> anyhow::Result<BpffsPruneReport>
1201where
1202    R: Fn(u32) -> Option<u32>,
1203    S: Fn(u32) -> io::Result<u64>,
1204{
1205    if let BpffsPruneMode::Instance(instance) = &options.mode {
1206        let path = root.join(instance);
1207        if !path.exists() {
1208            return Err(anyhow::anyhow!(
1209                "bpffs pin directory not found: {}",
1210                path.display()
1211            ));
1212        }
1213        if !path.is_dir() {
1214            return Err(anyhow::anyhow!(
1215                "bpffs pin path is not a directory: {}",
1216                path.display()
1217            ));
1218        }
1219
1220        let outcome = if options.dry_run {
1221            cleanup_outcome_without_mutation(&path)?
1222        } else {
1223            cleanup_pinned_maps_in_dir(&path)?
1224        };
1225
1226        return Ok(BpffsPruneReport {
1227            root: root.to_path_buf(),
1228            dry_run: options.dry_run,
1229            entries: vec![prune_entry_for_cleanup(
1230                instance.clone(),
1231                "explicit_instance",
1232                outcome,
1233            )],
1234        });
1235    }
1236
1237    let mut entries_out = Vec::new();
1238
1239    let entries = match std::fs::read_dir(root) {
1240        Ok(entries) => entries,
1241        Err(err) if err.kind() == io::ErrorKind::NotFound => {
1242            return Ok(BpffsPruneReport {
1243                root: root.to_path_buf(),
1244                dry_run: options.dry_run,
1245                entries: entries_out,
1246            });
1247        }
1248        Err(err) => return Err(err.into()),
1249    };
1250
1251    for entry in entries {
1252        let entry = entry?;
1253        if !entry.file_type()?.is_dir() {
1254            continue;
1255        }
1256
1257        let Some(name) = entry.file_name().to_str().map(str::to_owned) else {
1258            continue;
1259        };
1260        let Some((host_pid, dir_starttime)) = parse_pin_dir_name(&name) else {
1261            entries_out.push(BpffsPruneEntry {
1262                directory: name,
1263                status: BpffsPruneStatus::Ignore,
1264                reason: "non_matching_name".to_string(),
1265            });
1266            continue;
1267        };
1268
1269        let removal_reason = match &options.mode {
1270            BpffsPruneMode::Stale => stale_reason_for_dir(
1271                host_pid,
1272                dir_starttime,
1273                current,
1274                &resolve_proc_pid,
1275                &read_starttime,
1276            ),
1277            BpffsPruneMode::All => Some("force_all"),
1278            BpffsPruneMode::Instance(_) => unreachable!(),
1279        };
1280
1281        let Some(reason) = removal_reason else {
1282            entries_out.push(BpffsPruneEntry {
1283                directory: name,
1284                status: BpffsPruneStatus::SkipLive,
1285                reason: "live_instance".to_string(),
1286            });
1287            continue;
1288        };
1289
1290        let outcome = if options.dry_run {
1291            cleanup_outcome_without_mutation(&entry.path())?
1292        } else {
1293            cleanup_pinned_maps_in_dir(&entry.path())?
1294        };
1295        entries_out.push(prune_entry_for_cleanup(name, reason, outcome));
1296    }
1297
1298    entries_out.sort_by(|left, right| left.directory.cmp(&right.directory));
1299
1300    Ok(BpffsPruneReport {
1301        root: root.to_path_buf(),
1302        dry_run: options.dry_run,
1303        entries: entries_out,
1304    })
1305}
1306
1307fn cleanup_stale_pinned_maps_under<R, S>(
1308    root: &Path,
1309    current: CurrentProcessIdentity,
1310    resolve_proc_pid: R,
1311    read_starttime: S,
1312) -> anyhow::Result<usize>
1313where
1314    R: Fn(u32) -> Option<u32>,
1315    S: Fn(u32) -> io::Result<u64>,
1316{
1317    let report = prune_pinned_maps_under(
1318        root,
1319        current,
1320        &BpffsPruneOptions {
1321            mode: BpffsPruneMode::Stale,
1322            dry_run: false,
1323        },
1324        resolve_proc_pid,
1325        read_starttime,
1326    )?;
1327
1328    Ok(report
1329        .entries
1330        .iter()
1331        .filter(|entry| entry.status == BpffsPruneStatus::RemoveDir)
1332        .count())
1333}
1334
1335/// Remove the current process's pinned maps and its per-process directory (best effort).
1336/// Safe to call multiple times; missing paths are ignored.
1337pub fn cleanup_current_pinned_maps() -> anyhow::Result<()> {
1338    let _ = cleanup_pinned_maps_in_dir(&proc_offsets_pin_dir()?);
1339    Ok(())
1340}
1341
1342/// Remove stale per-process pinned map directories whose PID no longer exists.
1343pub fn cleanup_stale_pinned_maps_root() -> anyhow::Result<usize> {
1344    let current = current_process_identity()?;
1345    cleanup_stale_pinned_maps_under(
1346        Path::new(BPFFS_ROOT),
1347        current,
1348        |host_pid| resolve_proc_pid_for_host_pid(host_pid, current.initial_pid_namespace),
1349        process_starttime,
1350    )
1351}
1352
1353pub fn prune_pinned_maps_root(options: &BpffsPruneOptions) -> anyhow::Result<BpffsPruneReport> {
1354    let current = current_process_identity()?;
1355    prune_pinned_maps_under(
1356        Path::new(BPFFS_ROOT),
1357        current,
1358        options,
1359        |host_pid| resolve_proc_pid_for_host_pid(host_pid, current.initial_pid_namespace),
1360        process_starttime,
1361    )
1362}
1363
1364#[cfg(test)]
1365mod tests {
1366    use super::{
1367        cleanup_pinned_maps_in_dir, cleanup_stale_pinned_maps_under, parse_pin_dir_name,
1368        process_starttime, prune_pinned_maps_under, BpffsPruneMode, BpffsPruneOptions,
1369        BpffsPruneStatus, CurrentProcessIdentity, ALLOWED_PIDS_MAP_NAME, PROC_OFFSETS_MAP_NAME,
1370    };
1371    use std::{fs, io};
1372    use tempfile::tempdir;
1373
1374    fn host_test_identity(host_pid: u32, starttime: u64) -> CurrentProcessIdentity {
1375        CurrentProcessIdentity {
1376            host_pid,
1377            host_pid_reliable: true,
1378            starttime,
1379            initial_pid_namespace: true,
1380        }
1381    }
1382
1383    fn private_ns_test_identity(host_pid: u32, starttime: u64) -> CurrentProcessIdentity {
1384        CurrentProcessIdentity {
1385            host_pid,
1386            host_pid_reliable: false,
1387            starttime,
1388            initial_pid_namespace: false,
1389        }
1390    }
1391
1392    fn simulated_starttime(pid: u32) -> io::Result<u64> {
1393        match pid {
1394            222 => Ok(20),
1395            333 => Ok(30),
1396            _ => Err(io::Error::new(io::ErrorKind::NotFound, "missing pid")),
1397        }
1398    }
1399
1400    #[test]
1401    fn cleanup_removes_known_pinned_maps_and_empty_dir() {
1402        let temp = tempdir().unwrap();
1403        let dir = temp.path().join("1234");
1404        fs::create_dir_all(&dir).unwrap();
1405        fs::write(dir.join(PROC_OFFSETS_MAP_NAME), b"offsets").unwrap();
1406        fs::write(dir.join(ALLOWED_PIDS_MAP_NAME), b"allow").unwrap();
1407
1408        cleanup_pinned_maps_in_dir(&dir).unwrap();
1409
1410        assert!(!dir.exists());
1411    }
1412
1413    #[test]
1414    fn cleanup_removes_dir_even_when_unknown_files_remain() {
1415        let temp = tempdir().unwrap();
1416        let dir = temp.path().join("1234");
1417        fs::create_dir_all(&dir).unwrap();
1418        fs::write(dir.join(PROC_OFFSETS_MAP_NAME), b"offsets").unwrap();
1419        fs::write(dir.join(ALLOWED_PIDS_MAP_NAME), b"allow").unwrap();
1420        let extra = dir.join("keep-me");
1421        fs::write(&extra, b"extra").unwrap();
1422
1423        cleanup_pinned_maps_in_dir(&dir).unwrap();
1424
1425        assert!(!dir.exists());
1426        assert!(!extra.exists());
1427    }
1428
1429    #[test]
1430    fn stale_cleanup_removes_only_dead_pid_dirs() {
1431        let temp = tempdir().unwrap();
1432        let stale_dir = temp.path().join("111-10");
1433        let live_dir = temp.path().join("222-20");
1434        let current_dir = temp.path().join("333-30");
1435        let non_pid_dir = temp.path().join("not-a-pid");
1436
1437        for dir in [&stale_dir, &live_dir, &current_dir, &non_pid_dir] {
1438            fs::create_dir_all(dir).unwrap();
1439            fs::write(dir.join(PROC_OFFSETS_MAP_NAME), b"offsets").unwrap();
1440            fs::write(dir.join(ALLOWED_PIDS_MAP_NAME), b"allow").unwrap();
1441        }
1442
1443        let removed = cleanup_stale_pinned_maps_under(
1444            temp.path(),
1445            host_test_identity(333, 30),
1446            |host_pid| matches!(host_pid, 222 | 333).then_some(host_pid),
1447            simulated_starttime,
1448        )
1449        .unwrap();
1450
1451        assert_eq!(removed, 1);
1452        assert!(!stale_dir.exists());
1453        assert!(live_dir.exists());
1454        assert!(current_dir.exists());
1455        assert!(non_pid_dir.exists());
1456    }
1457
1458    #[test]
1459    fn stale_cleanup_removes_mismatched_starttime_for_reused_pid() {
1460        let temp = tempdir().unwrap();
1461        let stale_current_pid_dir = temp.path().join("333-10");
1462        let current_pid_dir = temp.path().join("333-30");
1463
1464        for dir in [&stale_current_pid_dir, &current_pid_dir] {
1465            fs::create_dir_all(dir).unwrap();
1466            fs::write(dir.join(PROC_OFFSETS_MAP_NAME), b"offsets").unwrap();
1467            fs::write(dir.join(ALLOWED_PIDS_MAP_NAME), b"allow").unwrap();
1468        }
1469
1470        let removed = cleanup_stale_pinned_maps_under(
1471            temp.path(),
1472            host_test_identity(333, 30),
1473            |host_pid| (host_pid == 333).then_some(host_pid),
1474            simulated_starttime,
1475        )
1476        .unwrap();
1477
1478        assert_eq!(removed, 1);
1479        assert!(!stale_current_pid_dir.exists());
1480        assert!(current_pid_dir.exists());
1481    }
1482
1483    #[test]
1484    fn parse_pin_dir_name_requires_pid_starttime_format() {
1485        assert_eq!(parse_pin_dir_name("1234"), None);
1486        assert_eq!(parse_pin_dir_name("1234-5678"), Some((1234, 5678)));
1487        assert_eq!(parse_pin_dir_name("bad"), None);
1488        assert_eq!(parse_pin_dir_name("1234-bad"), None);
1489    }
1490
1491    #[test]
1492    fn stale_cleanup_ignores_legacy_numeric_dirs() {
1493        let temp = tempdir().unwrap();
1494        let legacy_dir = temp.path().join("444");
1495        let current_dir = temp.path().join("333-30");
1496
1497        for dir in [&legacy_dir, &current_dir] {
1498            fs::create_dir_all(dir).unwrap();
1499            fs::write(dir.join(PROC_OFFSETS_MAP_NAME), b"offsets").unwrap();
1500            fs::write(dir.join(ALLOWED_PIDS_MAP_NAME), b"allow").unwrap();
1501        }
1502
1503        let removed = cleanup_stale_pinned_maps_under(
1504            temp.path(),
1505            host_test_identity(333, 30),
1506            |host_pid| matches!(host_pid, 333 | 444).then_some(host_pid),
1507            simulated_starttime,
1508        )
1509        .unwrap();
1510
1511        assert_eq!(removed, 0);
1512        assert!(legacy_dir.exists());
1513        assert!(current_dir.exists());
1514    }
1515
1516    #[test]
1517    fn dry_run_prune_reports_stale_and_keeps_dirs_intact() {
1518        let temp = tempdir().unwrap();
1519        let stale_dir = temp.path().join("111-10");
1520        let live_dir = temp.path().join("222-20");
1521        let legacy_dir = temp.path().join("legacy");
1522
1523        for dir in [&stale_dir, &live_dir, &legacy_dir] {
1524            fs::create_dir_all(dir).unwrap();
1525            fs::write(dir.join(PROC_OFFSETS_MAP_NAME), b"offsets").unwrap();
1526            fs::write(dir.join(ALLOWED_PIDS_MAP_NAME), b"allow").unwrap();
1527        }
1528
1529        let report = prune_pinned_maps_under(
1530            temp.path(),
1531            host_test_identity(333, 30),
1532            &BpffsPruneOptions {
1533                mode: BpffsPruneMode::Stale,
1534                dry_run: true,
1535            },
1536            |host_pid| (host_pid == 222).then_some(host_pid),
1537            simulated_starttime,
1538        )
1539        .unwrap();
1540
1541        assert!(stale_dir.exists());
1542        assert!(live_dir.exists());
1543        assert!(legacy_dir.exists());
1544        assert!(report.entries.iter().any(|entry| {
1545            entry.directory == "111-10"
1546                && entry.status == BpffsPruneStatus::RemoveDir
1547                && entry.reason == "pid_not_running"
1548        }));
1549        assert!(report.entries.iter().any(|entry| {
1550            entry.directory == "222-20"
1551                && entry.status == BpffsPruneStatus::SkipLive
1552                && entry.reason == "live_instance"
1553        }));
1554        assert!(report.entries.iter().any(|entry| {
1555            entry.directory == "legacy"
1556                && entry.status == BpffsPruneStatus::Ignore
1557                && entry.reason == "non_matching_name"
1558        }));
1559    }
1560
1561    #[test]
1562    fn instance_prune_removes_selected_dir_even_when_live() {
1563        let temp = tempdir().unwrap();
1564        let live_dir = temp.path().join("222-20");
1565        fs::create_dir_all(&live_dir).unwrap();
1566        fs::write(live_dir.join(PROC_OFFSETS_MAP_NAME), b"offsets").unwrap();
1567        fs::write(live_dir.join(ALLOWED_PIDS_MAP_NAME), b"allow").unwrap();
1568
1569        let report = prune_pinned_maps_under(
1570            temp.path(),
1571            host_test_identity(333, 30),
1572            &BpffsPruneOptions {
1573                mode: BpffsPruneMode::Instance("222-20".to_string()),
1574                dry_run: false,
1575            },
1576            |_host_pid| Some(222),
1577            simulated_starttime,
1578        )
1579        .unwrap();
1580
1581        assert!(!live_dir.exists());
1582        assert_eq!(report.entries.len(), 1);
1583        assert_eq!(report.entries[0].directory, "222-20");
1584        assert_eq!(report.entries[0].status, BpffsPruneStatus::RemoveDir);
1585        assert_eq!(report.entries[0].reason, "explicit_instance");
1586    }
1587
1588    #[test]
1589    fn force_all_prune_skips_legacy_dirs_but_removes_pid_starttime_dirs() {
1590        let temp = tempdir().unwrap();
1591        let live_dir = temp.path().join("222-20");
1592        let legacy_dir = temp.path().join("222");
1593
1594        for dir in [&live_dir, &legacy_dir] {
1595            fs::create_dir_all(dir).unwrap();
1596            fs::write(dir.join(PROC_OFFSETS_MAP_NAME), b"offsets").unwrap();
1597            fs::write(dir.join(ALLOWED_PIDS_MAP_NAME), b"allow").unwrap();
1598        }
1599
1600        let report = prune_pinned_maps_under(
1601            temp.path(),
1602            host_test_identity(333, 30),
1603            &BpffsPruneOptions {
1604                mode: BpffsPruneMode::All,
1605                dry_run: false,
1606            },
1607            |_host_pid| Some(222),
1608            simulated_starttime,
1609        )
1610        .unwrap();
1611
1612        assert!(!live_dir.exists());
1613        assert!(legacy_dir.exists());
1614        assert!(report.entries.iter().any(|entry| {
1615            entry.directory == "222-20"
1616                && entry.status == BpffsPruneStatus::RemoveDir
1617                && entry.reason == "force_all"
1618        }));
1619        assert!(report.entries.iter().any(|entry| {
1620            entry.directory == "222"
1621                && entry.status == BpffsPruneStatus::Ignore
1622                && entry.reason == "non_matching_name"
1623        }));
1624    }
1625
1626    #[test]
1627    fn stale_prune_skips_unresolvable_host_pid_in_private_namespace() {
1628        let temp = tempdir().unwrap();
1629        let foreign_live_dir = temp.path().join("999-10");
1630        fs::create_dir_all(&foreign_live_dir).unwrap();
1631        fs::write(foreign_live_dir.join(PROC_OFFSETS_MAP_NAME), b"offsets").unwrap();
1632        fs::write(foreign_live_dir.join(ALLOWED_PIDS_MAP_NAME), b"allow").unwrap();
1633
1634        let report = prune_pinned_maps_under(
1635            temp.path(),
1636            private_ns_test_identity(333, 30),
1637            &BpffsPruneOptions {
1638                mode: BpffsPruneMode::Stale,
1639                dry_run: false,
1640            },
1641            |_host_pid| None,
1642            simulated_starttime,
1643        )
1644        .unwrap();
1645
1646        assert!(foreign_live_dir.exists());
1647        assert!(report.entries.iter().any(|entry| {
1648            entry.directory == "999-10"
1649                && entry.status == BpffsPruneStatus::SkipLive
1650                && entry.reason == "live_instance"
1651        }));
1652    }
1653
1654    #[test]
1655    fn stale_prune_skips_same_numeric_pid_when_current_host_pid_is_not_reliable() {
1656        let temp = tempdir().unwrap();
1657        let foreign_live_dir = temp.path().join("333-10");
1658        fs::create_dir_all(&foreign_live_dir).unwrap();
1659        fs::write(foreign_live_dir.join(PROC_OFFSETS_MAP_NAME), b"offsets").unwrap();
1660        fs::write(foreign_live_dir.join(ALLOWED_PIDS_MAP_NAME), b"allow").unwrap();
1661
1662        let report = prune_pinned_maps_under(
1663            temp.path(),
1664            private_ns_test_identity(333, 30),
1665            &BpffsPruneOptions {
1666                mode: BpffsPruneMode::Stale,
1667                dry_run: false,
1668            },
1669            |_host_pid| None,
1670            simulated_starttime,
1671        )
1672        .unwrap();
1673
1674        assert!(foreign_live_dir.exists());
1675        assert!(report.entries.iter().any(|entry| {
1676            entry.directory == "333-10"
1677                && entry.status == BpffsPruneStatus::SkipLive
1678                && entry.reason == "live_instance"
1679        }));
1680    }
1681
1682    #[test]
1683    fn stale_prune_keeps_live_dir_when_host_pid_maps_to_proc_pid() {
1684        let temp = tempdir().unwrap();
1685        let proc_pid = std::process::id();
1686        let proc_starttime = process_starttime(proc_pid).unwrap();
1687        let live_dir = temp.path().join(format!("999-{proc_starttime}"));
1688        fs::create_dir_all(&live_dir).unwrap();
1689        fs::write(live_dir.join(PROC_OFFSETS_MAP_NAME), b"offsets").unwrap();
1690        fs::write(live_dir.join(ALLOWED_PIDS_MAP_NAME), b"allow").unwrap();
1691
1692        let report = prune_pinned_maps_under(
1693            temp.path(),
1694            private_ns_test_identity(333, 30),
1695            &BpffsPruneOptions {
1696                mode: BpffsPruneMode::Stale,
1697                dry_run: false,
1698            },
1699            |host_pid| (host_pid == 999).then_some(proc_pid),
1700            process_starttime,
1701        )
1702        .unwrap();
1703
1704        assert!(live_dir.exists());
1705        assert!(report.entries.iter().any(|entry| {
1706            entry.directory == format!("999-{proc_starttime}")
1707                && entry.status == BpffsPruneStatus::SkipLive
1708                && entry.reason == "live_instance"
1709        }));
1710    }
1711}
1712// Note: map open/write helpers will be added once we standardize on aya APIs across crates.
1713
1714#[cfg(test)]
1715mod bpffs_hint_tests {
1716    use super::bpffs_mount_hint_for_state;
1717    use std::path::Path;
1718
1719    #[test]
1720    fn bpffs_hint_mentions_mount_for_unmounted_sys_fs_bpf() {
1721        let hint =
1722            bpffs_mount_hint_for_state(Path::new("/sys/fs/bpf/ghostscope/1/test"), true, false)
1723                .expect("expected mount hint");
1724        assert!(hint.contains("mount -t bpf bpf /sys/fs/bpf"));
1725        assert!(hint.contains("WSL2"));
1726    }
1727
1728    #[test]
1729    fn bpffs_hint_omits_message_when_bpffs_is_mounted() {
1730        let hint =
1731            bpffs_mount_hint_for_state(Path::new("/sys/fs/bpf/ghostscope/1/test"), true, true);
1732        assert!(hint.is_none());
1733    }
1734
1735    #[test]
1736    fn bpffs_hint_ignores_non_bpffs_paths() {
1737        let hint = bpffs_mount_hint_for_state(Path::new("/tmp/ghostscope/test"), true, false);
1738        assert!(hint.is_none());
1739    }
1740}