Skip to main content

memf_format/
hiberfil.rs

1//! Windows hibernation file (`hiberfil.sys`) format provider.
2//!
3//! Parses hibernation files with `PO_MEMORY_IMAGE` header signatures:
4//! `hibr` (0x72626968), `wake` (0x656B6177), `RSTR` (0x52545352),
5//! `HORM` (0x4D524F48). Eagerly decompresses Xpress LZ77 blocks into a
6//! `HashMap<pfn, page>` for random-access reads. Extracts CR3 from the
7//! processor state page.
8
9use std::collections::HashMap;
10use std::path::Path;
11
12use crate::{DumpMetadata, Error, FormatPlugin, PhysicalMemoryProvider, PhysicalRange, Result};
13
14/// Magic values (little-endian u32).
15const HIBR_MAGIC: u32 = 0x7262_6968;
16const WAKE_MAGIC: u32 = 0x656B_6177;
17const RSTR_MAGIC: u32 = 0x5254_5352;
18const HORM_MAGIC: u32 = 0x4D52_4F48;
19
20/// Page size in bytes.
21const PAGE_SIZE: usize = 4096;
22
23/// Xpress block signature: `[0x81, 0x81, 'x', 'p', 'r', 'e', 's', 's']`.
24const XPRESS_SIG: [u8; 8] = [0x81, 0x81, b'x', b'p', b'r', b'e', b's', b's'];
25
26/// Block header size (padded to 0x20).
27const BLOCK_HEADER_SIZE: usize = 0x20;
28
29/// Offset of `LengthSelf` in the PO_MEMORY_IMAGE header.
30const OFF_LENGTH_SELF: usize = 0x0C;
31
32/// Offset of `FirstTablePage` in the PO_MEMORY_IMAGE header (u64).
33const OFF_FIRST_TABLE_PAGE: usize = 0x68;
34
35/// Offset of CR3 within the processor state page (page 1).
36const OFF_CR3_IN_PROC_STATE: usize = 0x28;
37
38/// Read a little-endian u32 from `data` at `offset`.
39fn read_u32(data: &[u8], offset: usize) -> crate::Result<u32> {
40    data.get(offset..offset + 4)
41        .and_then(|b| b.try_into().ok())
42        .map(u32::from_le_bytes)
43        .ok_or_else(|| {
44            crate::Error::Corrupt(format!("truncated header: need 4 bytes at offset {offset}"))
45        })
46}
47
48/// Read a little-endian u64 from `data` at `offset`.
49fn read_u64(data: &[u8], offset: usize) -> crate::Result<u64> {
50    data.get(offset..offset + 8)
51        .and_then(|b| b.try_into().ok())
52        .map(u64::from_le_bytes)
53        .ok_or_else(|| {
54            crate::Error::Corrupt(format!("truncated header: need 8 bytes at offset {offset}"))
55        })
56}
57
58/// Provider that exposes physical memory from a Windows hibernation file.
59///
60/// Stores decompressed pages in a `HashMap<pfn, Vec<u8>>` for O(1) lookup.
61pub struct HiberfilProvider {
62    pages: HashMap<u64, Vec<u8>>,
63    ranges: Vec<PhysicalRange>,
64    meta: DumpMetadata,
65}
66
67impl HiberfilProvider {
68    /// Parse a hibernation file from an in-memory byte slice.
69    pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
70        // Validate minimum size: at least 3 header pages (0x3000 bytes).
71        if bytes.len() < 3 * PAGE_SIZE {
72            return Err(Error::Corrupt(
73                "hiberfil too short: need at least 3 header pages".into(),
74            ));
75        }
76
77        // Validate magic.
78        let magic = read_u32(bytes, 0)?;
79        if !is_hiberfil_magic(magic) {
80            return Err(Error::Corrupt(format!(
81                "invalid hiberfil magic: 0x{magic:08X}"
82            )));
83        }
84
85        // Check LengthSelf to confirm 64-bit format (value 256).
86        let _length_self = read_u32(bytes, OFF_LENGTH_SELF)?;
87
88        // Extract CR3 from processor state page (page 1, offset 0x28).
89        let cr3 = read_u64(bytes, PAGE_SIZE + OFF_CR3_IN_PROC_STATE)?;
90
91        // Read the page table from page indicated by FirstTablePage.
92        let first_table_page = read_u64(bytes, OFF_FIRST_TABLE_PAGE)?;
93        let table_offset = first_table_page as usize * PAGE_SIZE;
94
95        if table_offset + PAGE_SIZE > bytes.len() {
96            return Err(Error::Corrupt("first table page beyond file end".into()));
97        }
98
99        // Parse PFN entries from the page table until sentinel 0xFFFFFFFFFFFFFFFF.
100        let mut pfn_list = Vec::new();
101        let mut pos = table_offset;
102        while pos + 8 <= table_offset + PAGE_SIZE {
103            let pfn = read_u64(bytes, pos)?;
104            if pfn == u64::MAX {
105                break;
106            }
107            pfn_list.push(pfn);
108            pos += 8;
109        }
110
111        // Decompress Xpress blocks starting after header pages (3 * PAGE_SIZE).
112        let mut pages = HashMap::new();
113        let mut block_offset = 3 * PAGE_SIZE;
114        let mut pfn_idx = 0;
115
116        while block_offset + BLOCK_HEADER_SIZE <= bytes.len() && pfn_idx < pfn_list.len() {
117            // Check Xpress signature.
118            if bytes[block_offset..block_offset + 8] != XPRESS_SIG {
119                break;
120            }
121
122            // Parse block header.
123            let num_pages_minus_1 = bytes[block_offset + 8] as usize;
124            let num_pages = num_pages_minus_1 + 1;
125
126            // compressed_size_field: 3 bytes LE at offset 9.
127            let csf_b0 = u32::from(bytes[block_offset + 9]);
128            let csf_b1 = u32::from(bytes[block_offset + 10]);
129            let csf_b2 = u32::from(bytes[block_offset + 11]);
130            let compressed_size_field = csf_b0 | (csf_b1 << 8) | (csf_b2 << 16);
131
132            // Decode compressed size: (compressed_size_field + 1) / 4.
133            let compressed_len = ((compressed_size_field + 1) / 4) as usize;
134
135            let data_start = block_offset + BLOCK_HEADER_SIZE;
136            let data_end = data_start + compressed_len;
137
138            if data_end > bytes.len() {
139                return Err(Error::Corrupt(format!(
140                    "xpress block at 0x{block_offset:X} extends beyond file (need {compressed_len} bytes)"
141                )));
142            }
143
144            // Decompress the block.
145            let compressed_data = &bytes[data_start..data_end];
146            let decompressed = lzxpress::data::decompress(compressed_data).map_err(|e| {
147                Error::Decompression(format!("xpress decompress at 0x{block_offset:X}: {e:?}"))
148            })?;
149
150            // Split decompressed data into individual pages.
151            for i in 0..num_pages {
152                if pfn_idx >= pfn_list.len() {
153                    break;
154                }
155                let pfn = pfn_list[pfn_idx];
156                let page_start = i * PAGE_SIZE;
157                let page_end = page_start + PAGE_SIZE;
158
159                if page_end <= decompressed.len() {
160                    pages.insert(pfn, decompressed[page_start..page_end].to_vec());
161                }
162                pfn_idx += 1;
163            }
164
165            block_offset = data_end;
166        }
167
168        // Build sorted ranges from the page map.
169        let mut pfns: Vec<u64> = pages.keys().copied().collect();
170        pfns.sort_unstable();
171        let ranges = build_ranges(&pfns);
172
173        let meta = DumpMetadata {
174            cr3: Some(cr3),
175            dump_type: Some("Hibernation".into()),
176            ..DumpMetadata::default()
177        };
178
179        Ok(Self {
180            pages,
181            ranges,
182            meta,
183        })
184    }
185
186    /// Parse a hibernation file from a file path.
187    pub fn from_path(path: &Path) -> Result<Self> {
188        let data = std::fs::read(path)?;
189        Self::from_bytes(&data)
190    }
191}
192
193/// Build coalesced `PhysicalRange` entries from a sorted list of PFNs.
194fn build_ranges(sorted_pfns: &[u64]) -> Vec<PhysicalRange> {
195    let mut ranges = Vec::new();
196    let mut iter = sorted_pfns.iter().copied();
197
198    let Some(first) = iter.next() else {
199        return ranges;
200    };
201
202    let mut range_start = first * PAGE_SIZE as u64;
203    let mut range_end = range_start + PAGE_SIZE as u64;
204
205    for pfn in iter {
206        let addr = pfn * PAGE_SIZE as u64;
207        if addr == range_end {
208            // Contiguous — extend.
209            range_end = addr + PAGE_SIZE as u64;
210        } else {
211            // Gap — push current and start new.
212            ranges.push(PhysicalRange {
213                start: range_start,
214                end: range_end,
215            });
216            range_start = addr;
217            range_end = addr + PAGE_SIZE as u64;
218        }
219    }
220    ranges.push(PhysicalRange {
221        start: range_start,
222        end: range_end,
223    });
224
225    ranges
226}
227
228/// Check whether a u32 matches one of the known hiberfil magic values.
229fn is_hiberfil_magic(magic: u32) -> bool {
230    matches!(magic, HIBR_MAGIC | WAKE_MAGIC | RSTR_MAGIC | HORM_MAGIC)
231}
232
233impl PhysicalMemoryProvider for HiberfilProvider {
234    fn read_phys(&self, addr: u64, buf: &mut [u8]) -> Result<usize> {
235        if buf.is_empty() {
236            return Ok(0);
237        }
238
239        let pfn = addr / PAGE_SIZE as u64;
240        let offset = (addr % PAGE_SIZE as u64) as usize;
241
242        let Some(page) = self.pages.get(&pfn) else {
243            return Ok(0);
244        };
245
246        let available = page.len().saturating_sub(offset);
247        let to_read = buf.len().min(available);
248        buf[..to_read].copy_from_slice(&page[offset..offset + to_read]);
249        Ok(to_read)
250    }
251
252    fn ranges(&self) -> &[PhysicalRange] {
253        &self.ranges
254    }
255
256    fn format_name(&self) -> &str {
257        "Hiberfil.sys"
258    }
259
260    fn metadata(&self) -> Option<DumpMetadata> {
261        Some(self.meta.clone())
262    }
263}
264
265/// FormatPlugin implementation for Windows hibernation files.
266pub struct HiberfilPlugin;
267
268impl FormatPlugin for HiberfilPlugin {
269    fn name(&self) -> &str {
270        "Hiberfil.sys"
271    }
272
273    fn probe(&self, header: &[u8]) -> u8 {
274        if header.len() < 4 {
275            return 0;
276        }
277        let Ok(magic) = read_u32(header, 0) else {
278            return 0;
279        };
280        match magic {
281            HIBR_MAGIC | WAKE_MAGIC => 90,
282            RSTR_MAGIC | HORM_MAGIC => 85,
283            _ => 0,
284        }
285    }
286
287    fn open(&self, path: &Path) -> Result<Box<dyn PhysicalMemoryProvider>> {
288        Ok(Box::new(HiberfilProvider::from_path(path)?))
289    }
290}
291
292inventory::submit!(&HiberfilPlugin as &dyn FormatPlugin);
293
294#[cfg(test)]
295mod tests {
296    use super::*;
297    use crate::test_builders::HiberfilBuilder;
298    use std::io::Write;
299
300    #[test]
301    fn probe_hiberfil_magic() {
302        let dump = HiberfilBuilder::new().build();
303        let plugin = HiberfilPlugin;
304        assert_eq!(plugin.probe(&dump), 90);
305    }
306
307    #[test]
308    fn probe_non_hiberfil() {
309        let plugin = HiberfilPlugin;
310        assert_eq!(plugin.probe(&[0u8; 64]), 0);
311    }
312
313    #[test]
314    fn probe_short_header_returns_zero() {
315        let plugin = HiberfilPlugin;
316        assert_eq!(plugin.probe(&[0x68, 0x69, 0x62]), 0); // 3 bytes
317        assert_eq!(plugin.probe(&[]), 0); // empty
318    }
319
320    #[test]
321    fn single_page_read() {
322        let mut page = [0u8; 4096];
323        page[0] = 0xAA;
324        page[100] = 0xBB;
325        page[4095] = 0xCC;
326
327        let dump = HiberfilBuilder::new().add_page(0, &page).build();
328        let provider = HiberfilProvider::from_bytes(&dump).unwrap();
329
330        // Read first byte
331        let mut buf = [0u8; 1];
332        let n = provider.read_phys(0, &mut buf).unwrap();
333        assert_eq!(n, 1);
334        assert_eq!(buf[0], 0xAA);
335
336        // Read byte at offset 100
337        let n = provider.read_phys(100, &mut buf).unwrap();
338        assert_eq!(n, 1);
339        assert_eq!(buf[0], 0xBB);
340
341        // Read last byte of page
342        let n = provider.read_phys(4095, &mut buf).unwrap();
343        assert_eq!(n, 1);
344        assert_eq!(buf[0], 0xCC);
345    }
346
347    #[test]
348    fn multi_page_read() {
349        let mut page0 = [0u8; 4096];
350        page0[0] = 0x11;
351        let mut page4 = [0u8; 4096];
352        page4[0] = 0x44;
353
354        let dump = HiberfilBuilder::new()
355            .add_page(0, &page0)
356            .add_page(4, &page4)
357            .build();
358        let provider = HiberfilProvider::from_bytes(&dump).unwrap();
359
360        let mut buf = [0u8; 1];
361
362        // Read from PFN 0
363        let n = provider.read_phys(0, &mut buf).unwrap();
364        assert_eq!(n, 1);
365        assert_eq!(buf[0], 0x11);
366
367        // Read from PFN 4 (physical address = 4 * 4096 = 0x4000)
368        let n = provider.read_phys(4 * 4096, &mut buf).unwrap();
369        assert_eq!(n, 1);
370        assert_eq!(buf[0], 0x44);
371    }
372
373    #[test]
374    fn read_gap_returns_zero() {
375        // Only PFN 2 is mapped; reading PFN 0 should return 0 bytes.
376        let page = [0xFFu8; 4096];
377        let dump = HiberfilBuilder::new().add_page(2, &page).build();
378        let provider = HiberfilProvider::from_bytes(&dump).unwrap();
379
380        let mut buf = [0u8; 4];
381        let n = provider.read_phys(0, &mut buf).unwrap();
382        assert_eq!(n, 0);
383    }
384
385    #[test]
386    fn read_empty_buffer() {
387        let page = [0u8; 4096];
388        let dump = HiberfilBuilder::new().add_page(0, &page).build();
389        let provider = HiberfilProvider::from_bytes(&dump).unwrap();
390
391        let mut buf = [];
392        let n = provider.read_phys(0, &mut buf).unwrap();
393        assert_eq!(n, 0);
394    }
395
396    #[test]
397    fn metadata_extraction() {
398        let cr3_val = 0x1ab000u64;
399        let dump = HiberfilBuilder::new().cr3(cr3_val).build();
400        let provider = HiberfilProvider::from_bytes(&dump).unwrap();
401
402        let meta = provider.metadata().expect("metadata should be Some");
403        assert_eq!(meta.cr3, Some(cr3_val));
404        assert_eq!(meta.dump_type.as_deref(), Some("Hibernation"));
405    }
406
407    #[test]
408    fn plugin_name() {
409        let plugin = HiberfilPlugin;
410        assert_eq!(plugin.name(), "Hiberfil.sys");
411    }
412
413    #[test]
414    fn from_path_roundtrip() {
415        let mut page = [0u8; 4096];
416        page[42] = 0xDE;
417
418        let dump = HiberfilBuilder::new().add_page(0, &page).build();
419
420        let dir = std::env::temp_dir().join("memf_hiberfil_test");
421        std::fs::create_dir_all(&dir).unwrap();
422        let path = dir.join("test.hiberfil");
423        {
424            let mut f = std::fs::File::create(&path).unwrap();
425            f.write_all(&dump).unwrap();
426        }
427
428        let provider = HiberfilProvider::from_path(&path).unwrap();
429        let mut buf = [0u8; 1];
430        let n = provider.read_phys(42, &mut buf).unwrap();
431        assert_eq!(n, 1);
432        assert_eq!(buf[0], 0xDE);
433
434        // Cleanup
435        let _ = std::fs::remove_dir_all(&dir);
436    }
437
438    #[test]
439    fn builder_produces_hibr_magic() {
440        let dump = HiberfilBuilder::new().build();
441        let magic = u32::from_le_bytes(dump[0..4].try_into().unwrap());
442        assert_eq!(magic, 0x7262_6968); // "hibr"
443    }
444
445    #[test]
446    fn builder_stores_cr3_in_processor_state() {
447        let cr3_val = 0xDEAD_BEEF_CAFE_0000u64;
448        let dump = HiberfilBuilder::new().cr3(cr3_val).build();
449        // CR3 is at page 1 (offset 0x1000) + 0x28
450        let cr3_offset = 0x1000 + 0x28;
451        let stored_cr3 = u64::from_le_bytes(dump[cr3_offset..cr3_offset + 8].try_into().unwrap());
452        assert_eq!(stored_cr3, cr3_val);
453    }
454
455    #[test]
456    fn from_bytes_empty_returns_error_not_panic() {
457        let result = HiberfilProvider::from_bytes(&[]);
458        assert!(result.is_err(), "empty input must return Err");
459    }
460}