Skip to main content

ghostscope_process/
sysmon.rs

1use crate::{
2    module_probe::cookie_for_path,
3    offsets::{PidOffsetsEntry, ProcessManager},
4    pid::{
5        resolve_event_pid_for_proc, resolve_proc_pid_for_event, runtime_pid_candidates_for_proc,
6        PidNamespaceId,
7    },
8    pinned_bpf_maps,
9    proc_maps::{
10        normalize_mapped_module_path, read_proc_maps, should_skip_mapped_module_path,
11        visit_proc_maps, ModuleIdentity,
12    },
13};
14use std::collections::{BTreeSet, HashMap};
15use std::ops::ControlFlow;
16use std::path::{Path, PathBuf};
17use std::sync::{mpsc, Arc, Mutex};
18use std::thread::{self, JoinHandle};
19use std::time::{Duration, Instant};
20use tracing::{error, info, warn};
21
22/// Kind of process lifecycle event
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum SysEventKind {
25    Exec,
26    Fork,
27    Exit,
28    MapChange,
29}
30
31impl SysEventKind {
32    fn from_u32(v: u32) -> Option<Self> {
33        match v {
34            1 => Some(SysEventKind::Exec),
35            2 => Some(SysEventKind::Fork),
36            3 => Some(SysEventKind::Exit),
37            4 => Some(SysEventKind::MapChange),
38            _ => None,
39        }
40    }
41
42    fn as_u32(self) -> u32 {
43        match self {
44            SysEventKind::Exec => 1,
45            SysEventKind::Fork => 2,
46            SysEventKind::Exit => 3,
47            SysEventKind::MapChange => 4,
48        }
49    }
50}
51
52/// Internal sysmon event selector.
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub struct SysmonEventMask {
55    pub exec: bool,
56    pub fork: bool,
57    pub exit: bool,
58    pub map_change: bool,
59}
60
61impl SysmonEventMask {
62    pub fn target_mode() -> Self {
63        Self {
64            exec: true,
65            fork: true,
66            exit: true,
67            map_change: false,
68        }
69    }
70
71    pub fn target_mode_with_map_changes() -> Self {
72        Self {
73            exec: true,
74            fork: true,
75            exit: true,
76            map_change: true,
77        }
78    }
79
80    pub fn pid_module_changes() -> Self {
81        Self {
82            exec: false,
83            fork: false,
84            exit: false,
85            map_change: true,
86        }
87    }
88
89    fn without_map_change(self) -> Self {
90        Self {
91            map_change: false,
92            ..self
93        }
94    }
95
96    fn has_lifecycle_events(self) -> bool {
97        self.exec || self.fork || self.exit
98    }
99
100    #[cfg(feature = "sysmon-ebpf")]
101    fn bits(self) -> u32 {
102        let mut bits = 0u32;
103        if self.exec {
104            bits |= SYSMON_EVENT_MASK_EXEC;
105        }
106        if self.fork {
107            bits |= SYSMON_EVENT_MASK_FORK;
108        }
109        if self.exit {
110            bits |= SYSMON_EVENT_MASK_EXIT;
111        }
112        if self.map_change {
113            bits |= SYSMON_EVENT_MASK_MAP_CHANGE;
114        }
115        bits
116    }
117}
118
119impl Default for SysmonEventMask {
120    fn default() -> Self {
121        Self::target_mode()
122    }
123}
124
125/// Raw SysEvent ABI — must match eBPF side exactly
126/// ABI note: This layout is mirrored in eBPF at
127/// `ghostscope-process/ebpf/sysmon-bpf/src/lib.rs`. We intentionally keep
128/// two copies for now to avoid entangling the BPF build with the workspace.
129/// Keep repr(C), field order and sizes identical on both sides. Current
130/// layout (12 bytes): { tgid: u32, host_tgid: u32, kind: u32 }.
131#[repr(C)]
132#[derive(Clone, Copy)]
133pub struct SysEvent {
134    /// Runtime TGID in the configured sysmon event namespace when available.
135    pub tgid: u32,
136    /// Host/initial-namespace TGID from bpf_get_current_pid_tgid().
137    pub host_tgid: u32,
138    pub kind: u32, // 1=exec,2=fork,3=exit,4=map-change
139}
140
141impl SysEvent {
142    pub fn event_kind(self) -> Option<SysEventKind> {
143        SysEventKind::from_u32(self.kind)
144    }
145}
146
147const PENDING_POLL_INTERVAL: Duration = Duration::from_millis(150);
148const PENDING_MAX_ATTEMPTS: u32 = 20;
149const MAP_CHANGE_DEBOUNCE_INTERVAL: Duration = Duration::from_millis(75);
150const MODULE_REFRESH_INTERVAL: Duration = Duration::from_millis(250);
151const SYSMON_EVENT_QUEUE_CAPACITY: usize = 1024;
152
153#[cfg(feature = "sysmon-ebpf")]
154const SYSMON_EVENT_MASK_EXEC: u32 = 1 << 0;
155#[cfg(feature = "sysmon-ebpf")]
156const SYSMON_EVENT_MASK_FORK: u32 = 1 << 1;
157#[cfg(feature = "sysmon-ebpf")]
158const SYSMON_EVENT_MASK_EXIT: u32 = 1 << 2;
159#[cfg(feature = "sysmon-ebpf")]
160const SYSMON_EVENT_MASK_MAP_CHANGE: u32 = 1 << 3;
161
162#[derive(Debug, Clone, Copy, PartialEq, Eq)]
163enum PendingOffsetsKind {
164    Retry,
165    MapChangeCandidate,
166}
167
168impl PendingOffsetsKind {
169    fn keep_for_map_changes_after_retry_exhaustion(self) -> bool {
170        matches!(self, Self::MapChangeCandidate)
171    }
172}
173
174#[derive(Debug, Clone)]
175pub(crate) struct PendingOffsetsEntry {
176    target_path: PathBuf,
177    attempts: u32,
178    kind: PendingOffsetsKind,
179    retry_exhausted: bool,
180    last_poll: Instant,
181}
182
183#[derive(Debug, Clone)]
184struct PendingOffsetsDue {
185    event_pid: u32,
186    target_path: PathBuf,
187    attempts: u32,
188    kind: PendingOffsetsKind,
189}
190
191#[derive(Debug, Default)]
192pub(crate) struct PendingOffsets {
193    entries: HashMap<u32, PendingOffsetsEntry>,
194}
195
196impl PendingOffsets {
197    fn new() -> Self {
198        Self {
199            entries: HashMap::new(),
200        }
201    }
202
203    fn register(&mut self, pid: u32, target: &Path) {
204        self.register_with_kind(pid, target, PendingOffsetsKind::Retry);
205    }
206
207    fn register_map_change_candidate(&mut self, pid: u32, target: &Path) {
208        self.register_with_kind(pid, target, PendingOffsetsKind::MapChangeCandidate);
209    }
210
211    fn register_with_kind(&mut self, pid: u32, target: &Path, kind: PendingOffsetsKind) {
212        let now = Instant::now();
213        let last_poll = now.checked_sub(PENDING_POLL_INTERVAL).unwrap_or(now);
214        self.entries
215            .entry(pid)
216            .and_modify(|entry| {
217                entry.target_path = target.to_path_buf();
218                entry.attempts = 0;
219                entry.kind = kind;
220                entry.retry_exhausted = false;
221                entry.last_poll = last_poll;
222            })
223            .or_insert(PendingOffsetsEntry {
224                target_path: target.to_path_buf(),
225                attempts: 0,
226                kind,
227                retry_exhausted: false,
228                last_poll,
229            });
230    }
231
232    fn remove(&mut self, pid: u32) {
233        self.entries.remove(&pid);
234    }
235
236    fn contains_map_change_candidate(&self, pid: u32, target: &Path) -> bool {
237        self.entries
238            .get(&pid)
239            .map(|entry| {
240                entry.kind == PendingOffsetsKind::MapChangeCandidate && entry.target_path == target
241            })
242            .unwrap_or(false)
243    }
244
245    fn mark_retry_exhausted(&mut self, pid: u32) {
246        if let Some(entry) = self.entries.get_mut(&pid) {
247            entry.retry_exhausted = true;
248        }
249    }
250
251    fn take_due(&mut self) -> Vec<PendingOffsetsDue> {
252        let mut due = Vec::new();
253        let now = Instant::now();
254        for (&pid, entry) in self.entries.iter_mut() {
255            if entry.retry_exhausted {
256                continue;
257            }
258            if now.duration_since(entry.last_poll) >= PENDING_POLL_INTERVAL {
259                entry.last_poll = now;
260                entry.attempts = entry.attempts.saturating_add(1);
261                due.push(PendingOffsetsDue {
262                    event_pid: pid,
263                    target_path: entry.target_path.clone(),
264                    attempts: entry.attempts,
265                    kind: entry.kind,
266                });
267            }
268        }
269        due
270    }
271}
272
273#[derive(Debug, Clone)]
274pub(crate) struct PendingMapRefreshEntry {
275    last_seen: Instant,
276    event_pid: u32,
277    host_pid: u32,
278}
279
280#[derive(Debug, Clone, Copy)]
281struct PendingMapRefreshDue {
282    event_pid: u32,
283    host_pid: u32,
284    proc_pid: u32,
285}
286
287#[derive(Debug, Clone, Copy, PartialEq, Eq)]
288struct PendingMapChangeCandidate {
289    event_pid: u32,
290    host_pid: u32,
291    proc_pid: u32,
292}
293
294#[derive(Debug, Default)]
295pub(crate) struct PendingMapRefreshes {
296    entries: HashMap<u32, PendingMapRefreshEntry>,
297}
298
299impl PendingMapRefreshes {
300    fn new() -> Self {
301        Self {
302            entries: HashMap::new(),
303        }
304    }
305
306    fn register(&mut self, event_pid: u32, host_pid: u32, proc_pid: u32) {
307        self.entries.insert(
308            proc_pid,
309            PendingMapRefreshEntry {
310                last_seen: Instant::now(),
311                event_pid,
312                host_pid,
313            },
314        );
315    }
316
317    fn take_due(&mut self) -> Vec<PendingMapRefreshDue> {
318        let now = Instant::now();
319        let due: Vec<PendingMapRefreshDue> = self
320            .entries
321            .iter()
322            .filter_map(|(&proc_pid, entry)| {
323                (now.duration_since(entry.last_seen) >= MAP_CHANGE_DEBOUNCE_INTERVAL).then_some(
324                    PendingMapRefreshDue {
325                        event_pid: entry.event_pid,
326                        host_pid: entry.host_pid,
327                        proc_pid,
328                    },
329                )
330            })
331            .collect();
332        for entry in &due {
333            self.entries.remove(&entry.proc_pid);
334        }
335        due
336    }
337}
338
339/// Configuration for sysmon
340#[derive(Debug, Clone)]
341pub struct SysmonConfig {
342    /// If set, only attempt offsets prefill for events whose binary/module path matches this target.
343    pub target_module: Option<PathBuf>,
344    /// Maximum number of entries for the pinned proc offsets map (used when ensuring existence).
345    pub proc_offsets_max_entries: u32,
346    /// PerfEventArray per-CPU buffer pages (used when ringbuf is unavailable).
347    pub perf_page_count: Option<usize>,
348    /// Internal event selector for the sysmon eBPF side.
349    pub event_mask: SysmonEventMask,
350    /// Whether map-change events should be emitted before the PID allowlist is populated.
351    pub map_change_unfiltered: bool,
352    /// Optional event PID to watch. `None` means system-wide.
353    pub watched_pid: Option<u32>,
354    /// Optional PID namespace for interpreting `watched_pid`.
355    pub watched_pid_ns: Option<PidNamespaceId>,
356    /// Optional PID namespace for reporting target-mode event TGIDs.
357    pub event_pid_ns: Option<PidNamespaceId>,
358    /// Optional `/proc` PID corresponding to `watched_pid`.
359    pub watched_proc_pid: Option<u32>,
360}
361
362impl SysmonConfig {
363    pub fn new() -> Self {
364        Self {
365            target_module: None,
366            proc_offsets_max_entries: 4096,
367            perf_page_count: None,
368            event_mask: SysmonEventMask::target_mode(),
369            map_change_unfiltered: false,
370            watched_pid: None,
371            watched_pid_ns: None,
372            event_pid_ns: None,
373            watched_proc_pid: None,
374        }
375    }
376}
377
378impl Default for SysmonConfig {
379    fn default() -> Self {
380        Self::new()
381    }
382}
383
384/// Process sysmon — userspace controller that listens for process lifecycle events and
385/// performs incremental prefill/cleanup of offsets.
386///
387/// Note: The low-level event source (tracepoints via eBPF or kernel proc connector) is pluggable.
388/// This initial implementation provides the public API and a background loop stub; the event source
389/// integration will be wired subsequently.
390pub struct ProcessSysmon {
391    cfg: SysmonConfig,
392    mgr: Arc<Mutex<ProcessManager>>, // shared manager to compute/prefill offsets
393    tx: mpsc::SyncSender<SysEvent>,
394    rx: mpsc::Receiver<SysEvent>,
395    pending_offsets: Arc<Mutex<PendingOffsets>>,
396    pending_map_refreshes: Arc<Mutex<PendingMapRefreshes>>,
397    handle: Option<JoinHandle<()>>,
398}
399
400impl core::fmt::Debug for ProcessSysmon {
401    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
402        f.write_str("ProcessSysmon{..}")
403    }
404}
405
406impl ProcessSysmon {
407    /// Create a new sysmon instance with shared ProcessManager and config.
408    pub fn new(mgr: Arc<Mutex<ProcessManager>>, cfg: SysmonConfig) -> Self {
409        let (tx, rx) = mpsc::sync_channel(SYSMON_EVENT_QUEUE_CAPACITY);
410        Self {
411            cfg,
412            mgr,
413            tx,
414            rx,
415            pending_offsets: Arc::new(Mutex::new(PendingOffsets::new())),
416            pending_map_refreshes: Arc::new(Mutex::new(PendingMapRefreshes::new())),
417            handle: None,
418        }
419    }
420
421    /// Start background monitoring thread and return immediately.
422    ///
423    /// In the next iteration we will attach eBPF tracepoints (sched_process_exec/exit/fork)
424    /// and stream events into this channel. For now, we ensure the pinned offsets map exists
425    /// and keep a placeholder loop that can be extended to consume a real source.
426    pub fn start(&mut self) {
427        let _ =
428            pinned_bpf_maps::ensure_pinned_proc_offsets_exists(self.cfg.proc_offsets_max_entries);
429        let _ =
430            pinned_bpf_maps::ensure_pinned_pid_aliases_exists(self.cfg.proc_offsets_max_entries);
431        let _ = pinned_bpf_maps::ensure_pinned_proc_module_ranges_exist(
432            self.cfg.proc_offsets_max_entries,
433        );
434        let _ = pinned_bpf_maps::ensure_pinned_allowed_pids_exists(16_384);
435
436        let tx = self.tx.clone();
437        let mgr = Arc::clone(&self.mgr);
438        let pending = Arc::clone(&self.pending_offsets);
439        let pending_map_refreshes = Arc::clone(&self.pending_map_refreshes);
440        let cfg = self.cfg.clone();
441
442        let handle = thread::Builder::new()
443            .name("gs-sysmon".to_string())
444            .spawn(move || {
445                info!("ProcessSysmon thread started");
446                #[cfg(feature = "sysmon-ebpf")]
447                {
448                    if let Err(e) = run_sysmon_loop(mgr, cfg, pending, pending_map_refreshes, tx) {
449                        error!("Sysmon loop error: {}", e);
450                    }
451                }
452                #[cfg(not(feature = "sysmon-ebpf"))]
453                {
454                    let _ = pending;
455                    let _ = pending_map_refreshes;
456                    let _ = cfg;
457                    warn!("sysmon-ebpf feature is disabled; sysmon is in stub mode");
458                    loop {
459                        std::thread::sleep(std::time::Duration::from_millis(5000));
460                    }
461                }
462                info!("ProcessSysmon thread exiting");
463            });
464        match handle {
465            Ok(h) => self.handle = Some(h),
466            Err(e) => {
467                error!("Failed to spawn ProcessSysmon thread: {}", e);
468                self.handle = None;
469            }
470        }
471    }
472
473    /// Blocking poll (with timeout) for the next system event.
474    pub fn recv_timeout(&self, timeout: std::time::Duration) -> Option<SysEvent> {
475        match self.rx.recv_timeout(timeout) {
476            Ok(ev) => Some(ev),
477            Err(mpsc::RecvTimeoutError::Timeout) => None,
478            Err(mpsc::RecvTimeoutError::Disconnected) => None,
479        }
480    }
481
482    /// Handle one system event: prefill on Exec/Fork, cleanup on Exit.
483    fn handle_event_with_proc_pid_resolver(
484        mgr: &Arc<Mutex<ProcessManager>>,
485        target: &Option<PathBuf>,
486        pending: &Arc<Mutex<PendingOffsets>>,
487        ev: &SysEvent,
488        proc_pid_for_event: impl Fn(u32) -> u32,
489    ) -> anyhow::Result<()> {
490        let kind = match SysEventKind::from_u32(ev.kind) {
491            Some(k) => k,
492            None => {
493                tracing::warn!(
494                    "Sysmon: invalid event kind {} for pid {}; ignoring",
495                    ev.kind,
496                    ev.tgid
497                );
498                return Ok(());
499            }
500        };
501        tracing::trace!("Sysmon event: kind={:?} event_pid={}", kind, ev.tgid);
502        match kind {
503            SysEventKind::Exec | SysEventKind::Fork => {
504                let proc_pid = proc_pid_for_event(ev.tgid);
505                record_runtime_pid_aliases_for_sys_event(mgr, ev, proc_pid);
506                if let Some(tpath) = target {
507                    let path = tpath.as_path();
508                    if crate::util::is_shared_object(path) {
509                        if kind == SysEventKind::Exec && !pid_maps_target_module(proc_pid, path) {
510                            if pid_alive(proc_pid) {
511                                tracing::debug!(
512                                    "Sysmon: event pid {} (proc pid {}) does not map target module yet; scheduling retry",
513                                    ev.tgid,
514                                    proc_pid
515                                );
516                                if let Ok(mut guard) = pending.lock() {
517                                    guard.register_map_change_candidate(ev.tgid, path);
518                                }
519                            } else {
520                                let host_pid = sys_event_host_pid(ev);
521                                if host_pid != ev.tgid && pid_alive(host_pid) {
522                                    tracing::debug!(
523                                        "Sysmon: event pid {} is not visible in current /proc namespace; scheduling host pid {} retry",
524                                        ev.tgid,
525                                        host_pid
526                                    );
527                                    if let Ok(mut guard) = pending.lock() {
528                                        guard.register_map_change_candidate(host_pid, path);
529                                    }
530                                } else {
531                                    tracing::debug!(
532                                        "Sysmon: event pid {} (host pid {}) is not visible in current /proc namespace; skip exec-based target retry",
533                                        ev.tgid,
534                                        host_pid
535                                    );
536                                }
537                            }
538                            return Ok(());
539                        } else if let Ok(mut guard) = pending.lock() {
540                            guard.remove(ev.tgid);
541                        }
542                    } else if kind == SysEventKind::Exec {
543                        if let Some(actual) = get_comm_from_proc(proc_pid) {
544                            let expected = truncate_basename_to_comm(path);
545                            if actual.as_bytes() != expected.as_slice() {
546                                tracing::warn!(
547                                    "Sysmon: comm mismatch for event pid {} (proc pid {}) (actual='{}', expected='{}'); skip prefill/insert",
548                                    ev.tgid,
549                                    proc_pid,
550                                    actual,
551                                    core::str::from_utf8(&expected).unwrap_or("")
552                                );
553                                return Ok(());
554                            }
555                        }
556                    }
557                }
558                let inserted =
559                    prefill_offsets_for_pid(mgr, ev.tgid, target.as_deref(), &proc_pid_for_event)?;
560                if inserted {
561                    let host_pid = sys_event_host_pid(ev);
562                    if host_pid != ev.tgid {
563                        let _ = crate::pinned_bpf_maps::insert_allowed_pid(host_pid);
564                    }
565                }
566                if kind == SysEventKind::Exec {
567                    if let Some(tpath) = target {
568                        if inserted {
569                            if let Ok(mut guard) = pending.lock() {
570                                guard.remove(ev.tgid);
571                            }
572                        } else if let Ok(mut guard) = pending.lock() {
573                            tracing::debug!(
574                                "Sysmon: event pid {} (proc pid {}) prefill inserted no matching offsets; scheduling retry",
575                                ev.tgid,
576                                proc_pid
577                            );
578                            guard.register(ev.tgid, tpath.as_path());
579                        }
580                    }
581                }
582            }
583            SysEventKind::Exit => {
584                let proc_pid = proc_pid_for_event(ev.tgid);
585                let host_pid = sys_event_host_pid(ev);
586                if let Ok(mut guard) = pending.lock() {
587                    guard.remove(ev.tgid);
588                    if host_pid != ev.tgid {
589                        guard.remove(host_pid);
590                    }
591                }
592                if let Ok(mut guard) = mgr.lock() {
593                    guard.forget_pid(proc_pid);
594                    if proc_pid != ev.tgid {
595                        guard.forget_pid(ev.tgid);
596                    }
597                    if host_pid != ev.tgid && host_pid != proc_pid {
598                        guard.forget_pid(host_pid);
599                    }
600                }
601                let purged = purge_runtime_pid_artifacts(proc_pid, ev.tgid, host_pid);
602                info!(
603                    "Sysmon: observed exit for event pid {} (host pid {}, proc pid {}) (purged {} entries)",
604                    ev.tgid,
605                    host_pid,
606                    proc_pid,
607                    purged
608                );
609            }
610            SysEventKind::MapChange => {
611                tracing::trace!(
612                    "Sysmon: map-change event for pid {} is handled by the debounce queue",
613                    ev.tgid
614                );
615            }
616        }
617        Ok(())
618    }
619}
620
621fn try_publish_sys_event(tx: &mpsc::SyncSender<SysEvent>, ev: SysEvent) -> bool {
622    match tx.try_send(ev) {
623        Ok(()) => true,
624        Err(mpsc::TrySendError::Full(ev)) => {
625            tracing::trace!(
626                "Sysmon event queue full; dropping lifecycle notification for pid {} kind {}",
627                ev.tgid,
628                ev.kind
629            );
630            false
631        }
632        Err(mpsc::TrySendError::Disconnected(ev)) => {
633            tracing::trace!(
634                "Sysmon event receiver disconnected; dropping lifecycle notification for pid {} kind {}",
635                ev.tgid,
636                ev.kind
637            );
638            false
639        }
640    }
641}
642
643fn dispatch_sysmon_event(
644    mgr: &Arc<Mutex<ProcessManager>>,
645    target: &Option<PathBuf>,
646    pending: &Arc<Mutex<PendingOffsets>>,
647    pending_map_refreshes: &Arc<Mutex<PendingMapRefreshes>>,
648    proc_pid_for_event: &impl Fn(u32) -> u32,
649    ev: &SysEvent,
650) -> bool {
651    match SysEventKind::from_u32(ev.kind) {
652        Some(SysEventKind::MapChange) => {
653            if let Some(proc_pid) =
654                proc_pid_for_map_change_event(mgr, target, proc_pid_for_event, ev)
655            {
656                let proc_pid = target
657                    .as_deref()
658                    .map(|target_path| {
659                        canonicalize_cached_target_proc_pid(mgr, target_path, proc_pid)
660                    })
661                    .unwrap_or(proc_pid);
662                record_runtime_pid_aliases_for_sys_event(mgr, ev, proc_pid);
663                if let Ok(mut guard) = pending_map_refreshes.lock() {
664                    guard.register(ev.tgid, sys_event_host_pid(ev), proc_pid);
665                }
666                true
667            } else if let Some(target_path) = target.as_deref() {
668                let candidates = pending
669                    .lock()
670                    .ok()
671                    .map(|guard| {
672                        pending_map_change_candidates(&guard, target_path, proc_pid_for_event, ev)
673                    })
674                    .unwrap_or_default();
675
676                if candidates.is_empty() {
677                    tracing::trace!(
678                        "Sysmon: map-change event pid {} (host pid {}) did not resolve to a target /proc pid; skipping per-pid refresh",
679                        ev.tgid,
680                        sys_event_host_pid(ev)
681                    );
682                    false
683                } else {
684                    if let Ok(mut guard) = pending_map_refreshes.lock() {
685                        for candidate in &candidates {
686                            guard.register(
687                                candidate.event_pid,
688                                candidate.host_pid,
689                                candidate.proc_pid,
690                            );
691                        }
692                    }
693                    tracing::trace!(
694                        "Sysmon: queued map-change refresh for {} pending target candidate(s) from event pid {} (host pid {})",
695                        candidates.len(),
696                        ev.tgid,
697                        sys_event_host_pid(ev)
698                    );
699                    false
700                }
701            } else {
702                tracing::trace!(
703                    "Sysmon: map-change event pid {} (host pid {}) did not resolve to a target /proc pid; skipping per-pid refresh",
704                    ev.tgid,
705                    sys_event_host_pid(ev)
706                );
707                false
708            }
709        }
710        Some(_) => {
711            match ProcessSysmon::handle_event_with_proc_pid_resolver(
712                mgr,
713                target,
714                pending,
715                ev,
716                proc_pid_for_event,
717            ) {
718                Ok(()) => true,
719                Err(e) => {
720                    tracing::debug!(
721                        "Sysmon: handle_event failed for pid {} kind {}: {}",
722                        ev.tgid,
723                        ev.kind,
724                        e
725                    );
726                    false
727                }
728            }
729        }
730        None => {
731            match ProcessSysmon::handle_event_with_proc_pid_resolver(
732                mgr,
733                target,
734                pending,
735                ev,
736                proc_pid_for_event,
737            ) {
738                Ok(()) => true,
739                Err(e) => {
740                    tracing::debug!(
741                        "Sysmon: handle_event rejected invalid event for pid {} kind {}: {}",
742                        ev.tgid,
743                        ev.kind,
744                        e
745                    );
746                    false
747                }
748            }
749        }
750    }
751}
752
753fn proc_pid_for_map_change_event(
754    mgr: &Arc<Mutex<ProcessManager>>,
755    target: &Option<PathBuf>,
756    proc_pid_for_event: &impl Fn(u32) -> u32,
757    ev: &SysEvent,
758) -> Option<u32> {
759    let host_pid = sys_event_host_pid(ev);
760    let mut candidates = Vec::with_capacity(2);
761    push_unique_pid(&mut candidates, proc_pid_for_event(ev.tgid));
762    if host_pid != ev.tgid {
763        push_unique_pid(&mut candidates, proc_pid_for_event(host_pid));
764    }
765
766    for proc_pid in candidates {
767        if !pid_alive(proc_pid) {
768            continue;
769        }
770        if target.is_some() && is_current_process_pid(proc_pid) {
771            tracing::trace!(
772                "Sysmon: ignoring self map-change candidate proc pid {}",
773                proc_pid
774            );
775            continue;
776        }
777
778        let Some(target_path) = target.as_deref() else {
779            return Some(proc_pid);
780        };
781
782        if pid_maps_target_module(proc_pid, target_path)
783            || cached_offsets_exist_for_target_pid(mgr, target_path, proc_pid)
784        {
785            return Some(proc_pid);
786        }
787    }
788
789    None
790}
791
792fn pending_map_change_candidates(
793    pending: &PendingOffsets,
794    target_path: &Path,
795    proc_pid_for_event: &impl Fn(u32) -> u32,
796    ev: &SysEvent,
797) -> Vec<PendingMapChangeCandidate> {
798    let host_pid = sys_event_host_pid(ev);
799    let mut candidates = Vec::with_capacity(2);
800    push_pending_map_change_candidate(
801        &mut candidates,
802        pending,
803        target_path,
804        proc_pid_for_event,
805        ev.tgid,
806        host_pid,
807    );
808    if host_pid != ev.tgid {
809        push_pending_map_change_candidate(
810            &mut candidates,
811            pending,
812            target_path,
813            proc_pid_for_event,
814            host_pid,
815            host_pid,
816        );
817    }
818    candidates
819}
820
821fn push_pending_map_change_candidate(
822    candidates: &mut Vec<PendingMapChangeCandidate>,
823    pending: &PendingOffsets,
824    target_path: &Path,
825    proc_pid_for_event: &impl Fn(u32) -> u32,
826    event_pid: u32,
827    host_pid: u32,
828) {
829    if !pending.contains_map_change_candidate(event_pid, target_path) {
830        return;
831    }
832
833    let proc_pid = proc_pid_for_event(event_pid);
834    if !pid_alive(proc_pid) || is_current_process_pid(proc_pid) {
835        return;
836    }
837
838    if candidates
839        .iter()
840        .any(|candidate| candidate.proc_pid == proc_pid)
841    {
842        return;
843    }
844
845    candidates.push(PendingMapChangeCandidate {
846        event_pid,
847        host_pid,
848        proc_pid,
849    });
850}
851
852fn is_current_process_pid(proc_pid: u32) -> bool {
853    proc_pid == std::process::id()
854}
855
856fn cached_single_target_proc_pid(
857    mgr: &Arc<Mutex<ProcessManager>>,
858    target_path: &Path,
859) -> Option<u32> {
860    let module_path = target_path.to_string_lossy();
861    let mut target_pids = BTreeSet::new();
862    let guard = mgr.lock().ok()?;
863    for (pid, _, _, _, _) in guard.cached_offsets_for_module(module_path.as_ref()) {
864        if !is_current_process_pid(pid)
865            && pid_alive(pid)
866            && pid_maps_target_module(pid, target_path)
867        {
868            target_pids.insert(pid);
869        }
870    }
871
872    if target_pids.len() == 1 {
873        target_pids.iter().next().copied()
874    } else {
875        None
876    }
877}
878
879fn canonicalize_cached_target_proc_pid(
880    mgr: &Arc<Mutex<ProcessManager>>,
881    target_path: &Path,
882    fallback_proc_pid: u32,
883) -> u32 {
884    if pid_alive(fallback_proc_pid) && pid_maps_target_module(fallback_proc_pid, target_path) {
885        return fallback_proc_pid;
886    }
887
888    let Some(proc_pid) = cached_single_target_proc_pid(mgr, target_path) else {
889        return fallback_proc_pid;
890    };
891
892    if proc_pid != fallback_proc_pid {
893        tracing::debug!(
894            "Sysmon: canonicalized map-change proc pid {} -> {} for target {}",
895            fallback_proc_pid,
896            proc_pid,
897            target_path.display()
898        );
899    }
900
901    proc_pid
902}
903
904fn push_unique_pid(pids: &mut Vec<u32>, pid: u32) {
905    if !pids.contains(&pid) {
906        pids.push(pid);
907    }
908}
909
910fn sysmon_proc_pid_resolver(
911    watched_event_pid: Option<u32>,
912    watched_proc_pid: Option<u32>,
913) -> impl Fn(u32) -> u32 {
914    move |event_pid| {
915        if watched_event_pid == Some(event_pid) {
916            if let Some(proc_pid) = watched_proc_pid {
917                return proc_pid;
918            }
919        }
920
921        resolve_proc_pid_for_event(event_pid)
922    }
923}
924
925fn write_pinned_runtime_pid_alias(runtime_pid: u32, proc_pid: u32) {
926    if runtime_pid == proc_pid {
927        return;
928    }
929    match crate::pinned_bpf_maps::insert_pid_alias(runtime_pid, proc_pid) {
930        Ok(()) => tracing::trace!(
931            "Sysmon: inserted PID alias runtime pid {} -> proc pid {}",
932            runtime_pid,
933            proc_pid
934        ),
935        Err(e) => {
936            tracing::debug!(
937                "Sysmon: failed to insert PID alias runtime pid {} -> proc pid {}: {}",
938                runtime_pid,
939                proc_pid,
940                e
941            );
942        }
943    }
944}
945
946fn record_runtime_pid_alias_for_event(
947    mgr: &Arc<Mutex<ProcessManager>>,
948    runtime_pid: u32,
949    proc_pid: u32,
950) {
951    write_pinned_runtime_pid_alias(runtime_pid, proc_pid);
952    if let Ok(mut guard) = mgr.lock() {
953        guard.record_runtime_pid_alias(runtime_pid, proc_pid);
954    }
955}
956
957fn record_runtime_pid_aliases_for_proc_pid_locked(guard: &mut ProcessManager, proc_pid: u32) {
958    for runtime_pid in runtime_pid_candidates_for_proc(proc_pid) {
959        write_pinned_runtime_pid_alias(runtime_pid, proc_pid);
960        guard.record_runtime_pid_alias(runtime_pid, proc_pid);
961    }
962}
963
964fn record_runtime_pid_aliases_for_proc_pid(mgr: &Arc<Mutex<ProcessManager>>, proc_pid: u32) {
965    if let Ok(mut guard) = mgr.lock() {
966        record_runtime_pid_aliases_for_proc_pid_locked(&mut guard, proc_pid);
967    } else {
968        for runtime_pid in runtime_pid_candidates_for_proc(proc_pid) {
969            write_pinned_runtime_pid_alias(runtime_pid, proc_pid);
970        }
971    }
972}
973
974fn runtime_pid_keys_for_proc_event(
975    proc_pid: u32,
976    event_pid: u32,
977    extra_runtime_pids: impl IntoIterator<Item = u32>,
978) -> Vec<u32> {
979    let mut keys = BTreeSet::new();
980    keys.insert(proc_pid);
981    keys.insert(event_pid);
982    for runtime_pid in runtime_pid_candidates_for_proc(proc_pid) {
983        keys.insert(runtime_pid);
984    }
985    for runtime_pid in extra_runtime_pids {
986        if runtime_pid != 0 {
987            keys.insert(runtime_pid);
988        }
989    }
990    keys.into_iter().collect()
991}
992
993fn record_runtime_pid_aliases_for_keys(
994    mgr: &Arc<Mutex<ProcessManager>>,
995    proc_pid: u32,
996    runtime_pids: &[u32],
997) {
998    for runtime_pid in runtime_pids {
999        write_pinned_runtime_pid_alias(*runtime_pid, proc_pid);
1000    }
1001    if let Ok(mut guard) = mgr.lock() {
1002        for runtime_pid in runtime_pids {
1003            guard.record_runtime_pid_alias(*runtime_pid, proc_pid);
1004        }
1005    }
1006}
1007
1008fn insert_allowed_runtime_pid_keys(runtime_pids: &[u32]) {
1009    for runtime_pid in runtime_pids {
1010        let _ = crate::pinned_bpf_maps::insert_allowed_pid(*runtime_pid);
1011    }
1012}
1013
1014fn publish_offsets_for_runtime_pid_keys(
1015    proc_pid: u32,
1016    event_pid: u32,
1017    runtime_pids: &[u32],
1018    items: &[(u64, crate::pinned_bpf_maps::ProcModuleOffsetsValue)],
1019    log_context: &str,
1020) -> anyhow::Result<usize> {
1021    use crate::pinned_bpf_maps::{insert_offsets_for_pid, replace_ranges_for_pid};
1022
1023    let mut total_inserted = 0usize;
1024    for runtime_pid in runtime_pids {
1025        match insert_offsets_for_pid(*runtime_pid, items) {
1026            Ok(inserted) => {
1027                if inserted == 0 {
1028                    tracing::warn!(
1029                        "Sysmon: no offsets inserted for {} runtime pid {} (event pid {}, proc pid {}) (entry count={})",
1030                        log_context,
1031                        runtime_pid,
1032                        event_pid,
1033                        proc_pid,
1034                        items.len()
1035                    );
1036                    continue;
1037                }
1038                total_inserted += inserted;
1039                if let Err(e) = replace_ranges_for_pid(*runtime_pid, items) {
1040                    tracing::warn!(
1041                        "Sysmon: failed to replace module ranges for {} runtime pid {} (event pid {}, proc pid {}): {}",
1042                        log_context,
1043                        runtime_pid,
1044                        event_pid,
1045                        proc_pid,
1046                        e
1047                    );
1048                }
1049            }
1050            Err(e) => {
1051                tracing::warn!(
1052                    "Sysmon: failed to insert offsets for {} runtime pid {} (event pid {}, proc pid {}): {}",
1053                    log_context,
1054                    runtime_pid,
1055                    event_pid,
1056                    proc_pid,
1057                    e
1058                );
1059            }
1060        }
1061    }
1062
1063    Ok(total_inserted)
1064}
1065
1066fn purge_offsets_for_runtime_pid_keys(runtime_pids: &[u32]) -> anyhow::Result<usize> {
1067    let mut purged = 0usize;
1068    for runtime_pid in runtime_pids {
1069        purged += crate::pinned_bpf_maps::purge_offsets_for_pid(*runtime_pid)?;
1070        let _ = crate::pinned_bpf_maps::purge_ranges_for_pid(*runtime_pid);
1071    }
1072    Ok(purged)
1073}
1074
1075fn purge_runtime_pid_artifacts(proc_pid: u32, event_pid: u32, host_pid: u32) -> usize {
1076    let runtime_pids = runtime_pid_keys_for_proc_event(proc_pid, event_pid, [host_pid]);
1077    let mut purged_offsets = 0usize;
1078    for runtime_pid in runtime_pids {
1079        if let Ok(purged) = crate::pinned_bpf_maps::purge_offsets_for_pid(runtime_pid) {
1080            purged_offsets += purged;
1081        }
1082        let _ = crate::pinned_bpf_maps::purge_ranges_for_pid(runtime_pid);
1083        let _ = crate::pinned_bpf_maps::remove_allowed_pid(runtime_pid);
1084        if runtime_pid != proc_pid {
1085            let _ = crate::pinned_bpf_maps::remove_pid_alias(runtime_pid);
1086        }
1087    }
1088    purged_offsets
1089}
1090
1091fn sys_event_host_pid(ev: &SysEvent) -> u32 {
1092    if ev.host_tgid != 0 {
1093        ev.host_tgid
1094    } else {
1095        ev.tgid
1096    }
1097}
1098
1099fn record_runtime_pid_aliases_for_sys_event(
1100    mgr: &Arc<Mutex<ProcessManager>>,
1101    ev: &SysEvent,
1102    proc_pid: u32,
1103) {
1104    record_runtime_pid_alias_for_event(mgr, ev.tgid, proc_pid);
1105    let host_pid = sys_event_host_pid(ev);
1106    if host_pid != ev.tgid {
1107        record_runtime_pid_alias_for_event(mgr, host_pid, proc_pid);
1108    }
1109    record_runtime_pid_aliases_for_proc_pid(mgr, proc_pid);
1110}
1111
1112#[cfg(feature = "sysmon-ebpf")]
1113#[derive(Debug, Clone, Copy)]
1114enum SysmonAttachBackend {
1115    Raw,
1116    Btf,
1117    Classic,
1118}
1119
1120#[cfg(feature = "sysmon-ebpf")]
1121impl SysmonAttachBackend {
1122    fn label(self) -> &'static str {
1123        match self {
1124            SysmonAttachBackend::Raw => "raw tracepoint",
1125            SysmonAttachBackend::Btf => "BTF tracepoint",
1126            SysmonAttachBackend::Classic => "classic tracepoint",
1127        }
1128    }
1129}
1130
1131#[cfg(feature = "sysmon-ebpf")]
1132struct SysmonTracepoint {
1133    event: &'static str,
1134    category: &'static str,
1135    classic_program: &'static str,
1136    raw_program: &'static str,
1137    btf_program: &'static str,
1138}
1139
1140#[cfg(feature = "sysmon-ebpf")]
1141const SYSMON_TRACEPOINTS: &[SysmonTracepoint] = &[
1142    SysmonTracepoint {
1143        event: "sched_process_exec",
1144        category: "sched",
1145        classic_program: "sched_process_exec",
1146        raw_program: "raw_sched_process_exec",
1147        btf_program: "btf_sched_process_exec",
1148    },
1149    SysmonTracepoint {
1150        event: "sched_process_exit",
1151        category: "sched",
1152        classic_program: "sched_process_exit",
1153        raw_program: "raw_sched_process_exit",
1154        btf_program: "btf_sched_process_exit",
1155    },
1156    SysmonTracepoint {
1157        event: "sched_process_fork",
1158        category: "sched",
1159        classic_program: "sched_process_fork",
1160        raw_program: "raw_sched_process_fork",
1161        btf_program: "btf_sched_process_fork",
1162    },
1163];
1164
1165#[cfg(feature = "sysmon-ebpf")]
1166struct SysmonMapChangeTracepoint {
1167    event: &'static str,
1168    category: &'static str,
1169    program: &'static str,
1170}
1171
1172#[cfg(feature = "sysmon-ebpf")]
1173const SYSMON_MAP_CHANGE_TRACEPOINTS: &[SysmonMapChangeTracepoint] = &[
1174    SysmonMapChangeTracepoint {
1175        event: "sys_exit_mmap",
1176        category: "syscalls",
1177        program: "sys_exit_mmap",
1178    },
1179    SysmonMapChangeTracepoint {
1180        event: "sys_exit_mprotect",
1181        category: "syscalls",
1182        program: "sys_exit_mprotect",
1183    },
1184    SysmonMapChangeTracepoint {
1185        event: "sys_exit_munmap",
1186        category: "syscalls",
1187        program: "sys_exit_munmap",
1188    },
1189    SysmonMapChangeTracepoint {
1190        event: "sys_exit_mremap",
1191        category: "syscalls",
1192        program: "sys_exit_mremap",
1193    },
1194];
1195
1196#[cfg(feature = "sysmon-ebpf")]
1197fn load_sysmon_bpf(obj: &[u8], use_verbose: bool) -> anyhow::Result<aya::Ebpf> {
1198    use aya::{EbpfLoader, VerifierLogLevel};
1199
1200    let mut loader = EbpfLoader::new();
1201    if use_verbose {
1202        loader.verifier_log_level(VerifierLogLevel::VERBOSE | VerifierLogLevel::STATS);
1203        tracing::info!("Sysmon verifier logs: VERBOSE (debug build/log)");
1204    } else {
1205        loader.verifier_log_level(VerifierLogLevel::DEBUG | VerifierLogLevel::STATS);
1206        tracing::info!("Sysmon verifier logs: DEBUG (release/info)");
1207    }
1208
1209    let pin_dir = crate::pinned_bpf_maps::proc_offsets_pin_dir()?;
1210    loader.map_pin_path(
1211        crate::pinned_bpf_maps::ALLOWED_PIDS_MAP_NAME,
1212        pin_dir.join(crate::pinned_bpf_maps::ALLOWED_PIDS_MAP_NAME),
1213    );
1214    loader.map_pin_path(
1215        crate::pinned_bpf_maps::TARGET_EXEC_COMM_MAP_NAME,
1216        pin_dir.join(crate::pinned_bpf_maps::TARGET_EXEC_COMM_MAP_NAME),
1217    );
1218    loader.map_pin_path(
1219        crate::pinned_bpf_maps::SYSMON_MAP_CHANGE_UNFILTERED_MAP_NAME,
1220        pin_dir.join(crate::pinned_bpf_maps::SYSMON_MAP_CHANGE_UNFILTERED_MAP_NAME),
1221    );
1222
1223    Ok(loader.load(obj)?)
1224}
1225
1226#[cfg(feature = "sysmon-ebpf")]
1227fn configure_sysmon_exec_comm_filter(
1228    bpf: &mut aya::Ebpf,
1229    target: Option<&Path>,
1230) -> anyhow::Result<()> {
1231    use aya::maps::Array;
1232
1233    let mut filter_bytes = [0u8; 16];
1234    let mut filter_len = 0usize;
1235    if let Some(tpath) = target {
1236        if !crate::util::is_shared_object(tpath) {
1237            if let Some(name) = tpath.file_name().and_then(|s| s.to_str()) {
1238                let bytes = name.as_bytes();
1239                // task->comm stores at most TASK_COMM_LEN - 1 visible bytes plus NUL.
1240                // Keep the filter null-terminated so long executable basenames compare
1241                // against the same truncation that bpf_get_current_comm() returns.
1242                let len = bytes.len().min(filter_bytes.len() - 1);
1243                filter_bytes[..len].copy_from_slice(&bytes[..len]);
1244                filter_len = len;
1245            } else {
1246                tracing::warn!(
1247                    "Sysmon: target basename contains non-UTF8 bytes; exec comm filter disabled"
1248                );
1249            }
1250        }
1251    }
1252
1253    if let Some(map) = bpf.map_mut("target_exec_comm") {
1254        let mut array: Array<_, [u8; 16]> = map.try_into()?;
1255        array.set(0, filter_bytes, 0)?;
1256        if filter_len > 0 {
1257            match std::str::from_utf8(&filter_bytes[..filter_len]) {
1258                Ok(name_str) => {
1259                    tracing::info!("Sysmon: exec comm filter configured for '{}'", name_str)
1260                }
1261                Err(_) => tracing::info!(
1262                    "Sysmon: exec comm filter configured (non-UTF8 basename, len={})",
1263                    filter_len
1264                ),
1265            }
1266        } else {
1267            tracing::info!("Sysmon: exec comm filter disabled");
1268        }
1269    } else if filter_len > 0 {
1270        tracing::warn!("Sysmon: target_exec_comm map missing; exec filtering unavailable");
1271    }
1272
1273    Ok(())
1274}
1275
1276#[cfg(feature = "sysmon-ebpf")]
1277fn configure_sysmon_event_filter(
1278    bpf: &mut aya::Ebpf,
1279    event_mask: SysmonEventMask,
1280    map_change_unfiltered: bool,
1281    watched_pid: Option<u32>,
1282    watched_pid_ns: Option<PidNamespaceId>,
1283    event_pid_ns: Option<PidNamespaceId>,
1284) -> anyhow::Result<()> {
1285    use aya::maps::Array;
1286
1287    if let Some(map) = bpf.map_mut("sysmon_event_mask") {
1288        let mut array: Array<_, u32> = map.try_into()?;
1289        array.set(0, event_mask.bits(), 0)?;
1290        tracing::info!(
1291            "Sysmon: event mask configured (exec={}, fork={}, exit={}, map_change={})",
1292            event_mask.exec,
1293            event_mask.fork,
1294            event_mask.exit,
1295            event_mask.map_change
1296        );
1297    } else {
1298        tracing::warn!("Sysmon: sysmon_event_mask map missing; event filtering unavailable");
1299    }
1300
1301    if let Some(map) = bpf.map_mut("sysmon_map_change_unfiltered") {
1302        let mut array: Array<_, u32> = map.try_into()?;
1303        array.set(0, u32::from(map_change_unfiltered), 0)?;
1304        tracing::info!(
1305            "Sysmon: map-change pre-allowlist emission {}",
1306            if map_change_unfiltered {
1307                "enabled"
1308            } else {
1309                "disabled"
1310            }
1311        );
1312    } else if map_change_unfiltered {
1313        tracing::warn!(
1314            "Sysmon: sysmon_map_change_unfiltered map missing; pre-allowlist map-change events unavailable"
1315        );
1316    }
1317
1318    if let Some(map) = bpf.map_mut("sysmon_watched_pid") {
1319        let mut array: Array<_, u32> = map.try_into()?;
1320        array.set(0, watched_pid.unwrap_or(0), 0)?;
1321        if let Some(pid) = watched_pid {
1322            tracing::info!("Sysmon: watched event pid configured: {}", pid);
1323        } else {
1324            tracing::info!("Sysmon: watched event pid disabled");
1325        }
1326    } else if watched_pid.is_some() {
1327        tracing::warn!("Sysmon: sysmon_watched_pid map missing; PID filtering unavailable");
1328    }
1329
1330    let watched_pid_ns = watched_pid.and(watched_pid_ns);
1331    let ns_spec = watched_pid_ns.and_then(|pid_ns| pid_ns.helper_dev_inode());
1332    let (ns_dev, ns_ino) = ns_spec.unwrap_or((0, 0));
1333
1334    if let Some(map) = bpf.map_mut("sysmon_watched_pid_ns_dev") {
1335        let mut array: Array<_, u64> = map.try_into()?;
1336        array.set(0, ns_dev, 0)?;
1337    } else if ns_spec.is_some() {
1338        tracing::warn!(
1339            "Sysmon: sysmon_watched_pid_ns_dev map missing; namespace PID filtering unavailable"
1340        );
1341    }
1342
1343    if let Some(map) = bpf.map_mut("sysmon_watched_pid_ns_ino") {
1344        let mut array: Array<_, u64> = map.try_into()?;
1345        array.set(0, ns_ino, 0)?;
1346    } else if ns_spec.is_some() {
1347        tracing::warn!(
1348            "Sysmon: sysmon_watched_pid_ns_ino map missing; namespace PID filtering unavailable"
1349        );
1350    }
1351
1352    if let (Some(pid), Some((dev, ino))) = (watched_pid, ns_spec) {
1353        tracing::info!(
1354            "Sysmon: watched PID namespace configured: pid={} ns_dev={} ns_inode={}",
1355            pid,
1356            dev,
1357            ino
1358        );
1359    }
1360
1361    let event_ns_spec = event_pid_ns.and_then(|pid_ns| pid_ns.helper_dev_inode());
1362    let (event_ns_dev, event_ns_ino) = event_ns_spec.unwrap_or((0, 0));
1363
1364    if let Some(map) = bpf.map_mut("sysmon_event_pid_ns_dev") {
1365        let mut array: Array<_, u64> = map.try_into()?;
1366        array.set(0, event_ns_dev, 0)?;
1367    } else if event_ns_spec.is_some() {
1368        tracing::warn!(
1369            "Sysmon: sysmon_event_pid_ns_dev map missing; event namespace reporting unavailable"
1370        );
1371    }
1372
1373    if let Some(map) = bpf.map_mut("sysmon_event_pid_ns_ino") {
1374        let mut array: Array<_, u64> = map.try_into()?;
1375        array.set(0, event_ns_ino, 0)?;
1376    } else if event_ns_spec.is_some() {
1377        tracing::warn!(
1378            "Sysmon: sysmon_event_pid_ns_ino map missing; event namespace reporting unavailable"
1379        );
1380    }
1381
1382    if let Some((dev, ino)) = event_ns_spec {
1383        tracing::info!(
1384            "Sysmon: event PID namespace configured: ns_dev={} ns_inode={}",
1385            dev,
1386            ino
1387        );
1388    }
1389
1390    Ok(())
1391}
1392
1393#[cfg(feature = "sysmon-ebpf")]
1394fn attach_sysmon_backend(bpf: &mut aya::Ebpf, backend: SysmonAttachBackend) -> anyhow::Result<()> {
1395    match backend {
1396        SysmonAttachBackend::Raw => attach_raw_sysmon_tracepoints(bpf),
1397        SysmonAttachBackend::Btf => attach_btf_sysmon_tracepoints(bpf),
1398        SysmonAttachBackend::Classic => attach_classic_sysmon_tracepoints(bpf),
1399    }
1400}
1401
1402#[cfg(feature = "sysmon-ebpf")]
1403fn attach_raw_sysmon_tracepoints(bpf: &mut aya::Ebpf) -> anyhow::Result<()> {
1404    use aya::programs::RawTracePoint;
1405
1406    for spec in SYSMON_TRACEPOINTS {
1407        let prog = bpf.program_mut(spec.raw_program).ok_or_else(|| {
1408            anyhow::anyhow!("missing program '{}' in sysmon-bpf", spec.raw_program)
1409        })?;
1410        let tp: &mut RawTracePoint = prog.try_into()?;
1411        tp.load()?;
1412        tp.attach(spec.event)?;
1413        info!("Attached raw tracepoint: {}", spec.event);
1414    }
1415    Ok(())
1416}
1417
1418#[cfg(feature = "sysmon-ebpf")]
1419fn attach_btf_sysmon_tracepoints(bpf: &mut aya::Ebpf) -> anyhow::Result<()> {
1420    use anyhow::Context as _;
1421    use aya::{programs::BtfTracePoint, Btf};
1422
1423    let btf = Btf::from_sys_fs().context("kernel BTF is unavailable")?;
1424    for spec in SYSMON_TRACEPOINTS {
1425        let prog = bpf.program_mut(spec.btf_program).ok_or_else(|| {
1426            anyhow::anyhow!("missing program '{}' in sysmon-bpf", spec.btf_program)
1427        })?;
1428        let tp: &mut BtfTracePoint = prog.try_into()?;
1429        tp.load(spec.event, &btf)?;
1430        tp.attach()?;
1431        info!("Attached BTF tracepoint: {}", spec.event);
1432    }
1433    Ok(())
1434}
1435
1436#[cfg(feature = "sysmon-ebpf")]
1437fn attach_classic_sysmon_tracepoints(bpf: &mut aya::Ebpf) -> anyhow::Result<()> {
1438    use aya::programs::TracePoint;
1439
1440    for spec in SYSMON_TRACEPOINTS {
1441        let prog = bpf.program_mut(spec.classic_program).ok_or_else(|| {
1442            anyhow::anyhow!("missing program '{}' in sysmon-bpf", spec.classic_program)
1443        })?;
1444        let tp: &mut TracePoint = prog.try_into()?;
1445        tp.load()?;
1446        tp.attach(spec.category, spec.event)?;
1447        info!(
1448            "Attached classic tracepoint: {}:{}",
1449            spec.category, spec.event
1450        );
1451    }
1452    Ok(())
1453}
1454
1455#[cfg(feature = "sysmon-ebpf")]
1456fn attach_classic_map_change_tracepoints(bpf: &mut aya::Ebpf) -> anyhow::Result<usize> {
1457    use aya::programs::TracePoint;
1458
1459    let mut attached = 0usize;
1460    for spec in SYSMON_MAP_CHANGE_TRACEPOINTS {
1461        let Some(prog) = bpf.program_mut(spec.program) else {
1462            tracing::warn!(
1463                "Sysmon: missing map-change program '{}' in sysmon-bpf",
1464                spec.program
1465            );
1466            continue;
1467        };
1468        let attach_result = (|| {
1469            let tp: &mut TracePoint = prog.try_into()?;
1470            tp.load()?;
1471            tp.attach(spec.category, spec.event)?;
1472            Ok::<_, anyhow::Error>(())
1473        })();
1474        match attach_result {
1475            Ok(()) => {
1476                attached += 1;
1477                info!(
1478                    "Attached map-change tracepoint: {}:{}",
1479                    spec.category, spec.event
1480                );
1481            }
1482            Err(err) => {
1483                tracing::warn!(
1484                    "Sysmon: map-change tracepoint {}:{} unavailable: {:#}",
1485                    spec.category,
1486                    spec.event,
1487                    err
1488                );
1489            }
1490        }
1491    }
1492
1493    Ok(attached)
1494}
1495
1496#[cfg(feature = "sysmon-ebpf")]
1497fn load_and_attach_sysmon_bpf(
1498    obj: &[u8],
1499    cfg: &SysmonConfig,
1500    use_verbose: bool,
1501) -> anyhow::Result<aya::Ebpf> {
1502    let mut failures = Vec::new();
1503    for backend in [
1504        SysmonAttachBackend::Raw,
1505        SysmonAttachBackend::Btf,
1506        SysmonAttachBackend::Classic,
1507    ] {
1508        tracing::info!("Sysmon: trying {} backend", backend.label());
1509        let result = (|| {
1510            let mut bpf = load_sysmon_bpf(obj, use_verbose)?;
1511            configure_sysmon_exec_comm_filter(&mut bpf, cfg.target_module.as_deref())?;
1512            configure_sysmon_event_filter(
1513                &mut bpf,
1514                cfg.event_mask,
1515                cfg.map_change_unfiltered,
1516                cfg.watched_pid,
1517                cfg.watched_pid_ns,
1518                cfg.event_pid_ns,
1519            )?;
1520            attach_sysmon_backend(&mut bpf, backend)?;
1521            if cfg.event_mask.map_change {
1522                let map_attached = attach_classic_map_change_tracepoints(&mut bpf)?;
1523                if map_attached == 0 {
1524                    if !cfg.event_mask.has_lifecycle_events() {
1525                        return Err(anyhow::anyhow!(
1526                            "map-change events requested but no syscall tracepoints attached"
1527                        ));
1528                    }
1529
1530                    let fallback_mask = cfg.event_mask.without_map_change();
1531                    configure_sysmon_event_filter(
1532                        &mut bpf,
1533                        fallback_mask,
1534                        cfg.map_change_unfiltered,
1535                        cfg.watched_pid,
1536                        cfg.watched_pid_ns,
1537                        cfg.event_pid_ns,
1538                    )?;
1539                    tracing::warn!(
1540                        "Sysmon: map-change events requested but no syscall tracepoints attached; \
1541                         continuing with exec/fork/exit lifecycle events only"
1542                    );
1543                }
1544            }
1545            Ok::<_, anyhow::Error>(bpf)
1546        })();
1547
1548        match result {
1549            Ok(bpf) => {
1550                tracing::info!("Sysmon: using {} backend", backend.label());
1551                return Ok(bpf);
1552            }
1553            Err(err) => {
1554                tracing::warn!("Sysmon: {} backend unavailable: {:#}", backend.label(), err);
1555                failures.push(format!("{}: {err:#}", backend.label()));
1556            }
1557        }
1558    }
1559
1560    Err(anyhow::anyhow!(
1561        "no sysmon tracepoint backend available ({})",
1562        failures.join("; ")
1563    ))
1564}
1565
1566#[cfg(feature = "sysmon-ebpf")]
1567fn run_sysmon_loop(
1568    mgr: Arc<Mutex<ProcessManager>>,
1569    cfg: SysmonConfig,
1570    pending: Arc<Mutex<PendingOffsets>>,
1571    pending_map_refreshes: Arc<Mutex<PendingMapRefreshes>>,
1572    tx: mpsc::SyncSender<SysEvent>,
1573) -> anyhow::Result<()> {
1574    use aya::include_bytes_aligned;
1575    use aya::maps::{
1576        perf::{PerfEvent, PerfEventArray},
1577        ring_buf::RingBuf,
1578        MapData,
1579    };
1580    use log::{log_enabled, Level as LogLevel};
1581    // Load eBPF object (copied to OUT_DIR at build time)
1582    #[allow(unused_variables)]
1583    let obj_le: &[u8] = include_bytes_aligned!(concat!(env!("OUT_DIR"), "/sysmon-bpf.bpfel.o"));
1584    #[allow(unused_variables)]
1585    let obj_be: &[u8] = include_bytes_aligned!(concat!(env!("OUT_DIR"), "/sysmon-bpf.bpfeb.o"));
1586    let obj: &[u8] = if cfg!(target_endian = "little") {
1587        obj_le
1588    } else {
1589        obj_be
1590    };
1591    if obj.is_empty() {
1592        warn!("sysmon-bpf object missing; running in stub mode (no realtime process events)");
1593        return Ok(());
1594    }
1595    let target = cfg.target_module.clone();
1596    let use_verbose =
1597        cfg!(debug_assertions) || log_enabled!(LogLevel::Trace) || log_enabled!(LogLevel::Debug);
1598    let mut bpf = load_and_attach_sysmon_bpf(obj, &cfg, use_verbose)?;
1599    let proc_pid_for_event = sysmon_proc_pid_resolver(cfg.watched_pid, cfg.watched_proc_pid);
1600
1601    // Using allowlist-based gating in kernel; userspace decides allow on exec.
1602
1603    // Initial prefill for late-start cases: compute and insert offsets for already-running PIDs.
1604    if let Some(tpath) = &target {
1605        let mut initial_target_pids: BTreeSet<u32> = BTreeSet::new();
1606        if let Ok(mut guard) = mgr.lock() {
1607            if let Ok(prefilled) = guard.ensure_prefill_module(tpath.to_string_lossy().as_ref()) {
1608                tracing::info!(
1609                    "Sysmon: initial prefill cached {} pid(s) for module {}",
1610                    prefilled,
1611                    tpath.display()
1612                );
1613                let entries = guard.cached_offsets_for_module(tpath.to_string_lossy().as_ref());
1614                if !entries.is_empty() {
1615                    use crate::pinned_bpf_maps::ProcModuleOffsetsValue;
1616                    let mut by_pid: HashMap<u32, Vec<(u64, ProcModuleOffsetsValue)>> =
1617                        HashMap::new();
1618                    for (pid, cookie, off, base, size) in entries {
1619                        if is_current_process_pid(pid) {
1620                            continue;
1621                        }
1622                        by_pid.entry(pid).or_default().push((
1623                            cookie,
1624                            ProcModuleOffsetsValue::new(
1625                                off.text, off.rodata, off.data, off.bss, base, size,
1626                            ),
1627                        ));
1628                    }
1629                    let mut total = 0usize;
1630                    for (pid, items) in by_pid {
1631                        initial_target_pids.insert(pid);
1632                        // Add event PID (kernel namespace) to allowlist so subsequent
1633                        // fork/exit events are filtered in-kernel.
1634                        let event_pid = resolve_event_pid_for_proc(pid);
1635                        let runtime_pids = runtime_pid_keys_for_proc_event(pid, event_pid, []);
1636                        for runtime_pid in &runtime_pids {
1637                            write_pinned_runtime_pid_alias(*runtime_pid, pid);
1638                            guard.record_runtime_pid_alias(*runtime_pid, pid);
1639                        }
1640                        if let Ok(n) = publish_offsets_for_runtime_pid_keys(
1641                            pid,
1642                            event_pid,
1643                            &runtime_pids,
1644                            &items,
1645                            "initial prefill",
1646                        ) {
1647                            total += n;
1648                        }
1649                        insert_allowed_runtime_pid_keys(&runtime_pids);
1650                    }
1651                    tracing::info!(
1652                        "Sysmon: initial inserted {} offset entries for module {}",
1653                        total,
1654                        tpath.display()
1655                    );
1656                }
1657            }
1658        }
1659        for pid in initial_target_pids {
1660            let event_pid = resolve_event_pid_for_proc(pid);
1661            if let Err(e) =
1662                prefill_full_offsets_for_pid_if_new(&mgr, event_pid, &proc_pid_for_event)
1663            {
1664                tracing::debug!(
1665                    "Sysmon: initial full offset prefill failed for proc pid {} (event pid {}): {}",
1666                    pid,
1667                    event_pid,
1668                    e
1669                );
1670            }
1671        }
1672    }
1673    tracing::info!("Sysmon: setup complete");
1674    // Initial prefill already ran above. Do not make the first periodic module
1675    // refresh immediately due: for `-t executable`, the exec event is the fast
1676    // path that inserts proc_module_offsets and allowed_pids. A fallback /proc
1677    // scan here can delay a short-lived target past its only probe.
1678    let mut last_module_refresh = Instant::now();
1679    let mut target_pid_map_signatures = HashMap::<u32, PidMapsSignature>::new();
1680
1681    // Event loop: prefer ringbuf; fallback to perf
1682    if let Some(map) = bpf.take_map("sysmon_events") {
1683        let mut rb: RingBuf<MapData> = map.try_into()?;
1684        loop {
1685            let mut had_event = false;
1686            // Drain queued lifecycle events before periodic refresh. In the
1687            // short-lived `-t executable` path, sched_process_exec must be
1688            // handled promptly so offsets are ready before the first uprobe.
1689            while let Some(item) = rb.next() {
1690                had_event = true;
1691                if item.len() == core::mem::size_of::<SysEvent>() {
1692                    // SAFETY: The ring buffer sample length was checked to match SysEvent;
1693                    // read_unaligned handles any alignment from the byte slice.
1694                    let ev = unsafe { core::ptr::read_unaligned(item.as_ptr() as *const SysEvent) };
1695                    let matched = dispatch_sysmon_event(
1696                        &mgr,
1697                        &target,
1698                        &pending,
1699                        &pending_map_refreshes,
1700                        &proc_pid_for_event,
1701                        &ev,
1702                    );
1703                    if matched {
1704                        try_publish_sys_event(&tx, ev);
1705                    }
1706                }
1707            }
1708            poll_pending_offsets(&mgr, &pending, &proc_pid_for_event);
1709            poll_pending_map_refreshes(
1710                &mgr,
1711                target.as_deref(),
1712                &pending_map_refreshes,
1713                &pending,
1714                &tx,
1715            );
1716            refresh_target_module_offsets(
1717                &mgr,
1718                target.as_deref(),
1719                &mut last_module_refresh,
1720                &mut target_pid_map_signatures,
1721                &tx,
1722            );
1723            if !had_event {
1724                std::thread::sleep(std::time::Duration::from_millis(5));
1725            }
1726        }
1727    } else if let Some(map) = bpf.take_map("sysmon_events_perf") {
1728        let mut perf: PerfEventArray<_> = map.try_into()?;
1729        let online = aya::util::online_cpus().map_err(|(_, e)| anyhow::anyhow!(e))?;
1730        let mut bufs = Vec::new();
1731        for cpu in online {
1732            match perf.open(cpu, cfg.perf_page_count) {
1733                Ok(buf) => bufs.push(buf),
1734                Err(e) => warn!("Perf open failed for CPU {}: {}", cpu, e),
1735            }
1736        }
1737        if bufs.is_empty() {
1738            return Err(anyhow::anyhow!("No perf buffers opened"));
1739        }
1740        loop {
1741            std::thread::sleep(std::time::Duration::from_millis(10));
1742            for buf in bufs.iter_mut() {
1743                if !buf.readable() {
1744                    continue;
1745                }
1746                buf.for_each(|event| match event {
1747                    PerfEvent::Sample { head, tail } => {
1748                        let mut raw = [0u8; core::mem::size_of::<SysEvent>()];
1749                        let mut copied = 0;
1750                        for chunk in [head, tail] {
1751                            let remaining = raw.len().saturating_sub(copied);
1752                            if remaining == 0 {
1753                                break;
1754                            }
1755                            let take = chunk.len().min(remaining);
1756                            raw[copied..copied + take].copy_from_slice(&chunk[..take]);
1757                            copied += take;
1758                        }
1759                        if copied == raw.len() {
1760                            // SAFETY: raw is exactly the size of SysEvent and read_unaligned
1761                            // handles the byte array's alignment.
1762                            let ev = unsafe {
1763                                core::ptr::read_unaligned(raw.as_ptr() as *const SysEvent)
1764                            };
1765                            let matched = dispatch_sysmon_event(
1766                                &mgr,
1767                                &target,
1768                                &pending,
1769                                &pending_map_refreshes,
1770                                &proc_pid_for_event,
1771                                &ev,
1772                            );
1773                            if matched {
1774                                try_publish_sys_event(&tx, ev);
1775                            }
1776                        }
1777                    }
1778                    PerfEvent::Lost { count } => {
1779                        warn!("Perf event buffer lost {} sysmon events", count);
1780                    }
1781                });
1782            }
1783            poll_pending_offsets(&mgr, &pending, &proc_pid_for_event);
1784            poll_pending_map_refreshes(
1785                &mgr,
1786                target.as_deref(),
1787                &pending_map_refreshes,
1788                &pending,
1789                &tx,
1790            );
1791            refresh_target_module_offsets(
1792                &mgr,
1793                target.as_deref(),
1794                &mut last_module_refresh,
1795                &mut target_pid_map_signatures,
1796                &tx,
1797            );
1798        }
1799    } else {
1800        return Err(anyhow::anyhow!("No sysmon events map found (ringbuf/perf)"));
1801    }
1802}
1803
1804/* moved to ghostscope_process::util::is_shared_object
1805fn looks_like_shared_object(path: &Path) -> bool {
1806    // Determine shared object by ELF metadata:
1807    // - ET_EXEC => executable (not shared)
1808    // - ET_DYN + PT_INTERP present => PIE executable (not shared)
1809    // - ET_DYN without PT_INTERP => shared library
1810    use std::fs::File;
1811    use std::io::{Read, Seek, SeekFrom};
1812
1813    const EI_CLASS: usize = 4; // 1=32-bit, 2=64-bit
1814    const EI_DATA: usize = 5; // 1=little, 2=big
1815    const ET_EXEC: u16 = 2;
1816    const ET_DYN: u16 = 3;
1817    const PT_INTERP: u32 = 3;
1818
1819    let mut f = match File::open(path) {
1820        Ok(f) => f,
1821        Err(_) => return false, // conservative: treat as executable (enable filtering)
1822    };
1823    let mut ehdr = [0u8; 64];
1824    if f.read(&mut ehdr).ok().filter(|&n| n >= 52).is_none() {
1825        return false;
1826    }
1827    // ELF magic
1828    if &ehdr[0..4] != b"\x7FELF" {
1829        return false;
1830    }
1831    let class = ehdr[EI_CLASS];
1832    let data = ehdr[EI_DATA];
1833    let is_le = data == 1;
1834    // read u16/u32/u64 helpers
1835    let rd16 = |b: &[u8]| -> u16 {
1836        if is_le {
1837            u16::from_le_bytes([b[0], b[1]])
1838        } else {
1839            u16::from_be_bytes([b[0], b[1]])
1840        }
1841    };
1842    let rd32 = |b: &[u8]| -> u32 {
1843        if is_le {
1844            u32::from_le_bytes([b[0], b[1], b[2], b[3]])
1845        } else {
1846            u32::from_be_bytes([b[0], b[1], b[2], b[3]])
1847        }
1848    };
1849    let rd64 = |b: &[u8]| -> u64 {
1850        if is_le {
1851            u64::from_le_bytes([b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7]])
1852        } else {
1853            u64::from_be_bytes([b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7]])
1854        }
1855    };
1856
1857    // e_type at 0x10
1858    let e_type = rd16(&ehdr[16..18]);
1859    if e_type == ET_EXEC {
1860        return false; // executable
1861    }
1862
1863    // program header table offsets
1864    let (e_phoff, e_phentsize, e_phnum) = match class {
1865        1 => {
1866            // ELF32: e_phoff @0x1C (4), e_phentsize @0x2A (2), e_phnum @0x2C (2)
1867            let phoff = rd32(&ehdr[28..32]) as u64;
1868            let entsz = rd16(&ehdr[42..44]) as u64;
1869            let phnum = rd16(&ehdr[44..46]) as u64;
1870            (phoff, entsz, phnum)
1871        }
1872        2 => {
1873            // ELF64: e_phoff @0x20 (8), e_phentsize @0x36 (2), e_phnum @0x38 (2)
1874            let phoff = rd64(&ehdr[32..40]);
1875            let entsz = rd16(&ehdr[54..56]) as u64;
1876            let phnum = rd16(&ehdr[56..58]) as u64;
1877            (phoff, entsz, phnum)
1878        }
1879        _ => return false,
1880    };
1881
1882    if e_type == ET_DYN {
1883        // Scan program headers for PT_INTERP
1884        if e_phoff == 0 || e_phentsize < 4 || e_phnum == 0 {
1885            // malformed
1886            // If cannot inspect, be conservative and treat as shared library (disable filtering)
1887            return true;
1888        }
1889        // Seek and read each p_type
1890        for i in 0..e_phnum {
1891            let off = e_phoff + i * e_phentsize;
1892            if f.seek(SeekFrom::Start(off)).is_err() {
1893                return true;
1894            }
1895            let mut p = [0u8; 8];
1896            if f.read(&mut p[..4]).ok().filter(|&n| n == 4).is_none() {
1897                return true;
1898            }
1899            let p_type = rd32(&p[..4]);
1900            if p_type == PT_INTERP {
1901                return false; // PIE executable
1902            }
1903        }
1904        return true; // ET_DYN w/o PT_INTERP => shared library
1905    }
1906
1907    // Unknown types: default to 'not shared' (enable filtering)
1908    false
1909}
1910*/
1911
1912fn pid_alive(pid: u32) -> bool {
1913    std::path::Path::new(&format!("/proc/{pid}")).exists()
1914}
1915
1916fn filter_entries_for_target<'a>(
1917    entries: &'a [PidOffsetsEntry],
1918    target: Option<&Path>,
1919) -> Vec<&'a PidOffsetsEntry> {
1920    use std::fs;
1921    use std::os::unix::fs::MetadataExt;
1922
1923    if let Some(tpath) = target {
1924        match fs::metadata(tpath) {
1925            Ok(meta) => {
1926                let t_dev = meta.dev();
1927                let t_ino = meta.ino();
1928                entries
1929                    .iter()
1930                    .filter(|e| {
1931                        fs::metadata(&e.module_path)
1932                            .map(|m| m.dev() == t_dev && m.ino() == t_ino)
1933                            .unwrap_or(false)
1934                    })
1935                    .collect()
1936            }
1937            Err(_) => {
1938                let tc = cookie_for_path(&tpath.to_string_lossy());
1939                let by_cookie: Vec<_> = entries.iter().filter(|e| e.cookie == tc).collect();
1940                if !by_cookie.is_empty() {
1941                    by_cookie
1942                } else {
1943                    let tnorm = tpath.to_string_lossy().replace("/./", "/");
1944                    entries.iter().filter(|e| e.module_path == tnorm).collect()
1945                }
1946            }
1947        }
1948    } else {
1949        entries.iter().collect()
1950    }
1951}
1952
1953fn prefill_offsets_for_pid(
1954    mgr: &Arc<Mutex<ProcessManager>>,
1955    event_pid: u32,
1956    target: Option<&Path>,
1957    proc_pid_for_event: &impl Fn(u32) -> u32,
1958) -> anyhow::Result<bool> {
1959    write_offsets_for_pid(mgr, event_pid, target, false, &[], proc_pid_for_event)
1960}
1961
1962fn refresh_offsets_for_known_proc_pid(
1963    mgr: &Arc<Mutex<ProcessManager>>,
1964    event_pid: u32,
1965    host_pid: u32,
1966    proc_pid: u32,
1967) -> anyhow::Result<bool> {
1968    let proc_pid_for_event = |_: u32| proc_pid;
1969    write_offsets_for_pid(mgr, event_pid, None, true, &[host_pid], &proc_pid_for_event)
1970}
1971
1972fn write_offsets_for_pid(
1973    mgr: &Arc<Mutex<ProcessManager>>,
1974    event_pid: u32,
1975    target: Option<&Path>,
1976    force_refresh: bool,
1977    extra_runtime_pids: &[u32],
1978    proc_pid_for_event: &impl Fn(u32) -> u32,
1979) -> anyhow::Result<bool> {
1980    let proc_pid = proc_pid_for_event(event_pid);
1981    let runtime_pids =
1982        runtime_pid_keys_for_proc_event(proc_pid, event_pid, extra_runtime_pids.iter().copied());
1983    record_runtime_pid_aliases_for_keys(mgr, proc_pid, &runtime_pids);
1984    let mut inserted_any = false;
1985    if let Ok(mut guard) = mgr.lock() {
1986        if target.is_some() && is_current_process_pid(proc_pid) {
1987            tracing::debug!(
1988                "Sysmon: skipping self proc pid {} for target-module offset prefill",
1989                proc_pid
1990            );
1991            return Ok(false);
1992        }
1993        let prefilled = match if force_refresh {
1994            guard.refresh_prefill_pid(proc_pid)
1995        } else {
1996            guard.ensure_prefill_pid(proc_pid)
1997        } {
1998            Ok(v) => v,
1999            Err(e) => {
2000                // In private PID namespaces, sysmon event PID may be in the initial namespace
2001                // and not resolvable via /proc/<event_pid>. Fall back to module-wide refresh.
2002                if let Some(target_path) = target {
2003                    let module_path = target_path.to_string_lossy().to_string();
2004                    tracing::debug!(
2005                        "Sysmon: pid prefill failed for event pid {} (proc pid {}): {}; falling back to module refresh for {}",
2006                        event_pid,
2007                        proc_pid,
2008                        e,
2009                        module_path
2010                    );
2011                    let refreshed = guard.refresh_prefill_module(&module_path)?;
2012                    if refreshed > 0 {
2013                        tracing::info!(
2014                            "Sysmon: module refresh cached {} pid(s) for {}",
2015                            refreshed,
2016                            module_path
2017                        );
2018                    }
2019                    let mut target_pids = BTreeSet::new();
2020                    for (pid, _, _, _, _) in guard.cached_offsets_for_module(&module_path) {
2021                        target_pids.insert(pid);
2022                    }
2023                    drop(guard);
2024
2025                    for pid in target_pids {
2026                        let runtime_pid = resolve_event_pid_for_proc(pid);
2027                        match refresh_full_offsets_for_pid(mgr, pid, runtime_pid) {
2028                            Ok(true) => inserted_any = true,
2029                            Ok(false) => {}
2030                            Err(err) => tracing::warn!(
2031                                "Sysmon: module refresh failed to publish full snapshot for proc pid {}: {}",
2032                                pid,
2033                                err
2034                            ),
2035                        }
2036                    }
2037                    return Ok(inserted_any);
2038                }
2039                return Err(e);
2040            }
2041        };
2042        if prefilled > 0 {
2043            info!(
2044                "Sysmon: {} {} entries for event pid {} (proc pid {})",
2045                if force_refresh {
2046                    "refreshed"
2047                } else {
2048                    "prefilled"
2049                },
2050                prefilled,
2051                event_pid,
2052                proc_pid
2053            );
2054        }
2055        let mut entries = guard
2056            .cached_offsets_with_paths_for_pid(proc_pid)
2057            .map(|entries| entries.to_vec())
2058            .unwrap_or_default();
2059        let mut target_match_count = filter_entries_for_target(&entries, target).len();
2060
2061        if target_match_count == 0 && target.is_some() {
2062            let refreshed = guard.refresh_prefill_pid(proc_pid)?;
2063            if refreshed > 0 {
2064                tracing::debug!(
2065                    "Sysmon: refreshed {} cached entries for event pid {} (proc pid {})",
2066                    refreshed,
2067                    event_pid,
2068                    proc_pid
2069                );
2070            }
2071            entries = guard
2072                .cached_offsets_with_paths_for_pid(proc_pid)
2073                .map(|entries| entries.to_vec())
2074                .unwrap_or_default();
2075            target_match_count = filter_entries_for_target(&entries, target).len();
2076        }
2077
2078        if force_refresh && target.is_none() {
2079            let purged = purge_offsets_for_runtime_pid_keys(&runtime_pids)?;
2080            if purged > 0 {
2081                tracing::debug!(
2082                    "Sysmon: purged {} stale offset entries before map-change refresh for event pid {} (proc pid {}, runtime keys={:?})",
2083                    purged,
2084                    event_pid,
2085                    proc_pid,
2086                    runtime_pids
2087                );
2088            }
2089        }
2090
2091        if !entries.is_empty() && (target.is_none() || target_match_count > 0) {
2092            let items = offset_items_from_entries(entries.iter());
2093            match publish_offsets_for_runtime_pid_keys(
2094                proc_pid,
2095                event_pid,
2096                &runtime_pids,
2097                &items,
2098                "prefill",
2099            ) {
2100                Ok(inserted) => {
2101                    if inserted == 0 {
2102                        tracing::warn!(
2103                            "Sysmon: no offsets inserted for event pid {} (proc pid {}, runtime keys={:?}) (entry count={})",
2104                            event_pid,
2105                            proc_pid,
2106                            runtime_pids,
2107                            items.len()
2108                        );
2109                    } else {
2110                        tracing::info!(
2111                            "Sysmon: inserted {} offset entries for event pid {} (proc pid {}, runtime keys={:?})",
2112                            inserted,
2113                            event_pid,
2114                            proc_pid,
2115                            runtime_pids
2116                        );
2117                        insert_allowed_runtime_pid_keys(&runtime_pids);
2118                        inserted_any = true;
2119                    }
2120                }
2121                Err(e) => {
2122                    tracing::warn!(
2123                        "Sysmon: failed to insert offsets for event pid {} (proc pid {}): {}",
2124                        event_pid,
2125                        proc_pid,
2126                        e
2127                    );
2128                }
2129            }
2130        } else if target.is_some() {
2131            tracing::debug!(
2132                "Sysmon: event pid {} (proc pid {}) does not map target module; skip",
2133                event_pid,
2134                proc_pid
2135            );
2136        }
2137    }
2138    Ok(inserted_any)
2139}
2140
2141fn prefill_full_offsets_for_pid_if_new(
2142    mgr: &Arc<Mutex<ProcessManager>>,
2143    event_pid: u32,
2144    proc_pid_for_event: &impl Fn(u32) -> u32,
2145) -> anyhow::Result<bool> {
2146    let proc_pid = proc_pid_for_event(event_pid);
2147    let runtime_pids = runtime_pid_keys_for_proc_event(proc_pid, event_pid, []);
2148    record_runtime_pid_aliases_for_keys(mgr, proc_pid, &runtime_pids);
2149
2150    let items = {
2151        let Ok(mut guard) = mgr.lock() else {
2152            return Ok(false);
2153        };
2154        let prefilled = guard.ensure_prefill_pid(proc_pid)?;
2155        if prefilled == 0 {
2156            return Ok(false);
2157        }
2158        let Some(entries) = guard.cached_offsets_with_paths_for_pid(proc_pid) else {
2159            return Ok(false);
2160        };
2161        offset_items_from_entries(entries.iter())
2162    };
2163
2164    if items.is_empty() {
2165        return Ok(false);
2166    }
2167
2168    match publish_offsets_for_runtime_pid_keys(
2169        proc_pid,
2170        event_pid,
2171        &runtime_pids,
2172        &items,
2173        "full prefill",
2174    ) {
2175        Ok(inserted) if inserted > 0 => {
2176            tracing::info!(
2177                "Sysmon: inserted {} full offset entries for event pid {} (proc pid {}, runtime keys={:?})",
2178                inserted,
2179                event_pid,
2180                proc_pid,
2181                runtime_pids
2182            );
2183            insert_allowed_runtime_pid_keys(&runtime_pids);
2184            Ok(true)
2185        }
2186        Ok(_) => Ok(false),
2187        Err(e) => {
2188            tracing::warn!(
2189                "Sysmon: failed to insert full offsets for event pid {} (proc pid {}): {}",
2190                event_pid,
2191                proc_pid,
2192                e
2193            );
2194            Ok(false)
2195        }
2196    }
2197}
2198
2199type PidMapsSignature = Vec<(String, u64, u64, u64, u64, u64, bool)>;
2200
2201fn pid_maps_signature(pid: u32) -> anyhow::Result<PidMapsSignature> {
2202    let mut signature = read_proc_maps(pid)?
2203        .into_iter()
2204        .filter_map(|entry| {
2205            let path = entry.path()?;
2206            if should_skip_mapped_module_path(path) {
2207                return None;
2208            }
2209            Some((
2210                normalize_mapped_module_path(path).to_string(),
2211                entry.start,
2212                entry.end,
2213                entry.offset,
2214                entry.inode,
2215                (entry.dev_major << 32) | entry.dev_minor,
2216                entry.executable(),
2217            ))
2218        })
2219        .collect::<Vec<_>>();
2220    signature.sort_unstable();
2221    Ok(signature)
2222}
2223
2224fn refresh_full_offsets_for_pid(
2225    mgr: &Arc<Mutex<ProcessManager>>,
2226    proc_pid: u32,
2227    event_pid: u32,
2228) -> anyhow::Result<bool> {
2229    let runtime_pids = runtime_pid_keys_for_proc_event(proc_pid, event_pid, []);
2230    record_runtime_pid_aliases_for_keys(mgr, proc_pid, &runtime_pids);
2231
2232    let items = {
2233        let Ok(mut guard) = mgr.lock() else {
2234            return Ok(false);
2235        };
2236        guard.refresh_prefill_pid(proc_pid)?;
2237        let Some(entries) = guard.cached_offsets_with_paths_for_pid(proc_pid) else {
2238            return Ok(false);
2239        };
2240        offset_items_from_entries(entries.iter())
2241    };
2242
2243    if items.is_empty() {
2244        return Ok(false);
2245    }
2246
2247    let purged = purge_offsets_for_runtime_pid_keys(&runtime_pids)?;
2248    if purged > 0 {
2249        tracing::debug!(
2250            "Sysmon: purged {} stale offset entries before periodic full refresh for proc pid {} (runtime keys={:?})",
2251            purged,
2252            proc_pid,
2253            runtime_pids
2254        );
2255    }
2256    let inserted = publish_offsets_for_runtime_pid_keys(
2257        proc_pid,
2258        event_pid,
2259        &runtime_pids,
2260        &items,
2261        "periodic full refresh",
2262    )?;
2263    insert_allowed_runtime_pid_keys(&runtime_pids);
2264    tracing::debug!(
2265        "Sysmon: periodic full refresh wrote {} offset entries for proc pid {} (event pid {}, runtime keys={:?})",
2266        inserted,
2267        proc_pid,
2268        event_pid,
2269        runtime_pids
2270    );
2271    Ok(inserted > 0)
2272}
2273
2274fn offset_items_from_entries<'a>(
2275    entries: impl IntoIterator<Item = &'a PidOffsetsEntry>,
2276) -> Vec<(u64, crate::pinned_bpf_maps::ProcModuleOffsetsValue)> {
2277    entries
2278        .into_iter()
2279        .map(|e| {
2280            (
2281                e.cookie,
2282                crate::pinned_bpf_maps::ProcModuleOffsetsValue::new(
2283                    e.offsets.text,
2284                    e.offsets.rodata,
2285                    e.offsets.data,
2286                    e.offsets.bss,
2287                    e.base,
2288                    e.size,
2289                ),
2290            )
2291        })
2292        .collect()
2293}
2294
2295fn refresh_target_module_offsets(
2296    mgr: &Arc<Mutex<ProcessManager>>,
2297    target: Option<&Path>,
2298    last_refresh: &mut Instant,
2299    target_pid_map_signatures: &mut HashMap<u32, PidMapsSignature>,
2300    tx: &mpsc::SyncSender<SysEvent>,
2301) {
2302    use crate::pinned_bpf_maps::{allowed_pid_exists, ProcModuleOffsetsValue};
2303
2304    let Some(target_path) = target else {
2305        return;
2306    };
2307    let now = Instant::now();
2308    if now.duration_since(*last_refresh) < MODULE_REFRESH_INTERVAL {
2309        return;
2310    }
2311    *last_refresh = now;
2312
2313    let module_path = target_path.to_string_lossy().to_string();
2314    let mut by_pid: HashMap<u32, Vec<(u64, ProcModuleOffsetsValue)>> = HashMap::new();
2315    let mut target_pids: BTreeSet<u32> = BTreeSet::new();
2316    if let Ok(mut guard) = mgr.lock() {
2317        if let Err(e) = guard.refresh_prefill_module(&module_path) {
2318            tracing::debug!(
2319                "Sysmon: periodic module refresh failed for {}: {}",
2320                module_path,
2321                e
2322            );
2323            return;
2324        }
2325        for (pid, cookie, off, base, size) in guard.cached_offsets_for_module(&module_path) {
2326            if is_current_process_pid(pid) {
2327                continue;
2328            }
2329            target_pids.insert(pid);
2330            by_pid.entry(pid).or_default().push((
2331                cookie,
2332                ProcModuleOffsetsValue::new(off.text, off.rodata, off.data, off.bss, base, size),
2333            ));
2334        }
2335    }
2336    if by_pid.is_empty() {
2337        return;
2338    }
2339
2340    let mut total = 0usize;
2341    let mut newly_allowed_event_pids = BTreeSet::new();
2342    for (pid, items) in by_pid {
2343        let event_pid = resolve_event_pid_for_proc(pid);
2344        let runtime_pids = runtime_pid_keys_for_proc_event(pid, event_pid, []);
2345        record_runtime_pid_aliases_for_keys(mgr, pid, &runtime_pids);
2346        let was_allowed = match allowed_pid_exists(event_pid) {
2347            Ok(value) => value,
2348            Err(e) => {
2349                tracing::debug!(
2350                    "Sysmon: allowed_pids lookup failed for event pid {} (proc pid {}): {}",
2351                    event_pid,
2352                    pid,
2353                    e
2354                );
2355                false
2356            }
2357        };
2358        match publish_offsets_for_runtime_pid_keys(
2359            pid,
2360            event_pid,
2361            &runtime_pids,
2362            &items,
2363            "periodic module refresh",
2364        ) {
2365            Ok(inserted) => {
2366                if inserted > 0 {
2367                    total += inserted;
2368                    insert_allowed_runtime_pid_keys(&runtime_pids);
2369                    if !was_allowed {
2370                        tracing::debug!(
2371                            "Sysmon: event pid {} became allowed during periodic module refresh",
2372                            event_pid
2373                        );
2374                        newly_allowed_event_pids.insert(event_pid);
2375                    }
2376                }
2377            }
2378            Err(e) => tracing::debug!(
2379                "Sysmon: periodic module refresh insert failed for pid {} ({}): {}",
2380                pid,
2381                module_path,
2382                e
2383            ),
2384        }
2385    }
2386    for pid in &target_pids {
2387        let event_pid = resolve_event_pid_for_proc(*pid);
2388        let maps_signature = match pid_maps_signature(*pid) {
2389            Ok(signature) => signature,
2390            Err(e) => {
2391                tracing::debug!(
2392                    "Sysmon: periodic maps signature failed for proc pid {} (event pid {}): {}",
2393                    *pid,
2394                    event_pid,
2395                    e
2396                );
2397                continue;
2398            }
2399        };
2400        if target_pid_map_signatures.get(pid) == Some(&maps_signature) {
2401            continue;
2402        }
2403        target_pid_map_signatures.insert(*pid, maps_signature);
2404
2405        match refresh_full_offsets_for_pid(mgr, *pid, event_pid) {
2406            Ok(true) => {
2407                newly_allowed_event_pids.insert(event_pid);
2408            }
2409            Ok(false) => {}
2410            Err(e) => {
2411                tracing::debug!(
2412                    "Sysmon: periodic full offset refresh failed for proc pid {} (event pid {}): {}",
2413                    *pid,
2414                    event_pid,
2415                    e
2416                );
2417            }
2418        }
2419    }
2420    target_pid_map_signatures.retain(|pid, _| target_pids.contains(pid));
2421    for event_pid in newly_allowed_event_pids {
2422        let ev = SysEvent {
2423            tgid: event_pid,
2424            host_tgid: event_pid,
2425            kind: SysEventKind::MapChange.as_u32(),
2426        };
2427        if try_publish_sys_event(tx, ev) {
2428            tracing::debug!(
2429                "Sysmon: published synthetic map-change for newly discovered target pid {}",
2430                event_pid
2431            );
2432        }
2433    }
2434    if total > 0 {
2435        tracing::debug!(
2436            "Sysmon: periodic module refresh inserted {} offset entries for {}",
2437            total,
2438            module_path
2439        );
2440    }
2441}
2442
2443fn poll_pending_offsets(
2444    mgr: &Arc<Mutex<ProcessManager>>,
2445    pending: &Arc<Mutex<PendingOffsets>>,
2446    proc_pid_for_event: &impl Fn(u32) -> u32,
2447) {
2448    let due = if let Ok(mut guard) = pending.lock() {
2449        guard.take_due()
2450    } else {
2451        Vec::new()
2452    };
2453
2454    if due.is_empty() {
2455        return;
2456    }
2457
2458    let mut to_remove: Vec<u32> = Vec::new();
2459    let mut to_exhaust: Vec<u32> = Vec::new();
2460
2461    for due in due {
2462        let event_pid = due.event_pid;
2463        let target_path = due.target_path;
2464        let attempts = due.attempts;
2465        let proc_pid = proc_pid_for_event(event_pid);
2466        if !pid_alive(proc_pid) {
2467            tracing::debug!(
2468                "Sysmon: event pid {} (proc pid {}) exited while waiting for offsets; removing from retry queue",
2469                event_pid,
2470                proc_pid
2471            );
2472            to_remove.push(event_pid);
2473            continue;
2474        }
2475
2476        if !pid_maps_target_module(proc_pid, &target_path) {
2477            if attempts >= PENDING_MAX_ATTEMPTS {
2478                if due.kind.keep_for_map_changes_after_retry_exhaustion() {
2479                    tracing::debug!(
2480                        "Sysmon: event pid {} (proc pid {}) still missing module {} after {} retries; waiting for map-change trigger",
2481                        event_pid,
2482                        proc_pid,
2483                        target_path.display(),
2484                        attempts
2485                    );
2486                    to_exhaust.push(event_pid);
2487                } else {
2488                    tracing::warn!(
2489                        "Sysmon: event pid {} (proc pid {}) still missing module {} after {} retries; giving up",
2490                        event_pid,
2491                        proc_pid,
2492                        target_path.display(),
2493                        attempts
2494                    );
2495                    to_remove.push(event_pid);
2496                }
2497            }
2498            continue;
2499        }
2500
2501        match prefill_offsets_for_pid(
2502            mgr,
2503            event_pid,
2504            Some(target_path.as_path()),
2505            proc_pid_for_event,
2506        ) {
2507            Ok(true) => {
2508                tracing::info!(
2509                    "Sysmon: deferred prefill succeeded for event pid {} (proc pid {}) (module {})",
2510                    event_pid,
2511                    proc_pid,
2512                    target_path.display()
2513                );
2514                to_remove.push(event_pid);
2515            }
2516            Ok(false) => {
2517                if attempts >= PENDING_MAX_ATTEMPTS {
2518                    if due.kind.keep_for_map_changes_after_retry_exhaustion() {
2519                        tracing::debug!(
2520                            "Sysmon: deferred prefill produced no entries for event pid {} (proc pid {}) after {} retries; waiting for map-change trigger",
2521                            event_pid,
2522                            proc_pid,
2523                            attempts
2524                        );
2525                        to_exhaust.push(event_pid);
2526                    } else {
2527                        tracing::warn!(
2528                            "Sysmon: deferred prefill produced no entries for event pid {} (proc pid {}) after {} retries; giving up",
2529                            event_pid,
2530                            proc_pid,
2531                            attempts
2532                        );
2533                        to_remove.push(event_pid);
2534                    }
2535                }
2536            }
2537            Err(e) => {
2538                tracing::warn!(
2539                    "Sysmon: deferred prefill failed for event pid {} (proc pid {}) (attempt {}): {}",
2540                    event_pid,
2541                    proc_pid,
2542                    attempts,
2543                    e
2544                );
2545                if attempts >= PENDING_MAX_ATTEMPTS {
2546                    if due.kind.keep_for_map_changes_after_retry_exhaustion() {
2547                        to_exhaust.push(event_pid);
2548                    } else {
2549                        to_remove.push(event_pid);
2550                    }
2551                }
2552            }
2553        }
2554    }
2555
2556    if !to_remove.is_empty() || !to_exhaust.is_empty() {
2557        if let Ok(mut guard) = pending.lock() {
2558            for pid in to_remove {
2559                guard.remove(pid);
2560            }
2561            for pid in to_exhaust {
2562                guard.mark_retry_exhausted(pid);
2563            }
2564        }
2565    }
2566}
2567
2568fn cached_offsets_exist_for_target_pid(
2569    mgr: &Arc<Mutex<ProcessManager>>,
2570    target_path: &Path,
2571    proc_pid: u32,
2572) -> bool {
2573    let module_path = target_path.to_string_lossy().to_string();
2574    mgr.lock()
2575        .ok()
2576        .map(|guard| {
2577            guard.cached_offsets_with_paths_for_pid(proc_pid).is_some()
2578                || guard
2579                    .cached_offsets_for_module(&module_path)
2580                    .iter()
2581                    .any(|(pid, _, _, _, _)| *pid == proc_pid)
2582        })
2583        .unwrap_or(false)
2584}
2585
2586fn forget_pid_offsets_after_target_unmap(
2587    mgr: &Arc<Mutex<ProcessManager>>,
2588    event_pid: u32,
2589    host_pid: u32,
2590    proc_pid: u32,
2591) {
2592    if let Ok(mut guard) = mgr.lock() {
2593        guard.forget_pid(proc_pid);
2594        if proc_pid != event_pid {
2595            guard.forget_pid(event_pid);
2596        }
2597        if host_pid != event_pid && host_pid != proc_pid {
2598            guard.forget_pid(host_pid);
2599        }
2600    }
2601
2602    let purged = purge_runtime_pid_artifacts(proc_pid, event_pid, host_pid);
2603    if purged > 0 {
2604        tracing::info!(
2605            "Sysmon: target unmapped for event pid {} (host pid {}, proc pid {}); purged {} offset entries",
2606            event_pid,
2607            host_pid,
2608            proc_pid,
2609            purged
2610        );
2611    }
2612}
2613
2614fn poll_pending_map_refreshes(
2615    mgr: &Arc<Mutex<ProcessManager>>,
2616    target: Option<&Path>,
2617    pending_map_refreshes: &Arc<Mutex<PendingMapRefreshes>>,
2618    pending: &Arc<Mutex<PendingOffsets>>,
2619    tx: &mpsc::SyncSender<SysEvent>,
2620) {
2621    let due = if let Ok(mut guard) = pending_map_refreshes.lock() {
2622        guard.take_due()
2623    } else {
2624        Vec::new()
2625    };
2626
2627    if due.is_empty() {
2628        return;
2629    }
2630
2631    for event in due {
2632        let event_pid = event.event_pid;
2633        let host_pid = event.host_pid;
2634        let proc_pid = target
2635            .map(|target_path| {
2636                canonicalize_cached_target_proc_pid(mgr, target_path, event.proc_pid)
2637            })
2638            .unwrap_or(event.proc_pid);
2639        if !pid_alive(proc_pid) {
2640            tracing::trace!(
2641                "Sysmon: event pid {} (proc pid {}) exited before map refresh",
2642                event_pid,
2643                proc_pid
2644            );
2645            continue;
2646        }
2647
2648        if let Some(target_path) = target {
2649            if !pid_maps_target_module(proc_pid, target_path) {
2650                if cached_offsets_exist_for_target_pid(mgr, target_path, proc_pid) {
2651                    forget_pid_offsets_after_target_unmap(mgr, event_pid, host_pid, proc_pid);
2652                    let ev = SysEvent {
2653                        tgid: event_pid,
2654                        host_tgid: host_pid,
2655                        kind: SysEventKind::MapChange.as_u32(),
2656                    };
2657                    try_publish_sys_event(tx, ev);
2658                }
2659                tracing::trace!(
2660                    "Sysmon: event pid {} (proc pid {}) map-change does not include target {}; skip",
2661                    event_pid,
2662                    proc_pid,
2663                    target_path.display()
2664                );
2665                continue;
2666            }
2667        }
2668
2669        match refresh_offsets_for_known_proc_pid(mgr, event_pid, host_pid, proc_pid) {
2670            Ok(true) => {
2671                tracing::debug!(
2672                    "Sysmon: refreshed offsets after map-change for event pid {} (host pid {}, proc pid {})",
2673                    event_pid,
2674                    host_pid,
2675                    proc_pid
2676                );
2677                if let Ok(mut guard) = pending.lock() {
2678                    guard.remove(event_pid);
2679                    if host_pid != event_pid {
2680                        guard.remove(host_pid);
2681                    }
2682                }
2683                let ev = SysEvent {
2684                    tgid: event_pid,
2685                    host_tgid: host_pid,
2686                    kind: SysEventKind::MapChange.as_u32(),
2687                };
2688                try_publish_sys_event(tx, ev);
2689            }
2690            Ok(false) => tracing::trace!(
2691                "Sysmon: map-change refresh inserted no offsets for event pid {} (proc pid {})",
2692                event_pid,
2693                proc_pid
2694            ),
2695            Err(e) => tracing::debug!(
2696                "Sysmon: map-change refresh failed for event pid {} (proc pid {}): {}",
2697                event_pid,
2698                proc_pid,
2699                e
2700            ),
2701        }
2702    }
2703}
2704
2705fn get_comm_from_proc(pid: u32) -> Option<String> {
2706    use std::io::Read;
2707    let path = format!("/proc/{pid}/comm");
2708    let mut f = std::fs::File::open(path).ok()?;
2709    let mut s = String::new();
2710    f.read_to_string(&mut s).ok()?;
2711    if s.ends_with('\n') {
2712        s.pop();
2713        if s.ends_with('\r') {
2714            s.pop();
2715        }
2716    }
2717    // Kernel task->comm is at most 15 bytes; /proc returns without NUL. We compare as-is.
2718    Some(s)
2719}
2720
2721fn truncate_basename_to_comm(path: &Path) -> Vec<u8> {
2722    use std::ffi::OsStr;
2723    let mut buf = Vec::with_capacity(16);
2724    if let Some(name) = path.file_name().and_then(OsStr::to_str) {
2725        let bytes = name.as_bytes();
2726        let n = core::cmp::min(bytes.len(), 15);
2727        buf.extend_from_slice(&bytes[..n]);
2728    }
2729    buf
2730}
2731
2732fn pid_maps_target_module(pid: u32, target: &Path) -> bool {
2733    let target = ModuleIdentity::from_path(target);
2734    let mut matched = false;
2735
2736    if visit_proc_maps(pid, |entry| {
2737        if target.matches(&entry) {
2738            matched = true;
2739            return ControlFlow::Break(());
2740        }
2741        ControlFlow::Continue(())
2742    })
2743    .is_err()
2744    {
2745        return false;
2746    }
2747
2748    matched
2749}
2750
2751#[cfg(test)]
2752mod tests {
2753    use super::*;
2754
2755    #[test]
2756    fn sysmon_target_mode_does_not_enable_map_change_events() {
2757        let mask = SysmonEventMask::target_mode();
2758        assert!(mask.exec);
2759        assert!(mask.fork);
2760        assert!(mask.exit);
2761        assert!(!mask.map_change);
2762    }
2763
2764    #[test]
2765    fn sysmon_pid_module_changes_only_enable_map_change_events() {
2766        let mask = SysmonEventMask::pid_module_changes();
2767        assert!(!mask.exec);
2768        assert!(!mask.fork);
2769        assert!(!mask.exit);
2770        assert!(mask.map_change);
2771    }
2772
2773    #[test]
2774    fn sysmon_event_publish_drops_when_queue_is_full() {
2775        let (tx, rx) = mpsc::sync_channel(1);
2776
2777        assert!(try_publish_sys_event(
2778            &tx,
2779            SysEvent {
2780                tgid: 1,
2781                host_tgid: 1,
2782                kind: 1
2783            }
2784        ));
2785        assert!(!try_publish_sys_event(
2786            &tx,
2787            SysEvent {
2788                tgid: 2,
2789                host_tgid: 2,
2790                kind: 2
2791            }
2792        ));
2793
2794        let queued = rx.try_recv().expect("first event should be queued");
2795        assert_eq!(queued.tgid, 1);
2796        assert_eq!(queued.kind, 1);
2797        assert!(matches!(rx.try_recv(), Err(mpsc::TryRecvError::Empty)));
2798    }
2799
2800    #[test]
2801    fn invisible_shared_object_exec_does_not_queue_unresolved_alias() -> anyhow::Result<()> {
2802        let dir = tempfile::tempdir()?;
2803        let target_path = dir.path().join("libtarget.so");
2804        let mut elf = [0u8; 64];
2805        elf[0..4].copy_from_slice(b"\x7FELF");
2806        elf[4] = 2;
2807        elf[5] = 1;
2808        elf[16..18].copy_from_slice(&3u16.to_le_bytes());
2809        std::fs::write(&target_path, elf)?;
2810
2811        let mgr = Arc::new(Mutex::new(ProcessManager::new()));
2812        let pending = Arc::new(Mutex::new(PendingOffsets::new()));
2813        let ev = SysEvent {
2814            tgid: u32::MAX - 1,
2815            host_tgid: u32::MAX - 2,
2816            kind: SysEventKind::Exec.as_u32(),
2817        };
2818
2819        ProcessSysmon::handle_event_with_proc_pid_resolver(
2820            &mgr,
2821            &Some(target_path),
2822            &pending,
2823            &ev,
2824            |pid| pid,
2825        )?;
2826
2827        assert!(
2828            pending
2829                .lock()
2830                .expect("pending offsets lock")
2831                .entries
2832                .is_empty(),
2833            "unresolved exec events must not be deferred without target-map proof"
2834        );
2835        Ok(())
2836    }
2837
2838    #[test]
2839    fn visible_shared_object_exec_queues_map_change_candidate() -> anyhow::Result<()> {
2840        let dir = tempfile::tempdir()?;
2841        let target_path = dir.path().join("libtarget.so");
2842        let mut elf = [0u8; 64];
2843        elf[0..4].copy_from_slice(b"\x7FELF");
2844        elf[4] = 2;
2845        elf[5] = 1;
2846        elf[16..18].copy_from_slice(&3u16.to_le_bytes());
2847        std::fs::write(&target_path, elf)?;
2848
2849        let event_pid = std::process::id();
2850        let mgr = Arc::new(Mutex::new(ProcessManager::new()));
2851        let pending = Arc::new(Mutex::new(PendingOffsets::new()));
2852        let ev = SysEvent {
2853            tgid: event_pid,
2854            host_tgid: event_pid,
2855            kind: SysEventKind::Exec.as_u32(),
2856        };
2857
2858        ProcessSysmon::handle_event_with_proc_pid_resolver(
2859            &mgr,
2860            &Some(target_path.clone()),
2861            &pending,
2862            &ev,
2863            |pid| pid,
2864        )?;
2865
2866        let guard = pending.lock().expect("pending offsets lock");
2867        let entry = guard
2868            .entries
2869            .get(&event_pid)
2870            .expect("visible shared-object exec should be queued for retry");
2871        assert_eq!(entry.target_path, target_path);
2872        assert_eq!(entry.kind, PendingOffsetsKind::MapChangeCandidate);
2873        Ok(())
2874    }
2875
2876    #[test]
2877    fn invisible_shared_object_exec_queues_visible_host_pid_retry() -> anyhow::Result<()> {
2878        let dir = tempfile::tempdir()?;
2879        let target_path = dir.path().join("libtarget.so");
2880        let mut elf = [0u8; 64];
2881        elf[0..4].copy_from_slice(b"\x7FELF");
2882        elf[4] = 2;
2883        elf[5] = 1;
2884        elf[16..18].copy_from_slice(&3u16.to_le_bytes());
2885        std::fs::write(&target_path, elf)?;
2886
2887        let host_pid = std::process::id();
2888        let mgr = Arc::new(Mutex::new(ProcessManager::new()));
2889        let pending = Arc::new(Mutex::new(PendingOffsets::new()));
2890        let ev = SysEvent {
2891            tgid: u32::MAX - 1,
2892            host_tgid: host_pid,
2893            kind: SysEventKind::Exec.as_u32(),
2894        };
2895
2896        ProcessSysmon::handle_event_with_proc_pid_resolver(
2897            &mgr,
2898            &Some(target_path.clone()),
2899            &pending,
2900            &ev,
2901            |pid| pid,
2902        )?;
2903
2904        let guard = pending.lock().expect("pending offsets lock");
2905        let entry = guard
2906            .entries
2907            .get(&host_pid)
2908            .expect("visible host PID should be queued for maps-based retry");
2909        assert_eq!(entry.target_path, target_path);
2910        assert_eq!(entry.kind, PendingOffsetsKind::MapChangeCandidate);
2911        Ok(())
2912    }
2913
2914    #[test]
2915    fn shared_object_candidate_survives_retry_exhaustion() -> anyhow::Result<()> {
2916        let dir = tempfile::tempdir()?;
2917        let target_path = dir.path().join("libtarget.so");
2918        std::fs::write(&target_path, b"not actually mapped")?;
2919
2920        let event_pid = std::process::id();
2921        let pending = Arc::new(Mutex::new(PendingOffsets::new()));
2922        {
2923            let mut guard = pending.lock().expect("pending offsets lock");
2924            guard.register_map_change_candidate(event_pid, &target_path);
2925            let entry = guard
2926                .entries
2927                .get_mut(&event_pid)
2928                .expect("pending candidate");
2929            entry.attempts = PENDING_MAX_ATTEMPTS - 1;
2930            entry.last_poll = Instant::now() - PENDING_POLL_INTERVAL;
2931        }
2932
2933        let mgr = Arc::new(Mutex::new(ProcessManager::new()));
2934        poll_pending_offsets(&mgr, &pending, &|pid| pid);
2935
2936        let guard = pending.lock().expect("pending offsets lock");
2937        let entry = guard
2938            .entries
2939            .get(&event_pid)
2940            .expect("map-change candidate should remain after retry exhaustion");
2941        assert!(entry.retry_exhausted);
2942        assert!(guard.contains_map_change_candidate(event_pid, &target_path));
2943        Ok(())
2944    }
2945}