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
141pub 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
150pub 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
158pub 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
464pub fn ensure_pinned_proc_offsets_exists(max_entries: u32) -> anyhow::Result<()> {
467 let pin_path = proc_offsets_pin_path()?;
468 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 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 let _ = std::fs::remove_file(&pin_path);
503 }
504 }
505 }
506
507 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 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 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 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 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
580pub 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
588pub 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
596pub 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
604pub 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
630pub 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
711pub 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
792pub 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
827pub 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
888pub 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
895pub 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
909pub 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
916pub 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
930pub 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
942pub 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
991pub 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
1019pub 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
1051pub 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
1335pub fn cleanup_current_pinned_maps() -> anyhow::Result<()> {
1338 let _ = cleanup_pinned_maps_in_dir(&proc_offsets_pin_dir()?);
1339 Ok(())
1340}
1341
1342pub 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, ¤t_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, ¤t_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, ¤t_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#[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}