ghostscope_process/
maps.rs

1use aya::maps::MapData;
2use aya_obj::maps::bpf_map_def;
3use aya_obj::{
4    generated::bpf_map_type::BPF_MAP_TYPE_HASH, maps::LegacyMap, EbpfSectionKind, Map as ObjMap,
5};
6use libc as c;
7use std::io;
8use std::os::fd::{AsFd, AsRawFd};
9use std::path::{Path, PathBuf};
10use tracing::{info, warn};
11
12/// Compute the bpffs pin path for the proc_module_offsets map for current process
13/// Using per-process directory avoids conflicts across multiple GhostScope instances
14pub fn proc_offsets_pin_path() -> PathBuf {
15    let pid = std::process::id();
16    PathBuf::from(format!("/sys/fs/bpf/ghostscope/{pid}/proc_module_offsets"))
17}
18
19/// Pin directory containing the per-process offsets map
20pub fn proc_offsets_pin_dir() -> PathBuf {
21    proc_offsets_pin_path()
22        .parent()
23        .map(|p| p.to_path_buf())
24        .unwrap_or_else(|| PathBuf::from("/sys/fs/bpf/ghostscope"))
25}
26
27/// Map name as embedded in BPF object
28pub const PROC_OFFSETS_MAP_NAME: &str = "proc_module_offsets";
29pub const ALLOWED_PIDS_MAP_NAME: &str = "allowed_pids";
30
31/// Key for proc_module_offsets map: { pid:u32, pad:u32, cookie_lo:u32, cookie_hi:u32 }
32#[repr(C)]
33#[derive(Debug, Clone, Copy)]
34pub struct ProcModuleKey {
35    pub pid: u32,
36    pub pad: u32,
37    pub cookie_lo: u32,
38    pub cookie_hi: u32,
39}
40
41/// Value for proc_module_offsets map - section offsets for a module
42#[repr(C)]
43#[derive(Debug, Clone, Copy)]
44pub struct ProcModuleOffsetsValue {
45    pub text: u64,
46    pub rodata: u64,
47    pub data: u64,
48    pub bss: u64,
49}
50
51unsafe impl aya::Pod for ProcModuleKey {}
52unsafe impl aya::Pod for ProcModuleOffsetsValue {}
53
54impl ProcModuleOffsetsValue {
55    pub fn new(text: u64, rodata: u64, data: u64, bss: u64) -> Self {
56        Self {
57            text,
58            rodata,
59            data,
60            bss,
61        }
62    }
63}
64
65fn ensure_pin_dir(path: &Path) -> std::io::Result<()> {
66    if let Some(dir) = path.parent() {
67        std::fs::create_dir_all(dir)
68    } else {
69        Ok(())
70    }
71}
72
73/// Ensure the pinned global proc_module_offsets map exists at the standard path.
74/// If not present, create and pin it with the specified capacity.
75pub fn ensure_pinned_proc_offsets_exists(max_entries: u32) -> anyhow::Result<()> {
76    let pin_path = proc_offsets_pin_path();
77    // Ensure parent dir exists
78    ensure_pin_dir(&pin_path)?;
79
80    // If pinned file already exists, try to reuse it directly (idempotent)
81    if pin_path.exists() {
82        if MapData::from_pin(&pin_path).is_ok() {
83            info!(
84                "Reusing existing pinned map at {} (no recreate)",
85                pin_path.display()
86            );
87            return Ok(());
88        } else {
89            // Stale/corrupted pin path, remove and recreate
90            let _ = std::fs::remove_file(&pin_path);
91        }
92    }
93
94    // Define the map as a legacy map (compatible with Aya expectations)
95    let obj_map = ObjMap::Legacy(LegacyMap {
96        section_index: 0,
97        section_kind: EbpfSectionKind::Maps,
98        symbol_index: None,
99        def: bpf_map_def {
100            map_type: BPF_MAP_TYPE_HASH as u32,
101            key_size: 16,   // pid:u32, pad:u32, cookie:u64
102            value_size: 32, // text, rodata, data, bss
103            max_entries,
104            map_flags: 0,
105            id: 0,
106            pinning: aya_obj::maps::PinningType::None,
107        },
108        data: Vec::new(),
109    });
110
111    // Create the map in kernel
112    let map = MapData::create(obj_map, PROC_OFFSETS_MAP_NAME, None)?;
113    info!(
114        "Created {} map with capacity {} entries",
115        PROC_OFFSETS_MAP_NAME, max_entries
116    );
117
118    // Pin to bpffs for global reuse; handle races safely
119    match map.pin(&pin_path) {
120        Ok(()) => {
121            info!("Pinned {} at {}", PROC_OFFSETS_MAP_NAME, pin_path.display());
122            Ok(())
123        }
124        Err(e) => {
125            // If another thread/process pinned concurrently, reuse the existing pin
126            match MapData::from_pin(&pin_path) {
127                Ok(_) => {
128                    info!(
129                        "Pin path {} already exists; reusing existing map ({}).",
130                        pin_path.display(),
131                        e
132                    );
133                    Ok(())
134                }
135                Err(_) => {
136                    // Best-effort cleanup and propagate error
137                    let _ = std::fs::remove_file(&pin_path);
138                    Err(anyhow::anyhow!(
139                        "Failed to pin {} at {}: {}",
140                        PROC_OFFSETS_MAP_NAME,
141                        pin_path.display(),
142                        e
143                    ))
144                }
145            }
146        }
147    }
148}
149
150// Low-level bpf syscall wrapper for map update (avoids tight coupling to aya map wrappers)
151#[repr(C)]
152struct BpfMapUpdateAttr {
153    map_fd: u32,
154    _pad: u32, // align to 64-bit for following fields
155    key: u64,
156    value: u64,
157    flags: u64,
158}
159
160const BPF_MAP_UPDATE_ELEM: c::c_long = 2; // from linux/bpf.h
161const BPF_MAP_DELETE_ELEM: c::c_long = 1; // from linux/bpf.h
162const BPF_MAP_GET_NEXT_KEY: c::c_long = 4; // from linux/bpf.h
163
164fn bpf_map_update_elem(
165    fd: i32,
166    key: *const c::c_void,
167    value: *const c::c_void,
168    flags: u64,
169) -> io::Result<()> {
170    let attr = BpfMapUpdateAttr {
171        map_fd: fd as u32,
172        _pad: 0,
173        key: key as usize as u64,
174        value: value as usize as u64,
175        flags,
176    };
177    let ret = unsafe {
178        c::syscall(
179            c::SYS_bpf,
180            BPF_MAP_UPDATE_ELEM,
181            &attr,
182            std::mem::size_of::<BpfMapUpdateAttr>(),
183        )
184    };
185    if ret < 0 {
186        Err(io::Error::last_os_error())
187    } else {
188        Ok(())
189    }
190}
191
192#[repr(C)]
193struct BpfMapKeyAttr {
194    map_fd: u32,
195    _pad: u32, // align to 64-bit for following fields
196    key: u64,
197    next_key: u64,
198}
199
200fn bpf_map_get_next_key(
201    fd: i32,
202    key: *const c::c_void,
203    next_key: *mut c::c_void,
204) -> io::Result<()> {
205    let attr = BpfMapKeyAttr {
206        map_fd: fd as u32,
207        _pad: 0,
208        key: key as usize as u64,
209        next_key: next_key as usize as u64,
210    };
211    let ret = unsafe {
212        c::syscall(
213            c::SYS_bpf,
214            BPF_MAP_GET_NEXT_KEY,
215            &attr,
216            std::mem::size_of::<BpfMapKeyAttr>(),
217        )
218    };
219    if ret < 0 {
220        Err(io::Error::last_os_error())
221    } else {
222        Ok(())
223    }
224}
225
226/// Compute the bpffs pin path for the allowed_pids map for current process
227pub fn allowed_pids_pin_path() -> PathBuf {
228    let pid = std::process::id();
229    PathBuf::from(format!("/sys/fs/bpf/ghostscope/{pid}/allowed_pids"))
230}
231
232/// Ensure the pinned allowed_pids map exists under the per-process directory.
233pub fn ensure_pinned_allowed_pids_exists(max_entries: u32) -> anyhow::Result<()> {
234    let pin_path = allowed_pids_pin_path();
235    ensure_pin_dir(&pin_path)?;
236
237    if pin_path.exists() {
238        if MapData::from_pin(&pin_path).is_ok() {
239            info!("Reusing existing pinned map at {}", pin_path.display());
240            return Ok(());
241        } else {
242            let _ = std::fs::remove_file(&pin_path);
243        }
244    }
245
246    let obj_map = ObjMap::Legacy(LegacyMap {
247        section_index: 0,
248        section_kind: EbpfSectionKind::Maps,
249        symbol_index: None,
250        def: bpf_map_def {
251            map_type: BPF_MAP_TYPE_HASH as u32,
252            key_size: 4,
253            value_size: 1,
254            max_entries,
255            map_flags: 0,
256            id: 0,
257            pinning: aya_obj::maps::PinningType::None,
258        },
259        data: Vec::new(),
260    });
261
262    let map = MapData::create(obj_map, ALLOWED_PIDS_MAP_NAME, None)?;
263    info!(
264        "Created {} map with capacity {} entries",
265        ALLOWED_PIDS_MAP_NAME, max_entries
266    );
267
268    match map.pin(&pin_path) {
269        Ok(()) => {
270            info!("Pinned {} at {}", ALLOWED_PIDS_MAP_NAME, pin_path.display());
271            Ok(())
272        }
273        Err(e) => match MapData::from_pin(&pin_path) {
274            Ok(_) => {
275                info!(
276                    "Pin path {} already exists; reusing existing map ({}).",
277                    pin_path.display(),
278                    e
279                );
280                Ok(())
281            }
282            Err(_) => {
283                let _ = std::fs::remove_file(&pin_path);
284                Err(anyhow::anyhow!(
285                    "Failed to pin {} at {}: {}",
286                    ALLOWED_PIDS_MAP_NAME,
287                    pin_path.display(),
288                    e
289                ))
290            }
291        },
292    }
293}
294
295/// Insert a PID into the allowed_pids pinned map.
296pub fn insert_allowed_pid(pid: u32) -> anyhow::Result<()> {
297    let map_data = MapData::from_pin(allowed_pids_pin_path())?;
298    let fd = map_data.fd().as_fd().as_raw_fd();
299    let key = pid;
300    let val: u8 = 1;
301    bpf_map_update_elem(
302        fd,
303        &key as *const _ as *const _,
304        &val as *const _ as *const _,
305        0,
306    )
307    .map_err(|e| anyhow::anyhow!("allowed_pids update failed for {}: {}", pid, e))
308}
309
310/// Remove a PID from the allowed_pids pinned map.
311pub fn remove_allowed_pid(pid: u32) -> anyhow::Result<()> {
312    let map_data = MapData::from_pin(allowed_pids_pin_path())?;
313    let fd = map_data.fd().as_fd().as_raw_fd();
314    bpf_map_delete_elem(fd, &pid as *const _ as *const _)
315        .map_err(|e| anyhow::anyhow!("allowed_pids delete failed for {}: {}", pid, e))
316}
317
318fn bpf_map_delete_elem(fd: i32, key: *const c::c_void) -> io::Result<()> {
319    let attr = BpfMapUpdateAttr {
320        map_fd: fd as u32,
321        _pad: 0,
322        key: key as usize as u64,
323        value: 0,
324        flags: 0,
325    };
326    let ret = unsafe {
327        c::syscall(
328            c::SYS_bpf,
329            BPF_MAP_DELETE_ELEM,
330            &attr,
331            std::mem::size_of::<BpfMapUpdateAttr>(),
332        )
333    };
334    if ret < 0 {
335        Err(io::Error::last_os_error())
336    } else {
337        Ok(())
338    }
339}
340
341/// Purge all entries for a given pid in the pinned proc_module_offsets map.
342pub fn purge_offsets_for_pid(pid: u32) -> anyhow::Result<usize> {
343    let map_data = MapData::from_pin(proc_offsets_pin_path())?;
344    let fd = map_data.fd().as_fd().as_raw_fd();
345    let mut deleted = 0usize;
346
347    // Iterate keys with GET_NEXT_KEY
348    let mut prev: Option<ProcModuleKey> = None;
349    loop {
350        let mut next: ProcModuleKey = ProcModuleKey {
351            pid: 0,
352            pad: 0,
353            cookie_lo: 0,
354            cookie_hi: 0,
355        };
356        let key_ptr = prev
357            .as_ref()
358            .map(|k| k as *const _ as *const c::c_void)
359            .unwrap_or(std::ptr::null());
360        let res = bpf_map_get_next_key(fd, key_ptr, &mut next as *mut _ as *mut _);
361        match res {
362            Ok(()) => {
363                // Check pid match
364                if next.pid == pid {
365                    // Delete and continue iteration from the same prev (do not advance prev)
366                    let _ = bpf_map_delete_elem(fd, &next as *const _ as *const _);
367                    deleted += 1;
368                    // Do not set prev = Some(next) to avoid skipping following keys
369                    continue;
370                } else {
371                    prev = Some(next);
372                }
373            }
374            Err(e) => {
375                // ENOENT means end of iteration
376                if e.raw_os_error() == Some(libc::ENOENT) {
377                    break;
378                } else {
379                    return Err(anyhow::anyhow!("bpf_map_get_next_key failed: {}", e));
380                }
381            }
382        }
383    }
384    Ok(deleted)
385}
386
387/// Open the pinned global proc_module_offsets map and insert entries via raw bpf syscall.
388pub fn insert_offsets_for_pid(
389    pid: u32,
390    items: &[(u64, ProcModuleOffsetsValue)],
391) -> anyhow::Result<usize> {
392    let map_data = MapData::from_pin(proc_offsets_pin_path())?;
393    let fd = map_data.fd().as_fd().as_raw_fd();
394    let mut inserted = 0usize;
395    for (cookie, off) in items {
396        let key = ProcModuleKey {
397            pid,
398            pad: 0,
399            cookie_lo: (*cookie & 0xffff_ffff) as u32,
400            cookie_hi: (*cookie >> 32) as u32,
401        };
402        match bpf_map_update_elem(
403            fd,
404            &key as *const _ as *const _,
405            off as *const _ as *const _,
406            0,
407        ) {
408            Ok(()) => {
409                tracing::debug!(
410                    "proc_module_offsets insert ok: pid={} cookie=0x{:08x}{:08x} text=0x{:x} rodata=0x{:x} data=0x{:x} bss=0x{:x}",
411                    pid, key.cookie_hi, key.cookie_lo, off.text, off.rodata, off.data, off.bss
412                );
413                inserted += 1
414            }
415            Err(e) => warn!(
416                "bpf_map_update_elem failed for pid={} cookie=0x{:08x}{:08x}: {}",
417                pid, key.cookie_hi, key.cookie_lo, e
418            ),
419        }
420    }
421    Ok(inserted)
422}
423
424/// Remove the pinned proc_module_offsets map and its per-process directory (best effort).
425/// Safe to call multiple times; missing paths are ignored.
426pub fn cleanup_pinned_proc_offsets() -> anyhow::Result<()> {
427    let path = proc_offsets_pin_path();
428    if path.exists() {
429        let _ = std::fs::remove_file(&path);
430    }
431    if let Some(dir) = path.parent() {
432        if let Ok(mut rd) = std::fs::read_dir(dir) {
433            if rd.next().is_none() {
434                let _ = std::fs::remove_dir(dir);
435            }
436        }
437    }
438    Ok(())
439}
440// Note: map open/write helpers will be added once we standardize on aya APIs across crates.