Add pread fallback when mmap fails (e.g. ESXi 6.5 VMkernel)

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.
This commit is contained in:
NK
2026-03-21 11:51:26 +01:00
parent 95162782e5
commit 3e811eaf88
5 changed files with 168 additions and 43 deletions
+6 -7
View File
@@ -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 {
+12 -6
View File
@@ -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<LoadSegment>,
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<u8>;
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 {
+10 -4
View File
@@ -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<MappedPage>,
/// 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)
+125 -12
View File
@@ -97,9 +97,114 @@ pub fn file_size(file: &mut std::fs::File) -> std::io::Result<u64> {
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<memmap2::Mmap> {
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<std::fs::File>,
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<Vec<u8>> {
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<MappedFile> {
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<memmap2::Mmap> {
"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,
})
}
}
}
+15 -14
View File
@@ -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<MemoryRegion>,
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<MemoryRegion>, Vec<Tag>)> {
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 {