Skip to main content

memf_linux/
dmesg.rs

1//! Linux kernel dmesg ring buffer extraction.
2//!
3//! Extracts the kernel log ring buffer (`log_buf`) from memory. The kernel
4//! stores dmesg messages in a circular buffer pointed to by the `log_buf`
5//! symbol with length `log_buf_len`.
6//!
7//! Each log record (`struct printk_log` / `log`) has:
8//! - `len` (u16) — total record length including text+dict
9//! - `text_len` (u16) — length of the text message
10//! - `dict_len` (u16) — length of the dict (key=value facility info)
11//! - `facility` (u8) — syslog facility
12//! - `level` (u8) — log level (0=EMERG..7=DEBUG)
13//! - `ts_nsec` (u64) — timestamp in nanoseconds since boot
14//! - Text immediately follows the 16-byte header
15
16use memf_core::object_reader::ObjectReader;
17use memf_format::PhysicalMemoryProvider;
18use serde::Serialize;
19
20/// A single parsed dmesg log entry from the kernel ring buffer.
21#[derive(Debug, Clone, Serialize)]
22pub struct DmesgEntry {
23    /// Timestamp in nanoseconds since boot.
24    pub timestamp_ns: u64,
25    /// Log level (0=EMERG, 1=ALERT, 2=CRIT, 3=ERR, 4=WARNING, 5=NOTICE, 6=INFO, 7=DEBUG).
26    pub level: u8,
27    /// Syslog facility code.
28    pub facility: u8,
29    /// The log message text.
30    pub message: String,
31}
32
33/// Size of the `printk_log` header in bytes.
34const PRINTK_HEADER_SIZE: usize = 16;
35
36/// Maximum number of entries to extract (safety limit against corrupt data).
37const MAX_ENTRIES: usize = 65_536;
38
39/// Extract dmesg entries from the kernel ring buffer.
40///
41/// Looks up the `log_buf` symbol, dereferences the pointer to obtain the
42/// buffer address, reads `log_buf_len` to determine buffer size, then
43/// iterates `printk_log` records until `len == 0` or the buffer is exhausted.
44///
45/// Returns an empty `Vec` if the `log_buf` symbol is not found (e.g., wrong
46/// profile or non-Linux image).
47///
48/// # Safety limit
49/// Caps extraction at 65,536 entries to prevent runaway iteration on corrupt data.
50pub fn extract_dmesg<P: PhysicalMemoryProvider>(
51    reader: &ObjectReader<P>,
52) -> crate::Result<Vec<DmesgEntry>> {
53    // Look up log_buf symbol; if absent, this isn't a compatible image.
54    let log_buf_sym = match reader.symbols().symbol_address("log_buf") {
55        Some(addr) => addr,
56        None => return Ok(Vec::new()),
57    };
58
59    // log_buf is a pointer — dereference it to get the actual buffer address.
60    let buf_vaddr: u64 = {
61        let mut buf = [0u8; 8];
62        reader.vas().read_virt(log_buf_sym, &mut buf)?;
63        u64::from_le_bytes(buf)
64    };
65
66    // Look up log_buf_len symbol and read the buffer length (u32).
67    let log_buf_len_sym = match reader.symbols().symbol_address("log_buf_len") {
68        Some(addr) => addr,
69        None => return Ok(Vec::new()),
70    };
71    let buf_len: u32 = {
72        let mut buf = [0u8; 4];
73        reader.vas().read_virt(log_buf_len_sym, &mut buf)?;
74        u32::from_le_bytes(buf)
75    };
76    let buf_len = buf_len as usize;
77
78    if buf_len == 0 {
79        return Ok(Vec::new());
80    }
81
82    // Read the entire ring buffer into local memory.
83    let ring = reader.read_bytes(buf_vaddr, buf_len)?;
84
85    // Iterate printk_log records.
86    let mut entries = Vec::new();
87    let mut offset: usize = 0;
88
89    while offset + PRINTK_HEADER_SIZE <= buf_len && entries.len() < MAX_ENTRIES {
90        // Parse header fields (all little-endian).
91        let ts_nsec = ring[offset..offset + 8]
92            .try_into()
93            .map_or(0, u64::from_le_bytes);
94        let len = ring[offset + 8..offset + 10]
95            .try_into()
96            .map_or(0, u16::from_le_bytes) as usize;
97        let text_len = ring[offset + 10..offset + 12]
98            .try_into()
99            .map_or(0, u16::from_le_bytes) as usize;
100        let _dict_len = ring[offset + 12..offset + 14]
101            .try_into()
102            .map_or(0, u16::from_le_bytes);
103        let facility = ring[offset + 14];
104        let level = ring[offset + 15];
105
106        // len == 0 signals end of valid records.
107        if len == 0 {
108            break;
109        }
110
111        // Sanity: record must not exceed remaining buffer.
112        if offset + len > buf_len {
113            break;
114        }
115
116        // Extract text immediately following the header.
117        let text_start = offset + PRINTK_HEADER_SIZE;
118        let text_end = text_start + text_len.min(buf_len - text_start);
119        let message = String::from_utf8_lossy(&ring[text_start..text_end]).into_owned();
120
121        entries.push(DmesgEntry {
122            timestamp_ns: ts_nsec,
123            level,
124            facility,
125            message,
126        });
127
128        offset += len;
129    }
130
131    Ok(entries)
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137    use memf_core::object_reader::ObjectReader;
138    use memf_core::test_builders::{flags, PageTableBuilder, SyntheticPhysMem};
139    use memf_core::vas::{TranslationMode, VirtualAddressSpace};
140    use memf_symbols::isf::IsfResolver;
141    use memf_symbols::test_builders::IsfBuilder;
142
143    /// Helper: build an ObjectReader from ISF and page table builders.
144    fn make_reader(isf: &IsfBuilder, ptb: PageTableBuilder) -> ObjectReader<SyntheticPhysMem> {
145        let json = isf.build_json();
146        let resolver = IsfResolver::from_value(&json).unwrap();
147        let (cr3, mem) = ptb.build();
148        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
149        ObjectReader::new(vas, Box::new(resolver))
150    }
151
152    /// No `log_buf` symbol present -> returns empty Vec (not an error).
153    #[test]
154    fn extract_dmesg_no_symbol() {
155        let isf = IsfBuilder::new().add_struct("printk_log", 16);
156        let ptb = PageTableBuilder::new();
157        let reader = make_reader(&isf, ptb);
158
159        let entries = extract_dmesg(&reader).unwrap();
160        assert!(
161            entries.is_empty(),
162            "expected empty Vec when log_buf symbol is missing"
163        );
164    }
165
166    /// `log_buf` symbol exists and points to a zero-filled buffer -> empty Vec.
167    #[test]
168    fn extract_dmesg_empty_buffer() {
169        // Layout:
170        //   log_buf symbol (vaddr) -> pointer to buffer vaddr
171        //   log_buf_len symbol (vaddr) -> u32 buffer length
172        //   buffer: all zeros (no records)
173        let log_buf_sym_vaddr: u64 = 0xFFFF_8000_0010_0000;
174        let log_buf_len_sym_vaddr: u64 = 0xFFFF_8000_0010_1000;
175        let buf_vaddr: u64 = 0xFFFF_8000_0020_0000;
176
177        let log_buf_sym_paddr: u64 = 0x0010_0000; // 1 MB
178        let log_buf_len_sym_paddr: u64 = 0x0010_1000;
179        let buf_paddr: u64 = 0x0020_0000; // 2 MB
180
181        let buf_len: u32 = 4096;
182
183        let isf = IsfBuilder::new()
184            .add_symbol("log_buf", log_buf_sym_vaddr)
185            .add_symbol("log_buf_len", log_buf_len_sym_vaddr);
186
187        let ptb = PageTableBuilder::new()
188            // Map the symbol locations
189            .map_4k(log_buf_sym_vaddr, log_buf_sym_paddr, flags::WRITABLE)
190            .map_4k(
191                log_buf_len_sym_vaddr,
192                log_buf_len_sym_paddr,
193                flags::WRITABLE,
194            )
195            // Map the buffer itself (one 4k page, zero-filled by default)
196            .map_4k(buf_vaddr, buf_paddr, flags::WRITABLE)
197            // Write the pointer value at log_buf symbol location
198            .write_phys_u64(log_buf_sym_paddr, buf_vaddr)
199            // Write the buffer length at log_buf_len symbol location
200            .write_phys(log_buf_len_sym_paddr, &buf_len.to_le_bytes());
201
202        let reader = make_reader(&isf, ptb);
203        let entries = extract_dmesg(&reader).unwrap();
204        assert!(
205            entries.is_empty(),
206            "expected empty Vec for zero-filled buffer"
207        );
208    }
209
210    /// Single valid printk_log record in the buffer -> one DmesgEntry.
211    #[test]
212    fn extract_dmesg_single_entry() {
213        // printk_log header layout (16 bytes):
214        //   offset 0: ts_nsec  (u64) — timestamp nanoseconds
215        //   offset 8: len      (u16) — total record length
216        //   offset 10: text_len (u16)
217        //   offset 12: dict_len (u16)
218        //   offset 14: facility (u8)
219        //   offset 15: level    (u8)
220        //   offset 16: text data (text_len bytes)
221        //   (padding to align to len)
222        let log_buf_sym_vaddr: u64 = 0xFFFF_8000_0010_0000;
223        let log_buf_len_sym_vaddr: u64 = 0xFFFF_8000_0010_1000;
224        let buf_vaddr: u64 = 0xFFFF_8000_0020_0000;
225
226        let log_buf_sym_paddr: u64 = 0x0010_0000;
227        let log_buf_len_sym_paddr: u64 = 0x0010_1000;
228        let buf_paddr: u64 = 0x0020_0000;
229
230        let message = b"Hello from kernel";
231        let text_len = message.len() as u16; // 17
232        let dict_len: u16 = 0;
233        // Total record length: header(16) + text(17) + dict(0) = 33, aligned to 4 -> 36
234        let record_len: u16 = (16 + text_len + dict_len).div_ceil(4) * 4;
235        let ts_nsec: u64 = 1_000_000_000; // 1 second
236        let facility: u8 = 0; // kern
237        let level: u8 = 6; // info
238
239        let buf_len: u32 = 4096;
240
241        let isf = IsfBuilder::new()
242            .add_symbol("log_buf", log_buf_sym_vaddr)
243            .add_symbol("log_buf_len", log_buf_len_sym_vaddr);
244
245        // Build the printk_log record in a local buffer
246        let mut record = vec![0u8; record_len as usize];
247        record[0..8].copy_from_slice(&ts_nsec.to_le_bytes());
248        record[8..10].copy_from_slice(&record_len.to_le_bytes());
249        record[10..12].copy_from_slice(&text_len.to_le_bytes());
250        record[12..14].copy_from_slice(&dict_len.to_le_bytes());
251        record[14] = facility;
252        record[15] = level;
253        record[16..16 + message.len()].copy_from_slice(message);
254
255        let ptb = PageTableBuilder::new()
256            .map_4k(log_buf_sym_vaddr, log_buf_sym_paddr, flags::WRITABLE)
257            .map_4k(
258                log_buf_len_sym_vaddr,
259                log_buf_len_sym_paddr,
260                flags::WRITABLE,
261            )
262            .map_4k(buf_vaddr, buf_paddr, flags::WRITABLE)
263            // log_buf pointer
264            .write_phys_u64(log_buf_sym_paddr, buf_vaddr)
265            // log_buf_len
266            .write_phys(log_buf_len_sym_paddr, &buf_len.to_le_bytes())
267            // The actual record data
268            .write_phys(buf_paddr, &record);
269
270        let reader = make_reader(&isf, ptb);
271        let entries = extract_dmesg(&reader).unwrap();
272
273        assert_eq!(entries.len(), 1);
274        assert_eq!(entries[0].timestamp_ns, 1_000_000_000);
275        assert_eq!(entries[0].level, 6);
276        assert_eq!(entries[0].facility, 0);
277        assert_eq!(entries[0].message, "Hello from kernel");
278    }
279}