use std::io::{self, Write};
use super::state_snap::VmaRecord;
pub const ARENA_FLOOR: u64 = 0x4000_0000;
pub const PAGE: u64 = 4096;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct MapEntry {
pub va: u64,
pub len: u64,
pub prot: u32,
}
pub fn perms_to_prot(perms: &[u8]) -> u32 {
let mut prot = 0u32;
if perms.first() == Some(&b'r') {
prot |= libc::PROT_READ as u32;
}
if perms.get(1) == Some(&b'w') {
prot |= libc::PROT_WRITE as u32;
}
if perms.get(2) == Some(&b'x') {
prot |= libc::PROT_EXEC as u32;
}
prot
}
pub fn parse_maps(raw: &[u8], floor: u64) -> Vec<MapEntry> {
let mut out = Vec::new();
for line in raw.split(|&b| b == b'\n') {
if line.is_empty() {
continue;
}
let dash = match line.iter().position(|&b| b == b'-') {
Some(d) => d,
None => continue,
};
let sp = match line[dash + 1..].iter().position(|&b| b == b' ') {
Some(s) => dash + 1 + s,
None => continue,
};
let start = match std::str::from_utf8(&line[..dash])
.ok()
.and_then(|s| u64::from_str_radix(s, 16).ok())
{
Some(v) => v,
None => continue,
};
let end = match std::str::from_utf8(&line[dash + 1..sp])
.ok()
.and_then(|s| u64::from_str_radix(s, 16).ok())
{
Some(v) => v,
None => continue,
};
if start >= floor || end <= start {
continue;
}
let after = &line[sp + 1..];
let pe = after.iter().position(|&b| b == b' ').unwrap_or(after.len());
let prot = perms_to_prot(&after[..pe]);
out.push(MapEntry {
va: start,
len: end - start,
prot,
});
}
out
}
#[inline]
fn page_up(n: u64) -> u64 {
(n + PAGE - 1) & !(PAGE - 1)
}
pub fn capture_regions(
entries: &[MapEntry],
read_at: impl Fn(u64, &mut [u8]) -> bool,
ram: &mut impl Write,
) -> io::Result<Vec<VmaRecord>> {
let mut recs = Vec::with_capacity(entries.len());
let mut off = 0u64; let mut buf = vec![0u8; PAGE as usize];
for e in entries {
let rec = VmaRecord {
va_start: e.va,
len: e.len,
prot: e.prot,
file_off: off,
};
let mut done = 0u64;
while done < e.len {
let chunk = ((e.len - done) as usize).min(PAGE as usize);
let dst = &mut buf[..chunk];
if !read_at(e.va + done, dst) {
return Err(io::Error::new(
io::ErrorKind::Other,
format!("memimage: read_at failed @ {:#x}+{:#x}", e.va, done),
));
}
ram.write_all(dst)?;
done += chunk as u64;
}
let padded = page_up(e.len);
if padded > e.len {
let pad = vec![0u8; (padded - e.len) as usize];
ram.write_all(&pad)?;
}
off += padded;
recs.push(rec);
}
Ok(recs)
}
pub unsafe fn restore_region(
rec: &VmaRecord,
file: &std::fs::File,
ram_base: u64,
) -> io::Result<*mut u8> {
use std::os::fd::AsRawFd;
let file_off = ram_base
.checked_add(rec.file_off)
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "memimage: file_off overflow"))?;
let file_len = file.metadata()?.len();
if !crate::snapshot_frame::ram_region_within(file_len, file_off, rec.len) {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"memimage: VMA [{:#x},+{:#x}) at blob off {file_off} exceeds dump len {file_len}",
rec.va_start, rec.len
),
));
}
if file_off % PAGE != 0 || rec.va_start % PAGE != 0 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"memimage: VA and blob offset must be page-aligned for MAP_FIXED",
));
}
let ptr = unsafe {
libc::mmap(
rec.va_start as *mut libc::c_void,
rec.len as usize,
rec.prot as libc::c_int,
libc::MAP_PRIVATE | libc::MAP_FIXED,
file.as_raw_fd(),
file_off as libc::off_t,
)
};
if ptr == libc::MAP_FAILED {
return Err(io::Error::last_os_error());
}
Ok(ptr as *mut u8)
}
#[cfg(test)]
mod tests {
use super::*;
use std::os::fd::FromRawFd;
fn memfd() -> std::fs::File {
let fd = unsafe { libc::memfd_create(b"nev-memimg\0".as_ptr() as *const libc::c_char, 0) };
assert!(fd >= 0, "memfd_create: {}", std::io::Error::last_os_error());
unsafe { std::fs::File::from_raw_fd(fd) }
}
#[test]
fn parse_maps_filters_to_arena_and_decodes_prot() {
let raw = b"\
00400000-00402000 r-xp 00000000 00:00 0 \n\
00402000-00403000 rw-p 00000000 00:00 0 \n\
3fffe000-40000000 rw-p 00000000 00:00 0 [stack-ish]\n\
40000000-40010000 r-xp 00000000 00:00 0 \n\
7f0000000000-7f0000001000 r--p 00000000 00:00 0 [vvar]\n\
ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0 [vsyscall]\n";
let m = parse_maps(raw, ARENA_FLOOR);
assert_eq!(m.len(), 3, "only the 3 sub-floor VMAs are kept");
assert_eq!(
m[0],
MapEntry {
va: 0x400000,
len: 0x2000,
prot: (libc::PROT_READ | libc::PROT_EXEC) as u32
}
);
assert_eq!(
m[1],
MapEntry {
va: 0x402000,
len: 0x1000,
prot: (libc::PROT_READ | libc::PROT_WRITE) as u32
}
);
assert_eq!(m[2].va, 0x3fffe000);
assert!(m.iter().all(|e| e.va < ARENA_FLOOR));
}
#[test]
fn parse_maps_skips_malformed_lines() {
let raw = b"garbage\n00400000-00401000 rw-p 0 0 0\nnodash here\n";
let m = parse_maps(raw, ARENA_FLOOR);
assert_eq!(m.len(), 1);
assert_eq!(m[0].va, 0x400000);
}
#[test]
fn recorded_va_round_trip_map_fixed() {
let len = 2 * PAGE as usize;
let a = unsafe {
libc::mmap(
std::ptr::null_mut(),
len,
libc::PROT_READ | libc::PROT_WRITE,
libc::MAP_PRIVATE | libc::MAP_ANONYMOUS,
-1,
0,
)
};
assert_ne!(a, libc::MAP_FAILED, "mmap anon");
let a = a as u64;
let pat = |i: usize| (i as u8).wrapping_mul(31).wrapping_add(7);
unsafe {
for i in 0..len {
*((a as *mut u8).add(i)) = pat(i);
}
}
let entries = [MapEntry {
va: a,
len: len as u64,
prot: (libc::PROT_READ | libc::PROT_WRITE) as u32,
}];
let mut file = memfd();
let recs = capture_regions(
&entries,
|va, buf| {
unsafe {
std::ptr::copy_nonoverlapping(va as *const u8, buf.as_mut_ptr(), buf.len())
};
true
},
&mut file,
)
.expect("capture");
assert_eq!(recs.len(), 1);
assert_eq!(recs[0].va_start, a);
assert_eq!(recs[0].file_off, 0);
unsafe { assert_eq!(libc::munmap(a as *mut libc::c_void, len), 0, "munmap") };
let restored = unsafe { restore_region(&recs[0], &file, 0).expect("restore") };
assert_eq!(restored as u64, a, "restored at the identical VA");
unsafe {
for i in 0..len {
assert_eq!(
*((a as *const u8).add(i)),
pat(i),
"byte {i} mismatch after restore"
);
}
libc::munmap(a as *mut libc::c_void, len);
}
}
#[test]
fn restore_rejects_unaligned_and_oob() {
let mut file = memfd();
file.write_all(&vec![0u8; PAGE as usize]).unwrap();
let bad_va = VmaRecord {
va_start: 0x1234,
len: PAGE,
prot: libc::PROT_READ as u32,
file_off: 0,
};
assert!(unsafe { restore_region(&bad_va, &file, 0) }.is_err());
let oob = VmaRecord {
va_start: 0,
len: 4 * PAGE,
prot: libc::PROT_READ as u32,
file_off: 0,
};
assert!(unsafe { restore_region(&oob, &file, 0) }.is_err());
}
#[test]
fn capture_surfaces_read_failure() {
let entries = [MapEntry {
va: 0x1000,
len: PAGE,
prot: libc::PROT_READ as u32,
}];
let mut blob = Vec::new();
let r = capture_regions(&entries, |_, _| false, &mut blob);
assert!(
r.is_err(),
"a failing read_at must surface as Err, not silent zero-fill"
);
}
}