Skip to main content

memf_format/
lime.rs

1//! LiME (Linux Memory Extractor) dump format provider.
2//!
3//! Parses the binary format produced by the LiME kernel module:
4//! <https://github.com/504ensicsLabs/LiME>
5
6use std::path::Path;
7
8use crate::{Error, FormatPlugin, PhysicalMemoryProvider, PhysicalRange, Result};
9
10const LIME_MAGIC: u32 = 0x4C694D45;
11const LIME_VERSION: u32 = 1;
12const HEADER_SIZE: usize = 32;
13
14/// Bounds-checked little-endian u32 read; out-of-range yields 0 (never panics).
15fn le_u32(data: &[u8], off: usize) -> u32 {
16    data.get(off..off + 4)
17        .and_then(|s| s.try_into().ok())
18        .map_or(0, u32::from_le_bytes)
19}
20
21/// Bounds-checked little-endian u64 read; out-of-range yields 0 (never panics).
22fn le_u64(data: &[u8], off: usize) -> u64 {
23    data.get(off..off + 8)
24        .and_then(|s| s.try_into().ok())
25        .map_or(0, u64::from_le_bytes)
26}
27
28/// A parsed LiME record: physical address range + byte offset into the dump data.
29#[derive(Debug)]
30struct LimeRecord {
31    range: PhysicalRange,
32    /// Byte offset into `LimeProvider::data` where this record's payload begins.
33    data_offset: usize,
34}
35
36/// Provider that exposes physical memory from a LiME dump.
37///
38/// Stores the raw dump bytes and a pre-parsed record table so that
39/// `read_phys` is a simple linear scan with no allocation.
40#[derive(Debug)]
41pub struct LimeProvider {
42    data: Vec<u8>,
43    records: Vec<LimeRecord>,
44    /// Pre-extracted ranges for the `ranges()` slice return.
45    ranges: Vec<PhysicalRange>,
46}
47
48impl LimeProvider {
49    /// Parse a LiME dump from an in-memory byte slice (used in tests).
50    pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
51        let data = bytes.to_vec();
52        let records = parse_records(&data)?;
53        let ranges = records.iter().map(|r| r.range.clone()).collect();
54        Ok(Self {
55            data,
56            records,
57            ranges,
58        })
59    }
60
61    /// Parse a LiME dump from a file path.
62    pub fn from_path(path: &Path) -> Result<Self> {
63        let data = std::fs::read(path)?;
64        let records = parse_records(&data)?;
65        let ranges = records.iter().map(|r| r.range.clone()).collect();
66        Ok(Self {
67            data,
68            records,
69            ranges,
70        })
71    }
72}
73
74/// Parse all LiME records from `data`, returning validated `LimeRecord`s.
75fn parse_records(data: &[u8]) -> Result<Vec<LimeRecord>> {
76    let mut records = Vec::new();
77    let mut pos = 0usize;
78
79    while pos < data.len() {
80        // Need at least a full 32-byte header.
81        if data.len() - pos < HEADER_SIZE {
82            return Err(Error::Corrupt(format!(
83                "truncated header at offset {pos}: only {} bytes remain",
84                data.len() - pos
85            )));
86        }
87
88        let magic = le_u32(data, pos);
89        if magic != LIME_MAGIC {
90            return Err(Error::Corrupt(format!(
91                "bad magic at offset {pos}: expected 0x{LIME_MAGIC:08X}, got 0x{magic:08X}"
92            )));
93        }
94
95        let version = le_u32(data, pos + 4);
96        if version != LIME_VERSION {
97            return Err(Error::Corrupt(format!(
98                "unsupported LiME version {version} at offset {pos}"
99            )));
100        }
101
102        let s_addr = le_u64(data, pos + 8);
103        let e_addr = le_u64(data, pos + 16);
104        // reserved bytes at [pos+24..pos+32] — ignored
105
106        if s_addr > e_addr {
107            return Err(Error::Corrupt(format!(
108                "record at offset {pos}: s_addr 0x{s_addr:016X} > e_addr 0x{e_addr:016X}"
109            )));
110        }
111
112        // e_addr is INCLUSIVE, so payload length = e_addr - s_addr + 1.
113        let payload_len = (e_addr - s_addr + 1) as usize;
114        let data_offset = pos + HEADER_SIZE;
115
116        if data.len() - data_offset < payload_len {
117            return Err(Error::Corrupt(format!(
118                "record at offset {pos}: payload truncated (need {payload_len}, have {})",
119                data.len() - data_offset
120            )));
121        }
122
123        records.push(LimeRecord {
124            range: PhysicalRange {
125                start: s_addr,
126                end: e_addr + 1, // convert inclusive e_addr to exclusive end
127            },
128            data_offset,
129        });
130
131        pos = data_offset + payload_len;
132    }
133
134    Ok(records)
135}
136
137impl PhysicalMemoryProvider for LimeProvider {
138    fn read_phys(&self, addr: u64, buf: &mut [u8]) -> Result<usize> {
139        if buf.is_empty() {
140            return Ok(0);
141        }
142
143        for record in &self.records {
144            if record.range.contains_addr(addr) {
145                let offset_in_range = (addr - record.range.start) as usize;
146                let available = (record.range.end - addr) as usize;
147                let to_read = buf.len().min(available);
148                let src_start = record.data_offset + offset_in_range;
149                buf[..to_read].copy_from_slice(&self.data[src_start..src_start + to_read]);
150                return Ok(to_read);
151            }
152        }
153
154        // Address not in any mapped range — gap.
155        Ok(0)
156    }
157
158    fn ranges(&self) -> &[PhysicalRange] {
159        &self.ranges
160    }
161
162    fn format_name(&self) -> &str {
163        "LiME"
164    }
165}
166
167/// FormatPlugin implementation for LiME dumps.
168pub struct LimePlugin;
169
170impl FormatPlugin for LimePlugin {
171    fn name(&self) -> &str {
172        "LiME"
173    }
174
175    fn probe(&self, header: &[u8]) -> u8 {
176        if header.len() < 8 {
177            return 0;
178        }
179        let magic = le_u32(header, 0);
180        let version = le_u32(header, 4);
181        if magic == LIME_MAGIC && version == LIME_VERSION {
182            90
183        } else {
184            0
185        }
186    }
187
188    fn open(&self, path: &Path) -> Result<Box<dyn PhysicalMemoryProvider>> {
189        Ok(Box::new(LimeProvider::from_path(path)?))
190    }
191}
192
193inventory::submit!(&LimePlugin as &dyn FormatPlugin);
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198    use crate::test_builders::LimeBuilder;
199
200    fn parse(bytes: &[u8]) -> Result<LimeProvider> {
201        LimeProvider::from_bytes(bytes)
202    }
203
204    #[test]
205    fn probe_lime_magic() {
206        let dump = LimeBuilder::new().add_range(0x1000, &[0u8; 0x1000]).build();
207        let plugin = LimePlugin;
208        assert_eq!(plugin.probe(&dump), 90);
209    }
210
211    #[test]
212    fn probe_non_lime() {
213        let zeros = vec![0u8; 64];
214        let plugin = LimePlugin;
215        assert_eq!(plugin.probe(&zeros), 0);
216    }
217
218    #[test]
219    fn single_range() {
220        let data: Vec<u8> = (0u8..=255).collect();
221        let dump = LimeBuilder::new().add_range(0x1000, &data).build();
222        let provider = parse(&dump).unwrap();
223
224        assert_eq!(provider.ranges().len(), 1);
225        assert_eq!(provider.ranges()[0].start, 0x1000);
226        assert_eq!(provider.ranges()[0].end, 0x1100); // exclusive: 0x1000 + 256
227
228        assert_eq!(provider.total_size(), 256);
229        assert_eq!(provider.format_name(), "LiME");
230
231        let mut buf = [0u8; 4];
232        let n = provider.read_phys(0x1000, &mut buf).unwrap();
233        assert_eq!(n, 4);
234        assert_eq!(&buf, &[0, 1, 2, 3]);
235    }
236
237    #[test]
238    fn two_ranges() {
239        let data_a = vec![0xAAu8; 0x2000];
240        let data_b = vec![0xBBu8; 0x1000];
241        let dump = LimeBuilder::new()
242            .add_range(0x0000, &data_a)
243            .add_range(0x4000, &data_b)
244            .build();
245        let provider = parse(&dump).unwrap();
246
247        assert_eq!(provider.ranges().len(), 2);
248        assert_eq!(provider.total_size(), 0x3000);
249
250        let mut buf = [0u8; 2];
251
252        let n = provider.read_phys(0x0000, &mut buf).unwrap();
253        assert_eq!(n, 2);
254        assert_eq!(buf, [0xAA, 0xAA]);
255
256        let n = provider.read_phys(0x4000, &mut buf).unwrap();
257        assert_eq!(n, 2);
258        assert_eq!(buf, [0xBB, 0xBB]);
259    }
260
261    #[test]
262    fn read_gap_returns_zero() {
263        let data = vec![0xCCu8; 0x1000];
264        let dump = LimeBuilder::new().add_range(0x1000, &data).build();
265        let provider = parse(&dump).unwrap();
266
267        // Address 0x0000 is not mapped.
268        let mut buf = [0xFFu8; 4];
269        let n = provider.read_phys(0x0000, &mut buf).unwrap();
270        assert_eq!(n, 0);
271    }
272
273    #[test]
274    fn corrupt_magic_errors() {
275        let mut dump = LimeBuilder::new().add_range(0x1000, &[0u8; 64]).build();
276        // Corrupt first byte of the magic.
277        dump[0] = 0xFF;
278        let err = parse(&dump).unwrap_err();
279        assert!(
280            matches!(err, Error::Corrupt(_)),
281            "expected Corrupt, got {err:?}"
282        );
283    }
284
285    #[test]
286    fn truncated_header_errors() {
287        // Only 4 bytes — not enough for a full 32-byte header.
288        let truncated = vec![0x45u8, 0x4D, 0x69, 0x4C]; // just the magic bytes
289        let err = parse(&truncated).unwrap_err();
290        assert!(
291            matches!(err, Error::Corrupt(_)),
292            "expected Corrupt, got {err:?}"
293        );
294    }
295
296    #[test]
297    fn from_path_roundtrip() {
298        let data: Vec<u8> = (0u8..=127).collect();
299        let dump = LimeBuilder::new().add_range(0x2000, &data).build();
300        let path = std::env::temp_dir().join("memf_test_lime_from_path.lime");
301        std::fs::write(&path, &dump).unwrap();
302        let provider = LimeProvider::from_path(&path).unwrap();
303        assert_eq!(provider.ranges().len(), 1);
304        assert_eq!(provider.total_size(), 128);
305        assert_eq!(provider.format_name(), "LiME");
306        let mut buf = [0u8; 4];
307        let n = provider.read_phys(0x2000, &mut buf).unwrap();
308        assert_eq!(n, 4);
309        assert_eq!(&buf, &[0, 1, 2, 3]);
310        std::fs::remove_file(&path).ok();
311    }
312
313    #[test]
314    fn plugin_name() {
315        let plugin = LimePlugin;
316        assert_eq!(plugin.name(), "LiME");
317    }
318
319    #[test]
320    fn probe_short_header_returns_zero() {
321        let plugin = LimePlugin;
322        assert_eq!(plugin.probe(&[0x45, 0x4D, 0x69]), 0); // only 3 bytes
323        assert_eq!(plugin.probe(&[]), 0);
324    }
325
326    #[test]
327    fn read_phys_empty_buffer() {
328        let dump = LimeBuilder::new().add_range(0x1000, &[0xAA; 64]).build();
329        let provider = parse(&dump).unwrap();
330        let mut buf = [];
331        let n = provider.read_phys(0x1000, &mut buf).unwrap();
332        assert_eq!(n, 0);
333    }
334}