1use crate::pid::{
2 host_pid_for_proc_pid, read_nspid_chain, read_pid_ns_inode, INITIAL_PID_NAMESPACE_INO,
3};
4use aya::maps::MapData;
5use aya_obj::maps::bpf_map_def;
6use aya_obj::{
7 generated::bpf_map_type::BPF_MAP_TYPE_HASH, maps::LegacyMap, EbpfSectionKind, Map as ObjMap,
8};
9use libc as c;
10use std::io;
11use std::os::fd::{AsFd, AsRawFd};
12use std::path::{Path, PathBuf};
13use tracing::{info, warn};
14
15const BPFFS_MOUNT_POINT: &str = "/sys/fs/bpf";
16const BPFFS_ROOT: &str = "/sys/fs/bpf/ghostscope";
17const PROC_STAT_STARTTIME_INDEX: usize = 19;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20struct CurrentProcessIdentity {
21 host_pid: u32,
22 host_pid_reliable: bool,
23 starttime: u64,
24 initial_pid_namespace: bool,
25}
26
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub enum BpffsPruneMode {
29 Stale,
30 Instance(String),
31 All,
32}
33
34#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct BpffsPruneOptions {
36 pub mode: BpffsPruneMode,
37 pub dry_run: bool,
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub enum BpffsPruneStatus {
42 RemoveDir,
43 CleanKnownPins,
44 SkipLive,
45 Ignore,
46}
47
48#[derive(Debug, Clone, PartialEq, Eq)]
49pub struct BpffsPruneEntry {
50 pub directory: String,
51 pub status: BpffsPruneStatus,
52 pub reason: String,
53}
54
55#[derive(Debug, Clone, PartialEq, Eq)]
56pub struct BpffsPruneReport {
57 pub root: PathBuf,
58 pub dry_run: bool,
59 pub entries: Vec<BpffsPruneEntry>,
60}
61
62fn process_starttime(pid: u32) -> io::Result<u64> {
63 let stat = std::fs::read_to_string(format!("/proc/{pid}/stat"))?;
64 let (_, rest) = stat
65 .rsplit_once(") ")
66 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "malformed /proc stat"))?;
67 let raw = rest
68 .split_whitespace()
69 .nth(PROC_STAT_STARTTIME_INDEX)
70 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "missing starttime field"))?;
71 raw.parse::<u64>()
72 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
73}
74
75fn host_pid_mapping_from_chain(
76 chain: Option<&[u32]>,
77 allow_single_value_nspid: bool,
78) -> Option<u32> {
79 match chain {
80 Some([]) => None,
81 Some([only]) if allow_single_value_nspid => Some(*only),
82 Some([_only]) => None,
83 Some(values) => values.first().copied(),
84 None if allow_single_value_nspid => None,
85 None => None,
86 }
87}
88
89fn resolve_proc_pid_for_host_pid(host_pid: u32, allow_single_value_nspid: bool) -> Option<u32> {
90 let direct = Path::new("/proc").join(host_pid.to_string());
91 if direct.exists() {
92 let chain = read_nspid_chain(host_pid);
93 if host_pid_mapping_from_chain(chain.as_deref(), allow_single_value_nspid) == Some(host_pid)
94 {
95 return Some(host_pid);
96 }
97 }
98
99 let entries = std::fs::read_dir("/proc").ok()?;
100 for entry in entries.flatten() {
101 let Ok(proc_pid) = entry.file_name().to_string_lossy().parse::<u32>() else {
102 continue;
103 };
104 let chain = read_nspid_chain(proc_pid);
105 if host_pid_mapping_from_chain(chain.as_deref(), allow_single_value_nspid) == Some(host_pid)
106 {
107 return Some(proc_pid);
108 }
109 }
110
111 None
112}
113
114fn current_process_identity() -> anyhow::Result<CurrentProcessIdentity> {
115 let proc_pid = std::process::id();
116 let initial_pid_namespace = read_pid_ns_inode(proc_pid) == Some(INITIAL_PID_NAMESPACE_INO);
117 let nspid_chain = read_nspid_chain(proc_pid);
118 let host_pid_reliable =
119 initial_pid_namespace || nspid_chain.as_ref().is_some_and(|chain| chain.len() > 1);
120 Ok(CurrentProcessIdentity {
121 host_pid: host_pid_for_proc_pid(proc_pid),
122 host_pid_reliable,
123 starttime: process_starttime(proc_pid)?,
124 initial_pid_namespace,
125 })
126}
127
128fn current_process_dir_name() -> anyhow::Result<String> {
129 let identity = current_process_identity()?;
130 Ok(format!("{}-{}", identity.host_pid, identity.starttime))
131}
132
133fn parse_pin_dir_name(name: &str) -> Option<(u32, u64)> {
134 let (pid, starttime) = name.split_once('-')?;
135 let pid = pid.parse::<u32>().ok()?;
136 let starttime = starttime.parse::<u64>().ok()?;
137 Some((pid, starttime))
138}
139
140pub fn proc_offsets_pin_path() -> anyhow::Result<PathBuf> {
143 Ok(PathBuf::from(format!(
144 "{BPFFS_ROOT}/{}/proc_module_offsets",
145 current_process_dir_name()?
146 )))
147}
148
149pub fn proc_offsets_pin_dir() -> anyhow::Result<PathBuf> {
151 proc_offsets_pin_path()?
152 .parent()
153 .map(|p| p.to_path_buf())
154 .ok_or_else(|| anyhow::anyhow!("bpffs root has no parent for proc offsets pin path"))
155}
156
157pub const PROC_OFFSETS_MAP_NAME: &str = "proc_module_offsets";
159pub const ALLOWED_PIDS_MAP_NAME: &str = "allowed_pids";
160pub const PID_ALIASES_MAP_NAME: &str = "pid_aliases";
161
162fn bpffs_is_mounted() -> bool {
163 let Ok(mountinfo) = std::fs::read_to_string("/proc/self/mountinfo") else {
164 return false;
165 };
166 mountinfo.lines().any(|line| {
167 let Some((left, right)) = line.split_once(" - ") else {
168 return false;
169 };
170 let mount_point = left.split_whitespace().nth(4);
171 let fs_type = right.split_whitespace().next();
172 mount_point == Some(BPFFS_MOUNT_POINT) && fs_type == Some("bpf")
173 })
174}
175
176fn bpffs_mount_hint_for_state(
177 pin_path: &Path,
178 bpffs_mount_point_exists: bool,
179 bpffs_mounted: bool,
180) -> Option<String> {
181 if !pin_path.starts_with(BPFFS_ROOT) {
182 return None;
183 }
184
185 if bpffs_mounted {
186 return None;
187 }
188
189 if !bpffs_mount_point_exists {
190 return Some(format!(
191 "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}`."
192 ));
193 }
194
195 Some(format!(
196 "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`."
197 ))
198}
199
200pub fn bpffs_mount_hint_for_pin_path(pin_path: &Path) -> Option<String> {
201 bpffs_mount_hint_for_state(
202 pin_path,
203 Path::new(BPFFS_MOUNT_POINT).exists(),
204 bpffs_is_mounted(),
205 )
206}
207
208#[repr(C)]
210#[derive(Debug, Clone, Copy)]
211pub struct ProcModuleKey {
212 pub pid: u32,
213 pub pad: u32,
214 pub cookie_lo: u32,
215 pub cookie_hi: u32,
216}
217
218#[repr(C)]
220#[derive(Debug, Clone, Copy)]
221pub struct ProcModuleOffsetsValue {
222 pub text: u64,
223 pub rodata: u64,
224 pub data: u64,
225 pub bss: u64,
226}
227
228unsafe impl aya::Pod for ProcModuleKey {}
229unsafe impl aya::Pod for ProcModuleOffsetsValue {}
230
231#[repr(C)]
232#[derive(Debug, Clone, Copy)]
233pub struct PidAliasValue {
234 pub proc_pid: u32,
235}
236
237unsafe impl aya::Pod for PidAliasValue {}
238
239impl ProcModuleOffsetsValue {
240 pub fn new(text: u64, rodata: u64, data: u64, bss: u64) -> Self {
241 Self {
242 text,
243 rodata,
244 data,
245 bss,
246 }
247 }
248}
249
250fn ensure_pin_dir(path: &Path) -> std::io::Result<()> {
251 if let Some(dir) = path.parent() {
252 std::fs::create_dir_all(dir)
253 } else {
254 Ok(())
255 }
256}
257
258pub fn ensure_pinned_proc_offsets_exists(max_entries: u32) -> anyhow::Result<()> {
261 let pin_path = proc_offsets_pin_path()?;
262 ensure_pin_dir(&pin_path).map_err(|e| {
264 let hint = bpffs_mount_hint_for_pin_path(&pin_path)
265 .map(|hint| format!(" {hint}"))
266 .unwrap_or_default();
267 anyhow::anyhow!(
268 "Failed to create pin directory for {} at {}: {}.{}",
269 PROC_OFFSETS_MAP_NAME,
270 pin_path.display(),
271 e,
272 hint
273 )
274 })?;
275
276 if pin_path.exists() {
278 if MapData::from_pin(&pin_path).is_ok() {
279 info!(
280 "Reusing existing pinned map at {} (no recreate)",
281 pin_path.display()
282 );
283 return Ok(());
284 } else {
285 let _ = std::fs::remove_file(&pin_path);
287 }
288 }
289
290 let obj_map = ObjMap::Legacy(LegacyMap {
292 section_index: 0,
293 section_kind: EbpfSectionKind::Maps,
294 symbol_index: None,
295 def: bpf_map_def {
296 map_type: BPF_MAP_TYPE_HASH as u32,
297 key_size: 16, value_size: 32, max_entries,
300 map_flags: 0,
301 id: 0,
302 pinning: aya_obj::maps::PinningType::None,
303 },
304 data: Vec::new(),
305 });
306
307 let map = MapData::create(obj_map, PROC_OFFSETS_MAP_NAME, None)?;
309 info!(
310 "Created {} map with capacity {} entries",
311 PROC_OFFSETS_MAP_NAME, max_entries
312 );
313
314 match map.pin(&pin_path) {
316 Ok(()) => {
317 info!("Pinned {} at {}", PROC_OFFSETS_MAP_NAME, pin_path.display());
318 Ok(())
319 }
320 Err(e) => {
321 match MapData::from_pin(&pin_path) {
323 Ok(_) => {
324 info!(
325 "Pin path {} already exists; reusing existing map ({}).",
326 pin_path.display(),
327 e
328 );
329 Ok(())
330 }
331 Err(_) => {
332 let _ = std::fs::remove_file(&pin_path);
334 let hint = bpffs_mount_hint_for_pin_path(&pin_path)
335 .map(|hint| format!(" {hint}"))
336 .unwrap_or_default();
337 Err(anyhow::anyhow!(
338 "Failed to pin {} at {}: {}",
339 PROC_OFFSETS_MAP_NAME,
340 pin_path.display(),
341 e
342 )
343 .context(format!(
344 "Unable to persist {PROC_OFFSETS_MAP_NAME} in bpffs.{hint}"
345 )))
346 }
347 }
348 }
349 }
350}
351
352#[repr(C)]
354struct BpfMapUpdateAttr {
355 map_fd: u32,
356 _pad: u32, key: u64,
358 value: u64,
359 flags: u64,
360}
361
362const BPF_MAP_UPDATE_ELEM: c::c_long = 2; const BPF_MAP_DELETE_ELEM: c::c_long = 1; const BPF_MAP_GET_NEXT_KEY: c::c_long = 4; fn bpf_map_update_elem(
367 fd: i32,
368 key: *const c::c_void,
369 value: *const c::c_void,
370 flags: u64,
371) -> io::Result<()> {
372 let attr = BpfMapUpdateAttr {
373 map_fd: fd as u32,
374 _pad: 0,
375 key: key as usize as u64,
376 value: value as usize as u64,
377 flags,
378 };
379 let ret = unsafe {
380 c::syscall(
381 c::SYS_bpf,
382 BPF_MAP_UPDATE_ELEM,
383 &attr,
384 std::mem::size_of::<BpfMapUpdateAttr>(),
385 )
386 };
387 if ret < 0 {
388 Err(io::Error::last_os_error())
389 } else {
390 Ok(())
391 }
392}
393
394#[repr(C)]
395struct BpfMapKeyAttr {
396 map_fd: u32,
397 _pad: u32, key: u64,
399 next_key: u64,
400}
401
402fn bpf_map_get_next_key(
403 fd: i32,
404 key: *const c::c_void,
405 next_key: *mut c::c_void,
406) -> io::Result<()> {
407 let attr = BpfMapKeyAttr {
408 map_fd: fd as u32,
409 _pad: 0,
410 key: key as usize as u64,
411 next_key: next_key as usize as u64,
412 };
413 let ret = unsafe {
414 c::syscall(
415 c::SYS_bpf,
416 BPF_MAP_GET_NEXT_KEY,
417 &attr,
418 std::mem::size_of::<BpfMapKeyAttr>(),
419 )
420 };
421 if ret < 0 {
422 Err(io::Error::last_os_error())
423 } else {
424 Ok(())
425 }
426}
427
428pub fn allowed_pids_pin_path() -> anyhow::Result<PathBuf> {
430 Ok(PathBuf::from(format!(
431 "{BPFFS_ROOT}/{}/allowed_pids",
432 current_process_dir_name()?
433 )))
434}
435
436pub fn pid_aliases_pin_path() -> anyhow::Result<PathBuf> {
438 Ok(PathBuf::from(format!(
439 "{BPFFS_ROOT}/{}/pid_aliases",
440 current_process_dir_name()?
441 )))
442}
443
444pub fn ensure_pinned_allowed_pids_exists(max_entries: u32) -> anyhow::Result<()> {
446 let pin_path = allowed_pids_pin_path()?;
447 ensure_pin_dir(&pin_path).map_err(|e| {
448 let hint = bpffs_mount_hint_for_pin_path(&pin_path)
449 .map(|hint| format!(" {hint}"))
450 .unwrap_or_default();
451 anyhow::anyhow!(
452 "Failed to create pin directory for {} at {}: {}.{}",
453 ALLOWED_PIDS_MAP_NAME,
454 pin_path.display(),
455 e,
456 hint
457 )
458 })?;
459
460 if pin_path.exists() {
461 if MapData::from_pin(&pin_path).is_ok() {
462 info!("Reusing existing pinned map at {}", pin_path.display());
463 return Ok(());
464 } else {
465 let _ = std::fs::remove_file(&pin_path);
466 }
467 }
468
469 let obj_map = ObjMap::Legacy(LegacyMap {
470 section_index: 0,
471 section_kind: EbpfSectionKind::Maps,
472 symbol_index: None,
473 def: bpf_map_def {
474 map_type: BPF_MAP_TYPE_HASH as u32,
475 key_size: 4,
476 value_size: 1,
477 max_entries,
478 map_flags: 0,
479 id: 0,
480 pinning: aya_obj::maps::PinningType::None,
481 },
482 data: Vec::new(),
483 });
484
485 let map = MapData::create(obj_map, ALLOWED_PIDS_MAP_NAME, None)?;
486 info!(
487 "Created {} map with capacity {} entries",
488 ALLOWED_PIDS_MAP_NAME, max_entries
489 );
490
491 match map.pin(&pin_path) {
492 Ok(()) => {
493 info!("Pinned {} at {}", ALLOWED_PIDS_MAP_NAME, pin_path.display());
494 Ok(())
495 }
496 Err(e) => match MapData::from_pin(&pin_path) {
497 Ok(_) => {
498 info!(
499 "Pin path {} already exists; reusing existing map ({}).",
500 pin_path.display(),
501 e
502 );
503 Ok(())
504 }
505 Err(_) => {
506 let _ = std::fs::remove_file(&pin_path);
507 let hint = bpffs_mount_hint_for_pin_path(&pin_path)
508 .map(|hint| format!(" {hint}"))
509 .unwrap_or_default();
510 Err(anyhow::anyhow!(
511 "Failed to pin {} at {}: {}",
512 ALLOWED_PIDS_MAP_NAME,
513 pin_path.display(),
514 e
515 )
516 .context(format!(
517 "Unable to persist {ALLOWED_PIDS_MAP_NAME} in bpffs.{hint}"
518 )))
519 }
520 },
521 }
522}
523
524pub fn ensure_pinned_pid_aliases_exists(max_entries: u32) -> anyhow::Result<()> {
526 let pin_path = pid_aliases_pin_path()?;
527 ensure_pin_dir(&pin_path).map_err(|e| {
528 let hint = bpffs_mount_hint_for_pin_path(&pin_path)
529 .map(|hint| format!(" {hint}"))
530 .unwrap_or_default();
531 anyhow::anyhow!(
532 "Failed to create pin directory for {} at {}: {}.{}",
533 PID_ALIASES_MAP_NAME,
534 pin_path.display(),
535 e,
536 hint
537 )
538 })?;
539
540 if pin_path.exists() {
541 if MapData::from_pin(&pin_path).is_ok() {
542 info!("Reusing existing pinned map at {}", pin_path.display());
543 return Ok(());
544 } else {
545 let _ = std::fs::remove_file(&pin_path);
546 }
547 }
548
549 let obj_map = ObjMap::Legacy(LegacyMap {
550 section_index: 0,
551 section_kind: EbpfSectionKind::Maps,
552 symbol_index: None,
553 def: bpf_map_def {
554 map_type: BPF_MAP_TYPE_HASH as u32,
555 key_size: 4,
556 value_size: 4,
557 max_entries,
558 map_flags: 0,
559 id: 0,
560 pinning: aya_obj::maps::PinningType::None,
561 },
562 data: Vec::new(),
563 });
564
565 let map = MapData::create(obj_map, PID_ALIASES_MAP_NAME, None)?;
566 info!(
567 "Created {} map with capacity {} entries",
568 PID_ALIASES_MAP_NAME, max_entries
569 );
570
571 match map.pin(&pin_path) {
572 Ok(()) => {
573 info!("Pinned {} at {}", PID_ALIASES_MAP_NAME, pin_path.display());
574 Ok(())
575 }
576 Err(e) => match MapData::from_pin(&pin_path) {
577 Ok(_) => {
578 info!(
579 "Pin path {} already exists; reusing existing map ({}).",
580 pin_path.display(),
581 e
582 );
583 Ok(())
584 }
585 Err(_) => {
586 let _ = std::fs::remove_file(&pin_path);
587 let hint = bpffs_mount_hint_for_pin_path(&pin_path)
588 .map(|hint| format!(" {hint}"))
589 .unwrap_or_default();
590 Err(anyhow::anyhow!(
591 "Failed to pin {} at {}: {}",
592 PID_ALIASES_MAP_NAME,
593 pin_path.display(),
594 e
595 )
596 .context(format!(
597 "Unable to persist {PID_ALIASES_MAP_NAME} in bpffs.{hint}"
598 )))
599 }
600 },
601 }
602}
603
604pub fn insert_allowed_pid(pid: u32) -> anyhow::Result<()> {
606 let map_data = MapData::from_pin(allowed_pids_pin_path()?)?;
607 let fd = map_data.fd().as_fd().as_raw_fd();
608 let key = pid;
609 let val: u8 = 1;
610 bpf_map_update_elem(
611 fd,
612 &key as *const _ as *const _,
613 &val as *const _ as *const _,
614 0,
615 )
616 .map_err(|e| anyhow::anyhow!("allowed_pids update failed for {}: {}", pid, e))
617}
618
619pub fn remove_allowed_pid(pid: u32) -> anyhow::Result<()> {
621 let map_data = MapData::from_pin(allowed_pids_pin_path()?)?;
622 let fd = map_data.fd().as_fd().as_raw_fd();
623 bpf_map_delete_elem(fd, &pid as *const _ as *const _)
624 .map_err(|e| anyhow::anyhow!("allowed_pids delete failed for {}: {}", pid, e))
625}
626
627pub fn insert_pid_alias(runtime_pid: u32, proc_pid: u32) -> anyhow::Result<()> {
629 let map_data = MapData::from_pin(pid_aliases_pin_path()?)?;
630 let fd = map_data.fd().as_fd().as_raw_fd();
631 let key = runtime_pid;
632 let val = PidAliasValue { proc_pid };
633 bpf_map_update_elem(
634 fd,
635 &key as *const _ as *const _,
636 &val as *const _ as *const _,
637 0,
638 )
639 .map_err(|e| {
640 anyhow::anyhow!(
641 "pid_aliases update failed for runtime_pid={} proc_pid={}: {}",
642 runtime_pid,
643 proc_pid,
644 e
645 )
646 })
647}
648
649pub fn remove_pid_alias(runtime_pid: u32) -> anyhow::Result<()> {
651 let map_data = MapData::from_pin(pid_aliases_pin_path()?)?;
652 let fd = map_data.fd().as_fd().as_raw_fd();
653 bpf_map_delete_elem(fd, &runtime_pid as *const _ as *const _).map_err(|e| {
654 anyhow::anyhow!(
655 "pid_aliases delete failed for runtime_pid={}: {}",
656 runtime_pid,
657 e
658 )
659 })
660}
661
662fn bpf_map_delete_elem(fd: i32, key: *const c::c_void) -> io::Result<()> {
663 let attr = BpfMapUpdateAttr {
664 map_fd: fd as u32,
665 _pad: 0,
666 key: key as usize as u64,
667 value: 0,
668 flags: 0,
669 };
670 let ret = unsafe {
671 c::syscall(
672 c::SYS_bpf,
673 BPF_MAP_DELETE_ELEM,
674 &attr,
675 std::mem::size_of::<BpfMapUpdateAttr>(),
676 )
677 };
678 if ret < 0 {
679 Err(io::Error::last_os_error())
680 } else {
681 Ok(())
682 }
683}
684
685pub fn purge_offsets_for_pid(pid: u32) -> anyhow::Result<usize> {
687 let map_data = MapData::from_pin(proc_offsets_pin_path()?)?;
688 let fd = map_data.fd().as_fd().as_raw_fd();
689 let mut deleted = 0usize;
690
691 let mut prev: Option<ProcModuleKey> = None;
693 loop {
694 let mut next: ProcModuleKey = ProcModuleKey {
695 pid: 0,
696 pad: 0,
697 cookie_lo: 0,
698 cookie_hi: 0,
699 };
700 let key_ptr = prev
701 .as_ref()
702 .map(|k| k as *const _ as *const c::c_void)
703 .unwrap_or(std::ptr::null());
704 let res = bpf_map_get_next_key(fd, key_ptr, &mut next as *mut _ as *mut _);
705 match res {
706 Ok(()) => {
707 if next.pid == pid {
709 let _ = bpf_map_delete_elem(fd, &next as *const _ as *const _);
711 deleted += 1;
712 continue;
714 } else {
715 prev = Some(next);
716 }
717 }
718 Err(e) => {
719 if e.raw_os_error() == Some(libc::ENOENT) {
721 break;
722 } else {
723 return Err(anyhow::anyhow!("bpf_map_get_next_key failed: {}", e));
724 }
725 }
726 }
727 }
728 Ok(deleted)
729}
730
731pub fn insert_offsets_for_pid(
733 pid: u32,
734 items: &[(u64, ProcModuleOffsetsValue)],
735) -> anyhow::Result<usize> {
736 let map_data = MapData::from_pin(proc_offsets_pin_path()?)?;
737 let fd = map_data.fd().as_fd().as_raw_fd();
738 let mut inserted = 0usize;
739 for (cookie, off) in items {
740 let key = ProcModuleKey {
741 pid,
742 pad: 0,
743 cookie_lo: (*cookie & 0xffff_ffff) as u32,
744 cookie_hi: (*cookie >> 32) as u32,
745 };
746 match bpf_map_update_elem(
747 fd,
748 &key as *const _ as *const _,
749 off as *const _ as *const _,
750 0,
751 ) {
752 Ok(()) => {
753 tracing::debug!(
754 "proc_module_offsets insert ok: pid={} cookie=0x{:08x}{:08x} text=0x{:x} rodata=0x{:x} data=0x{:x} bss=0x{:x}",
755 pid, key.cookie_hi, key.cookie_lo, off.text, off.rodata, off.data, off.bss
756 );
757 inserted += 1
758 }
759 Err(e) => warn!(
760 "bpf_map_update_elem failed for pid={} cookie=0x{:08x}{:08x}: {}",
761 pid, key.cookie_hi, key.cookie_lo, e
762 ),
763 }
764 }
765 Ok(inserted)
766}
767
768#[derive(Debug, Clone, Copy, PartialEq, Eq)]
769enum DirCleanupOutcome {
770 RemovedDir,
771}
772
773fn cleanup_outcome_without_mutation(dir: &Path) -> anyhow::Result<DirCleanupOutcome> {
774 if let Err(err) = std::fs::metadata(dir) {
775 if err.kind() != io::ErrorKind::NotFound {
776 return Err(err.into());
777 }
778 }
779 Ok(DirCleanupOutcome::RemovedDir)
780}
781
782fn cleanup_pinned_maps_in_dir(dir: &Path) -> anyhow::Result<DirCleanupOutcome> {
783 match std::fs::remove_dir_all(dir) {
784 Ok(()) => Ok(DirCleanupOutcome::RemovedDir),
785 Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(DirCleanupOutcome::RemovedDir),
786 Err(err) => Err(err.into()),
787 }
788}
789
790fn stale_reason_for_dir<R, S>(
791 host_pid: u32,
792 dir_starttime: u64,
793 current: CurrentProcessIdentity,
794 resolve_proc_pid: &R,
795 read_starttime: &S,
796) -> Option<&'static str>
797where
798 R: Fn(u32) -> Option<u32>,
799 S: Fn(u32) -> io::Result<u64>,
800{
801 if current.host_pid_reliable && host_pid == current.host_pid {
802 return (dir_starttime != current.starttime).then_some("starttime_mismatch");
803 }
804
805 let Some(proc_pid) = resolve_proc_pid(host_pid) else {
806 return current.initial_pid_namespace.then_some("pid_not_running");
807 };
808
809 match read_starttime(proc_pid) {
810 Ok(live_starttime) if live_starttime != dir_starttime => Some("starttime_mismatch"),
811 Ok(_) => None,
812 Err(_) => current.initial_pid_namespace.then_some("pid_not_running"),
813 }
814}
815
816fn prune_entry_for_cleanup(
817 directory: String,
818 reason: &str,
819 outcome: DirCleanupOutcome,
820) -> BpffsPruneEntry {
821 BpffsPruneEntry {
822 directory,
823 status: match outcome {
824 DirCleanupOutcome::RemovedDir => BpffsPruneStatus::RemoveDir,
825 },
826 reason: reason.to_string(),
827 }
828}
829
830fn prune_pinned_maps_under<R, S>(
831 root: &Path,
832 current: CurrentProcessIdentity,
833 options: &BpffsPruneOptions,
834 resolve_proc_pid: R,
835 read_starttime: S,
836) -> anyhow::Result<BpffsPruneReport>
837where
838 R: Fn(u32) -> Option<u32>,
839 S: Fn(u32) -> io::Result<u64>,
840{
841 if let BpffsPruneMode::Instance(instance) = &options.mode {
842 let path = root.join(instance);
843 if !path.exists() {
844 return Err(anyhow::anyhow!(
845 "bpffs pin directory not found: {}",
846 path.display()
847 ));
848 }
849 if !path.is_dir() {
850 return Err(anyhow::anyhow!(
851 "bpffs pin path is not a directory: {}",
852 path.display()
853 ));
854 }
855
856 let outcome = if options.dry_run {
857 cleanup_outcome_without_mutation(&path)?
858 } else {
859 cleanup_pinned_maps_in_dir(&path)?
860 };
861
862 return Ok(BpffsPruneReport {
863 root: root.to_path_buf(),
864 dry_run: options.dry_run,
865 entries: vec![prune_entry_for_cleanup(
866 instance.clone(),
867 "explicit_instance",
868 outcome,
869 )],
870 });
871 }
872
873 let mut entries_out = Vec::new();
874
875 let entries = match std::fs::read_dir(root) {
876 Ok(entries) => entries,
877 Err(err) if err.kind() == io::ErrorKind::NotFound => {
878 return Ok(BpffsPruneReport {
879 root: root.to_path_buf(),
880 dry_run: options.dry_run,
881 entries: entries_out,
882 });
883 }
884 Err(err) => return Err(err.into()),
885 };
886
887 for entry in entries {
888 let entry = entry?;
889 if !entry.file_type()?.is_dir() {
890 continue;
891 }
892
893 let Some(name) = entry.file_name().to_str().map(str::to_owned) else {
894 continue;
895 };
896 let Some((host_pid, dir_starttime)) = parse_pin_dir_name(&name) else {
897 entries_out.push(BpffsPruneEntry {
898 directory: name,
899 status: BpffsPruneStatus::Ignore,
900 reason: "non_matching_name".to_string(),
901 });
902 continue;
903 };
904
905 let removal_reason = match &options.mode {
906 BpffsPruneMode::Stale => stale_reason_for_dir(
907 host_pid,
908 dir_starttime,
909 current,
910 &resolve_proc_pid,
911 &read_starttime,
912 ),
913 BpffsPruneMode::All => Some("force_all"),
914 BpffsPruneMode::Instance(_) => unreachable!(),
915 };
916
917 let Some(reason) = removal_reason else {
918 entries_out.push(BpffsPruneEntry {
919 directory: name,
920 status: BpffsPruneStatus::SkipLive,
921 reason: "live_instance".to_string(),
922 });
923 continue;
924 };
925
926 let outcome = if options.dry_run {
927 cleanup_outcome_without_mutation(&entry.path())?
928 } else {
929 cleanup_pinned_maps_in_dir(&entry.path())?
930 };
931 entries_out.push(prune_entry_for_cleanup(name, reason, outcome));
932 }
933
934 entries_out.sort_by(|left, right| left.directory.cmp(&right.directory));
935
936 Ok(BpffsPruneReport {
937 root: root.to_path_buf(),
938 dry_run: options.dry_run,
939 entries: entries_out,
940 })
941}
942
943fn cleanup_stale_pinned_maps_under<R, S>(
944 root: &Path,
945 current: CurrentProcessIdentity,
946 resolve_proc_pid: R,
947 read_starttime: S,
948) -> anyhow::Result<usize>
949where
950 R: Fn(u32) -> Option<u32>,
951 S: Fn(u32) -> io::Result<u64>,
952{
953 let report = prune_pinned_maps_under(
954 root,
955 current,
956 &BpffsPruneOptions {
957 mode: BpffsPruneMode::Stale,
958 dry_run: false,
959 },
960 resolve_proc_pid,
961 read_starttime,
962 )?;
963
964 Ok(report
965 .entries
966 .iter()
967 .filter(|entry| entry.status == BpffsPruneStatus::RemoveDir)
968 .count())
969}
970
971pub fn cleanup_current_pinned_maps() -> anyhow::Result<()> {
974 let _ = cleanup_pinned_maps_in_dir(&proc_offsets_pin_dir()?);
975 Ok(())
976}
977
978pub fn cleanup_stale_pinned_maps_root() -> anyhow::Result<usize> {
980 let current = current_process_identity()?;
981 cleanup_stale_pinned_maps_under(
982 Path::new(BPFFS_ROOT),
983 current,
984 |host_pid| resolve_proc_pid_for_host_pid(host_pid, current.initial_pid_namespace),
985 process_starttime,
986 )
987}
988
989pub fn prune_pinned_maps_root(options: &BpffsPruneOptions) -> anyhow::Result<BpffsPruneReport> {
990 let current = current_process_identity()?;
991 prune_pinned_maps_under(
992 Path::new(BPFFS_ROOT),
993 current,
994 options,
995 |host_pid| resolve_proc_pid_for_host_pid(host_pid, current.initial_pid_namespace),
996 process_starttime,
997 )
998}
999
1000#[cfg(test)]
1001mod tests {
1002 use super::{
1003 cleanup_pinned_maps_in_dir, cleanup_stale_pinned_maps_under, parse_pin_dir_name,
1004 process_starttime, prune_pinned_maps_under, BpffsPruneMode, BpffsPruneOptions,
1005 BpffsPruneStatus, CurrentProcessIdentity, ALLOWED_PIDS_MAP_NAME, PROC_OFFSETS_MAP_NAME,
1006 };
1007 use std::{fs, io};
1008 use tempfile::tempdir;
1009
1010 fn host_test_identity(host_pid: u32, starttime: u64) -> CurrentProcessIdentity {
1011 CurrentProcessIdentity {
1012 host_pid,
1013 host_pid_reliable: true,
1014 starttime,
1015 initial_pid_namespace: true,
1016 }
1017 }
1018
1019 fn private_ns_test_identity(host_pid: u32, starttime: u64) -> CurrentProcessIdentity {
1020 CurrentProcessIdentity {
1021 host_pid,
1022 host_pid_reliable: false,
1023 starttime,
1024 initial_pid_namespace: false,
1025 }
1026 }
1027
1028 fn simulated_starttime(pid: u32) -> io::Result<u64> {
1029 match pid {
1030 222 => Ok(20),
1031 333 => Ok(30),
1032 _ => Err(io::Error::new(io::ErrorKind::NotFound, "missing pid")),
1033 }
1034 }
1035
1036 #[test]
1037 fn cleanup_removes_known_pinned_maps_and_empty_dir() {
1038 let temp = tempdir().unwrap();
1039 let dir = temp.path().join("1234");
1040 fs::create_dir_all(&dir).unwrap();
1041 fs::write(dir.join(PROC_OFFSETS_MAP_NAME), b"offsets").unwrap();
1042 fs::write(dir.join(ALLOWED_PIDS_MAP_NAME), b"allow").unwrap();
1043
1044 cleanup_pinned_maps_in_dir(&dir).unwrap();
1045
1046 assert!(!dir.exists());
1047 }
1048
1049 #[test]
1050 fn cleanup_removes_dir_even_when_unknown_files_remain() {
1051 let temp = tempdir().unwrap();
1052 let dir = temp.path().join("1234");
1053 fs::create_dir_all(&dir).unwrap();
1054 fs::write(dir.join(PROC_OFFSETS_MAP_NAME), b"offsets").unwrap();
1055 fs::write(dir.join(ALLOWED_PIDS_MAP_NAME), b"allow").unwrap();
1056 let extra = dir.join("keep-me");
1057 fs::write(&extra, b"extra").unwrap();
1058
1059 cleanup_pinned_maps_in_dir(&dir).unwrap();
1060
1061 assert!(!dir.exists());
1062 assert!(!extra.exists());
1063 }
1064
1065 #[test]
1066 fn stale_cleanup_removes_only_dead_pid_dirs() {
1067 let temp = tempdir().unwrap();
1068 let stale_dir = temp.path().join("111-10");
1069 let live_dir = temp.path().join("222-20");
1070 let current_dir = temp.path().join("333-30");
1071 let non_pid_dir = temp.path().join("not-a-pid");
1072
1073 for dir in [&stale_dir, &live_dir, ¤t_dir, &non_pid_dir] {
1074 fs::create_dir_all(dir).unwrap();
1075 fs::write(dir.join(PROC_OFFSETS_MAP_NAME), b"offsets").unwrap();
1076 fs::write(dir.join(ALLOWED_PIDS_MAP_NAME), b"allow").unwrap();
1077 }
1078
1079 let removed = cleanup_stale_pinned_maps_under(
1080 temp.path(),
1081 host_test_identity(333, 30),
1082 |host_pid| matches!(host_pid, 222 | 333).then_some(host_pid),
1083 simulated_starttime,
1084 )
1085 .unwrap();
1086
1087 assert_eq!(removed, 1);
1088 assert!(!stale_dir.exists());
1089 assert!(live_dir.exists());
1090 assert!(current_dir.exists());
1091 assert!(non_pid_dir.exists());
1092 }
1093
1094 #[test]
1095 fn stale_cleanup_removes_mismatched_starttime_for_reused_pid() {
1096 let temp = tempdir().unwrap();
1097 let stale_current_pid_dir = temp.path().join("333-10");
1098 let current_pid_dir = temp.path().join("333-30");
1099
1100 for dir in [&stale_current_pid_dir, ¤t_pid_dir] {
1101 fs::create_dir_all(dir).unwrap();
1102 fs::write(dir.join(PROC_OFFSETS_MAP_NAME), b"offsets").unwrap();
1103 fs::write(dir.join(ALLOWED_PIDS_MAP_NAME), b"allow").unwrap();
1104 }
1105
1106 let removed = cleanup_stale_pinned_maps_under(
1107 temp.path(),
1108 host_test_identity(333, 30),
1109 |host_pid| (host_pid == 333).then_some(host_pid),
1110 simulated_starttime,
1111 )
1112 .unwrap();
1113
1114 assert_eq!(removed, 1);
1115 assert!(!stale_current_pid_dir.exists());
1116 assert!(current_pid_dir.exists());
1117 }
1118
1119 #[test]
1120 fn parse_pin_dir_name_requires_pid_starttime_format() {
1121 assert_eq!(parse_pin_dir_name("1234"), None);
1122 assert_eq!(parse_pin_dir_name("1234-5678"), Some((1234, 5678)));
1123 assert_eq!(parse_pin_dir_name("bad"), None);
1124 assert_eq!(parse_pin_dir_name("1234-bad"), None);
1125 }
1126
1127 #[test]
1128 fn stale_cleanup_ignores_legacy_numeric_dirs() {
1129 let temp = tempdir().unwrap();
1130 let legacy_dir = temp.path().join("444");
1131 let current_dir = temp.path().join("333-30");
1132
1133 for dir in [&legacy_dir, ¤t_dir] {
1134 fs::create_dir_all(dir).unwrap();
1135 fs::write(dir.join(PROC_OFFSETS_MAP_NAME), b"offsets").unwrap();
1136 fs::write(dir.join(ALLOWED_PIDS_MAP_NAME), b"allow").unwrap();
1137 }
1138
1139 let removed = cleanup_stale_pinned_maps_under(
1140 temp.path(),
1141 host_test_identity(333, 30),
1142 |host_pid| matches!(host_pid, 333 | 444).then_some(host_pid),
1143 simulated_starttime,
1144 )
1145 .unwrap();
1146
1147 assert_eq!(removed, 0);
1148 assert!(legacy_dir.exists());
1149 assert!(current_dir.exists());
1150 }
1151
1152 #[test]
1153 fn dry_run_prune_reports_stale_and_keeps_dirs_intact() {
1154 let temp = tempdir().unwrap();
1155 let stale_dir = temp.path().join("111-10");
1156 let live_dir = temp.path().join("222-20");
1157 let legacy_dir = temp.path().join("legacy");
1158
1159 for dir in [&stale_dir, &live_dir, &legacy_dir] {
1160 fs::create_dir_all(dir).unwrap();
1161 fs::write(dir.join(PROC_OFFSETS_MAP_NAME), b"offsets").unwrap();
1162 fs::write(dir.join(ALLOWED_PIDS_MAP_NAME), b"allow").unwrap();
1163 }
1164
1165 let report = prune_pinned_maps_under(
1166 temp.path(),
1167 host_test_identity(333, 30),
1168 &BpffsPruneOptions {
1169 mode: BpffsPruneMode::Stale,
1170 dry_run: true,
1171 },
1172 |host_pid| (host_pid == 222).then_some(host_pid),
1173 simulated_starttime,
1174 )
1175 .unwrap();
1176
1177 assert!(stale_dir.exists());
1178 assert!(live_dir.exists());
1179 assert!(legacy_dir.exists());
1180 assert!(report.entries.iter().any(|entry| {
1181 entry.directory == "111-10"
1182 && entry.status == BpffsPruneStatus::RemoveDir
1183 && entry.reason == "pid_not_running"
1184 }));
1185 assert!(report.entries.iter().any(|entry| {
1186 entry.directory == "222-20"
1187 && entry.status == BpffsPruneStatus::SkipLive
1188 && entry.reason == "live_instance"
1189 }));
1190 assert!(report.entries.iter().any(|entry| {
1191 entry.directory == "legacy"
1192 && entry.status == BpffsPruneStatus::Ignore
1193 && entry.reason == "non_matching_name"
1194 }));
1195 }
1196
1197 #[test]
1198 fn instance_prune_removes_selected_dir_even_when_live() {
1199 let temp = tempdir().unwrap();
1200 let live_dir = temp.path().join("222-20");
1201 fs::create_dir_all(&live_dir).unwrap();
1202 fs::write(live_dir.join(PROC_OFFSETS_MAP_NAME), b"offsets").unwrap();
1203 fs::write(live_dir.join(ALLOWED_PIDS_MAP_NAME), b"allow").unwrap();
1204
1205 let report = prune_pinned_maps_under(
1206 temp.path(),
1207 host_test_identity(333, 30),
1208 &BpffsPruneOptions {
1209 mode: BpffsPruneMode::Instance("222-20".to_string()),
1210 dry_run: false,
1211 },
1212 |_host_pid| Some(222),
1213 simulated_starttime,
1214 )
1215 .unwrap();
1216
1217 assert!(!live_dir.exists());
1218 assert_eq!(report.entries.len(), 1);
1219 assert_eq!(report.entries[0].directory, "222-20");
1220 assert_eq!(report.entries[0].status, BpffsPruneStatus::RemoveDir);
1221 assert_eq!(report.entries[0].reason, "explicit_instance");
1222 }
1223
1224 #[test]
1225 fn force_all_prune_skips_legacy_dirs_but_removes_pid_starttime_dirs() {
1226 let temp = tempdir().unwrap();
1227 let live_dir = temp.path().join("222-20");
1228 let legacy_dir = temp.path().join("222");
1229
1230 for dir in [&live_dir, &legacy_dir] {
1231 fs::create_dir_all(dir).unwrap();
1232 fs::write(dir.join(PROC_OFFSETS_MAP_NAME), b"offsets").unwrap();
1233 fs::write(dir.join(ALLOWED_PIDS_MAP_NAME), b"allow").unwrap();
1234 }
1235
1236 let report = prune_pinned_maps_under(
1237 temp.path(),
1238 host_test_identity(333, 30),
1239 &BpffsPruneOptions {
1240 mode: BpffsPruneMode::All,
1241 dry_run: false,
1242 },
1243 |_host_pid| Some(222),
1244 simulated_starttime,
1245 )
1246 .unwrap();
1247
1248 assert!(!live_dir.exists());
1249 assert!(legacy_dir.exists());
1250 assert!(report.entries.iter().any(|entry| {
1251 entry.directory == "222-20"
1252 && entry.status == BpffsPruneStatus::RemoveDir
1253 && entry.reason == "force_all"
1254 }));
1255 assert!(report.entries.iter().any(|entry| {
1256 entry.directory == "222"
1257 && entry.status == BpffsPruneStatus::Ignore
1258 && entry.reason == "non_matching_name"
1259 }));
1260 }
1261
1262 #[test]
1263 fn stale_prune_skips_unresolvable_host_pid_in_private_namespace() {
1264 let temp = tempdir().unwrap();
1265 let foreign_live_dir = temp.path().join("999-10");
1266 fs::create_dir_all(&foreign_live_dir).unwrap();
1267 fs::write(foreign_live_dir.join(PROC_OFFSETS_MAP_NAME), b"offsets").unwrap();
1268 fs::write(foreign_live_dir.join(ALLOWED_PIDS_MAP_NAME), b"allow").unwrap();
1269
1270 let report = prune_pinned_maps_under(
1271 temp.path(),
1272 private_ns_test_identity(333, 30),
1273 &BpffsPruneOptions {
1274 mode: BpffsPruneMode::Stale,
1275 dry_run: false,
1276 },
1277 |_host_pid| None,
1278 simulated_starttime,
1279 )
1280 .unwrap();
1281
1282 assert!(foreign_live_dir.exists());
1283 assert!(report.entries.iter().any(|entry| {
1284 entry.directory == "999-10"
1285 && entry.status == BpffsPruneStatus::SkipLive
1286 && entry.reason == "live_instance"
1287 }));
1288 }
1289
1290 #[test]
1291 fn stale_prune_skips_same_numeric_pid_when_current_host_pid_is_not_reliable() {
1292 let temp = tempdir().unwrap();
1293 let foreign_live_dir = temp.path().join("333-10");
1294 fs::create_dir_all(&foreign_live_dir).unwrap();
1295 fs::write(foreign_live_dir.join(PROC_OFFSETS_MAP_NAME), b"offsets").unwrap();
1296 fs::write(foreign_live_dir.join(ALLOWED_PIDS_MAP_NAME), b"allow").unwrap();
1297
1298 let report = prune_pinned_maps_under(
1299 temp.path(),
1300 private_ns_test_identity(333, 30),
1301 &BpffsPruneOptions {
1302 mode: BpffsPruneMode::Stale,
1303 dry_run: false,
1304 },
1305 |_host_pid| None,
1306 simulated_starttime,
1307 )
1308 .unwrap();
1309
1310 assert!(foreign_live_dir.exists());
1311 assert!(report.entries.iter().any(|entry| {
1312 entry.directory == "333-10"
1313 && entry.status == BpffsPruneStatus::SkipLive
1314 && entry.reason == "live_instance"
1315 }));
1316 }
1317
1318 #[test]
1319 fn stale_prune_keeps_live_dir_when_host_pid_maps_to_proc_pid() {
1320 let temp = tempdir().unwrap();
1321 let proc_pid = std::process::id();
1322 let proc_starttime = process_starttime(proc_pid).unwrap();
1323 let live_dir = temp.path().join(format!("999-{proc_starttime}"));
1324 fs::create_dir_all(&live_dir).unwrap();
1325 fs::write(live_dir.join(PROC_OFFSETS_MAP_NAME), b"offsets").unwrap();
1326 fs::write(live_dir.join(ALLOWED_PIDS_MAP_NAME), b"allow").unwrap();
1327
1328 let report = prune_pinned_maps_under(
1329 temp.path(),
1330 private_ns_test_identity(333, 30),
1331 &BpffsPruneOptions {
1332 mode: BpffsPruneMode::Stale,
1333 dry_run: false,
1334 },
1335 |host_pid| (host_pid == 999).then_some(proc_pid),
1336 process_starttime,
1337 )
1338 .unwrap();
1339
1340 assert!(live_dir.exists());
1341 assert!(report.entries.iter().any(|entry| {
1342 entry.directory == format!("999-{proc_starttime}")
1343 && entry.status == BpffsPruneStatus::SkipLive
1344 && entry.reason == "live_instance"
1345 }));
1346 }
1347}
1348#[cfg(test)]
1351mod bpffs_hint_tests {
1352 use super::bpffs_mount_hint_for_state;
1353 use std::path::Path;
1354
1355 #[test]
1356 fn bpffs_hint_mentions_mount_for_unmounted_sys_fs_bpf() {
1357 let hint =
1358 bpffs_mount_hint_for_state(Path::new("/sys/fs/bpf/ghostscope/1/test"), true, false)
1359 .expect("expected mount hint");
1360 assert!(hint.contains("mount -t bpf bpf /sys/fs/bpf"));
1361 assert!(hint.contains("WSL2"));
1362 }
1363
1364 #[test]
1365 fn bpffs_hint_omits_message_when_bpffs_is_mounted() {
1366 let hint =
1367 bpffs_mount_hint_for_state(Path::new("/sys/fs/bpf/ghostscope/1/test"), true, true);
1368 assert!(hint.is_none());
1369 }
1370
1371 #[test]
1372 fn bpffs_hint_ignores_non_bpffs_paths() {
1373 let hint = bpffs_mount_hint_for_state(Path::new("/tmp/ghostscope/test"), true, false);
1374 assert!(hint.is_none());
1375 }
1376}