bpf/logging/
mod.rs

1// SPDX-License-Identifier: Apache-2.0
2
3use std::sync::OnceLock;
4
5use anyhow::Result;
6use libbpf_rs::{RingBuffer, RingBufferBuilder};
7use nix::sys::signal::Signal;
8use zerocopy::FromBytes;
9
10include!(concat!(env!("OUT_DIR"), "/logging_types.rs"));
11
12impl log_hdr {
13    /// Parse the type_ field from the generated C binding of struct log_hdr into
14    /// a variant of the LogLevel enum derived from logging_types.h using strum.
15    ///
16    /// Defaults to LOG_LEVEL_OFF.
17    pub fn level(&self) -> LogLevel {
18        // from_repr is derived from strum_macros::FromRepr in build.rs
19        LogLevel::from_repr(self.level as u32).unwrap_or(LogLevel::LOG_LEVEL_ERROR)
20    }
21
22    /// Parse the type_ field from the generated C binding of struct log_hdr into
23    /// a variant of the LogReason enum derived from logging_types.h using strum.
24    ///
25    /// Defaults to LOG_REASON_UNKNOWN
26    fn reason(&self) -> LogReason {
27        // from_repr is derived from strum_macros::FromRepr in build.rs
28        LogReason::from_repr(self.reason as u32).unwrap_or(LogReason::LOG_REASON_UNKNOWN)
29    }
30
31    /// Convert LogReason enum variant to a minimized string
32    fn reason_str(&self) -> String {
33        enum_str_no_prefix(self.reason(), "LOG_REASON_")
34    }
35
36    /// Parse the type_ field from the generated C binding of struct log_hdr into
37    /// a variant of the EventType enum derived from logging_types.h using strum.
38    ///
39    /// Defaults to LOG_REASON_UNKNOWN
40    pub fn type_(&self) -> EventType {
41        // from_repr is derived from strum_macros::FromRepr in build.rs
42        EventType::from_repr(self.type_ as u32).unwrap_or(EventType::EVENT_TYPE_UNKNOWN)
43    }
44
45    /// Convert EventType enum variant to a minimized string
46    fn type_str(&self) -> String {
47        enum_str_no_prefix(self.type_(), "EVENT_TYPE_")
48    }
49}
50
51impl std::fmt::Display for log_hdr {
52    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53        let mut comm_str = char_array_to_str(&self.comm);
54        if comm_str.is_empty() {
55            comm_str = "(empty)";
56        }
57        let policy_str: String = if self.pol_id == crate::seabee::NO_POL_ID.into()
58            || self.pol_id == crate::seabee::BASE_POLICY_ID.into()
59        {
60            String::from("SeaBee")
61        } else {
62            //TODO: would be cool if we could use the policy name rather than id
63            format!("SeaBee policy {}", self.pol_id)
64        };
65        write!(
66            f,
67            "{}: {}: {} {}({})",
68            policy_str,
69            self.type_str(),
70            self.reason_str(),
71            comm_str,
72            self.pid
73        )
74    }
75}
76
77/// Builds and reads from an eBPF ringbuffer.
78///
79/// This method loops continuously until interrupted (usually a SIGINT signal)
80pub fn setup_logger<'a>(ringbuf: &libbpf_rs::MapHandle) -> Result<RingBuffer<'a>> {
81    let mut rbb = RingBufferBuilder::new();
82    rbb.add(ringbuf, rb_callback)?;
83    Ok(rbb.build()?)
84}
85
86/// Reduce boilerplate when adding a new log struct to logging.rs
87#[macro_export]
88macro_rules! log_struct {
89    ($t:ty,$h:ident,$d:ident) => {
90        match <$t>::ref_from_bytes($d) {
91            Ok(log_struct) => {
92                log(
93                    $h.level(),
94                    // ref_from_bytes is derived from zerocopy_derive::FromBytes in build.rs
95                    format!("{} {}", $h, log_struct.to_string()),
96                )
97            }
98            Err(e) => log(
99                $h.level(),
100                format!("{} (unable to parse log struct - {}) {:?}", $h, e, $d),
101            ),
102        }
103    };
104}
105
106/// Callback that can be registered with a ringbuffer map.
107///
108/// Every log struct defined by BPF programs will have an associated
109/// EventType specified in the log_hdr that links the log to a generated
110/// C binding of the log struct.
111///
112/// When a new BPF program is implemented, you must add a new match arm
113/// in this function to get the logging system to print out your structure
114///
115/// An example is provided:
116///
117/// ```ignore
118/// EventType::EVENT_TYPE_GENERIC => {
119///     log_struct!(generic::generic_log, data)
120/// }
121/// ```
122///
123/// If you do not register your BPF program's structure, the raw bytes from
124/// the ringbuf will be output to the string
125fn rb_callback(data: &[u8]) -> i32 {
126    if let Some(header) = log_header(data) {
127        match header.type_() {
128            EventType::EVENT_TYPE_MSG => {
129                log_struct!(generic_msg_log, header, data)
130            }
131            EventType::EVENT_TYPE_FILE_ACCESS => {
132                log_struct!(inode_access_log, header, data)
133            }
134            EventType::EVENT_TYPE_SB_UMOUNT => {
135                log_struct!(sb_umount_log, header, data)
136            }
137            EventType::EVENT_TYPE_BPF_MAP => {
138                log_struct!(bpf_map_log, header, data)
139            }
140            EventType::EVENT_TYPE_TASK_KILL => {
141                log_struct!(task_kill_log, header, data)
142            }
143            EventType::EVENT_TYPE_KERNEL_MODULE_REQUEST => {
144                log_struct!(kernel_module_request_log, header, data)
145            }
146            EventType::EVENT_TYPE_KERNEL_READ_FILE => {
147                log_struct!(kernel_read_file_log, header, data)
148            }
149            EventType::EVENT_TYPE_KERNEL_LOAD_DATA => {
150                log_struct!(kernel_load_data_log, header, data)
151            }
152            EventType::EVENT_TYPE_PTRACE_ACCESS_CHECK => {
153                log_struct!(ptrace_access_check_log, header, data)
154            }
155            // the default case is for log_generic() and other log structures
156            // that haven't yet been implemented in Rust
157            _ => {
158                let hdr_size = std::mem::size_of::<log_hdr>();
159                if data.len() == hdr_size {
160                    log(header.level(), format!("{header}"))
161                } else {
162                    log(
163                        header.level(),
164                        format!(
165                            "{} (unimplemented log struct for {} bytes of data) {:?}",
166                            header,
167                            hdr_size - data.len(),
168                            data
169                        ),
170                    )
171                }
172            }
173        }
174    }
175    0
176}
177
178/// Outputs a [tracing::Event] according to the log level.
179pub fn log(level: LogLevel, log: String) {
180    match level {
181        LogLevel::LOG_LEVEL_OFF => (),
182        LogLevel::LOG_LEVEL_TRACE => tracing::trace!("{}", log),
183        LogLevel::LOG_LEVEL_DEBUG => tracing::debug!("{}", log),
184        LogLevel::LOG_LEVEL_INFO => tracing::info!("{}", log),
185        LogLevel::LOG_LEVEL_WARN => tracing::warn!("{}", log),
186        LogLevel::LOG_LEVEL_ERROR => tracing::error!("{}", log),
187    };
188}
189
190/// Static global to use within ringbuffer callbacks
191pub static LOG_LEVEL: OnceLock<LogLevel> = OnceLock::new();
192
193pub static LOG_FILTER: OnceLock<std::collections::HashSet<EventType>> = OnceLock::new();
194
195/// Attempts to transmute raw bytes into a [log_hdr]
196///
197/// The input bytes are expected to come from a eBPF ringbuffer
198pub fn log_header(data: &[u8]) -> Option<&log_hdr> {
199    // `ref_from_prefix` is derived from `zerocopy_derive::FromBytes` in `build.rs`
200    if let Ok((header, _)) = log_hdr::ref_from_prefix(data) {
201        // don't parse a log that isn't going to be printed
202        if header.level() as u32 > *LOG_LEVEL.get().unwrap() as u32 {
203            return None;
204        }
205        if LOG_FILTER.get().unwrap().contains(&header.type_()) {
206            return None;
207        }
208        Some(header)
209    } else {
210        log(
211            LogLevel::LOG_LEVEL_ERROR,
212            format!("(unable to parse log header) {data:?}"),
213        );
214        None
215    }
216}
217
218/// Utility function which attempts to decode an array of signed bytes
219/// (char[] in C) into a UTF-8 string.
220pub fn char_array_to_str(array: &[u8]) -> &str {
221    match std::ffi::CStr::from_bytes_until_nul(array) {
222        Ok(cstr) => cstr.to_str().unwrap_or("(non-utf8 string)"),
223        Err(_) => "(bad bytes)",
224    }
225}
226
227/// Utility function which takes any type that implements AsRef<str> (&str) and
228/// outputs a String that has the provided prefix stripped and ASCII characters
229/// converted to lowercase.
230fn enum_str_no_prefix<T: AsRef<str>>(bindgen_enum: T, prefix: &str) -> String {
231    bindgen_enum
232        .as_ref()
233        .strip_prefix(prefix)
234        .unwrap_or("(error)")
235        .to_ascii_lowercase()
236}
237
238impl std::fmt::Display for generic_msg_log {
239    /// Formats a log from the sb_umount BPF program
240    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
241        let msg = char_array_to_str(&self.msg);
242        write!(f, "{msg}")
243    }
244}
245
246impl std::fmt::Display for sb_umount_log {
247    /// Formats a log from the sb_umount BPF program
248    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
249        write!(f, "dev {}", self.target_dev)
250    }
251}
252
253impl std::fmt::Display for bpf_map_log {
254    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
255        let name = char_array_to_str(&self.name);
256        write!(f, "access to map {}({})", name, self.map_id)
257    }
258}
259
260impl std::fmt::Display for task_kill_log {
261    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
262        let signal_str = if let Ok(signal) = Signal::try_from(self.signum) {
263            format!("{}({})", signal.as_str(), self.signum)
264        } else {
265            format!("unknown({})", self.signum)
266        };
267        let target_comm = char_array_to_str(&self.target_comm);
268
269        write!(
270            f,
271            "send {} to {}({})",
272            signal_str, target_comm, self.target_pid
273        )
274    }
275}
276impl std::fmt::Display for kernel_module_request_log {
277    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
278        let name = char_array_to_str(&self.kmod_name);
279        write!(f, "auto load module: '{name}'")
280    }
281}
282
283impl std::fmt::Display for kernel_read_file_log {
284    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
285        let filename = char_array_to_str(&self.filename);
286        write!(f, "load file '{}', id: {}", filename, self.id)
287    }
288}
289
290impl std::fmt::Display for kernel_load_data_log {
291    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
292        write!(f, "id: {}", self.id)
293    }
294}
295
296impl std::fmt::Display for ptrace_access_check_log {
297    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
298        let target_comm = char_array_to_str(&self.target_comm);
299        write!(
300            f,
301            "ptrace mode {} on {}({})",
302            self.mode, target_comm, self.target_pid
303        )
304    }
305}
306
307impl std::fmt::Display for inode_access_log {
308    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
309        let name = char_array_to_str(&self.name);
310        let action =
311            InodeAction::from_repr(self.action).unwrap_or(InodeAction::INODE_ACTION_UNKNOWN);
312        let action_str = enum_str_no_prefix(action, "");
313        match action {
314            InodeAction::INODE_PERMISSION => write!(f, "{action_str}"),
315            _ => write!(f, "{action_str} on {name}"),
316        }
317    }
318}