mirror of
https://github.com/nikaiw/VMkatz
synced 2026-09-25 07:51:06 +00:00
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:
+6
-7
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user