From 3e811eaf88e016765ff75ae2658797e8a12e6763 Mon Sep 17 00:00:00 2001 From: NK Date: Sat, 21 Mar 2026 11:51:26 +0100 Subject: [PATCH] Add pread fallback when mmap fails (e.g. ESXi 6.5 VMkernel) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MappedFile now has a Pread variant that uses read_exact_at() instead of memory mapping. When mmap returns EINVAL (unsupported by kernel), vmkatz falls back to file I/O with a clear warning message. Layers updated to use read_at() for PhysicalMemory::read_phys: - VMware: pread for runtime reads, header parsed from first 8MB buffer - Hyper-V: pread for all reads - QEMU ELF: pread for runtime reads, ELF header parsed from 1MB buffer - QEMU savevm: requires mmap (stream parsing needs full slice access) Zero host RAM impact — pread reads directly from disk to caller buffer. --- src/hyperv/layer.rs | 13 ++--- src/qemu/layer.rs | 18 ++++-- src/qemu/savevm.rs | 14 +++-- src/utils.rs | 137 ++++++++++++++++++++++++++++++++++++++++---- src/vmware/layer.rs | 29 +++++----- 5 files changed, 168 insertions(+), 43 deletions(-) diff --git a/src/hyperv/layer.rs b/src/hyperv/layer.rs index 105d4ec..bf1b697 100644 --- a/src/hyperv/layer.rs +++ b/src/hyperv/layer.rs @@ -8,14 +8,12 @@ use std::fs; use std::path::Path; -use memmap2::Mmap; - -use crate::error::Result; +use crate::error::{VmkatzError, Result}; use crate::memory::PhysicalMemory; /// Hyper-V memory layer: provides physical memory from .bin or raw dump files. pub struct HypervLayer { - mmap: Mmap, + mmap: crate::utils::MappedFile, size: u64, } @@ -52,12 +50,13 @@ impl PhysicalMemory for HypervLayer { buf.fill(0); if phys_addr < self.size { let avail = (self.size - phys_addr) as usize; - buf[..avail].copy_from_slice(&self.mmap[phys_addr as usize..self.size as usize]); + self.mmap.read_at(phys_addr as usize, &mut buf[..avail]) + .map_err(|_| VmkatzError::UnmappablePhysical(phys_addr))?; } return Ok(()); } - buf.copy_from_slice(&self.mmap[phys_addr as usize..end as usize]); - Ok(()) + self.mmap.read_at(phys_addr as usize, buf) + .map_err(|_| VmkatzError::UnmappablePhysical(phys_addr)) } fn phys_size(&self) -> u64 { diff --git a/src/qemu/layer.rs b/src/qemu/layer.rs index fa00e47..2ed3929 100644 --- a/src/qemu/layer.rs +++ b/src/qemu/layer.rs @@ -10,7 +10,6 @@ use std::fs; use std::path::Path; -use memmap2::Mmap; use crate::error::{VmkatzError, Result}; use crate::memory::PhysicalMemory; @@ -39,7 +38,7 @@ struct LoadSegment { /// QEMU ELF core dump memory layer. pub struct QemuElfLayer { - mmap: Mmap, + mmap: crate::utils::MappedFile, segments: Vec, phys_end: u64, } @@ -54,8 +53,14 @@ impl QemuElfLayer { return Err(VmkatzError::InvalidMagic(0)); } - // Parse ELF64 header - let data = &mmap[..]; + // For pread fallback: read the ELF header + program headers (first 1MB covers it) + let header_buf: Vec; + let data: &[u8] = if mmap.is_pread() { + header_buf = crate::utils::read_file_header(&file, 1024 * 1024)?; + &header_buf + } else { + mmap.as_bytes() + }; if data[0..4] != ELF_MAGIC { return Err(VmkatzError::InvalidMagic(u32::from_le_bytes([ data[0], data[1], data[2], data[3], @@ -189,7 +194,8 @@ impl PhysicalMemory for QemuElfLayer { let file_off = (seg.file_offset + offset_in_seg) as usize; let end = file_off + buf.len(); if end <= self.mmap.len() { - buf.copy_from_slice(&self.mmap[file_off..end]); + self.mmap.read_at(file_off, buf) + .map_err(|_| VmkatzError::UnmappablePhysical(phys_addr))?; return Ok(()); } } @@ -209,7 +215,7 @@ impl PhysicalMemory for QemuElfLayer { let end = file_off + to_copy; if end <= self.mmap.len() { let dst_start = pos as usize; - buf[dst_start..dst_start + to_copy].copy_from_slice(&self.mmap[file_off..end]); + let _ = self.mmap.read_at(file_off, &mut buf[dst_start..dst_start + to_copy]); } pos += to_copy as u64; } else { diff --git a/src/qemu/savevm.rs b/src/qemu/savevm.rs index 7f1cc03..6b95257 100644 --- a/src/qemu/savevm.rs +++ b/src/qemu/savevm.rs @@ -14,7 +14,6 @@ use std::fs; use std::path::Path; -use memmap2::Mmap; use crate::error::{VmkatzError, Result}; use crate::memory::PhysicalMemory; @@ -63,7 +62,7 @@ struct RamBlock { /// QEMU savevm state memory layer. pub struct QemuSavevmLayer { - mmap: Mmap, + mmap: crate::utils::MappedFile, /// Sorted by GPA for binary search. pages: Vec, /// Total physical address space (max GPA of pc.ram block). @@ -81,7 +80,14 @@ impl QemuSavevmLayer { return Err(VmkatzError::InvalidMagic(0)); } - let data = &mmap[..]; + // parse_ram_stream needs slice access to the entire file — pread fallback + // would be too slow (millions of small reads). Require mmap. + if mmap.is_pread() { + return Err(io_err( + "QEMU savevm requires mmap support (file I/O fallback too slow for stream parsing)".to_string() + )); + } + let data = mmap.as_bytes(); // Verify magic let magic = u32::from_be_bytes([data[0], data[1], data[2], data[3]]); @@ -441,7 +447,7 @@ impl PhysicalMemory for QemuSavevmLayer { let file_off = page.file_offset as usize + offset_in_page; let end = file_off + to_copy; if end <= self.mmap.len() { - buf[pos..pos + to_copy].copy_from_slice(&self.mmap[file_off..end]); + let _ = self.mmap.read_at(file_off, &mut buf[pos..pos + to_copy]); } } // Unmapped pages stay as zeros (already filled) diff --git a/src/utils.rs b/src/utils.rs index c717f27..1d6200c 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -97,9 +97,114 @@ pub fn file_size(file: &mut std::fs::File) -> std::io::Result { Ok(size) } -/// Memory-map a file, handling block devices where fstat returns size 0. +/// File-backed memory: mmap when available, pread fallback for platforms +/// where mmap is unsupported (e.g. ESXi 6.5 VMkernel returns EINVAL on VMFS). #[cfg(any(feature = "vmware", feature = "qemu", feature = "hyperv"))] -pub fn mmap_file(file: &std::fs::File) -> std::io::Result { +pub enum MappedFile { + Mmap(memmap2::Mmap), + /// Fallback: file handle for pread-based access. + /// The Vec is a read buffer used by `slice()` — grown on demand. + Pread { + file: std::sync::Mutex, + size: u64, + }, +} + +#[cfg(any(feature = "vmware", feature = "qemu", feature = "hyperv"))] +impl MappedFile { + pub fn len(&self) -> usize { + match self { + MappedFile::Mmap(m) => m.len(), + MappedFile::Pread { size, .. } => *size as usize, + } + } + + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// Read bytes at an offset into the provided buffer. + /// Works for both mmap (memcpy) and pread (syscall) variants. + pub fn read_at(&self, offset: usize, buf: &mut [u8]) -> std::io::Result<()> { + match self { + MappedFile::Mmap(m) => { + let end = offset + buf.len(); + if end > m.len() { + return Err(std::io::Error::new( + std::io::ErrorKind::UnexpectedEof, + format!("mmap read_at: offset=0x{:x} len={} exceeds file size {}", offset, buf.len(), m.len()), + )); + } + buf.copy_from_slice(&m[offset..end]); + Ok(()) + } + MappedFile::Pread { file, size } => { + let end = offset as u64 + buf.len() as u64; + if end > *size { + return Err(std::io::Error::new( + std::io::ErrorKind::UnexpectedEof, + format!("pread read_at: offset=0x{:x} len={} exceeds file size {}", offset, buf.len(), size), + )); + } + #[cfg(unix)] + { + use std::os::unix::fs::FileExt; + let f = file.lock().unwrap(); + f.read_exact_at(buf, offset as u64)?; + } + #[cfg(not(unix))] + { + use std::io::{Read, Seek, SeekFrom}; + let mut f = file.lock().unwrap(); + f.seek(SeekFrom::Start(offset as u64))?; + f.read_exact(buf)?; + } + Ok(()) + } + } + } + + /// Get a byte slice (only works for mmap variant). + /// Panics on Pread variant — callers that need slicing must use read_at instead. + pub fn as_bytes(&self) -> &[u8] { + match self { + MappedFile::Mmap(m) => m, + MappedFile::Pread { .. } => panic!("as_bytes() not supported on pread fallback — use read_at()"), + } + } + + /// Whether this is using the pread fallback (for logging). + pub fn is_pread(&self) -> bool { + matches!(self, MappedFile::Pread { .. }) + } +} + +#[cfg(any(feature = "vmware", feature = "qemu", feature = "hyperv"))] +impl std::ops::Deref for MappedFile { + type Target = [u8]; + fn deref(&self) -> &[u8] { + self.as_bytes() + } +} + +/// Read the first `max_bytes` of a file into a Vec. +/// Used to parse headers/tags from files where mmap is unavailable. +#[cfg(any(feature = "vmware", feature = "qemu", feature = "hyperv"))] +pub fn read_file_header(file: &std::fs::File, max_bytes: usize) -> std::io::Result> { + use std::io::{Read, Seek, SeekFrom}; + let mut f = file.try_clone()?; + let size = f.seek(SeekFrom::End(0))?; + f.seek(SeekFrom::Start(0))?; + let to_read = (size as usize).min(max_bytes); + let mut buf = vec![0u8; to_read]; + f.read_exact(&mut buf)?; + Ok(buf) +} + +/// Open a file as MappedFile: tries mmap first, falls back to pread on failure. +/// Handles block devices where fstat returns size 0. +#[cfg(any(feature = "vmware", feature = "qemu", feature = "hyperv"))] +pub fn mmap_file(file: &std::fs::File) -> std::io::Result { use std::io::{Seek, SeekFrom}; let mut f = file.try_clone()?; let size = f.seek(SeekFrom::End(0))?; @@ -110,20 +215,28 @@ pub fn mmap_file(file: &std::fs::File) -> std::io::Result { "Empty file or unreadable device", )); } - unsafe { + + // Try mmap first + let mmap_result = unsafe { memmap2::MmapOptions::new() .len(size as usize) .map(file) - .map_err(|e| { - std::io::Error::new( - e.kind(), - format!( - "Failed to memory-map file ({:.1} MB): {}", - size as f64 / (1024.0 * 1024.0), - e - ), - ) + }; + + match mmap_result { + Ok(m) => Ok(MappedFile::Mmap(m)), + Err(mmap_err) => { + eprintln!( + "[!] mmap failed ({:.1} MB): {} — falling back to file I/O (slower)", + size as f64 / (1024.0 * 1024.0), + mmap_err, + ); + let f = file.try_clone()?; + Ok(MappedFile::Pread { + file: std::sync::Mutex::new(f), + size, }) + } } } diff --git a/src/vmware/layer.rs b/src/vmware/layer.rs index 30d1eb9..35a022f 100644 --- a/src/vmware/layer.rs +++ b/src/vmware/layer.rs @@ -1,7 +1,6 @@ use std::fs; use std::path::Path; -use memmap2::Mmap; use crate::error::{VmkatzError, Result}; use crate::memory::PhysicalMemory; @@ -22,7 +21,7 @@ pub struct MemoryRegion { /// VMware memory layer: provides physical memory access from .vmsn + .vmem files. pub struct VmwareLayer { - data: Mmap, + data: crate::utils::MappedFile, pub regions: Vec, truncated: bool, /// Byte offset within the data where guest physical memory starts. @@ -90,11 +89,9 @@ impl VmwareLayer { let align_mask = tags::find_tag(&all_tags, "align_mask", &[0, 0]) .and_then(|t| { let off = t.data_offset as usize; - if off + 4 <= data.len() { - Some(crate::utils::read_u32_le(&data, off).unwrap_or(0) as u64) - } else { - None - } + let mut buf = [0u8; 4]; + data.read_at(off, &mut buf).ok()?; + Some(u32::from_le_bytes(buf) as u64) }) .unwrap_or(0xFFF); let offset = (tag.data_offset + align_mask) & !align_mask; @@ -153,7 +150,16 @@ impl VmwareLayer { /// Parse a .vmsn file and return (regions, tags). fn parse_vmsn_metadata(vmsn_path: &Path) -> Result<(Vec, Vec)> { let vmsn_file = fs::File::open(vmsn_path)?; - let vmsn_data = crate::utils::mmap_file(&vmsn_file)?; + let vmsn_mapped = crate::utils::mmap_file(&vmsn_file)?; + + // For mmap: use the mapping directly. For pread fallback: read the header + // portion (first 8 MB covers all tag structures) for slice-based parsing. + let vmsn_data: std::borrow::Cow<[u8]> = if vmsn_mapped.is_pread() { + let header_bytes = crate::utils::read_file_header(&vmsn_file, 8 * 1024 * 1024)?; + std::borrow::Cow::Owned(header_bytes) + } else { + std::borrow::Cow::Borrowed(vmsn_mapped.as_bytes()) + }; let (hdr, groups) = header::parse_vmsn(&vmsn_data)?; log::info!( @@ -279,12 +285,7 @@ impl VmwareLayer { impl PhysicalMemory for VmwareLayer { fn read_phys(&self, phys_addr: u64, buf: &mut [u8]) -> Result<()> { let offset = self.guest_phys_to_vmem_offset(phys_addr)?; - let end = offset + buf.len(); - if end > self.data.len() { - return Err(VmkatzError::UnmappablePhysical(phys_addr)); - } - buf.copy_from_slice(&self.data[offset..end]); - Ok(()) + self.data.read_at(offset, buf).map_err(|_| VmkatzError::UnmappablePhysical(phys_addr)) } fn phys_size(&self) -> u64 {