1use memf_core::object_reader::ObjectReader;
8use memf_format::PhysicalMemoryProvider;
9
10use crate::Result;
11
12const MAX_ENTRIES: usize = 8192;
14
15#[derive(Debug, Clone, serde::Serialize)]
17pub struct KmsgEntry {
18 pub sequence: u64,
20 pub timestamp_ns: u64,
22 pub level: u8,
24 pub text: String,
26 pub is_suspicious: bool,
28}
29
30pub use crate::heuristics::classify_kmsg;
32
33pub 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 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
82pub 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 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, 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 #[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; 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()); record[8..10].copy_from_slice(&total_len.to_le_bytes()); record[10..12].copy_from_slice(&text_len.to_le_bytes()); record[12..14].copy_from_slice(&0u16.to_le_bytes()); record[14] = 0; record[15] = 6; 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 #[test]
208 fn walk_kmsg_with_symbol_returns_entries() {
209 use memf_core::test_builders::flags;
210
211 let msg = b"rootkit module loaded";
213 let text_len = msg.len() as u16;
214 let total_len: u16 = 16 + text_len; 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()); ring_buf[8..10].copy_from_slice(&total_len.to_le_bytes()); ring_buf[10..12].copy_from_slice(&text_len.to_le_bytes()); ring_buf[12..14].copy_from_slice(&0u16.to_le_bytes()); ring_buf[14] = 0; ring_buf[15] = 4; 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}