1use std::path::Path;
9
10use crate::{DumpMetadata, Error, FormatPlugin, PhysicalMemoryProvider, PhysicalRange, Result};
11
12const 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
18const HEADER_SIZE: usize = 12;
20
21const GROUP_ENTRY_SIZE: usize = 80;
23
24const TAG_FLAGS_LARGE_DATA: u8 = 0x06;
26const TAG_FLAGS_INDEXED_8BYTE: u8 = 0x46;
27
28fn 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
36fn 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
44fn 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
52struct MemoryRegion {
54 paddr: u64,
55 file_offset: usize,
56 size: usize,
57}
58
59pub struct VmwareStateProvider {
64 data: Vec<u8>,
65 regions: Vec<MemoryRegion>,
66 ranges: Vec<PhysicalRange>,
67 meta: DumpMetadata,
68}
69
70fn 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 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 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 if pos + 10 > data.len() {
138 return Err(Error::Corrupt("truncated indexed tag".into()));
139 }
140 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 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 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 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 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 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 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 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 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
279pub 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); 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); 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 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 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}