Skip to main content

memf_format/
vmware.rs

1//! VMware `.vmss`/`.vmsn` state file format provider.
2//!
3//! Parses VMware suspension (`.vmss`) and snapshot (`.vmsn`) state files.
4//! These files use a group/tag binary structure containing memory regions
5//! and CPU state (CR3). Supports four VMware magic values:
6//! `0xBED2BED0`, `0xBAD1BAD1`, `0xBED2BED2`, `0xBED3BED3`.
7
8use std::path::Path;
9
10use crate::{DumpMetadata, Error, FormatPlugin, PhysicalMemoryProvider, PhysicalRange, Result};
11
12/// VMware state file magic values (little-endian u32).
13const VMSS_MAGIC: u32 = 0xBED2_BED0;
14const VMSN_MAGIC_1: u32 = 0xBAD1_BAD1;
15const VMSN_MAGIC_2: u32 = 0xBED2_BED2;
16const VMSN_MAGIC_3: u32 = 0xBED3_BED3;
17
18/// File header size: magic(4) + unknown(4) + group_count(4).
19const HEADER_SIZE: usize = 12;
20
21/// Group entry size: name(64) + tags_offset(8) + padding(8).
22const GROUP_ENTRY_SIZE: usize = 80;
23
24/// Tag flags constants.
25const TAG_FLAGS_LARGE_DATA: u8 = 0x06;
26const TAG_FLAGS_INDEXED_8BYTE: u8 = 0x46;
27
28/// Check whether a u32 matches one of the known VMware magic values.
29fn is_vmware_magic(magic: u32) -> bool {
30    matches!(
31        magic,
32        VMSS_MAGIC | VMSN_MAGIC_1 | VMSN_MAGIC_2 | VMSN_MAGIC_3
33    )
34}
35
36/// Read a little-endian u32 from `data` at `offset`.
37fn read_u32(data: &[u8], offset: usize) -> Result<u32> {
38    data.get(offset..offset + 4)
39        .and_then(|s| s.try_into().ok())
40        .map(u32::from_le_bytes)
41        .ok_or_else(|| Error::Corrupt(format!("read_u32 out of bounds at offset {offset}")))
42}
43
44/// Read a little-endian u64 from `data` at `offset`.
45fn read_u64(data: &[u8], offset: usize) -> Result<u64> {
46    data.get(offset..offset + 8)
47        .and_then(|s| s.try_into().ok())
48        .map(u64::from_le_bytes)
49        .ok_or_else(|| Error::Corrupt(format!("read_u64 out of bounds at offset {offset}")))
50}
51
52/// A contiguous memory region extracted from a VMware state file.
53struct MemoryRegion {
54    paddr: u64,
55    file_offset: usize,
56    size: usize,
57}
58
59/// Provider that exposes physical memory from a VMware state file.
60///
61/// Stores the raw file bytes and a pre-parsed list of memory regions
62/// so that `read_phys` is a simple linear scan with no allocation.
63pub struct VmwareStateProvider {
64    data: Vec<u8>,
65    regions: Vec<MemoryRegion>,
66    ranges: Vec<PhysicalRange>,
67    meta: DumpMetadata,
68}
69
70/// Parse tags within a group, returning memory regions and an optional CR3 value.
71///
72/// `data` is the full file, `offset` is the start of the tag stream for this group.
73/// `group_name` determines which tags we look for.
74fn parse_tags(
75    data: &[u8],
76    mut pos: usize,
77    group_name: &str,
78) -> Result<(Vec<MemoryRegion>, Option<u64>)> {
79    let mut regions = Vec::new();
80    let mut cr3: Option<u64> = None;
81    let mut current_ppn: Option<u64> = None;
82
83    loop {
84        if pos >= data.len() {
85            break;
86        }
87
88        let flags = data[pos];
89        if flags == 0 {
90            // Tag terminator.
91            break;
92        }
93        pos += 1;
94
95        if pos >= data.len() {
96            return Err(Error::Corrupt("truncated tag: no name_length byte".into()));
97        }
98        let name_length = data[pos] as usize;
99        pos += 1;
100
101        if pos + name_length > data.len() {
102            return Err(Error::Corrupt(
103                "truncated tag: name extends beyond data".into(),
104            ));
105        }
106        let tag_name = &data[pos..pos + name_length];
107        pos += name_length;
108
109        if flags == TAG_FLAGS_LARGE_DATA {
110            // Large data tag: next 4 bytes are data_length, then payload.
111            let data_length = read_u32(data, pos)? as usize;
112            pos += 4;
113
114            if pos + data_length > data.len() {
115                return Err(Error::Corrupt(format!(
116                    "truncated tag payload: need {data_length} bytes at offset {pos}"
117                )));
118            }
119
120            if group_name == "memory" {
121                if tag_name == b"regionPPN" && data_length == 8 {
122                    current_ppn = Some(read_u64(data, pos)?);
123                } else if tag_name == b"regionBytes" {
124                    if let Some(ppn) = current_ppn.take() {
125                        regions.push(MemoryRegion {
126                            paddr: ppn,
127                            file_offset: pos,
128                            size: data_length,
129                        });
130                    }
131                }
132            }
133
134            pos += data_length;
135        } else if flags == TAG_FLAGS_INDEXED_8BYTE {
136            // Indexed 8-byte data tag: index0(1) + index1(1) + value(8).
137            if pos + 10 > data.len() {
138                return Err(Error::Corrupt("truncated indexed tag".into()));
139            }
140            // index0 and index1 identify the CPU and register (skipped).
141            let value = read_u64(data, pos + 2)?;
142            pos += 10;
143
144            if group_name == "cpu" && tag_name == b"CR3" {
145                cr3 = Some(value);
146            }
147        } else {
148            // Unknown tag type — we cannot determine its size, so stop parsing
149            // this group's tags. This is safe because our test builder only
150            // emits the two tag types above plus the terminator.
151            return Err(Error::Corrupt(format!(
152                "unknown tag flags 0x{flags:02X} in group '{group_name}'"
153            )));
154        }
155    }
156
157    Ok((regions, cr3))
158}
159
160impl VmwareStateProvider {
161    /// Parse a VMware state file from an in-memory byte slice.
162    pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
163        if bytes.len() < HEADER_SIZE {
164            return Err(Error::Corrupt(
165                "VMware state file too short for header".into(),
166            ));
167        }
168
169        let magic = read_u32(bytes, 0)?;
170        if !is_vmware_magic(magic) {
171            return Err(Error::Corrupt(format!(
172                "invalid VMware magic: 0x{magic:08X}"
173            )));
174        }
175
176        // unknown field at offset 4 — ignored.
177        let group_count = read_u32(bytes, 8)? as usize;
178
179        let groups_end = HEADER_SIZE + group_count * GROUP_ENTRY_SIZE;
180        if groups_end > bytes.len() {
181            return Err(Error::Corrupt("group entries extend beyond file".into()));
182        }
183
184        let mut all_regions = Vec::new();
185        let mut cr3: Option<u64> = None;
186
187        for i in 0..group_count {
188            let entry_offset = HEADER_SIZE + i * GROUP_ENTRY_SIZE;
189
190            // Read null-terminated group name from first 64 bytes.
191            let name_bytes = &bytes[entry_offset..entry_offset + 64];
192            let name_end = name_bytes.iter().position(|&b| b == 0).unwrap_or(64);
193            let group_name = std::str::from_utf8(&name_bytes[..name_end]).unwrap_or("???");
194
195            // tags_offset at entry_offset + 64.
196            let tags_offset = read_u64(bytes, entry_offset + 64)? as usize;
197
198            if tags_offset >= bytes.len() {
199                return Err(Error::Corrupt(format!(
200                    "group '{group_name}' tags_offset {tags_offset} beyond file"
201                )));
202            }
203
204            let (mut regions, group_cr3) = parse_tags(bytes, tags_offset, group_name)?;
205
206            all_regions.append(&mut regions);
207            if let Some(v) = group_cr3 {
208                cr3 = Some(v);
209            }
210        }
211
212        // Build ranges from regions.
213        let ranges: Vec<PhysicalRange> = all_regions
214            .iter()
215            .map(|r| PhysicalRange {
216                start: r.paddr,
217                end: r.paddr + r.size as u64,
218            })
219            .collect();
220
221        let meta = DumpMetadata {
222            cr3,
223            dump_type: Some("VMware State".into()),
224            ..DumpMetadata::default()
225        };
226
227        Ok(Self {
228            data: bytes.to_vec(),
229            regions: all_regions,
230            ranges,
231            meta,
232        })
233    }
234
235    /// Parse a VMware state file from a file path.
236    pub fn from_path(path: &Path) -> Result<Self> {
237        let data = std::fs::read(path)?;
238        Self::from_bytes(&data)
239    }
240}
241
242impl PhysicalMemoryProvider for VmwareStateProvider {
243    fn read_phys(&self, addr: u64, buf: &mut [u8]) -> Result<usize> {
244        if buf.is_empty() {
245            return Ok(0);
246        }
247
248        for region in &self.regions {
249            let region_start = region.paddr;
250            let region_end = region.paddr + region.size as u64;
251
252            if addr >= region_start && addr < region_end {
253                let offset_in_region = (addr - region_start) as usize;
254                let available = region.size - offset_in_region;
255                let to_read = buf.len().min(available);
256                let src_start = region.file_offset + offset_in_region;
257                buf[..to_read].copy_from_slice(&self.data[src_start..src_start + to_read]);
258                return Ok(to_read);
259            }
260        }
261
262        // Address not in any mapped region — gap.
263        Ok(0)
264    }
265
266    fn ranges(&self) -> &[PhysicalRange] {
267        &self.ranges
268    }
269
270    fn format_name(&self) -> &str {
271        "VMware State"
272    }
273
274    fn metadata(&self) -> Option<DumpMetadata> {
275        Some(self.meta.clone())
276    }
277}
278
279/// FormatPlugin implementation for VMware state files.
280pub struct VmwarePlugin;
281
282impl FormatPlugin for VmwarePlugin {
283    fn name(&self) -> &str {
284        "VMware State"
285    }
286
287    fn probe(&self, header: &[u8]) -> u8 {
288        if header.len() < 4 {
289            return 0;
290        }
291        let magic = read_u32(header, 0).unwrap_or(0);
292        if is_vmware_magic(magic) {
293            85
294        } else {
295            0
296        }
297    }
298
299    fn open(&self, path: &Path) -> Result<Box<dyn PhysicalMemoryProvider>> {
300        Ok(Box::new(VmwareStateProvider::from_path(path)?))
301    }
302}
303
304inventory::submit!(&VmwarePlugin as &dyn FormatPlugin);
305
306#[cfg(test)]
307mod tests {
308    use super::*;
309    use crate::test_builders::VmwareStateBuilder;
310
311    #[test]
312    fn probe_vmware_magic() {
313        let dump = VmwareStateBuilder::new()
314            .add_region(0x1000, &[0u8; 64])
315            .build();
316        let plugin = VmwarePlugin;
317        assert_eq!(plugin.probe(&dump), 85);
318    }
319
320    #[test]
321    fn probe_non_vmware() {
322        let zeros = vec![0u8; 64];
323        let plugin = VmwarePlugin;
324        assert_eq!(plugin.probe(&zeros), 0);
325    }
326
327    #[test]
328    fn probe_short_header_returns_zero() {
329        let plugin = VmwarePlugin;
330        assert_eq!(plugin.probe(&[0xD0, 0xBE, 0xD2]), 0); // only 3 bytes
331        assert_eq!(plugin.probe(&[]), 0);
332    }
333
334    #[test]
335    fn single_region_read() {
336        let data: Vec<u8> = (0u8..=255).collect();
337        let dump = VmwareStateBuilder::new().add_region(0x1000, &data).build();
338        let provider = VmwareStateProvider::from_bytes(&dump).unwrap();
339
340        assert_eq!(provider.ranges().len(), 1);
341        assert_eq!(provider.ranges()[0].start, 0x1000);
342        assert_eq!(provider.ranges()[0].end, 0x1100); // 0x1000 + 256
343
344        let mut buf = [0u8; 4];
345        let n = provider.read_phys(0x1000, &mut buf).unwrap();
346        assert_eq!(n, 4);
347        assert_eq!(&buf, &[0, 1, 2, 3]);
348    }
349
350    #[test]
351    fn multi_region_read() {
352        let data_a = vec![0xAAu8; 128];
353        let data_b = vec![0xBBu8; 128];
354        let dump = VmwareStateBuilder::new()
355            .add_region(0x0000, &data_a)
356            .add_region(0x2000, &data_b)
357            .build();
358        let provider = VmwareStateProvider::from_bytes(&dump).unwrap();
359
360        assert_eq!(provider.ranges().len(), 2);
361
362        let mut buf = [0u8; 2];
363        let n = provider.read_phys(0x0000, &mut buf).unwrap();
364        assert_eq!(n, 2);
365        assert_eq!(buf, [0xAA, 0xAA]);
366
367        let n = provider.read_phys(0x2000, &mut buf).unwrap();
368        assert_eq!(n, 2);
369        assert_eq!(buf, [0xBB, 0xBB]);
370    }
371
372    #[test]
373    fn read_gap_returns_zero() {
374        let data = vec![0xCCu8; 64];
375        let dump = VmwareStateBuilder::new().add_region(0x1000, &data).build();
376        let provider = VmwareStateProvider::from_bytes(&dump).unwrap();
377
378        // Address 0x0000 is not mapped.
379        let mut buf = [0xFFu8; 4];
380        let n = provider.read_phys(0x0000, &mut buf).unwrap();
381        assert_eq!(n, 0);
382    }
383
384    #[test]
385    fn read_empty_buffer() {
386        let dump = VmwareStateBuilder::new()
387            .add_region(0x1000, &[0xAA; 64])
388            .build();
389        let provider = VmwareStateProvider::from_bytes(&dump).unwrap();
390        let mut buf = [];
391        let n = provider.read_phys(0x1000, &mut buf).unwrap();
392        assert_eq!(n, 0);
393    }
394
395    #[test]
396    fn metadata_cr3_extraction() {
397        let cr3_val = 0x1ab000u64;
398        let dump = VmwareStateBuilder::new()
399            .add_region(0x1000, &[0u8; 64])
400            .cr3(cr3_val)
401            .build();
402        let provider = VmwareStateProvider::from_bytes(&dump).unwrap();
403
404        let meta = provider.metadata().expect("metadata should be Some");
405        assert_eq!(meta.cr3, Some(cr3_val));
406        assert_eq!(meta.dump_type.as_deref(), Some("VMware State"));
407    }
408
409    #[test]
410    fn metadata_no_cr3() {
411        let dump = VmwareStateBuilder::new()
412            .add_region(0x1000, &[0u8; 64])
413            .build();
414        let provider = VmwareStateProvider::from_bytes(&dump).unwrap();
415
416        let meta = provider.metadata().expect("metadata should be Some");
417        assert!(meta.cr3.is_none());
418        assert_eq!(meta.dump_type.as_deref(), Some("VMware State"));
419    }
420
421    #[test]
422    fn plugin_name() {
423        let plugin = VmwarePlugin;
424        assert_eq!(plugin.name(), "VMware State");
425    }
426
427    #[test]
428    fn from_path_roundtrip() {
429        let data: Vec<u8> = (0u8..=127).collect();
430        let dump = VmwareStateBuilder::new().add_region(0x2000, &data).build();
431        let path = std::env::temp_dir().join("memf_test_vmware_roundtrip.vmss");
432        std::fs::write(&path, &dump).unwrap();
433        let provider = VmwareStateProvider::from_path(&path).unwrap();
434        assert_eq!(provider.ranges().len(), 1);
435        assert_eq!(provider.total_size(), 128);
436        assert_eq!(provider.format_name(), "VMware State");
437        let mut buf = [0u8; 4];
438        let n = provider.read_phys(0x2000, &mut buf).unwrap();
439        assert_eq!(n, 4);
440        assert_eq!(&buf, &[0, 1, 2, 3]);
441        std::fs::remove_file(&path).ok();
442    }
443
444    #[test]
445    fn builder_produces_valid_magic() {
446        let dump = VmwareStateBuilder::new()
447            .add_region(0x1000, &[0u8; 64])
448            .build();
449        let magic = u32::from_le_bytes(dump[0..4].try_into().unwrap());
450        assert_eq!(magic, 0xBED2_BED0);
451        // group_count should be 1 (memory only, no cr3)
452        let group_count = u32::from_le_bytes(dump[8..12].try_into().unwrap());
453        assert_eq!(group_count, 1);
454    }
455
456    #[test]
457    fn builder_with_cr3_has_two_groups() {
458        let dump = VmwareStateBuilder::new()
459            .add_region(0x1000, &[0u8; 64])
460            .cr3(0x1ab000)
461            .build();
462        let group_count = u32::from_le_bytes(dump[8..12].try_into().unwrap());
463        assert_eq!(group_count, 2);
464    }
465}