From a5e263ff27dc77ea9ad7c501b5c16a481027ce9d Mon Sep 17 00:00:00 2001 From: swananan Date: Tue, 9 Jun 2026 19:25:51 +0800 Subject: [PATCH] fix: add sysmon tracepoint backend fallback Try raw tracepoint, BTF tracepoint, then classic tracepoint internally so sysmon does not require tracefs when raw attach works. Keep the public configuration surface unchanged and preserve the existing sysmon event ABI. --- .../ebpf/obj/sysmon-bpf.bpfeb.o | Bin 4192 -> 7368 bytes .../ebpf/obj/sysmon-bpf.bpfel.o | Bin 4192 -> 7368 bytes ghostscope-process/ebpf/sysmon-bpf/src/lib.rs | 147 ++++---- ghostscope-process/src/sysmon.rs | 315 +++++++++++++----- 4 files changed, 322 insertions(+), 140 deletions(-) diff --git a/ghostscope-process/ebpf/obj/sysmon-bpf.bpfeb.o b/ghostscope-process/ebpf/obj/sysmon-bpf.bpfeb.o index 1010455cee13fe6cbf5dd49f355bfdfe1a1f46c3..fdd2f1dbb09e4d0abc47afb8b7a6c3796b9bf37c 100644 GIT binary patch literal 7368 zcmeHMPmB{)7=JSbyP)ir4GSwuXgH{7#C9QUO*E;yEJh>Ex`5&2Y}=heWxJhrhV5=N zN=QgNVvLDLQA13;aBH|&Z*Jm2jvP4|PNqf;7}4?feeb>Lv~(JYU^ZsoGVgo8-}k;h z@0)ouZhL566<~(erUeN%~>7|DSwZD zq7)l>%0fJ-dXA;UBjm9*CH=ZPq>QWk5l=8S=>DS_n}z>74TAge3Lzo(D*NXTi8x{S z-A?j;{i^58r?~Vyz|Ro*X{k6P(}aNMq#QXeW$mPt?u?W=f8Zqcu|8SHo3$jvVBNr+G3vk)*rXZ2EP-PRvjo<4 zV+p)j=~)8va|C;qz+5k2mcV=;fms4?QT8l>w<^pM_)&#f0zalOOWmBM*pLxC??WV&}wAA~mnUq+)-xBIR`91oCs(a$35WI879L_V* z>Z$+8F6pnvyMpuJy7b=Hk*DRnxXj0>?UQ)VsQ)XCP;9nSafcgHo$QHsqm`5jmA|3$ z*Uxk4d27>5?XNX2?HW=hi&y0LpeN6}1D-#mi28P~zukID`c?VF)>%I0fP6OgBW@k` z)5d(_(W!=Bd_TzfVNAJgb@GvBQpZwpCp4RjKj90NMMQ2qUsN0JNJbT3x^?bu}i)ZWw z%c)n3w(DBQ}(x(EYYW-}V3NRg6fk4~00v+w(3h=zHrCW|& zFNwu+L9qp=B(R=Jb=mHdIxVYD>P$zb9%%cf-q8+DUCwj8x|c7A1MX^tq&4?iZh6`* zE!bz@I(D{pJU3&dQ=OHh(~WxBv*lgRdGs^w9&AjywO4bQ{Ok!UlV8nSHrAZM<9WKC zuej+$`~U{IOr+-WjS~cC>A9M&F41#6Gc{p7gy4r|g(gx{Ug^JznNE4ZV$+lTqvCz_ zFPoW%1OA)Uy^coPl8fEw(dqXq(e!Q8xc57nr3rBVp_D;S?^xYv%zx8@%xY4~?HbPm z@4`jza%$Qh-zRe~a76sr8qb#BPdSvmwg)ol%oP>?GdW$PASu2+pJ;o;7X@4q@gJe@ z5w%NF1iiK=;%!_}@vke3BqYVx_0{%>Z!Cb1ioXUOwE^*sdFZ3!x5*eSiHqXvxLP7U z|0haB{P-!@pz--d4rQP<-=c@KNz^L5DV| z{^pCA5EcJCS)=LtQ@yE$;*0aZ5%IVEgY}E|U>_C#Ds&VV0>2G+l#Xl|P?zDF**7 J8&by~{~M+R?G^w4 literal 4192 zcmc&$ONd)l7(O@CHe+8Ci?!2gX=sF&f;G1@%}jNZRz`{xE!x?*Ni)eECpwutZu%G$ zEtNuXrHIg-twM1lxDZlt5wHvE$^jR4C0!`i_lTBOtzN(Xags9^Q!5A_$oc;7`_DQ5 zf1bH7P8@h7k?>5nc;-56N9cNBtU1gquk~ua5I0m7@bE2lX)3;I4#`P!x#_oBFozc*&5Vw zH!e5PHBu(COUDn_NspxVPo|iAX#X2o>$7GBVPhZF#)NkX#@o|;QK;XqWZ3*Wdx;sd zMf=}BYLdh;?wy4Hc3fT8i1MWQVw^!5$Hp0Ym>BpIf;AaG1u6{;vJ{Rya zg7bQTGlKKJfHQ*Mq5T=bS1M-&U!|N8e6?~$@HNUA!PhEh1Rqk)*w_>;5T8s*d|j`# z$4E3L-C4R`mpW%u#FKig^H+COFc$NVYHBQQ4Kn$klNcmE z&IR$M*^;k19=(bJCp0c<;t8}~~`KkT%B4>sRH;BiNP2lk+HSN|NE9UuAG;+=8y zOIAhxpE>$fM}M`aduc~M@91va1xL?2?T}y3dx=IKw^OJwv*F-eveU;%C>B#@pO$Z|bmVFaOzJ4XUO3yxG|d zW_PxN<1MyY&0;C2*D7}>5=P%hLfWhfXQAJ0=%wTjKBgBH36r9!Dzt=>+o>00yIr9leypgC=3D^nVJ zsylK&CamZY!#<~^HsvaQ;+%C-gv%$tXXLVGAn;ia0{ByTC9ikQK-kc9CyHV3&X|kQEp!_%aH^ZegPWKHxtp*D(&B_P;Bu z=-i=r{0$Fl~S7J)-#{UFTpZNTVOg-p660z@~?N9Mf;@2ns3cB-DG!G%DZGRG8z^`9? zic2MpAH}!pYx`4t`ZDep|15k|ZWSNTX5aV+xpyzVjcbwOKaQV`A3I0M?b^StXu+-{ aarzFm`)d7)$unXM><3be4eGG*hyMcD`yRC zNJxCd7!x1G5MtsBZw)WjH%WYuM;>`H`()Or0VBGe@1Aqp>3GYc0kbiCcc*90cmB`4 zcjmSq?s;vm)6=8O=utORn=`0VSJt=h3;MR8`bo1Xcc-PE-Cg;r8&N2!|& z3k&@Tr;U_SD@moXg7wZ4IYXDPRw+_nnDrQMUSOIgJ?)45#YaE+fxf{Sz3dnM9--?~ z5iIA)jksI=%%<+s4ui+oZ}<|^w9H4E<{MLv`3LP)zv($@&Br>{-z@gejHES?O|8}R zpZY}{tOLXa&8D!rlxj+xz8#uX&BvMcJ;yXS%rrd2)a2i*Zgk|EP0<3R5#~DeOr5>z z_YVCAtEp^?>oR^#U8XO=8u3SYqfeU(yni+q7&rYoOM1UjU2*>B7=LQRH66bJCp1bM z<~1MRhx4SL+=tjFFv+SHV5XYqHBqSRI_U?p-%Qa3S0F=i&%%KWC>FFOBT#der)R60%97P{i` zt|8A@{_tb+H|yD~zf-cljK3yNpPD8Lzoqy1eGt#{7WwbK{&+pa^Fe=`MRG<1{ltn| zO8KBw-Sb)AkC?aR{do8HBc2#bX+JKNe9V5d=d-*Ym-i!`NAJP=@yR8&AD2o#WQLQqw}2M_QY4WlVeN$mj}bG1>vLp;b}q5U%4aW7DdTqea8>L-0hYJqZ-Qt;S0 zek90=;a`dv=Vai9H?&BH7T^aYA%lNMcEIud72^OC^#kc;bQKKLB!v{=Yt?#nNc0{R zdVy$$&fs&RHz4>|M4@l=4816RhD6Wcn4^r6Lmb_o6ZzLf;l~W4|Fq6?rGZ=e-)o%D z;t-nrGjyL(dcpO5K2Lw{x%Kc8qnc=GcwIKjFmGI*-FKYz9A^1!Je?sJNkE&}i9^XK&#(2oh`4{?-&q!Xz z{+k#NdE7s`{h7$&YZYBbB!BR@HS-EJe8ZWc3*%1Z|Mz&R;#W$wS+%Y1Pi%|)nMl>W zX*Vn#_m8=?dbQ+-q3h3-BUOyXY%uI#5!Ifx7(p7{A z;n&^b$?`;0o(#8>yPc@IQd32IL|%Q|kElw&*dJjSssn>Hxhc=_xo?{eq;C8oBQ1%Z`N~t zs(FZ(=Xl+#g!vtM0k(1(960Dr9|C&->uRJriFLhac+kBE!FO_p1_y?tvHvP&ejsWU zo8M)R%1^m}|LM`gQy%RdPUhuRb1fm1$8bv*Z#H1mSdpyhI13aGRg*XUa ze)l;l_`^i;oHzF7{j`}tTl^~$*Sv?tP0QZ!8_C`l|8t2yAc}~OGgdNwkuY2QA!*1t zu{Xrn8~$CovBkeC@wbQ~;=`q}H}xN*;I{Zz?lAsQGPcE^x5P*6Wc;2k{XfD#yS2_i zQbx<(@PBlPzed(yy#IKYC*!|Nn6-WsMPsr47f8oee?j7JAvp&6kF}nRzn6mB;vYe> zG!~0L+@=1bmiRa=CgZ=@rT=phf3fwyyUY4NZRtPOTr&P?!mRc4&&-R~|32B<`hUq% zKcHm%GhO;WcZcym=+ghImiRbjC*xn1`V)^iH6us+8p-1W&ye^iQ~&AzZ(~mE%{do6 N#+EYi`B!7%Q6nYF8j4B!cby*ew^gD5!@DdLS1rTW7N-7IEzHY@0+y zP!&~*IDrsSZjcI9T#z^*8N|U790(^Oaex!*0YrfUMNp+cA-p&5O}trB5g{Z-+WqEz z^L}RDn;Ea4pFIAU)71sGb-@O7B+CMvysgtOnQjU8qGtO}=Dou=HmT=5c->(80`qYP z;QHp~W-HF}V8nSV2GEN`t!^Gak8lp3TAUSMWg3>s+-bkT7)O2bJBa?nZ4mt-ZRS9H ztB3uQ-@EbYfoo=LDz@Ff&X6`hTR+3r_kXXoF( zgY^%H{X>s(LXxrl|H?1Lp>@(c36n9(B7lw4KW0Q|-_O`T&Zwps^^=Tt{yng5mKK9gEA@mu5so0qz3W1`Y4?Kbr>R&Vfaa=Cd!=CGQ znfX@8p0Tqq>g#1V?yefmpUe- zj(MqLLh6{8Iwqu!d8y;-pC)fQ^@BmR`iIEhi_G_(PF)fCcNzaqWWG0a>RXZdKG3Oe zMCNm%QIBo2(bdZAlqZ z2U#A<{9^R&dAH~DjGVvde)0Nv-C_SXa2_{5mz>}71+iO|dEg|>{=xE4#q2)$Kj%R^ z!Xszj&Jj5I#+tCe*&f{(#v;a;&hO#1kn zXY-a7{)p&{xTR0mJ^8Wr7GFVr0QjyXn`L@oqj;oWG!kstJ%I9n`O@P7t53_K`PjOY zco;qse%P!V;BDc{hzC)BSn@z;huSPY#1|vHT_3qg;d2%rY9!N{9wq@B$%FW;PzM|R z8j~wq2a@UgG3;*$%;4aiXY0q7-NtIxasX@5S@P%Aeyl;`H^3;vATsLmjR{%Z}GrdLpIKJ5o+ z?t89Ruhn)Ft5R=1vptC0@S7Ewt4>Sk=|+X3)as}G-8iJ^FrtHIP@YCysu8+RJ)X;U z{N-{B&1&HDs&a){uQE}nWWCB*&MTJlho&cuK>^p$sLuIKcluOyE~w7yQJlvFuNbW+ za)olmsMS=-;f~9eSf;-0KD8aaEV2mNlFyZEDY?L9-kYmU) z6xzi1oiByzPHw+;S0ge$UNZLElh|Iv7FcTOs6mc-y#c6N#nvVMEsNTlD=X#8`? z?G%4dCbW#>;b!eE_Z)V1il3GEX;CDRTv~g(e)@a7Q~cL=8GiuncZz>vhximb8vhB2 pAKgR4{P$$*{z5g=D2L%j9~x4sFbykWe@q = 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 {