Skip to main content

memf_format/
elf_core.rs

1//! ELF core dump format provider.
2//!
3//! Parses ELF core dumps (ET_CORE) and exposes PT_LOAD segments as physical
4//! memory ranges. This covers Linux kernel crash dumps (makedumpfile, QEMU).
5
6use std::path::Path;
7
8use crate::{Error, FormatPlugin, PhysicalMemoryProvider, PhysicalRange, Result};
9
10/// A segment from an ELF core's PT_LOAD program header.
11#[derive(Debug, Clone)]
12struct LoadSegment {
13    /// Physical address (p_paddr).
14    paddr: u64,
15    /// File offset where data begins (p_offset).
16    file_offset: u64,
17    /// Size in the file (p_filesz).
18    file_size: u64,
19}
20
21/// Physical memory provider backed by an ELF core dump.
22pub struct ElfCoreProvider {
23    data: Vec<u8>,
24    segments: Vec<LoadSegment>,
25    ranges: Vec<PhysicalRange>,
26}
27
28impl ElfCoreProvider {
29    /// Parse an ELF core dump from a byte slice.
30    pub fn from_bytes(data: Vec<u8>) -> Result<Self> {
31        let elf = goblin::elf::Elf::parse(&data)
32            .map_err(|e| Error::Corrupt(format!("ELF parse error: {e}")))?;
33
34        if elf.header.e_type != goblin::elf::header::ET_CORE {
35            return Err(Error::Corrupt("not an ELF core dump".into()));
36        }
37
38        let mut segments = Vec::new();
39        for phdr in &elf.program_headers {
40            if phdr.p_type == goblin::elf::program_header::PT_LOAD && phdr.p_filesz > 0 {
41                segments.push(LoadSegment {
42                    paddr: phdr.p_paddr,
43                    file_offset: phdr.p_offset,
44                    file_size: phdr.p_filesz,
45                });
46            }
47        }
48
49        segments.sort_by_key(|s| s.paddr);
50
51        let ranges: Vec<PhysicalRange> = segments
52            .iter()
53            .map(|s| PhysicalRange {
54                start: s.paddr,
55                end: s.paddr + s.file_size,
56            })
57            .collect();
58
59        Ok(Self {
60            data,
61            segments,
62            ranges,
63        })
64    }
65}
66
67impl PhysicalMemoryProvider for ElfCoreProvider {
68    fn read_phys(&self, addr: u64, buf: &mut [u8]) -> Result<usize> {
69        if buf.is_empty() {
70            return Ok(0);
71        }
72
73        for seg in &self.segments {
74            let seg_end = seg.paddr + seg.file_size;
75            if addr >= seg.paddr && addr < seg_end {
76                let offset_in_seg = addr - seg.paddr;
77                let available = (seg.file_size - offset_in_seg) as usize;
78                let to_read = buf.len().min(available);
79                let file_pos = seg.file_offset + offset_in_seg;
80                let file_pos_usize = file_pos as usize;
81                buf[..to_read]
82                    .copy_from_slice(&self.data[file_pos_usize..file_pos_usize + to_read]);
83                return Ok(to_read);
84            }
85        }
86
87        Ok(0)
88    }
89
90    fn ranges(&self) -> &[PhysicalRange] {
91        &self.ranges
92    }
93
94    fn format_name(&self) -> &str {
95        "ELF Core"
96    }
97}
98
99/// Format plugin for ELF core dumps.
100struct ElfCorePlugin;
101
102impl FormatPlugin for ElfCorePlugin {
103    fn name(&self) -> &str {
104        "ELF Core"
105    }
106
107    fn probe(&self, header: &[u8]) -> u8 {
108        if header.len() < 18 {
109            return 0;
110        }
111        // Check ELF magic
112        if header[0..4] != [0x7F, b'E', b'L', b'F'] {
113            return 0;
114        }
115        // Check ET_CORE (e_type at offset 16, little-endian u16)
116        let e_type = u16::from_le_bytes([header[16], header[17]]);
117        if e_type == 4 {
118            90
119        } else {
120            0
121        }
122    }
123
124    fn open(&self, path: &Path) -> Result<Box<dyn PhysicalMemoryProvider>> {
125        let data = std::fs::read(path)?;
126        let provider = ElfCoreProvider::from_bytes(data)?;
127        Ok(Box::new(provider))
128    }
129}
130
131inventory::submit!(&ElfCorePlugin as &dyn FormatPlugin);
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136    use crate::test_builders::ElfCoreBuilder;
137
138    #[test]
139    fn probe_elf_core() {
140        let dump = ElfCoreBuilder::new()
141            .add_segment(0x1000, &[0xAA; 128])
142            .build();
143        let plugin = ElfCorePlugin;
144        assert_eq!(plugin.probe(&dump[..64.min(dump.len())]), 90);
145    }
146
147    #[test]
148    fn probe_non_core_elf() {
149        // Build an ELF header with ET_EXEC (2) instead of ET_CORE (4)
150        let mut header = vec![0u8; 64];
151        header[0..4].copy_from_slice(&[0x7F, b'E', b'L', b'F']);
152        header[4] = 2; // ELFCLASS64
153        header[5] = 1; // ELFDATA2LSB
154        header[16..18].copy_from_slice(&2u16.to_le_bytes()); // ET_EXEC
155        let plugin = ElfCorePlugin;
156        assert_eq!(plugin.probe(&header), 0);
157    }
158
159    #[test]
160    fn probe_non_elf() {
161        let data = vec![0u8; 128];
162        let plugin = ElfCorePlugin;
163        assert_eq!(plugin.probe(&data), 0);
164    }
165
166    #[test]
167    fn single_segment() {
168        let payload = vec![0xBB; 256];
169        let dump = ElfCoreBuilder::new().add_segment(0x1000, &payload).build();
170        let provider = ElfCoreProvider::from_bytes(dump).unwrap();
171
172        assert_eq!(provider.format_name(), "ELF Core");
173        assert_eq!(provider.ranges().len(), 1);
174        assert_eq!(provider.ranges()[0].start, 0x1000);
175        assert_eq!(provider.ranges()[0].end, 0x1000 + 256);
176
177        let mut buf = [0u8; 8];
178        let n = provider.read_phys(0x1000, &mut buf).unwrap();
179        assert_eq!(n, 8);
180        assert_eq!(buf, [0xBB; 8]);
181    }
182
183    #[test]
184    fn two_segments() {
185        let dump = ElfCoreBuilder::new()
186            .add_segment(0x1000, &[0xAA; 128])
187            .add_segment(0x5000, &[0xCC; 256])
188            .build();
189        let provider = ElfCoreProvider::from_bytes(dump).unwrap();
190
191        assert_eq!(provider.ranges().len(), 2);
192        assert_eq!(provider.total_size(), 128 + 256);
193
194        let mut buf = [0u8; 4];
195        let n = provider.read_phys(0x5000, &mut buf).unwrap();
196        assert_eq!(n, 4);
197        assert_eq!(buf, [0xCC; 4]);
198    }
199
200    #[test]
201    fn read_gap_returns_zero() {
202        let dump = ElfCoreBuilder::new()
203            .add_segment(0x1000, &[0xAA; 128])
204            .build();
205        let provider = ElfCoreProvider::from_bytes(dump).unwrap();
206
207        let mut buf = [0xFF; 8];
208        let n = provider.read_phys(0x9000, &mut buf).unwrap();
209        assert_eq!(n, 0);
210        // buf should be unchanged since nothing was read
211        assert_eq!(buf, [0xFF; 8]);
212    }
213
214    #[test]
215    fn from_path_via_plugin_open() {
216        let payload = vec![0xDD; 256];
217        let dump = ElfCoreBuilder::new().add_segment(0x3000, &payload).build();
218        let path = std::env::temp_dir().join("memf_test_elf_core_from_path.core");
219        std::fs::write(&path, &dump).unwrap();
220        let plugin = ElfCorePlugin;
221        let provider = plugin.open(&path).unwrap();
222        assert_eq!(provider.format_name(), "ELF Core");
223        assert_eq!(provider.ranges().len(), 1);
224        assert_eq!(provider.total_size(), 256);
225        std::fs::remove_file(&path).ok();
226    }
227
228    #[test]
229    fn plugin_name() {
230        let plugin = ElfCorePlugin;
231        assert_eq!(plugin.name(), "ELF Core");
232    }
233
234    #[test]
235    fn probe_too_short_returns_zero() {
236        let plugin = ElfCorePlugin;
237        assert_eq!(plugin.probe(&[0x7F, b'E', b'L', b'F']), 0); // only 4 bytes, need 18
238        assert_eq!(plugin.probe(&[]), 0);
239    }
240
241    #[test]
242    fn read_phys_empty_buffer() {
243        let dump = ElfCoreBuilder::new()
244            .add_segment(0x1000, &[0xAA; 128])
245            .build();
246        let provider = ElfCoreProvider::from_bytes(dump).unwrap();
247        let mut buf = [];
248        let n = provider.read_phys(0x1000, &mut buf).unwrap();
249        assert_eq!(n, 0);
250    }
251
252    // -------------------------------------------------------------------------
253    // Gap coverage (TDD audit 2026-03-31)
254    // -------------------------------------------------------------------------
255
256    /// Completely empty data must return `Error::Corrupt`, not panic.
257    #[test]
258    fn empty_data_returns_corrupt_error() {
259        let result = ElfCoreProvider::from_bytes(vec![]);
260        assert!(result.is_err(), "empty input must be rejected");
261        let err = result.err().unwrap();
262        assert!(
263            matches!(err, crate::Error::Corrupt(_)),
264            "error must be Corrupt variant, got: {err}"
265        );
266    }
267
268    /// Truncated ELF data (too short to contain a valid ELF header) must
269    /// return `Error::Corrupt`, not panic or index out of bounds.
270    #[test]
271    fn truncated_elf_data_returns_corrupt_error() {
272        // Only 8 bytes — not enough for a valid 64-byte ELF header.
273        let truncated = vec![0x7F, b'E', b'L', b'F', 2, 1, 1, 0];
274        let result = ElfCoreProvider::from_bytes(truncated);
275        assert!(result.is_err(), "truncated ELF must be rejected");
276        let err = result.err().unwrap();
277        assert!(
278            matches!(err, crate::Error::Corrupt(_)),
279            "error must be Corrupt, got: {err}"
280        );
281    }
282
283    /// An ELF file that is structurally valid (parseable) but not an ET_CORE
284    /// type must be rejected with `Error::Corrupt`.
285    #[test]
286    fn non_core_elf_type_returns_corrupt_error() {
287        // Build a minimal ELF header with ET_EXEC (2) instead of ET_CORE (4).
288        let mut header = vec![0u8; 64];
289        header[0..4].copy_from_slice(&[0x7F, b'E', b'L', b'F']);
290        header[4] = 2; // ELFCLASS64
291        header[5] = 1; // ELFDATA2LSB
292        header[6] = 1; // EV_CURRENT
293        header[16..18].copy_from_slice(&2u16.to_le_bytes()); // ET_EXEC
294        header[20..24].copy_from_slice(&1u32.to_le_bytes()); // e_version
295                                                             // e_phoff = 64 (right after ELF header), e_phnum = 0
296        header[32..40].copy_from_slice(&64u64.to_le_bytes()); // e_phoff
297        header[52..54].copy_from_slice(&64u16.to_le_bytes()); // e_ehsize
298        header[54..56].copy_from_slice(&56u16.to_le_bytes()); // e_phentsize
299        header[56..58].copy_from_slice(&0u16.to_le_bytes()); // e_phnum = 0
300
301        let result = ElfCoreProvider::from_bytes(header);
302        assert!(result.is_err(), "non-core ELF must be rejected");
303        let err = result.err().unwrap();
304        assert!(
305            matches!(err, crate::Error::Corrupt(_)),
306            "error must be Corrupt, got: {err}"
307        );
308        assert!(
309            err.to_string().contains("not an ELF core dump"),
310            "error message should explain the rejection reason, got: {err}"
311        );
312    }
313
314    /// Garbage bytes that are not ELF at all must return `Error::Corrupt`.
315    #[test]
316    fn garbage_data_returns_corrupt_error() {
317        let mut garbage = vec![0x00u8; 128];
318        garbage[0..4].copy_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF]);
319        let result = ElfCoreProvider::from_bytes(garbage);
320        assert!(result.is_err(), "garbage input must be rejected");
321        let err = result.err().unwrap();
322        assert!(
323            matches!(err, crate::Error::Corrupt(_)),
324            "error must be Corrupt, got: {err}"
325        );
326    }
327}