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#[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#[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#[repr(C)]
132#[derive(Clone, Copy)]
133pub struct SysEvent {
134 pub tgid: u32,
136 pub host_tgid: u32,
138 pub kind: u32, }
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#[derive(Debug, Clone)]
341pub struct SysmonConfig {
342 pub target_module: Option<PathBuf>,
344 pub proc_offsets_max_entries: u32,
346 pub perf_page_count: Option<usize>,
348 pub event_mask: SysmonEventMask,
350 pub map_change_unfiltered: bool,
352 pub watched_pid: Option<u32>,
354 pub watched_pid_ns: Option<PidNamespaceId>,
356 pub event_pid_ns: Option<PidNamespaceId>,
358 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
384pub struct ProcessSysmon {
391 cfg: SysmonConfig,
392 mgr: Arc<Mutex<ProcessManager>>, 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 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 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 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 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 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 #[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 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 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 let mut last_module_refresh = Instant::now();
1679 let mut target_pid_map_signatures = HashMap::<u32, PidMapsSignature>::new();
1680
1681 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 while let Some(item) = rb.next() {
1690 had_event = true;
1691 if item.len() == core::mem::size_of::<SysEvent>() {
1692 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 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
1804fn 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 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 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}