diff --git a/ghostscope-process/ebpf/obj/sysmon-bpf.bpfeb.o b/ghostscope-process/ebpf/obj/sysmon-bpf.bpfeb.o index 1010455c..fdd2f1db 100644 Binary files a/ghostscope-process/ebpf/obj/sysmon-bpf.bpfeb.o and b/ghostscope-process/ebpf/obj/sysmon-bpf.bpfeb.o differ diff --git a/ghostscope-process/ebpf/obj/sysmon-bpf.bpfel.o b/ghostscope-process/ebpf/obj/sysmon-bpf.bpfel.o index 3ecbb408..f8efde1a 100644 Binary files a/ghostscope-process/ebpf/obj/sysmon-bpf.bpfel.o and b/ghostscope-process/ebpf/obj/sysmon-bpf.bpfel.o differ diff --git a/ghostscope-process/ebpf/sysmon-bpf/src/lib.rs b/ghostscope-process/ebpf/sysmon-bpf/src/lib.rs index a17c20f8..e82014b2 100644 --- a/ghostscope-process/ebpf/sysmon-bpf/src/lib.rs +++ b/ghostscope-process/ebpf/sysmon-bpf/src/lib.rs @@ -2,9 +2,10 @@ #![no_main] use aya_ebpf::{ - macros::{map, tracepoint}, + macros::{btf_tracepoint, map, raw_tracepoint, tracepoint}, maps::{Array, HashMap, PerfEventArray, RingBuf}, - programs::TracePointContext, + programs::{BtfTracePointContext, RawTracePointContext, TracePointContext}, + EbpfContext, }; // ABI note: @@ -20,35 +21,59 @@ pub struct SysEvent { } #[map(name = "sysmon_events")] -static mut SYS_EVENTS: RingBuf = RingBuf::with_byte_size(1 << 20, 0); // 1MB +static SYS_EVENTS: RingBuf = RingBuf::with_byte_size(1 << 20, 0); // 1MB #[map(name = "sysmon_events_perf")] -static mut SYS_EVENTS_PERF: PerfEventArray = PerfEventArray::new(0); +static SYS_EVENTS_PERF: PerfEventArray = PerfEventArray::new(0); // Allowed pids for -t mode: fork/exit events are only emitted when pid is present. #[map(name = "allowed_pids")] -static mut ALLOWED_PIDS: HashMap = HashMap::pinned(16384, 0); +static ALLOWED_PIDS: HashMap = HashMap::pinned(16384, 0); // Optional comm filter for exec events (truncated basename, null-terminated). #[map(name = "target_exec_comm")] -static mut TARGET_EXEC_COMM: Array<[u8; 16]> = Array::pinned(1, 0); - -fn write_event(ctx: &TracePointContext, mut ev: SysEvent) { - // Prefer direct helper to avoid large memcpy/memmove codegen - let size = core::mem::size_of::() as u64; - // SAFETY: eBPF map statics are accessed only through BPF helper-compatible - // pointers while the verifier controls program concurrency. - let map_ptr = unsafe { core::ptr::addr_of_mut!(SYS_EVENTS) } as *mut _; - let data_ptr = &mut ev as *mut _ as *mut _; - // SAFETY: map_ptr points to SYS_EVENTS and data_ptr/size describe the local - // SysEvent value for the duration of the helper call. - let ret = unsafe { aya_ebpf::helpers::bpf_ringbuf_output(map_ptr, data_ptr, size, 0) }; - if ret < 0 { - // Fallback to perf event if ringbuf output fails - // SAFETY: SYS_EVENTS_PERF is the statically declared BPF perf map and ev - // remains valid for the helper call. - let _ = unsafe { SYS_EVENTS_PERF.output(ctx, &mut ev, 0) }; +static TARGET_EXEC_COMM: Array<[u8; 16]> = Array::pinned(1, 0); + +fn write_event(ctx: &C, ev: SysEvent) { + if SYS_EVENTS.output::(&ev, 0).is_err() { + SYS_EVENTS_PERF.output(ctx, &ev, 0); + } +} + +#[inline(always)] +fn emit_exec(ctx: &C) -> u32 { + if !exec_comm_matches() { + return 0; + } + // Emit exec only when filter (if present) passes; userspace handles mapping and allowlist. + let ev = SysEvent { + tgid: current_tgid(), + kind: 1, + }; + write_event(ctx, ev); + 0 +} + +#[inline(always)] +fn emit_fork(ctx: &C) -> u32 { + let pid = current_tgid(); + if ALLOWED_PIDS.get_ptr(&pid).is_none() { + return 0; } + let ev = SysEvent { tgid: pid, kind: 2 }; + write_event(ctx, ev); + 0 +} + +#[inline(always)] +fn emit_exit(ctx: &C) -> u32 { + let pid = current_tgid(); + if ALLOWED_PIDS.get_ptr(&pid).is_none() { + return 0; + } + let ev = SysEvent { tgid: pid, kind: 3 }; + write_event(ctx, ev); + 0 } #[inline(always)] @@ -60,9 +85,7 @@ fn current_tgid() -> u32 { #[inline(always)] fn exec_comm_matches() -> bool { - // SAFETY: TARGET_EXEC_COMM is a single-entry BPF array; get_ptr returns None - // when index 0 is unavailable. - let filter = match unsafe { TARGET_EXEC_COMM.get_ptr(0) } { + let filter = match TARGET_EXEC_COMM.get_ptr(0) { Some(ptr) => { // SAFETY: get_ptr returned a valid pointer to the array value for this // helper invocation; [u8; 16] is Copy. @@ -73,10 +96,16 @@ fn exec_comm_matches() -> bool { if filter[0] == 0 { return true; } - let comm = match aya_ebpf::helpers::bpf_get_current_comm() { - Ok(val) => val, - Err(_) => return false, + let mut comm = [0u8; 16]; + let ret = unsafe { + aya_ebpf::helpers::generated::bpf_get_current_comm( + comm.as_mut_ptr().cast(), + core::mem::size_of_val(&comm) as u32, + ) }; + if ret != 0 { + return false; + } let mut matched = true; for i in 0..16 { let expected = filter[i]; @@ -93,43 +122,47 @@ fn exec_comm_matches() -> bool { #[tracepoint(name = "sched_process_exec", category = "sched")] pub fn sched_process_exec(ctx: TracePointContext) -> u32 { - if !exec_comm_matches() { - return 0; - } - // Emit exec only when filter (if present) passes; userspace handles mapping and allowlist - let ev = SysEvent { tgid: current_tgid(), kind: 1 }; - write_event(&ctx, ev); - 0 + emit_exec(&ctx) } #[tracepoint(name = "sched_process_fork", category = "sched")] pub fn sched_process_fork(ctx: TracePointContext) -> u32 { - let pid = current_tgid(); - // SAFETY: ALLOWED_PIDS is the statically declared BPF hash map; aya's map API - // performs verifier-compatible map lookup for the stack key. - unsafe { - if ALLOWED_PIDS.get(&pid).is_none() { - return 0; - } - } - let ev = SysEvent { tgid: pid, kind: 2 }; - write_event(&ctx, ev); - 0 + emit_fork(&ctx) } #[tracepoint(name = "sched_process_exit", category = "sched")] pub fn sched_process_exit(ctx: TracePointContext) -> u32 { - let pid = current_tgid(); - // SAFETY: ALLOWED_PIDS is the statically declared BPF hash map; aya's map API - // performs verifier-compatible map lookup for the stack key. - unsafe { - if ALLOWED_PIDS.get(&pid).is_none() { - return 0; - } - } - let ev = SysEvent { tgid: pid, kind: 3 }; - write_event(&ctx, ev); - 0 + emit_exit(&ctx) +} + +#[raw_tracepoint(tracepoint = "sched_process_exec")] +pub fn raw_sched_process_exec(ctx: RawTracePointContext) -> u32 { + emit_exec(&ctx) +} + +#[raw_tracepoint(tracepoint = "sched_process_fork")] +pub fn raw_sched_process_fork(ctx: RawTracePointContext) -> u32 { + emit_fork(&ctx) +} + +#[raw_tracepoint(tracepoint = "sched_process_exit")] +pub fn raw_sched_process_exit(ctx: RawTracePointContext) -> u32 { + emit_exit(&ctx) +} + +#[btf_tracepoint(function = "sched_process_exec")] +pub fn btf_sched_process_exec(ctx: BtfTracePointContext) -> u32 { + emit_exec(&ctx) +} + +#[btf_tracepoint(function = "sched_process_fork")] +pub fn btf_sched_process_fork(ctx: BtfTracePointContext) -> u32 { + emit_fork(&ctx) +} + +#[btf_tracepoint(function = "sched_process_exit")] +pub fn btf_sched_process_exit(ctx: BtfTracePointContext) -> u32 { + emit_exit(&ctx) } // Required by aya-bpf for panic handling in no_std diff --git a/ghostscope-process/src/sysmon.rs b/ghostscope-process/src/sysmon.rs index df594392..6ca3b1eb 100644 --- a/ghostscope-process/src/sysmon.rs +++ b/ghostscope-process/src/sysmon.rs @@ -356,38 +356,63 @@ fn try_publish_sys_event(tx: &mpsc::SyncSender, ev: SysEvent) -> bool } #[cfg(feature = "sysmon-ebpf")] -fn run_sysmon_loop( - mgr: Arc>, - target: Option, - pending: Arc>, - perf_pages: Option, - tx: mpsc::SyncSender, -) -> anyhow::Result<()> { - use aya::maps::{ - perf::{PerfEvent, PerfEventArray}, - ring_buf::RingBuf, - Array, MapData, - }; - use aya::programs::TracePoint; - use aya::{include_bytes_aligned, EbpfLoader, VerifierLogLevel}; - use log::{log_enabled, Level as LogLevel}; - // Load eBPF object (copied to OUT_DIR at build time) - #[allow(unused_variables)] - let obj_le: &[u8] = include_bytes_aligned!(concat!(env!("OUT_DIR"), "/sysmon-bpf.bpfel.o")); - #[allow(unused_variables)] - let obj_be: &[u8] = include_bytes_aligned!(concat!(env!("OUT_DIR"), "/sysmon-bpf.bpfeb.o")); - let obj: &[u8] = if cfg!(target_endian = "little") { - obj_le - } else { - obj_be - }; - if obj.is_empty() { - warn!("sysmon-bpf object missing; running in stub mode (no realtime process events)"); - return Ok(()); +#[derive(Debug, Clone, Copy)] +enum SysmonAttachBackend { + Raw, + Btf, + Classic, +} + +#[cfg(feature = "sysmon-ebpf")] +impl SysmonAttachBackend { + fn label(self) -> &'static str { + match self { + SysmonAttachBackend::Raw => "raw tracepoint", + SysmonAttachBackend::Btf => "BTF tracepoint", + SysmonAttachBackend::Classic => "classic tracepoint", + } } +} + +#[cfg(feature = "sysmon-ebpf")] +struct SysmonTracepoint { + event: &'static str, + category: &'static str, + classic_program: &'static str, + raw_program: &'static str, + btf_program: &'static str, +} + +#[cfg(feature = "sysmon-ebpf")] +const SYSMON_TRACEPOINTS: &[SysmonTracepoint] = &[ + SysmonTracepoint { + event: "sched_process_exec", + category: "sched", + classic_program: "sched_process_exec", + raw_program: "raw_sched_process_exec", + btf_program: "btf_sched_process_exec", + }, + SysmonTracepoint { + event: "sched_process_exit", + category: "sched", + classic_program: "sched_process_exit", + raw_program: "raw_sched_process_exit", + btf_program: "btf_sched_process_exit", + }, + SysmonTracepoint { + event: "sched_process_fork", + category: "sched", + classic_program: "sched_process_fork", + raw_program: "raw_sched_process_fork", + btf_program: "btf_sched_process_fork", + }, +]; + +#[cfg(feature = "sysmon-ebpf")] +fn load_sysmon_bpf(obj: &[u8], use_verbose: bool) -> anyhow::Result { + use aya::{EbpfLoader, VerifierLogLevel}; + let mut loader = EbpfLoader::new(); - let use_verbose = - cfg!(debug_assertions) || log_enabled!(LogLevel::Trace) || log_enabled!(LogLevel::Debug); if use_verbose { loader.verifier_log_level(VerifierLogLevel::VERBOSE | VerifierLogLevel::STATS); tracing::info!("Sysmon verifier logs: VERBOSE (debug build/log)"); @@ -395,7 +420,7 @@ fn run_sysmon_loop( loader.verifier_log_level(VerifierLogLevel::DEBUG | VerifierLogLevel::STATS); tracing::info!("Sysmon verifier logs: DEBUG (release/info)"); } - // Reuse pinned maps by name under our per-process dir. + let pin_dir = crate::pinned_bpf_maps::proc_offsets_pin_dir()?; loader.map_pin_path( crate::pinned_bpf_maps::ALLOWED_PIDS_MAP_NAME, @@ -405,70 +430,194 @@ fn run_sysmon_loop( crate::pinned_bpf_maps::TARGET_EXEC_COMM_MAP_NAME, pin_dir.join(crate::pinned_bpf_maps::TARGET_EXEC_COMM_MAP_NAME), ); - let mut bpf = loader.load(obj)?; - // Configure optional exec comm filter when targeting executables (-t binary). - // This is only a first-pass narrowing filter in BPF; userspace still validates - // the proc comm and target module/path before inserting offsets or allowed PIDs. - { - let mut filter_bytes = [0u8; 16]; - let mut filter_len = 0usize; - if let Some(tpath) = target.as_ref() { - if !crate::util::is_shared_object(tpath) { - if let Some(name) = tpath.file_name().and_then(|s| s.to_str()) { - let bytes = name.as_bytes(); - // task->comm stores at most TASK_COMM_LEN - 1 visible bytes plus NUL. - // Keep the filter null-terminated so long executable basenames compare - // against the same truncation that bpf_get_current_comm() returns. - let len = bytes.len().min(filter_bytes.len() - 1); - filter_bytes[..len].copy_from_slice(&bytes[..len]); - filter_len = len; - } else { - tracing::warn!( - "Sysmon: target basename contains non-UTF8 bytes; exec comm filter disabled" - ); - } + Ok(loader.load(obj)?) +} + +#[cfg(feature = "sysmon-ebpf")] +fn configure_sysmon_exec_comm_filter( + bpf: &mut aya::Ebpf, + target: Option<&Path>, +) -> anyhow::Result<()> { + use aya::maps::Array; + + let mut filter_bytes = [0u8; 16]; + let mut filter_len = 0usize; + if let Some(tpath) = target { + if !crate::util::is_shared_object(tpath) { + if let Some(name) = tpath.file_name().and_then(|s| s.to_str()) { + let bytes = name.as_bytes(); + // task->comm stores at most TASK_COMM_LEN - 1 visible bytes plus NUL. + // Keep the filter null-terminated so long executable basenames compare + // against the same truncation that bpf_get_current_comm() returns. + let len = bytes.len().min(filter_bytes.len() - 1); + filter_bytes[..len].copy_from_slice(&bytes[..len]); + filter_len = len; + } else { + tracing::warn!( + "Sysmon: target basename contains non-UTF8 bytes; exec comm filter disabled" + ); } } - if let Some(map) = bpf.map_mut("target_exec_comm") { - let mut array: Array<_, [u8; 16]> = map.try_into()?; - array.set(0, filter_bytes, 0)?; - if filter_len > 0 { - match std::str::from_utf8(&filter_bytes[..filter_len]) { - Ok(name_str) => { - tracing::info!("Sysmon: exec comm filter configured for '{}'", name_str) - } - Err(_) => tracing::info!( - "Sysmon: exec comm filter configured (non-UTF8 basename, len={})", - filter_len - ), + } + + if let Some(map) = bpf.map_mut("target_exec_comm") { + let mut array: Array<_, [u8; 16]> = map.try_into()?; + array.set(0, filter_bytes, 0)?; + if filter_len > 0 { + match std::str::from_utf8(&filter_bytes[..filter_len]) { + Ok(name_str) => { + tracing::info!("Sysmon: exec comm filter configured for '{}'", name_str) } - } else { - tracing::info!("Sysmon: exec comm filter disabled"); + Err(_) => tracing::info!( + "Sysmon: exec comm filter configured (non-UTF8 basename, len={})", + filter_len + ), } - } else if filter_len > 0 { - tracing::warn!("Sysmon: target_exec_comm map missing; exec filtering unavailable"); + } else { + tracing::info!("Sysmon: exec comm filter disabled"); } + } else if filter_len > 0 { + tracing::warn!("Sysmon: target_exec_comm map missing; exec filtering unavailable"); } - // Using allowlist-based gating in kernel; userspace decides allow on exec. + Ok(()) +} + +#[cfg(feature = "sysmon-ebpf")] +fn attach_sysmon_backend(bpf: &mut aya::Ebpf, backend: SysmonAttachBackend) -> anyhow::Result<()> { + match backend { + SysmonAttachBackend::Raw => attach_raw_sysmon_tracepoints(bpf), + SysmonAttachBackend::Btf => attach_btf_sysmon_tracepoints(bpf), + SysmonAttachBackend::Classic => attach_classic_sysmon_tracepoints(bpf), + } +} + +#[cfg(feature = "sysmon-ebpf")] +fn attach_raw_sysmon_tracepoints(bpf: &mut aya::Ebpf) -> anyhow::Result<()> { + use aya::programs::RawTracePoint; + + for spec in SYSMON_TRACEPOINTS { + let prog = bpf.program_mut(spec.raw_program).ok_or_else(|| { + anyhow::anyhow!("missing program '{}' in sysmon-bpf", spec.raw_program) + })?; + let tp: &mut RawTracePoint = prog.try_into()?; + tp.load()?; + tp.attach(spec.event)?; + info!("Attached raw tracepoint: {}", spec.event); + } + Ok(()) +} + +#[cfg(feature = "sysmon-ebpf")] +fn attach_btf_sysmon_tracepoints(bpf: &mut aya::Ebpf) -> anyhow::Result<()> { + use anyhow::Context as _; + use aya::{programs::BtfTracePoint, Btf}; + + let btf = Btf::from_sys_fs().context("kernel BTF is unavailable")?; + for spec in SYSMON_TRACEPOINTS { + let prog = bpf.program_mut(spec.btf_program).ok_or_else(|| { + anyhow::anyhow!("missing program '{}' in sysmon-bpf", spec.btf_program) + })?; + let tp: &mut BtfTracePoint = prog.try_into()?; + tp.load(spec.event, &btf)?; + tp.attach()?; + info!("Attached BTF tracepoint: {}", spec.event); + } + Ok(()) +} - // Attach tracepoints - for (name, cat, evt) in [ - ("sched_process_exec", "sched", "sched_process_exec"), - ("sched_process_exit", "sched", "sched_process_exit"), - ("sched_process_fork", "sched", "sched_process_fork"), +#[cfg(feature = "sysmon-ebpf")] +fn attach_classic_sysmon_tracepoints(bpf: &mut aya::Ebpf) -> anyhow::Result<()> { + use aya::programs::TracePoint; + + for spec in SYSMON_TRACEPOINTS { + let prog = bpf.program_mut(spec.classic_program).ok_or_else(|| { + anyhow::anyhow!("missing program '{}' in sysmon-bpf", spec.classic_program) + })?; + let tp: &mut TracePoint = prog.try_into()?; + tp.load()?; + tp.attach(spec.category, spec.event)?; + info!( + "Attached classic tracepoint: {}:{}", + spec.category, spec.event + ); + } + Ok(()) +} + +#[cfg(feature = "sysmon-ebpf")] +fn load_and_attach_sysmon_bpf( + obj: &[u8], + target: Option<&Path>, + use_verbose: bool, +) -> anyhow::Result { + let mut failures = Vec::new(); + for backend in [ + SysmonAttachBackend::Raw, + SysmonAttachBackend::Btf, + SysmonAttachBackend::Classic, ] { - if let Some(prog) = bpf.program_mut(name) { - let tp: &mut TracePoint = prog.try_into()?; - tp.load()?; - tp.attach(cat, evt)?; - info!("Attached tracepoint: {}:{}", cat, evt); - } else { - warn!("Missing program '{}' in sysmon-bpf", name); + tracing::info!("Sysmon: trying {} backend", backend.label()); + let result = (|| { + let mut bpf = load_sysmon_bpf(obj, use_verbose)?; + configure_sysmon_exec_comm_filter(&mut bpf, target)?; + attach_sysmon_backend(&mut bpf, backend)?; + Ok::<_, anyhow::Error>(bpf) + })(); + + match result { + Ok(bpf) => { + tracing::info!("Sysmon: using {} backend", backend.label()); + return Ok(bpf); + } + Err(err) => { + tracing::warn!("Sysmon: {} backend unavailable: {:#}", backend.label(), err); + failures.push(format!("{}: {err:#}", backend.label())); + } } } - tracing::info!("Sysmon: attached all tracepoints"); + + Err(anyhow::anyhow!( + "no sysmon tracepoint backend available ({})", + failures.join("; ") + )) +} + +#[cfg(feature = "sysmon-ebpf")] +fn run_sysmon_loop( + mgr: Arc>, + target: Option, + pending: Arc>, + perf_pages: Option, + tx: mpsc::SyncSender, +) -> anyhow::Result<()> { + use aya::include_bytes_aligned; + use aya::maps::{ + perf::{PerfEvent, PerfEventArray}, + ring_buf::RingBuf, + MapData, + }; + use log::{log_enabled, Level as LogLevel}; + // Load eBPF object (copied to OUT_DIR at build time) + #[allow(unused_variables)] + let obj_le: &[u8] = include_bytes_aligned!(concat!(env!("OUT_DIR"), "/sysmon-bpf.bpfel.o")); + #[allow(unused_variables)] + let obj_be: &[u8] = include_bytes_aligned!(concat!(env!("OUT_DIR"), "/sysmon-bpf.bpfeb.o")); + let obj: &[u8] = if cfg!(target_endian = "little") { + obj_le + } else { + obj_be + }; + if obj.is_empty() { + warn!("sysmon-bpf object missing; running in stub mode (no realtime process events)"); + return Ok(()); + } + let use_verbose = + cfg!(debug_assertions) || log_enabled!(LogLevel::Trace) || log_enabled!(LogLevel::Debug); + let mut bpf = load_and_attach_sysmon_bpf(obj, target.as_deref(), use_verbose)?; + + // Using allowlist-based gating in kernel; userspace decides allow on exec. // Initial prefill for late-start cases: compute and insert offsets for already-running PIDs. if let Some(tpath) = &target {