Skip to main content

memf_linux/
kmsg.rs

1//! Kernel message ring buffer extraction.
2//!
3//! Reads the kernel log (printk) ring buffer from `__log_buf` and
4//! `log_buf_len`.  Each record uses the kernel 3.x+ `printk_log` format.
5//! Suspicious messages (rootkit indicators, kernel oops) are flagged.
6
7use memf_core::object_reader::ObjectReader;
8use memf_format::PhysicalMemoryProvider;
9
10use crate::Result;
11
12/// Maximum number of kmsg records to extract (runaway protection).
13const MAX_ENTRIES: usize = 8192;
14
15/// A single kernel log record.
16#[derive(Debug, Clone, serde::Serialize)]
17pub struct KmsgEntry {
18    /// Sequence number.
19    pub sequence: u64,
20    /// Timestamp in nanoseconds.
21    pub timestamp_ns: u64,
22    /// Log level (low 3 bits of `flags_level`).
23    pub level: u8,
24    /// Text content of the log record.
25    pub text: String,
26    /// True when the text contains known suspicious patterns.
27    pub is_suspicious: bool,
28}
29
30/// Classify whether a kernel log message is suspicious.
31pub use crate::heuristics::classify_kmsg;
32
33/// Walk the kernel log ring buffer and return parsed entries.
34///
35/// Returns `Ok(Vec::new())` when `__log_buf` symbol is absent.
36pub fn walk_kmsg<P: PhysicalMemoryProvider>(reader: &ObjectReader<P>) -> Result<Vec<KmsgEntry>> {
37    let Some(buf_addr) = reader.symbols().symbol_address("__log_buf") else {
38        return Ok(Vec::new());
39    };
40
41    // Read log_buf_len (u32) from its symbol if present; default to 4096.
42    let buf_len: usize = if let Some(len_addr) = reader.symbols().symbol_address("log_buf_len") {
43        match reader.read_bytes(len_addr, 4) {
44            Ok(b) if b.len() == 4 => {
45                let v = b.try_into().map_or(0, u32::from_le_bytes) as usize;
46                if v == 0 {
47                    4096
48                } else {
49                    v.min(1024 * 1024)
50                }
51            }
52            _ => 4096,
53        }
54    } else {
55        4096
56    };
57
58    let data = match reader.read_bytes(buf_addr, buf_len) {
59        Ok(d) => d,
60        Err(_) => return Ok(Vec::new()),
61    };
62
63    let mut entries = Vec::new();
64    let mut offset = 0usize;
65
66    for _ in 0..MAX_ENTRIES {
67        match parse_printk_record(&data, offset) {
68            Some((entry, consumed)) => {
69                entries.push(entry);
70                offset += consumed;
71            }
72            None => break,
73        }
74        if offset >= data.len() {
75            break;
76        }
77    }
78
79    Ok(entries)
80}
81
82/// Parse raw `printk_log` record bytes into a `KmsgEntry`.
83///
84/// `printk_log` header layout (kernel 3.x+, matches `struct printk_log`):
85///   +0:  ts_nsec   (u64) — timestamp in nanoseconds since boot
86///   +8:  len       (u16) — total record length including header
87///   +10: text_len  (u16) — byte length of the text
88///   +12: dict_len  (u16) — byte length of the dict
89///   +14: facility  (u8)
90///   +15: level     (u8)  — log level (0=EMERG..7=DEBUG)
91///
92/// Text immediately follows the 16-byte header.
93pub fn parse_printk_record(data: &[u8], offset: usize) -> Option<(KmsgEntry, usize)> {
94    const HDR_LEN: usize = 16;
95    if offset + HDR_LEN > data.len() {
96        return None;
97    }
98    let hdr = &data[offset..];
99    let ts_nsec = u64::from_le_bytes(hdr[0..8].try_into().ok()?);
100    let len = u16::from_le_bytes([hdr[8], hdr[9]]) as usize;
101    if len == 0 || offset + len > data.len() {
102        return None;
103    }
104    let text_len = u16::from_le_bytes([hdr[10], hdr[11]]) as usize;
105    let level = hdr[15] & 0x07;
106    // seq is not present in the 16-byte printk_log header; use 0 as placeholder.
107    let seq: u64 = 0;
108
109    let text_start = offset + HDR_LEN;
110    let text_end = (text_start + text_len).min(offset + len);
111    let text = String::from_utf8_lossy(&data[text_start..text_end])
112        .trim_end_matches('\0')
113        .to_string();
114
115    let is_suspicious = classify_kmsg(&text);
116
117    Some((
118        KmsgEntry {
119            sequence: seq, // always 0; printk_log 16-byte header has no seq field
120            timestamp_ns: ts_nsec,
121            level,
122            text,
123            is_suspicious,
124        },
125        len,
126    ))
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132    use memf_core::test_builders::{PageTableBuilder, SyntheticPhysMem};
133    use memf_core::vas::{TranslationMode, VirtualAddressSpace};
134    use memf_symbols::isf::IsfResolver;
135    use memf_symbols::test_builders::IsfBuilder;
136
137    fn make_no_symbol_reader() -> ObjectReader<SyntheticPhysMem> {
138        let isf = IsfBuilder::new().build_json();
139        let resolver = IsfResolver::from_value(&isf).unwrap();
140        let (cr3, mem) = PageTableBuilder::new().build();
141        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
142        ObjectReader::new(vas, Box::new(resolver))
143    }
144
145    #[test]
146    fn no_symbol_returns_empty() {
147        let reader = make_no_symbol_reader();
148        let result = walk_kmsg(&reader).unwrap();
149        assert!(result.is_empty(), "no __log_buf symbol → empty vec");
150    }
151
152    #[test]
153    fn classify_suspicious_rootkit_message() {
154        assert!(
155            classify_kmsg("rootkit detected in module list"),
156            "message containing 'rootkit' should be suspicious"
157        );
158        assert!(
159            classify_kmsg("Call Trace:"),
160            "kernel oops call trace should be suspicious"
161        );
162    }
163
164    #[test]
165    fn classify_benign_message_not_flagged() {
166        assert!(
167            !classify_kmsg("usb 1-1: new full-speed USB device number 2"),
168            "normal USB message should not be suspicious"
169        );
170        assert!(
171            !classify_kmsg("EXT4-fs (sda1): mounted filesystem"),
172            "normal mount message should not be suspicious"
173        );
174    }
175
176    // RED test: parse_printk_record parses a synthetic record correctly.
177    // Header layout (16 bytes): ts_nsec@0, len@8, text_len@10, dict_len@12,
178    //                           facility@14, level@15
179    #[test]
180    fn parse_printk_record_extracts_text() {
181        let text = b"Linux version 5.15.0";
182        let text_len = text.len() as u16;
183        let total_len: u16 = 16 + text_len; // 16-byte header + text
184        let ts_nsec: u64 = 123_456_789;
185
186        let mut record = vec![0u8; total_len as usize];
187        record[0..8].copy_from_slice(&ts_nsec.to_le_bytes()); // ts_nsec
188        record[8..10].copy_from_slice(&total_len.to_le_bytes()); // len
189        record[10..12].copy_from_slice(&text_len.to_le_bytes()); // text_len
190        record[12..14].copy_from_slice(&0u16.to_le_bytes()); // dict_len = 0
191        record[14] = 0; // facility
192        record[15] = 6; // level 6 (INFO)
193        record[16..16 + text.len()].copy_from_slice(text);
194
195        let (entry, consumed) = parse_printk_record(&record, 0).unwrap();
196        assert_eq!(entry.sequence, 0, "seq is always 0 in 16-byte layout");
197        assert_eq!(entry.timestamp_ns, ts_nsec);
198        assert_eq!(entry.level, 6);
199        assert_eq!(entry.text, "Linux version 5.15.0");
200        assert!(!entry.is_suspicious);
201        assert_eq!(consumed, total_len as usize);
202    }
203
204    // RED test: walk_kmsg with symbol and mapped buffer returns entries.
205    // Header layout (16 bytes): ts_nsec@0, len@8, text_len@10, dict_len@12,
206    //                           facility@14, level@15
207    #[test]
208    fn walk_kmsg_with_symbol_returns_entries() {
209        use memf_core::test_builders::flags;
210
211        // Build a minimal ring buffer with one record.
212        let msg = b"rootkit module loaded";
213        let text_len = msg.len() as u16;
214        let total_len: u16 = 16 + text_len; // 16-byte header
215        let buf_len: u32 = 4096;
216
217        let mut ring_buf = vec![0u8; buf_len as usize];
218        ring_buf[0..8].copy_from_slice(&1_000_000u64.to_le_bytes()); // ts_nsec
219        ring_buf[8..10].copy_from_slice(&total_len.to_le_bytes()); // len
220        ring_buf[10..12].copy_from_slice(&text_len.to_le_bytes()); // text_len
221        ring_buf[12..14].copy_from_slice(&0u16.to_le_bytes()); // dict_len
222        ring_buf[14] = 0; // facility
223        ring_buf[15] = 4; // KERN_WARNING
224        ring_buf[16..16 + msg.len()].copy_from_slice(msg);
225
226        let buf_vaddr: u64 = 0xFFFF_8000_0020_0000;
227        let buf_paddr: u64 = 0x0082_0000;
228        let len_vaddr: u64 = 0xFFFF_8000_0020_1000;
229        let len_paddr: u64 = 0x0083_0000;
230
231        let isf = IsfBuilder::new()
232            .add_symbol("__log_buf", buf_vaddr)
233            .add_symbol("log_buf_len", len_vaddr)
234            .build_json();
235
236        let resolver = IsfResolver::from_value(&isf).unwrap();
237        let (cr3, mut mem) = PageTableBuilder::new()
238            .map_4k(buf_vaddr, buf_paddr, flags::PRESENT | flags::WRITABLE)
239            .map_4k(len_vaddr, len_paddr, flags::PRESENT | flags::WRITABLE)
240            .build();
241        mem.write_bytes(buf_paddr, &ring_buf);
242        mem.write_bytes(len_paddr, &buf_len.to_le_bytes());
243
244        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
245        let reader = ObjectReader::new(vas, Box::new(resolver));
246
247        let entries = walk_kmsg(&reader).unwrap();
248        assert!(!entries.is_empty(), "should return at least one kmsg entry");
249        assert!(
250            entries[0].is_suspicious,
251            "rootkit message should be flagged"
252        );
253    }
254}