Skip to main content

memf_core/
lib.rs

1#![deny(unsafe_code)]
2#![warn(missing_docs)]
3//! Virtual address translation and kernel object reading.
4//!
5//! This crate provides:
6//! - [`VirtualAddressSpace`] — page table walking for x86_64 (4-level, 5-level),
7//!   AArch64, and x86 PAE/non-PAE modes
8//! - [`ObjectReader`] — high-level kernel struct traversal using symbol information
9
10pub mod lzo;
11// Folded in from the former memf-framebuffer crate (cross-OS framebuffer extraction).
12#[allow(missing_docs)]
13pub mod framebuffer;
14pub mod object_reader;
15pub mod pagefile;
16pub mod proto_pte;
17pub mod test_builders;
18pub mod vas;
19
20/// Error type for memf-core operations.
21#[non_exhaustive]
22#[derive(Debug, thiserror::Error)]
23pub enum Error {
24    /// Physical memory read error.
25    #[error("physical memory error: {0}")]
26    Physical(#[from] memf_format::Error),
27
28    /// Symbol resolution error.
29    #[error("symbol error: {0}")]
30    Symbol(#[from] memf_symbols::Error),
31
32    /// Page table entry not present (page fault).
33    #[error("page not present at virtual address {0:#018x}")]
34    PageNotPresent(u64),
35
36    /// Read crossed a page boundary and the next page is not mapped.
37    #[error("partial read: got {got} of {requested} bytes at {addr:#018x}")]
38    PartialRead {
39        /// Virtual address of the read.
40        addr: u64,
41        /// Bytes requested.
42        requested: usize,
43        /// Bytes actually read.
44        got: usize,
45    },
46
47    /// A required symbol or field was not found.
48    #[error("missing symbol or field: {0}")]
49    MissingSymbol(String),
50
51    /// Type size mismatch during Pod cast.
52    #[error("type size mismatch: expected {expected}, got {got}")]
53    SizeMismatch {
54        /// Expected size in bytes.
55        expected: usize,
56        /// Actual size available.
57        got: usize,
58    },
59
60    /// The list walk exceeded the maximum iteration count (cycle protection).
61    #[error("list walk exceeded {0} iterations (possible cycle)")]
62    ListCycle(usize),
63
64    /// Page is in a pagefile that was not provided.
65    #[error("page at {vaddr:#018x} paged out to pagefile {pagefile_num} offset {page_offset:#x}")]
66    PagedOut {
67        /// Virtual address of the faulting page.
68        vaddr: u64,
69        /// Pagefile number (0 = pagefile.sys, 1-15 = secondary).
70        pagefile_num: u8,
71        /// Page offset within the pagefile.
72        page_offset: u64,
73    },
74
75    /// Page uses a prototype PTE (shared section, not yet supported).
76    #[error("prototype PTE at {0:#018x} (not yet supported)")]
77    PrototypePte(u64),
78}
79
80/// A Result alias for memf-core.
81pub type Result<T> = std::result::Result<T, Error>;
82
83/// Output from a walker that may encounter unreadable entries.
84///
85/// Wraps the collected items with a counter of entries that were
86/// skipped due to unreadable memory or parse errors. Provides
87/// analysts visibility into partial walks ("500/512 processes walked,
88/// 12 skipped").
89#[derive(Debug, Clone, Default, serde::Serialize)]
90pub struct WalkResult<T: serde::Serialize> {
91    /// Successfully walked entries.
92    pub items: Vec<T>,
93    /// Number of entries skipped due to unreadable memory or parse errors.
94    pub skipped: u32,
95}
96
97impl<T: serde::Serialize> WalkResult<T> {
98    /// Create a new `WalkResult` with the given items and skip count.
99    pub fn new(items: Vec<T>, skipped: u32) -> Self {
100        Self { items, skipped }
101    }
102
103    /// Push a successfully walked item.
104    pub fn push(&mut self, item: T) {
105        self.items.push(item);
106    }
107
108    /// Increment the skip counter for one unreadable entry.
109    pub fn skip(&mut self) {
110        self.skipped += 1;
111    }
112}
113
114#[cfg(test)]
115mod walk_result_tests {
116    use super::WalkResult;
117
118    #[test]
119    fn walk_result_new_has_correct_counts() {
120        let r: WalkResult<u32> = WalkResult::new(vec![1, 2, 3], 5);
121        assert_eq!(r.items.len(), 3);
122        assert_eq!(r.skipped, 5);
123    }
124
125    #[test]
126    fn walk_result_skip_increments_counter() {
127        let mut r: WalkResult<u32> = WalkResult::default();
128        r.skip();
129        r.skip();
130        assert_eq!(r.skipped, 2);
131        assert!(r.items.is_empty());
132    }
133
134    #[test]
135    fn walk_result_push_adds_item() {
136        let mut r: WalkResult<u32> = WalkResult::default();
137        r.push(42u32);
138        assert_eq!(r.items, vec![42]);
139        assert_eq!(r.skipped, 0);
140    }
141
142    #[test]
143    fn walk_result_serializes_with_skipped_field() {
144        let r = WalkResult::new(vec![1u32, 2], 3);
145        let json = serde_json::to_string(&r).unwrap();
146        assert!(json.contains("\"skipped\":3"), "json: {json}");
147        assert!(json.contains("\"items\""), "json: {json}");
148    }
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154
155    #[test]
156    fn error_display_page_not_present() {
157        let e = Error::PageNotPresent(0xFFFF_8000_0000_1000);
158        assert!(e.to_string().contains("0xffff800000001000"));
159    }
160
161    #[test]
162    fn error_display_partial_read() {
163        let e = Error::PartialRead {
164            addr: 0x1000,
165            requested: 8,
166            got: 4,
167        };
168        assert!(e.to_string().contains("4 of 8"));
169    }
170
171    #[test]
172    fn error_display_list_cycle() {
173        let e = Error::ListCycle(10000);
174        assert!(e.to_string().contains("10000"));
175    }
176
177    #[test]
178    fn error_display_missing_symbol() {
179        let e = Error::MissingSymbol("task_struct.pid".into());
180        assert!(e.to_string().contains("task_struct.pid"));
181    }
182
183    #[test]
184    fn error_display_size_mismatch() {
185        let e = Error::SizeMismatch {
186            expected: 8,
187            got: 4,
188        };
189        let msg = e.to_string();
190        assert!(msg.contains('8'));
191        assert!(msg.contains('4'));
192    }
193
194    #[test]
195    fn error_from_physical() {
196        let phys_err = memf_format::Error::UnknownFormat;
197        let e: Error = Error::from(phys_err);
198        assert!(matches!(e, Error::Physical(_)));
199        assert!(e.to_string().contains("unknown dump format"));
200    }
201
202    #[test]
203    fn error_from_symbol() {
204        let sym_err = memf_symbols::Error::NotFound("init_task".into());
205        let e: Error = Error::from(sym_err);
206        assert!(matches!(e, Error::Symbol(_)));
207        assert!(e.to_string().contains("init_task"));
208    }
209
210    #[test]
211    fn error_from_io_via_physical() {
212        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file gone");
213        let phys_err = memf_format::Error::from(io_err);
214        let e: Error = Error::from(phys_err);
215        assert!(matches!(e, Error::Physical(_)));
216    }
217
218    #[test]
219    fn error_display_paged_out() {
220        let e = Error::PagedOut {
221            vaddr: 0xFFFF_8000_0000_2000,
222            pagefile_num: 0,
223            page_offset: 0x1234,
224        };
225        let msg = e.to_string();
226        assert!(msg.contains("0xffff800000002000"));
227        assert!(msg.contains("pagefile 0"));
228        assert!(msg.contains("0x1234"));
229    }
230
231    #[test]
232    fn error_display_prototype_pte() {
233        let e = Error::PrototypePte(0xFFFF_8000_DEAD_0000);
234        let msg = e.to_string();
235        assert!(msg.contains("0xffff8000dead0000"));
236        assert!(msg.contains("prototype PTE"));
237    }
238}