1use std::io::{self, Read};
16use std::os::fd::AsRawFd;
17use std::sync::atomic::{AtomicBool, Ordering};
18
19use aya::maps::{Array, HashMap, RingBuf};
20use aya::programs::{Lsm, TracePoint};
21use aya::{Btf, Ebpf, EbpfLoader};
22
23#[repr(align(8))]
25struct Aligned<T: ?Sized>(T);
26static OBJECT: &Aligned<[u8]> = &Aligned(*include_bytes!(concat!(env!("OUT_DIR"), "/process.bpf.o")));
27fn object_bytes() -> &'static [u8] {
28 &OBJECT.0
29}
30
31const PAT: usize = 64;
34const ARG: usize = 24;
35const MAX_SOURCES: usize = 128;
36const MAX_RULES: usize = 128;
37const MAX_XFORMS: usize = 64;
38const MAX_GATES: usize = 64;
39const MAX_INVALS: usize = 64;
40
41#[repr(C)]
42#[derive(Clone, Copy)]
43struct CSource {
44 kind: u8,
45 m: u8,
46 pat: [u8; PAT],
47 label: u64,
48 ipv4: u32,
49 ipv4_mask: u32,
50}
51#[repr(C)]
52#[derive(Clone, Copy)]
53struct CRule {
54 op: u8,
55 m: u8,
56 cond_kind: u8,
57 cond_neg: u8,
58 cond_match: u8,
59 effect: u8,
60 target: [u8; PAT],
61 arg: [u8; ARG],
62 cond_pat: [u8; PAT],
63 req: u64,
64 forbid: u64,
65 gate: u64,
66 rule_id: u32,
67 ipv4: u32,
68 ipv4_mask: u32,
69 cond_ipv4: u32,
70 cond_ipv4_mask: u32,
71 gate_idx: u32,
72 since_mask: u64,
73}
74#[repr(C)]
75#[derive(Clone, Copy)]
76struct CXform {
77 m: u8,
78 add: u8,
79 gate: [u8; PAT],
80 label: u64,
81}
82#[repr(C)]
83#[derive(Clone, Copy)]
84struct CGate {
85 m: u8,
86 pat: [u8; PAT],
87 bit: u64,
88}
89#[repr(C)]
90#[derive(Clone, Copy)]
91struct CInval {
92 op: u8,
93 m: u8,
94 pat: [u8; PAT],
95}
96#[repr(C)]
97#[derive(Clone, Copy)]
98struct CConfig {
99 n_sources: u32,
100 n_rules: u32,
101 n_xforms: u32,
102 n_gates: u32,
103 n_invals: u32,
104 sources: [CSource; MAX_SOURCES],
105 rules: [CRule; MAX_RULES],
106 xforms: [CXform; MAX_XFORMS],
107 gates: [CGate; MAX_GATES],
108 invals: [CInval; MAX_INVALS],
109}
110
111#[repr(C)]
113#[derive(Clone, Copy)]
114struct ProcState {
115 labels: u64,
116 lin_gates: u64,
117}
118
119unsafe impl aya::Pod for CSource {}
120unsafe impl aya::Pod for CRule {}
121unsafe impl aya::Pod for CXform {}
122unsafe impl aya::Pod for CGate {}
123unsafe impl aya::Pod for CInval {}
124unsafe impl aya::Pod for ProcState {}
125
126const EVENT_TYPE_TAINT_VIOLATION: i32 = 3;
128const COMM_LEN: usize = 16;
129const FILENAME_LEN: usize = 127;
130
131#[repr(C)]
132#[derive(Clone, Copy)]
133struct Event {
134 etype: i32,
135 pid: i32,
136 ppid: i32,
137 blocked: u32,
138 killed: u32,
139 effect: u32,
140 timestamp_ns: u64,
141 comm: [u8; COMM_LEN],
142 filename: [u8; FILENAME_LEN],
143 taint_rule_id: u32,
144 conn_ip: u32,
145 taint_label: u64,
146}
147
148#[derive(Debug, Clone)]
150pub struct Violation {
151 pub effect: u32, pub blocked: bool,
153 pub killed: bool,
154 pub comm: String,
155 pub pid: i32,
156 pub ppid: i32,
157 pub target: String, pub rule_id: u32,
159 pub label: u64,
160 pub timestamp_ns: u64,
161}
162
163const TRACEPOINTS: &[(&str, &str, &str)] = &[
165 ("handle_fork", "sched", "sched_process_fork"),
166 ("handle_exec", "sched", "sched_process_exec"),
167 ("handle_exit", "sched", "sched_process_exit"),
168 ("trace_openat", "syscalls", "sys_enter_openat"),
169 ("trace_openat_exit", "syscalls", "sys_exit_openat"),
170 ("trace_open", "syscalls", "sys_enter_open"),
171 ("trace_open_exit", "syscalls", "sys_exit_open"),
172 ("trace_openat2", "syscalls", "sys_enter_openat2"),
173 ("trace_openat2_exit", "syscalls", "sys_exit_openat2"),
174 ("trace_creat", "syscalls", "sys_enter_creat"),
175 ("trace_creat_exit", "syscalls", "sys_exit_creat"),
176 ("trace_truncate", "syscalls", "sys_enter_truncate"),
177 ("trace_truncate_exit", "syscalls", "sys_exit_truncate"),
178 ("trace_unlink", "syscalls", "sys_enter_unlink"),
179 ("trace_unlinkat", "syscalls", "sys_enter_unlinkat"),
180 ("trace_rename", "syscalls", "sys_enter_rename"),
181 ("trace_renameat", "syscalls", "sys_enter_renameat"),
182 ("trace_renameat2", "syscalls", "sys_enter_renameat2"),
183 ("trace_connect", "syscalls", "sys_enter_connect"),
184];
185
186const LSM_PROGS: &[(&str, &str)] = &[
188 ("enforce_bprm_check_security", "bprm_check_security"),
189 ("enforce_file_open", "file_open"),
190 ("enforce_file_permission", "file_permission"),
191 ("enforce_file_truncate", "file_truncate"),
192 ("enforce_path_truncate", "path_truncate"),
193 ("enforce_path_unlink", "path_unlink"),
194 ("enforce_path_rename", "path_rename"),
195 ("enforce_socket_connect", "socket_connect"),
196];
197
198pub fn bpf_lsm_active() -> bool {
200 let mut s = String::new();
201 if let Ok(mut f) = std::fs::File::open("/sys/kernel/security/lsm") {
202 let _ = f.read_to_string(&mut s);
203 }
204 s.split(',').any(|x| x.trim() == "bpf")
205}
206
207fn err(msg: impl Into<String>) -> io::Error {
208 io::Error::new(io::ErrorKind::Other, msg.into())
209}
210
211pub struct Loader {
212 bpf: Ebpf,
213 enforce: bool,
214}
215
216impl Loader {
217 pub fn load(config_blob: &[u8]) -> io::Result<Self> {
219 if config_blob.len() != std::mem::size_of::<CConfig>() {
220 return Err(err(format!(
221 "config size mismatch: got {}, expected {}",
222 config_blob.len(),
223 std::mem::size_of::<CConfig>()
224 )));
225 }
226 let cfg: Box<CConfig> =
228 Box::new(unsafe { std::ptr::read_unaligned(config_blob.as_ptr() as *const CConfig) });
229
230 let enforce = bpf_lsm_active();
231 let enforce_mode: u32 = if enforce { 1 } else { 0 };
232
233 let mut loader = EbpfLoader::new();
234 loader
235 .set_global("enforce_mode", &enforce_mode, true)
236 .set_global("n_sources", &cfg.n_sources, true)
237 .set_global("n_rules", &cfg.n_rules, true)
238 .set_global("n_xforms", &cfg.n_xforms, true)
239 .set_global("n_gates", &cfg.n_gates, true)
240 .set_global("n_invals", &cfg.n_invals, true)
241 .set_global("taint_sources", &cfg.sources[..], true)
242 .set_global("taint_rules", &cfg.rules[..], true)
243 .set_global("taint_xforms", &cfg.xforms[..], true)
244 .set_global("taint_gates", &cfg.gates[..], true)
245 .set_global("taint_invals", &cfg.invals[..], true);
246
247 let mut bpf = loader
248 .load(object_bytes())
249 .map_err(|e| err(format!("Ebpf::load: {e}")))?;
250
251 {
254 let mut counts: Array<_, u32> = Array::try_from(
255 bpf.map_mut("ts_counts").ok_or_else(|| err("map ts_counts missing"))?,
256 )
257 .map_err(|e| err(format!("ts_counts: {e}")))?;
258 let vals = [
259 cfg.n_rules,
260 cfg.n_sources,
261 cfg.n_xforms,
262 cfg.n_gates,
263 cfg.n_invals,
264 ];
265 for (i, v) in vals.iter().enumerate() {
266 counts
267 .set(i as u32, *v, 0)
268 .map_err(|e| err(format!("ts_counts[{i}]: {e}")))?;
269 }
270 }
271
272 for (name, cat, event) in TRACEPOINTS {
274 let p: &mut TracePoint = bpf
275 .program_mut(name)
276 .ok_or_else(|| err(format!("program {name} missing")))?
277 .try_into()
278 .map_err(|e| err(format!("{name} not a tracepoint: {e}")))?;
279 p.load().map_err(|e| err(format!("{name}.load: {e}")))?;
280 p.attach(cat, event)
281 .map_err(|e| err(format!("{name}.attach: {e}")))?;
282 }
283 if enforce {
284 let btf = Btf::from_sys_fs().map_err(|e| err(format!("btf: {e}")))?;
285 for (name, hook) in LSM_PROGS {
286 let p: &mut Lsm = bpf
287 .program_mut(name)
288 .ok_or_else(|| err(format!("program {name} missing")))?
289 .try_into()
290 .map_err(|e| err(format!("{name} not an lsm: {e}")))?;
291 p.load(hook, &btf)
292 .map_err(|e| err(format!("{name}.load: {e}")))?;
293 p.attach().map_err(|e| err(format!("{name}.attach: {e}")))?;
294 }
295 }
296
297 Ok(Loader { bpf, enforce })
298 }
299
300 pub fn enforce_mode(&self) -> bool {
301 self.enforce
302 }
303
304 pub fn seed_agent(&mut self, pid: i32, label: u64) -> io::Result<()> {
306 if pid <= 0 || label == 0 {
307 return Err(err("agent pid and label must both be set"));
308 }
309 {
310 let mut proc: HashMap<_, i32, ProcState> = HashMap::try_from(
311 self.bpf.map_mut("ts_proc").ok_or_else(|| err("ts_proc missing"))?,
312 )
313 .map_err(|e| err(format!("ts_proc: {e}")))?;
314 proc.insert(pid, ProcState { labels: label, lin_gates: 0 }, 0)
315 .map_err(|e| err(format!("seed ts_proc: {e}")))?;
316 }
317 {
318 let mut root: HashMap<_, i32, i32> = HashMap::try_from(
319 self.bpf.map_mut("ts_root").ok_or_else(|| err("ts_root missing"))?,
320 )
321 .map_err(|e| err(format!("ts_root: {e}")))?;
322 root.insert(pid, pid, 0)
323 .map_err(|e| err(format!("seed ts_root: {e}")))?;
324 }
325 Ok(())
326 }
327
328 pub fn run(&mut self, stop: &AtomicBool, mut on: impl FnMut(Violation)) -> io::Result<()> {
330 let mut ring = RingBuf::try_from(
331 self.bpf.map_mut("rb").ok_or_else(|| err("rb missing"))?,
332 )
333 .map_err(|e| err(format!("rb: {e}")))?;
334 let fd = ring.as_raw_fd();
335
336 while !stop.load(Ordering::Relaxed) {
337 let mut pfd = libc::pollfd { fd, events: libc::POLLIN, revents: 0 };
338 let r = unsafe { libc::poll(&mut pfd, 1, 100) };
339 if r < 0 {
340 let e = io::Error::last_os_error();
341 if e.kind() == io::ErrorKind::Interrupted {
342 continue;
343 }
344 return Err(e);
345 }
346 while let Some(item) = ring.next() {
347 let bytes: &[u8] = &item;
348 if bytes.len() < std::mem::size_of::<Event>() {
349 continue;
350 }
351 let e: Event =
352 unsafe { std::ptr::read_unaligned(bytes.as_ptr() as *const Event) };
353 if e.etype != EVENT_TYPE_TAINT_VIOLATION {
354 continue;
355 }
356 on(decode(&e));
357 }
358 }
359 Ok(())
360 }
361}
362
363fn cstr(buf: &[u8]) -> String {
364 let end = buf.iter().position(|&b| b == 0).unwrap_or(buf.len());
365 String::from_utf8_lossy(&buf[..end]).into_owned()
366}
367
368fn decode(e: &Event) -> Violation {
369 let target = if e.conn_ip != 0 {
370 let ip = e.conn_ip; format!(
372 "{}.{}.{}.{}",
373 ip & 0xff,
374 (ip >> 8) & 0xff,
375 (ip >> 16) & 0xff,
376 (ip >> 24) & 0xff
377 )
378 } else {
379 cstr(&e.filename)
380 };
381 Violation {
382 effect: e.effect,
383 blocked: e.blocked != 0,
384 killed: e.killed != 0,
385 comm: cstr(&e.comm),
386 pid: e.pid,
387 ppid: e.ppid,
388 target,
389 rule_id: e.taint_rule_id,
390 label: e.taint_label,
391 timestamp_ns: e.timestamp_ns,
392 }
393}
394
395#[cfg(test)]
396mod tests {
397 use super::*;
398
399 #[test]
402 fn abi_sizes() {
403 assert_eq!(std::mem::size_of::<ProcState>(), 16);
404 assert_eq!(std::mem::align_of::<CConfig>(), 8);
407 assert!(std::mem::size_of::<CConfig>() > 0);
408 }
409
410 #[test]
411 fn object_is_aligned_elf() {
412 let b = object_bytes();
413 assert_eq!(b.as_ptr() as usize % 8, 0, "object must be 8-aligned");
414 assert_eq!(&b[..4], b"\x7fELF");
415 }
416}