From 1fcc9d5a36ab2e6bb229365e548a495d87c322e6 Mon Sep 17 00:00:00 2001 From: 0vercl0k <1476421+0vercl0k@users.noreply.github.com> Date: Wed, 8 Oct 2025 19:54:02 -0700 Subject: [PATCH 1/8] turn pedantic on --- Cargo.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 9f3e528..ca43663 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,3 +24,6 @@ serde_json = "1.0" [[example]] name = "parser" + +[lints.clippy] +pedantic = "warn" \ No newline at end of file From eacac122fee8e6e42815b76dade6fbfdd4af71c5 Mon Sep 17 00:00:00 2001 From: 0vercl0k <1476421+0vercl0k@users.noreply.github.com> Date: Wed, 8 Oct 2025 19:57:21 -0700 Subject: [PATCH 2/8] pedantic --- Cargo.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ca43663..8e109bd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,8 +22,8 @@ clap = { version = "4.5.45", features = ["derive"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" +[lints.clippy] +pedantic = "warn" + [[example]] name = "parser" - -[lints.clippy] -pedantic = "warn" \ No newline at end of file From 185251c26e87d1d8553508bf339e0ef2d7d00705 Mon Sep 17 00:00:00 2001 From: 0vercl0k <1476421+0vercl0k@users.noreply.github.com> Date: Wed, 8 Oct 2025 21:52:16 -0700 Subject: [PATCH 3/8] get rid of dependencies, clippy --- Cargo.toml | 5 --- src/bits.rs | 2 + src/error.rs | 109 +++++++++++++++++++++++++++++++++------------- src/gxa.rs | 20 +++++++-- src/lib.rs | 1 + src/map.rs | 41 +++++++++--------- src/parse.rs | 73 +++++++++++++++---------------- src/pxe.rs | 115 ++++++++++++++++++++++++++++++++++++++----------- src/structs.rs | 42 ++++++++++-------- 9 files changed, 268 insertions(+), 140 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 8e109bd..359a36e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,11 +11,6 @@ license = "MIT" repository = "https://github.com/0vercl0k/kdmp-parser-rs" rust-version = "1.85" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html -[dependencies] -bitflags = "2.9.2" -thiserror = "2" - [dev-dependencies] anyhow = "1.0" clap = { version = "4.5.45", features = ["derive"] } diff --git a/src/bits.rs b/src/bits.rs index ac8fcc3..f2bf58f 100644 --- a/src/bits.rs +++ b/src/bits.rs @@ -16,9 +16,11 @@ use std::ops::RangeInclusive; /// Utility trait to make it easier to extract ranges of bits. pub trait Bits: Sized { /// Get a range of bits. + #[must_use] fn bits(&self, r: RangeInclusive) -> Self; /// Get a bit. + #[must_use] fn bit(&self, n: usize) -> Self { self.bits(n..=n) } diff --git a/src/error.rs b/src/error.rs index 2fdacd6..b2cf307 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,9 +1,9 @@ // Axel '0vercl0k' Souchet - March 19 2024 //! This is the error type used across the codebase. -use std::fmt::Display; -use std::{io, string}; - -use thiserror::Error; +use std::error::Error; +use std::fmt::{self, Display}; +use std::io; +use std::string::FromUtf16Error; use crate::structs::{DUMP_HEADER64_EXPECTED_SIGNATURE, DUMP_HEADER64_EXPECTED_VALID_DUMP}; use crate::{Gpa, Gva}; @@ -17,55 +17,106 @@ pub enum PxeNotPresent { Pte, } -#[derive(Debug, Error)] +#[derive(Debug)] pub enum AddrTranslationError { Virt(Gva, PxeNotPresent), Phys(Gpa), } +impl Error for AddrTranslationError {} + impl Display for AddrTranslationError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - AddrTranslationError::Virt(gva, not_pres) => f.write_fmt(format_args!( - "virt to phys translation of {gva}: {not_pres:?}" - )), - AddrTranslationError::Phys(gpa) => { - f.write_fmt(format_args!("phys to offset translation of {gpa}")) + AddrTranslationError::Virt(gva, not_pres) => { + write!(f, "virt to phys translation of {gva}: {not_pres:?}") } + AddrTranslationError::Phys(gpa) => write!(f, "phys to offset translation of {gpa}"), } } } -#[derive(Error, Debug)] +#[derive(Debug)] pub enum KdmpParserError { - #[error("invalid UNICODE_STRING")] InvalidUnicodeString, - #[error("utf16: {0}")] - Utf16(#[from] string::FromUtf16Error), - #[error("overflow: {0}")] + Utf16(FromUtf16Error), Overflow(&'static str), - #[error("io: {0}")] - Io(#[from] io::Error), - #[error("invalid data: {0}")] + Io(io::Error), InvalidData(&'static str), - #[error("unsupported dump type {0:#x}")] UnknownDumpType(u32), - #[error("duplicate gpa found in physmem map for {0}")] DuplicateGpa(Gpa), - #[error("header's signature looks wrong: {0:#x} vs {DUMP_HEADER64_EXPECTED_SIGNATURE:#x}")] InvalidSignature(u32), - #[error("header's valid dump looks wrong: {0:#x} vs {DUMP_HEADER64_EXPECTED_VALID_DUMP:#x}")] InvalidValidDump(u32), - #[error("overflow for phys addr w/ run {0} page {1}")] PhysAddrOverflow(u32, u64), - #[error("overflow for page offset w/ run {0} page {1}")] PageOffsetOverflow(u32, u64), - #[error("overflow for page offset w/ bitmap_idx {0} bit_idx {1}")] BitmapPageOffsetOverflow(u64, usize), - #[error("partial physical memory read")] PartialPhysRead, - #[error("partial virtual memory read")] PartialVirtRead, - #[error("memory translation: {0}")] - AddrTranslation(#[from] AddrTranslationError), + AddrTranslation(AddrTranslationError), +} + +impl From for KdmpParserError { + fn from(value: io::Error) -> Self { + KdmpParserError::Io(value) + } +} + +impl From for KdmpParserError { + fn from(value: FromUtf16Error) -> Self { + KdmpParserError::Utf16(value) + } +} + +impl From for KdmpParserError { + fn from(value: AddrTranslationError) -> Self { + KdmpParserError::AddrTranslation(value) + } +} + +impl Display for KdmpParserError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + KdmpParserError::InvalidUnicodeString => write!(f, "invalid UNICODE_STRING"), + KdmpParserError::Utf16(_) => write!(f, "utf16"), + KdmpParserError::Overflow(o) => write!(f, "overflow: {o}"), + KdmpParserError::Io(_) => write!(f, "io"), + KdmpParserError::InvalidData(i) => write!(f, "invalid data: {i}"), + KdmpParserError::UnknownDumpType(u) => write!(f, "unsupported dump type {u:#x}"), + KdmpParserError::DuplicateGpa(gpa) => { + write!(f, "duplicate gpa found in physmem map for {gpa}") + } + KdmpParserError::InvalidSignature(sig) => write!( + f, + "header's signature looks wrong: {sig:#x} vs {DUMP_HEADER64_EXPECTED_SIGNATURE:#x}" + ), + KdmpParserError::InvalidValidDump(dump) => write!( + f, + "header's valid dump looks wrong: {dump:#x} vs {DUMP_HEADER64_EXPECTED_VALID_DUMP:#x}" + ), + KdmpParserError::PhysAddrOverflow(run, page) => { + write!(f, "overflow for phys addr w/ run {run} page {page}") + } + KdmpParserError::PageOffsetOverflow(run, page) => { + write!(f, "overflow for page offset w/ run {run} page {page}") + } + KdmpParserError::BitmapPageOffsetOverflow(bitmap_idx, bit_idx) => write!( + f, + "overflow for page offset w/ bitmap_idx {bitmap_idx} bit_idx {bit_idx}" + ), + KdmpParserError::PartialPhysRead => write!(f, "partial physical memory read"), + KdmpParserError::PartialVirtRead => write!(f, "partial virtual memory read"), + KdmpParserError::AddrTranslation(_) => write!(f, "memory translation"), + } + } +} + +impl Error for KdmpParserError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match self { + KdmpParserError::Utf16(u) => Some(u), + KdmpParserError::Io(e) => Some(e), + KdmpParserError::AddrTranslation(a) => Some(a), + _ => None, + } + } } diff --git a/src/gxa.rs b/src/gxa.rs index 5bd0797..eb033d8 100644 --- a/src/gxa.rs +++ b/src/gxa.rs @@ -13,7 +13,7 @@ //! let page_aligned_gva = gva.page_align(); //! let page_offset = gva.offset(); //! ``` -use std::fmt::Display; +use std::fmt::{self, Display}; use std::num::ParseIntError; use std::ops::AddAssign; use std::str::FromStr; @@ -33,16 +33,19 @@ pub trait Gxa: Sized + Default + Copy + From { } /// Is it page aligned? + #[must_use] fn page_aligned(&self) -> bool { self.offset() == 0 } /// Page-align it. + #[must_use] fn page_align(&self) -> Self { Self::from(self.u64() & !0xf_ff) } /// Get the next aligned page. + #[must_use] fn next_aligned_page(self) -> Self { Self::from( self.page_align() @@ -84,6 +87,7 @@ impl Gpa { /// let gpa = Gpa::new(1337); /// # } /// ``` + #[must_use] pub const fn new(addr: u64) -> Self { Self(addr) } @@ -99,6 +103,7 @@ impl Gpa { /// assert_eq!(gpa.u64(), 0x1337_000); /// # } /// ``` + #[must_use] pub const fn from_pfn(pfn: Pfn) -> Self { Self(pfn.u64() << (4 * 3)) } @@ -115,6 +120,7 @@ impl Gpa { /// assert_eq!(gpa.u64(), 0x1337_011); /// # } /// ``` + #[must_use] pub const fn from_pfn_with_offset(pfn: Pfn, offset: u64) -> Self { let base = pfn.u64() << (4 * 3); @@ -132,6 +138,7 @@ impl Gpa { /// assert_eq!(gpa.pfn(), 0x1337); /// # } /// ``` + #[must_use] pub const fn pfn(&self) -> u64 { self.0 >> (4 * 3) } @@ -140,7 +147,7 @@ impl Gpa { /// Operator += for [`Gpa`]. impl AddAssign for Gpa { fn add_assign(&mut self, rhs: Self) { - self.0 += rhs.0 + self.0 += rhs.0; } } @@ -222,7 +229,7 @@ impl From<&Gpa> for u64 { /// Format a [`Gpa`] as a string. impl Display for Gpa { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "GPA:{:#x}", self.0) } } @@ -272,6 +279,7 @@ impl Gva { /// let gva = Gva::new(0xdeadbeef); /// # } /// ``` + #[must_use] pub const fn new(addr: u64) -> Self { Self(addr) } @@ -290,6 +298,7 @@ impl Gva { /// # } /// ``` #[allow(clippy::erasing_op, clippy::identity_op)] + #[must_use] pub const fn pte_idx(&self) -> u64 { (self.0 >> (12 + (9 * 0))) & 0b1_1111_1111 } @@ -308,6 +317,7 @@ impl Gva { /// # } /// ``` #[allow(clippy::identity_op)] + #[must_use] pub const fn pde_idx(&self) -> u64 { (self.0 >> (12 + (9 * 1))) & 0b1_1111_1111 } @@ -325,6 +335,7 @@ impl Gva { /// assert_eq!(second.pdpe_idx(), 0x88); /// # } /// ``` + #[must_use] pub const fn pdpe_idx(&self) -> u64 { (self.0 >> (12 + (9 * 2))) & 0b1_1111_1111 } @@ -342,6 +353,7 @@ impl Gva { /// assert_eq!(second.pml4e_idx(), 0x22); /// # } /// ``` + #[must_use] pub fn pml4e_idx(&self) -> u64 { (self.0 >> (12 + (9 * 3))) & 0b1_1111_1111 } @@ -350,7 +362,7 @@ impl Gva { /// Operator += for [`Gva`]. impl AddAssign for Gva { fn add_assign(&mut self, rhs: Self) { - self.0 += rhs.0 + self.0 += rhs.0; } } diff --git a/src/lib.rs b/src/lib.rs index 0c8dd89..e150b1a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ // Axel '0vercl0k' Souchet - February 25 2024 +#![allow(clippy::doc_markdown)] #![doc = include_str!("../README.md")] mod bits; mod error; diff --git a/src/map.rs b/src/map.rs index f25176c..e188480 100644 --- a/src/map.rs +++ b/src/map.rs @@ -1,10 +1,11 @@ // Axel '0vercl0k' Souchet - July 18 2023 //! This implements logic that allows to memory map a file on both //! Unix and Windows (cf [`memory_map_file`] / [`unmap_memory_mapped_file`]). -use std::fmt::Debug; -use std::io::{Read, Seek}; +use std::fmt::{self, Debug}; +use std::fs::File; +use std::io; +use std::io::{Cursor, Read, Seek}; use std::path::Path; -use std::{fs, io, ptr, slice}; pub trait Reader: Read + Seek {} @@ -14,11 +15,11 @@ impl Reader for T where T: Read + Seek {} /// mapping and a cursor to be able to access the region. pub struct MappedFileReader<'map> { mapped_file: &'map [u8], - cursor: io::Cursor<&'map [u8]>, + cursor: Cursor<&'map [u8]>, } impl Debug for MappedFileReader<'_> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("MappedFileReader").finish() } } @@ -27,14 +28,14 @@ impl MappedFileReader<'_> { /// Create a new [`MappedFileReader`] from a path using a memory map. pub fn new(path: impl AsRef) -> io::Result { // Open the file.. - let file = fs::File::open(path)?; + let file = File::open(path)?; // ..and memory map it using the underlying OS-provided APIs. - let mapped_file = memory_map_file(file)?; + let mapped_file = memory_map_file(&file)?; Ok(Self { mapped_file, - cursor: io::Cursor::new(mapped_file), + cursor: Cursor::new(mapped_file), }) } } @@ -55,19 +56,19 @@ impl Seek for MappedFileReader<'_> { /// need to drop the mapping using OS-provided APIs. impl Drop for MappedFileReader<'_> { fn drop(&mut self) { - unmap_memory_mapped_file(self.mapped_file).expect("failed to unmap") + unmap_memory_mapped_file(self.mapped_file).expect("failed to unmap"); } } #[cfg(windows)] #[allow(non_camel_case_types, clippy::upper_case_acronyms)] -/// Module that implements memory mapping on Windows using CreateFileMappingA / -/// MapViewOfFile. +/// Module that implements memory mapping on Windows using `CreateFileMappingA` +/// / `MapViewOfFile`. mod windows { + use std::fs::File; use std::os::windows::prelude::AsRawHandle; use std::os::windows::raw::HANDLE; - - use super::*; + use std::{io, ptr, slice}; const PAGE_READONLY: DWORD = 2; const FILE_MAP_READ: DWORD = 4; @@ -117,7 +118,7 @@ mod windows { } /// Memory map a file into memory. - pub fn memory_map_file<'map>(file: fs::File) -> Result<&'map [u8], io::Error> { + pub fn memory_map_file<'map>(file: &File) -> Result<&'map [u8], io::Error> { // Grab the underlying HANDLE. let file_handle = file.as_raw_handle(); @@ -161,9 +162,7 @@ mod windows { } // Make sure the size is not bigger than what [`slice::from_raw_parts`] wants. - if size > isize::MAX.try_into().unwrap() { - panic!("slice is too large"); - } + assert!(size <= isize::MAX.try_into().unwrap(), "slice is too large"); // Create the slice over the mapping. // SAFETY: This is safe because: @@ -191,14 +190,14 @@ mod windows { } #[cfg(windows)] -use windows::*; +use windows::{memory_map_file, unmap_memory_mapped_file}; #[cfg(unix)] /// Module that implements memory mapping on Unix using the mmap syscall. mod unix { + use std::fs::File; use std::os::fd::AsRawFd; - - use super::*; + use std::{io, ptr, slice}; const PROT_READ: i32 = 1; const MAP_SHARED: i32 = 1; @@ -217,7 +216,7 @@ mod unix { fn munmap(addr: *const u8, length: usize) -> i32; } - pub fn memory_map_file<'map>(file: fs::File) -> Result<&'map [u8], io::Error> { + pub fn memory_map_file<'map>(file: &File) -> Result<&'map [u8], io::Error> { // Grab the underlying file descriptor. let file_fd = file.as_raw_fd(); diff --git a/src/parse.rs b/src/parse.rs index 1f42d06..c5139d9 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -4,8 +4,9 @@ use core::slice; use std::cell::RefCell; use std::cmp::min; use std::collections::HashMap; -use std::fmt::Debug; +use std::fmt::{self, Debug}; use std::fs::File; +use std::mem::MaybeUninit; use std::ops::Range; use std::path::Path; use std::{io, mem}; @@ -68,6 +69,7 @@ impl VirtTranslationDetails { } } + #[must_use] pub fn gpa(&self) -> Gpa { self.pfn.gpa_with_offset(self.offset) } @@ -107,8 +109,8 @@ macro_rules! impl_checked_add { impl_checked_add!(u32, u64); -/// Walk a LIST_ENTRY of LdrDataTableEntry. It is used to dump both the user & -/// driver / module lists. +/// Walk a `LIST_ENTRY` of `LdrDataTableEntry`. It is used to dump both the user +/// & driver / module lists. fn try_read_module_map

(parser: &mut KernelDumpParser, head: Gva) -> Result> where P: PtrSize, @@ -389,10 +391,10 @@ pub struct KernelDumpParser { } impl Debug for KernelDumpParser { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("KernelDumpParser") .field("dump_type", &self.dump_type) - .finish() + .finish_non_exhaustive() } } @@ -428,8 +430,8 @@ impl KernelDumpParser { headers, physmem, reader, - kernel_modules: Default::default(), - user_modules: Default::default(), + kernel_modules: HashMap::default(), + user_modules: HashMap::default(), }; // Extract the kernel modules if we can. If it fails because of a memory @@ -468,22 +470,19 @@ impl KernelDumpParser { } pub fn new(dump_path: impl AsRef) -> Result { + const FOUR_GIGS: u64 = 1_024 * 1_024 * 1_024 * 4; // We'll assume that if you are opening a dump file larger than 4gb, you don't // want it memory mapped. let size = dump_path.as_ref().metadata()?.len(); - const FOUR_GIGS: u64 = 1_024 * 1_024 * 1_024 * 4; - match size { - 0..=FOUR_GIGS => { - let mapped_file = MappedFileReader::new(dump_path.as_ref())?; + if let 0..=FOUR_GIGS = size { + let mapped_file = MappedFileReader::new(dump_path.as_ref())?; - Self::with_reader(mapped_file) - } - _ => { - let file = File::open(dump_path)?; + Self::with_reader(mapped_file) + } else { + let file = File::open(dump_path)?; - Self::with_reader(file) - } + Self::with_reader(file) } } @@ -555,7 +554,7 @@ impl KernelDumpParser { // So let's figure out the maximum amount of bytes we can read off this page. // Either, we read it until its end, or we stop if the user wants us to read // less. - let left_in_page = (PageKind::Normal.size() - gpa.offset()) as usize; + let left_in_page = usize::try_from(PageKind::Normal.size() - gpa.offset()).unwrap(); let amount_wanted = min(amount_left, left_in_page); // Figure out where we should read into. let slice = &mut buf[total_read..total_read + amount_wanted]; @@ -595,10 +594,10 @@ impl KernelDumpParser { /// Read a `T` from physical memory. pub fn phys_read_struct(&self, gpa: Gpa) -> Result { - let mut t = mem::MaybeUninit::uninit(); - let size_of_t = mem::size_of_val(&t); + let mut t: MaybeUninit = MaybeUninit::uninit(); + let size_of_t = size_of_val(&t); let slice_over_t = - unsafe { slice::from_raw_parts_mut(t.as_mut_ptr() as *mut u8, size_of_t) }; + unsafe { slice::from_raw_parts_mut(t.as_mut_ptr().cast::(), size_of_t) }; self.phys_read_exact(gpa, slice_over_t)?; @@ -612,6 +611,7 @@ impl KernelDumpParser { /// Translate a [`Gva`] into a [`Gpa`] using a specific directory table base /// / set of page tables. + #[allow(clippy::similar_names)] pub fn virt_translate_with_dtb(&self, gva: Gva, dtb: Gpa) -> Result { // Aligning in case PCID bits are set (bits 11:0) let pml4_base = dtb.page_align(); @@ -630,7 +630,7 @@ impl KernelDumpParser { // huge pages: // 7 (PS) - Page size; must be 1 (otherwise, this entry references a page - // directory; see Table 4-1 + // directory; see Table 4-1. let pd_base = pdpte.pfn.gpa(); if pdpte.large_page() { return Ok(VirtTranslationDetails::new(&[pml4e, pdpte], gva)); @@ -644,7 +644,7 @@ impl KernelDumpParser { // large pages: // 7 (PS) - Page size; must be 1 (otherwise, this entry references a page - // table; see Table 4-18 + // table; see Table 4-18. let pt_base = pde.pfn.gpa(); if pde.large_page() { return Ok(VirtTranslationDetails::new(&[pml4e, pdpte, pde], gva)); @@ -695,7 +695,8 @@ impl KernelDumpParser { // We need to take care of reads that straddle different virtual memory pages. // First, figure out the maximum amount of bytes we can read off this page. - let left_in_page = (translation.page_kind.size() - translation.offset) as usize; + let left_in_page = + usize::try_from(translation.page_kind.size() - translation.offset).unwrap(); // Then, either we read it until its end, or we stop before if we can get by // with less. let amount_wanted = min(amount_left, left_in_page); @@ -789,10 +790,10 @@ impl KernelDumpParser { /// Read a `T` from virtual memory using a specific directory table base / /// set of page tables. pub fn virt_read_struct_with_dtb(&self, gva: Gva, dtb: Gpa) -> Result { - let mut t = mem::MaybeUninit::uninit(); - let size_of_t = mem::size_of_val(&t); + let mut t: MaybeUninit = MaybeUninit::uninit(); + let size_of_t = size_of_val(&t); let slice_over_t = - unsafe { slice::from_raw_parts_mut(t.as_mut_ptr() as *mut u8, size_of_t) }; + unsafe { slice::from_raw_parts_mut(t.as_mut_ptr().cast::(), size_of_t) }; self.virt_read_exact_with_dtb(gva, slice_over_t, dtb)?; @@ -850,11 +851,11 @@ impl KernelDumpParser { let mut buffer = vec![0; unicode_str.length.into()]; match self.virt_read_exact_with_dtb(Gva::new(unicode_str.buffer.into()), &mut buffer, dtb) { - Ok(_) => {} + Ok(()) => {} // If we encountered a memory translation error, we don't consider this a failure. Err(KdmpParserError::AddrTranslation(_)) => return Ok(None), Err(e) => return Err(e), - }; + } let n = unicode_str.length / 2; @@ -869,17 +870,17 @@ impl KernelDumpParser { /// physical pages starting at a `PFN`. This means that you can have /// "holes" in the physical address space and you don't need to write any /// data for them. Here is a small example: - /// - Run[0]: BasePage = 1_337, PageCount = 2 - /// - Run[1]: BasePage = 1_400, PageCount = 1 + /// - `Run[0]`: `BasePage = 1_337`, `PageCount = 2` + /// - `Run[1]`: `BasePage = 1_400`, `PageCount = 1` /// - /// In the above, there is a "hole" between the two runs. It has 2+1 memory - /// pages at: Pfn(1_337+0), Pfn(1_337+1) and Pfn(1_400+0) (but nothing - /// at Pfn(1_339)). + /// In the above, there is a "hole" between the two runs. It has `2+1` + /// memory pages at: `Pfn(1_337+0)`, `Pfn(1_337+1)` and `Pfn(1_400+0)` + /// (but nothing at `Pfn(1_339)`). /// /// In terms of the content of those physical memory pages, they are packed /// and stored one after another. If the first page of the first run is - /// at file offset 0x2_000, then the first page of the second run is at - /// file offset 0x2_000+(2*0x1_000). + /// at file offset `0x2_000`, then the first page of the second run is at + /// file offset `0x2_000+(2*0x1_000)`. fn full_physmem(headers: &Header64, reader: &mut impl Reader) -> Result { let mut page_offset = reader.stream_position()?; let mut run_cursor = io::Cursor::new(headers.physical_memory_block_buffer); diff --git a/src/pxe.rs b/src/pxe.rs index fcc83b6..c45ae5a 100644 --- a/src/pxe.rs +++ b/src/pxe.rs @@ -13,24 +13,76 @@ //! let encoded = u64::from(pxe); //! let decoded = Pxe::from(encoded); //! ``` -use bitflags::bitflags; - -use crate::Gpa; - -bitflags! { - /// The various bits and flags that a [`Pxe`] has. - #[derive(Debug, Clone, Copy, Eq, Hash, PartialEq, Default, PartialOrd, Ord)] - pub struct PxeFlags : u64 { - const Present = 1 << 0; - const Writable = 1 << 1; - const UserAccessible = 1 << 2; - const WriteThrough = 1 << 3; - const CacheDisabled = 1 << 4; - const Accessed = 1 << 5; - const Dirty = 1 << 6; - const LargePage = 1 << 7; - const Transition = 1 << 11; - const NoExecute = 1 << 63; +use std::ops::Deref; + +use crate::{Bits, Gpa}; + +/// The various bits and flags that a [`Pxe`] has. +#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq, Default, PartialOrd, Ord)] +pub struct PxeFlags(u64); + +impl PxeFlags { + #[must_use] + pub fn new(bits: u64) -> Self { + Self(bits) + } + + #[must_use] + pub fn present(&self) -> bool { + self.0.bit(0) != 0 + } + + #[must_use] + pub fn writable(&self) -> bool { + self.0.bit(1) != 0 + } + + #[must_use] + pub fn user_accessible(&self) -> bool { + self.0.bit(2) != 0 + } + + #[must_use] + pub fn write_through(&self) -> bool { + self.0.bit(3) != 0 + } + + #[must_use] + pub fn cache_disabled(&self) -> bool { + self.0.bit(4) != 0 + } + + #[must_use] + pub fn accessed(&self) -> bool { + self.0.bit(5) != 0 + } + + #[must_use] + pub fn dirty(&self) -> bool { + self.0.bit(6) != 0 + } + + #[must_use] + pub fn large_page(&self) -> bool { + self.0.bit(7) != 0 + } + + #[must_use] + pub fn transition(&self) -> bool { + self.0.bit(11) != 0 + } + + #[must_use] + pub fn no_execute(&self) -> bool { + self.0.bit(63) != 0 + } +} + +impl Deref for PxeFlags { + type Target = u64; + + fn deref(&self) -> &Self::Target { + &self.0 } } @@ -49,18 +101,22 @@ bitflags! { pub struct Pfn(u64); impl Pfn { + #[must_use] pub const fn new(pfn: u64) -> Self { Self(pfn) } + #[must_use] pub const fn u64(&self) -> u64 { self.0 } + #[must_use] pub const fn gpa(&self) -> Gpa { Gpa::from_pfn(*self) } + #[must_use] pub const fn gpa_with_offset(&self, offset: u64) -> Gpa { Gpa::from_pfn_with_offset(*self, offset) } @@ -105,6 +161,7 @@ impl Pxe { /// assert_eq!(pxe.pfn.u64(), 0x6d600); /// # } /// ``` + #[must_use] pub fn new(pfn: Pfn, flags: PxeFlags) -> Self { Self { pfn, flags } } @@ -128,8 +185,9 @@ impl Pxe { /// assert!(!np.present()); /// # } /// ``` + #[must_use] pub fn present(&self) -> bool { - self.flags.contains(PxeFlags::Present) + self.flags.present() } /// Is it a large page? @@ -151,8 +209,9 @@ impl Pxe { /// assert!(!np.large_page()); /// # } /// ``` + #[must_use] pub fn large_page(&self) -> bool { - self.flags.contains(PxeFlags::LargePage) + self.flags.large_page() } /// Is it a transition PTE? @@ -168,8 +227,9 @@ impl Pxe { /// assert!(!np.transition()); /// # } /// ``` + #[must_use] pub fn transition(&self) -> bool { - !self.present() && self.flags.contains(PxeFlags::Transition) + !self.present() && self.flags.transition() } /// Is the memory described by this [`Pxe`] writable? @@ -185,8 +245,9 @@ impl Pxe { /// assert!(!ro.writable()); /// # } /// ``` + #[must_use] pub fn writable(&self) -> bool { - self.flags.contains(PxeFlags::Writable) + self.flags.writable() } /// Is the memory described by this [`Pxe`] executable? @@ -202,8 +263,9 @@ impl Pxe { /// assert!(!nx.executable()); /// # } /// ``` + #[must_use] pub fn executable(&self) -> bool { - !self.flags.contains(PxeFlags::NoExecute) + !self.flags.no_execute() } /// Is the memory described by this [`Pxe`] accessible by user-mode? @@ -219,8 +281,9 @@ impl Pxe { /// assert!(!s.user_accessible()); /// # } /// ``` + #[must_use] pub fn user_accessible(&self) -> bool { - self.flags.contains(PxeFlags::UserAccessible) + self.flags.user_accessible() } } @@ -240,7 +303,7 @@ impl From for Pxe { /// ``` fn from(value: u64) -> Self { let pfn = Pfn::new((value >> 12) & 0xf_ffff_ffff); - let flags = PxeFlags::from_bits(value & PxeFlags::all().bits()).expect("PxeFlags"); + let flags = PxeFlags::new(value); Self::new(pfn, flags) } @@ -265,6 +328,6 @@ impl From for u64 { fn from(pxe: Pxe) -> Self { debug_assert!(pxe.pfn.u64() <= 0xf_ffff_ffffu64); - pxe.flags.bits() | (pxe.pfn.u64() << 12u64) + *pxe.flags | (pxe.pfn.u64() << 12u64) } } diff --git a/src/structs.rs b/src/structs.rs index e85084f..a0584a3 100644 --- a/src/structs.rs +++ b/src/structs.rs @@ -2,7 +2,9 @@ //! This has all the raw structures that makes up Windows kernel crash-dumps. use std::collections::BTreeMap; use std::fmt::Debug; -use std::{io, mem, slice}; +use std::io::SeekFrom; +use std::mem::MaybeUninit; +use std::slice; use crate::error::Result; use crate::{Gpa, KdmpParserError, Reader}; @@ -20,6 +22,7 @@ pub enum PageKind { impl PageKind { /// Size in bytes of the page. + #[must_use] pub fn size(&self) -> u64 { match self { Self::Normal => 4 * 1_024, @@ -29,6 +32,7 @@ impl PageKind { } /// Extract the page offset of `addr`. + #[must_use] pub fn page_offset(&self, addr: u64) -> u64 { let mask = self.size() - 1; @@ -40,12 +44,12 @@ impl PageKind { #[derive(Debug, Clone, Copy, PartialEq)] #[repr(u32)] pub enum DumpType { - // Old dump types from dbgeng.dll + // Old dump types from `dbgeng.dll`. Full = 0x1, Bmp = 0x5, /// Produced by `.dump /m`. // Mini = 0x4, - /// (22H2+) Produced by TaskMgr > System > Create live kernel Memory Dump. + /// (22H2+) Produced by `TaskMgr > System > Create live kernel Memory Dump`. LiveKernelMemory = 0x6, /// Produced by `.dump /k`. KernelMemory = 0x8, @@ -163,7 +167,7 @@ impl Debug for Header64 { .field("kd_secondary_version", &self.kd_secondary_version) .field("attributes", &self.attributes) .field("boot_id", &self.boot_id) - .finish() + .finish_non_exhaustive() } } @@ -183,13 +187,13 @@ pub struct BmpHeader64 { // )]], // # The offset of the first page in the file. // 'FirstPage': [0x20, ['unsigned long long']], - padding1: [u8; 0x20 - (0x4 + mem::size_of::())], + padding1: [u8; 0x20 - (0x4 + size_of::())], /// The offset of the first page in the file. pub first_page: u64, /// Total number of pages present in the bitmap. pub total_present_pages: u64, /// Total number of pages in image. This dictates the total size of the - /// bitmap.This is not the same as the TotalPresentPages which is only + /// bitmap. This is not the same as the `TotalPresentPages` which is only /// the sum of the bits set to 1. pub pages: u64, // Bitmap follows @@ -237,7 +241,7 @@ impl TryFrom<&[u8]> for PhysmemDesc { type Error = KdmpParserError; fn try_from(slice: &[u8]) -> Result { - let expected_len = mem::size_of::(); + let expected_len = size_of::(); if slice.len() < expected_len { return Err(KdmpParserError::InvalidData("physmem desc is too small")); } @@ -379,19 +383,19 @@ impl Debug for Context { .field("last_branch_from_rip", &self.last_branch_from_rip) .field("last_exception_to_rip", &self.last_exception_to_rip) .field("last_exception_from_rip", &self.last_exception_from_rip) - .finish() + .finish_non_exhaustive() } } /// Peek for a `T` from the cursor. pub fn peek_struct(reader: &mut impl Reader) -> Result { - let mut s = mem::MaybeUninit::uninit(); - let size_of_s = mem::size_of_val(&s); - let slice_over_s = unsafe { slice::from_raw_parts_mut(s.as_mut_ptr() as *mut u8, size_of_s) }; + let mut s: MaybeUninit = MaybeUninit::uninit(); + let size_of_s = size_of_val(&s); + let slice_over_s = unsafe { slice::from_raw_parts_mut(s.as_mut_ptr().cast::(), size_of_s) }; let pos = reader.stream_position()?; reader.read_exact(slice_over_s)?; - reader.seek(io::SeekFrom::Start(pos))?; + reader.seek(SeekFrom::Start(pos))?; Ok(unsafe { s.assume_init() }) } @@ -399,9 +403,9 @@ pub fn peek_struct(reader: &mut impl Reader) -> Result { /// Read a `T` from the cursor. pub fn read_struct(reader: &mut impl Reader) -> Result { let s = peek_struct(reader)?; - let size_of_s = mem::size_of_val(&s); + let size_of_s = size_of_val(&s); - reader.seek(io::SeekFrom::Current(size_of_s.try_into().unwrap()))?; + reader.seek(SeekFrom::Current(size_of_s.try_into().unwrap()))?; Ok(s) } @@ -520,18 +524,18 @@ pub struct KdDebuggerData64 { pub header: DbgKdDebugDataHeader64, /// Base address of kernel image pub kern_base: u64, - /// DbgBreakPointWithStatus is a function which takes an argument - /// and hits a breakpoint. This field contains the address of the - /// breakpoint instruction. When the debugger sees a breakpoint + /// `DbgBreakPointWithStatus` is a function which takes an argument + /// and hits a breakpoint. This field contains the address of the + /// breakpoint instruction. When the debugger sees a breakpoint /// at this address, it may retrieve the argument from the first /// argument register, or on x86 the eax register. pub breakpoint_with_status: u64, /// Address of the saved context record during a bugcheck - /// N.B. This is an automatic in KeBugcheckEx's frame, and + /// N.B. This is an automatic in `KeBugcheckEx`'s frame, and /// is only valid after a bugcheck. pub saved_context: u64, /// The address of the thread structure is provided in the - /// WAIT_STATE_CHANGE packet. This is the offset from the base of + /// `WAIT_STATE_CHANGE` packet. This is the offset from the base of /// the thread structure to the pointer to the kernel stack frame /// for the currently active usermode callback. pub th_callback_stack: u16, From de5f7c953d964ced30030921107b7f5783760af1 Mon Sep 17 00:00:00 2001 From: 0vercl0k <1476421+0vercl0k@users.noreply.github.com> Date: Thu, 9 Oct 2025 01:18:28 -0700 Subject: [PATCH 4/8] bit of comment --- src/error.rs | 2 +- src/map.rs | 13 ++++++------ src/parse.rs | 57 +++++++++++++++++++++++++++++++++++++++------------- 3 files changed, 51 insertions(+), 21 deletions(-) diff --git a/src/error.rs b/src/error.rs index b2cf307..3f28010 100644 --- a/src/error.rs +++ b/src/error.rs @@ -26,7 +26,7 @@ pub enum AddrTranslationError { impl Error for AddrTranslationError {} impl Display for AddrTranslationError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { AddrTranslationError::Virt(gva, not_pres) => { write!(f, "virt to phys translation of {gva}: {not_pres:?}") diff --git a/src/map.rs b/src/map.rs index e188480..f14d86a 100644 --- a/src/map.rs +++ b/src/map.rs @@ -3,8 +3,7 @@ //! Unix and Windows (cf [`memory_map_file`] / [`unmap_memory_mapped_file`]). use std::fmt::{self, Debug}; use std::fs::File; -use std::io; -use std::io::{Cursor, Read, Seek}; +use std::io::{self, Cursor, Read, Seek}; use std::path::Path; pub trait Reader: Read + Seek {} @@ -26,6 +25,10 @@ impl Debug for MappedFileReader<'_> { impl MappedFileReader<'_> { /// Create a new [`MappedFileReader`] from a path using a memory map. + /// + /// # Errors + /// + /// Returns an error if the file cannot be opened or memory mapped. pub fn new(path: impl AsRef) -> io::Result { // Open the file.. let file = File::open(path)?; @@ -233,9 +236,7 @@ mod unix { } // Make sure the size is not bigger than what [`slice::from_raw_parts`] wants. - if size > isize::MAX.try_into().unwrap() { - panic!("slice is too large"); - } + assert!(size <= isize::MAX.try_into().unwrap(), "slice is too large"); // Create the slice over the mapping. // SAFETY: This is safe because: @@ -263,7 +264,7 @@ mod unix { } #[cfg(unix)] -use unix::*; +use unix::{memory_map_file, unmap_memory_mapped_file}; #[cfg(not(any(windows, unix)))] /// Your system hasn't been implemented; if you do it, send a PR! diff --git a/src/parse.rs b/src/parse.rs index c5139d9..6e1a488 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -6,6 +6,7 @@ use std::cmp::min; use std::collections::HashMap; use std::fmt::{self, Debug}; use std::fs::File; +use std::io::SeekFrom; use std::mem::MaybeUninit; use std::ops::Range; use std::path::Path; @@ -46,6 +47,11 @@ pub struct VirtTranslationDetails { } impl VirtTranslationDetails { + /// Create a new instance from a slice of PXEs and the original GVA. + /// + /// # Panics + /// + /// Panics if `pxes` is malformed (i.e. not between 2 and 4 entries). pub fn new(pxes: &[Pxe], gva: Gva) -> Self { let writable = pxes.iter().all(Pxe::writable); let executable = pxes.iter().all(Pxe::executable); @@ -399,8 +405,12 @@ impl Debug for KernelDumpParser { } impl KernelDumpParser { - /// Create an instance from a file path. This memory maps the file and - /// parses it. + /// Create an instance from a [`Reader`] & parse the file. + /// + /// # Errors + /// + /// Returns an error if the dump is malformed or if we encounter an I/O + /// error. pub fn with_reader(mut reader: impl Reader + 'static) -> Result { // Parse the dump header and check if things look right. let headers = Box::new(read_struct::(&mut reader)?); @@ -441,7 +451,7 @@ impl KernelDumpParser { } // Now let's try to find out user-modules. For that we need the - // KDDEBUGGER_DATA_BLOCK structure to know where a bunch of things are. + // `KDDEBUGGER_DATA_BLOCK` structure to know where a bunch of things are. // If we can't read the block, we'll have to stop the adventure here as we won't // be able to read the things we need to keep going. let Some(kd_debugger_data_block) = parser.try_virt_read_struct::( @@ -469,6 +479,12 @@ impl KernelDumpParser { Ok(parser) } + /// Create an instance from a file path; depending on the file size, it'll + /// either memory maps it or open it as a regular file. + /// + /// # Errors + /// + /// Returns an error if the file can't be memory mapped or opened. pub fn new(dump_path: impl AsRef) -> Result { const FOUR_GIGS: u64 = 1_024 * 1_024 * 1_024 * 4; // We'll assume that if you are opening a dump file larger than 4gb, you don't @@ -525,7 +541,13 @@ impl KernelDumpParser { /// Translate a [`Gpa`] into a file offset of where the content of the page /// resides in. - pub fn phys_translate(&self, gpa: Gpa) -> Result { + /// + /// # Errors + /// + /// Returns an error if the `gpa` has no backing page or if an integer + /// overflow is triggered while calculating where in the input file the + /// backing page is at. + pub fn phys_translate(&self, gpa: Gpa) -> Result { let offset = *self .physmem .get(&gpa.page_align()) @@ -533,6 +555,7 @@ impl KernelDumpParser { offset .checked_add(gpa.offset()) + .map(SeekFrom::Start) .ok_or(KdmpParserError::Overflow("w/ gpa offset")) } @@ -549,7 +572,7 @@ impl KernelDumpParser { // Translate the gpa into a file offset.. let phy_offset = self.phys_translate(addr)?; // ..and seek the reader there. - self.seek(io::SeekFrom::Start(phy_offset))?; + self.seek(phy_offset)?; // We need to take care of reads that straddle different physical memory pages. // So let's figure out the maximum amount of bytes we can read off this page. // Either, we read it until its end, or we stop if the user wants us to read @@ -683,14 +706,10 @@ impl KernelDumpParser { // occured if we already have read some bytes. let translation = match self.virt_translate_with_dtb(addr, dtb) { Ok(tr) => tr, - Err(e) => { - if total_read > 0 { - // If we already read some bytes, return how many we read. - return Ok(total_read); - } - - return Err(e); - } + // If we already read some bytes, return how many we read.. + Err(_) if total_read > 0 => return Ok(total_read), + // ..otherwise this is an error. + Err(e) => return Err(e), }; // We need to take care of reads that straddle different virtual memory pages. @@ -800,7 +819,7 @@ impl KernelDumpParser { Ok(unsafe { t.assume_init() }) } - /// Try to read a `T` from virtual memory . If a memory translation error + /// Try to read a `T` from virtual memory. If a memory translation error /// occurs, it'll return `None` instead of an error. pub fn try_virt_read_struct(&self, gva: Gva) -> Result> { self.try_virt_read_struct_with_dtb::(gva, Gpa::new(self.headers.directory_table_base)) @@ -813,10 +832,20 @@ impl KernelDumpParser { filter_addr_translation_err(self.virt_read_struct_with_dtb::(gva, dtb)) } + /// Seek to `pos`. + /// + /// # Errors + /// + /// Returns an error if the file cannot be seeked to `pos`. pub fn seek(&self, pos: io::SeekFrom) -> Result { Ok(self.reader.borrow_mut().seek(pos)?) } + /// Read however many bytes in `buf` and returns the amount of bytes read. + /// + /// # Errors + /// + /// Returns an error if it encountered any kind of I/O error. pub fn read(&self, buf: &mut [u8]) -> Result { Ok(self.reader.borrow_mut().read(buf)?) } From e35321c40c75e1c1b09b744a132116b8914d3600 Mon Sep 17 00:00:00 2001 From: 0vercl0k <1476421+0vercl0k@users.noreply.github.com> Date: Tue, 21 Oct 2025 16:43:17 -0700 Subject: [PATCH 5/8] rework errors, remove dependencies --- Cargo.toml | 2 +- README.md | 2 +- src/error.rs | 132 ++++++++++++++--- src/lib.rs | 2 +- src/parse.rs | 352 ++++++++++++++++++++++++++++++-------------- src/pxe.rs | 46 ++++-- tests/regression.rs | 46 +++--- 7 files changed, 412 insertions(+), 170 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 359a36e..029976f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "kdmp-parser" -version = "0.7.0" +version = "0.8.0" edition = "2024" authors = ["Axel '0vercl0k' Souchet"] categories = ["parser-implementations"] diff --git a/README.md b/README.md index b8e1ede..9575d15 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@

kdmp-parser

- A KISS Rust crate to parse Windows kernel crash-dumps created by Windows & its debugger. + A KISS, dependency free Rust crate to parse Windows kernel crash-dumps created by Windows & its debugger.

diff --git a/src/error.rs b/src/error.rs index 3f28010..ef9d801 100644 --- a/src/error.rs +++ b/src/error.rs @@ -9,29 +9,117 @@ use crate::structs::{DUMP_HEADER64_EXPECTED_SIGNATURE, DUMP_HEADER64_EXPECTED_VA use crate::{Gpa, Gva}; pub type Result = std::result::Result; -#[derive(Debug)] -pub enum PxeNotPresent { +/// Identifies which page table entry level. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PxeKind { Pml4e, Pdpte, Pde, Pte, } -#[derive(Debug)] -pub enum AddrTranslationError { - Virt(Gva, PxeNotPresent), - Phys(Gpa), +/// Represent the fundamental reason a single page read can fail. +#[derive(Debug, Clone)] +pub enum PageReadError { + /// Virtual address translation failed because a page table entry is not + /// present (it exists in the dump but is marked as not present). + NotPresent { gva: Gva, which_pxe: PxeKind }, + /// A physical page is missing from the dump. XXX: + NotInDump { + gva: Option<(Gva, Option)>, + gpa: Gpa, + }, +} + +impl Error for PageReadError {} + +/// Recoverable memory errors that can occur during memory reads. +/// +/// There are several failure conditions that can happen while trying to read +/// virtual (or physical) memory out of a crash-dump that might not be obvious. +/// +/// For example, consider reading two 4K pages from the virtual address +/// `0x1337_000`; it can fail because: +/// - The virtual address (the first 4K page) isn't present in the address space +/// at the `Pde` level: `MemoryError::PageRead(PageReadError::PageNotPresent { +/// gva: 0x1337_000, which_pxe: PxeKind::Pde })` +/// - The `Pde` that needs reading as part of the translation (of the first +/// page) isn't part of the crash-dump: +/// `MemoryError::PageRead(PageReadError::TranslationPageNotInDump { gva: +/// 0x1337_000, gpa: .., which_pxe: PxeKind::Pde })` +/// - The physical page backing that virtual address isn't included in the +/// crash-dump: `MemoryError::PageRead(PageReadError::BackingPageNotInDump { +/// gva: 0x1337_000, gpa: .. })` +/// - Reading the second (and only the second) page failed because of any of the +/// previous reasons: `MemoryError::PartialRead { expected_amount: 8_192, +/// actual_amount: 4_096, reason: PageReadError::PageNotPresent { .. } }` +/// +/// Similarly, for physical memory reads starting at `0x1337_000`: +/// - A direct physical page isn't in the crash-dump: +/// `MemoryError::PageRead(PageReadError::PhysicalPageNotInDump { gpa: +/// 0x1337_000 })` +/// - Reading the second page failed: `MemoryError::PartialRead { +/// expected_amount: 8_192, actual_amount: 4_096, reason: +/// PageReadError::PhysicalPageNotInDump { gpa: 0x1338_000 } }` +/// +/// We consider any of those errors 'recoverable' which means that we won't even +/// bubble those up to the callers with the regular APIs. Only the `strict` +/// versions will. +#[derive(Debug, Clone)] +pub enum MemoryReadError { + /// A single page/read failed. + PageRead(PageReadError), + /// A read request was only partially fulfilled. + PartialRead { + expected_amount: usize, + actual_amount: usize, + reason: PageReadError, + }, +} + +impl Display for PageReadError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + PageReadError::NotPresent { gva, which_pxe } => { + write!(f, "{gva} isn't present at the {which_pxe:?} level") + } + PageReadError::NotInDump { gva, gpa } => match gva { + Some((gva, Some(which_pxe))) => write!( + f, + "{gpa} was needed while translating {gva} at the {which_pxe:?} level but is missing from the dump)" + ), + Some((gva, None)) => write!(f, "{gpa} backs {gva} but is missing from the dump)"), + None => { + write!(f, "{gpa} is missing from the dump)") + } + }, + } + } } -impl Error for AddrTranslationError {} +impl Error for MemoryReadError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match self { + MemoryReadError::PageRead(e) => Some(e), + MemoryReadError::PartialRead { reason, .. } => Some(reason), + } + } +} -impl Display for AddrTranslationError { +impl Display for MemoryReadError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - AddrTranslationError::Virt(gva, not_pres) => { - write!(f, "virt to phys translation of {gva}: {not_pres:?}") + MemoryReadError::PageRead(_) => write!(f, "page read"), + MemoryReadError::PartialRead { + expected_amount, + actual_amount, + .. + } => { + write!( + f, + "partially read {actual_amount} off {expected_amount} wanted bytes" + ) } - AddrTranslationError::Phys(gpa) => write!(f, "phys to offset translation of {gpa}"), } } } @@ -50,9 +138,7 @@ pub enum KdmpParserError { PhysAddrOverflow(u32, u64), PageOffsetOverflow(u32, u64), BitmapPageOffsetOverflow(u64, usize), - PartialPhysRead, - PartialVirtRead, - AddrTranslation(AddrTranslationError), + MemoryRead(MemoryReadError), } impl From for KdmpParserError { @@ -67,9 +153,15 @@ impl From for KdmpParserError { } } -impl From for KdmpParserError { - fn from(value: AddrTranslationError) -> Self { - KdmpParserError::AddrTranslation(value) +impl From for KdmpParserError { + fn from(value: MemoryReadError) -> Self { + KdmpParserError::MemoryRead(value) + } +} + +impl From for KdmpParserError { + fn from(value: PageReadError) -> Self { + Self::MemoryRead(MemoryReadError::PageRead(value)) } } @@ -103,9 +195,7 @@ impl Display for KdmpParserError { f, "overflow for page offset w/ bitmap_idx {bitmap_idx} bit_idx {bit_idx}" ), - KdmpParserError::PartialPhysRead => write!(f, "partial physical memory read"), - KdmpParserError::PartialVirtRead => write!(f, "partial virtual memory read"), - KdmpParserError::AddrTranslation(_) => write!(f, "memory translation"), + KdmpParserError::MemoryRead(_) => write!(f, "memory read"), } } } @@ -115,7 +205,7 @@ impl Error for KdmpParserError { match self { KdmpParserError::Utf16(u) => Some(u), KdmpParserError::Io(e) => Some(e), - KdmpParserError::AddrTranslation(a) => Some(a), + KdmpParserError::MemoryRead(m) => Some(m), _ => None, } } diff --git a/src/lib.rs b/src/lib.rs index e150b1a..f9eaee0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,7 +10,7 @@ mod pxe; mod structs; pub use bits::Bits; -pub use error::{AddrTranslationError, KdmpParserError, PxeNotPresent, Result}; +pub use error::{KdmpParserError, MemoryReadError, PageReadError, PxeKind, Result}; pub use gxa::{Gpa, Gva, Gxa}; pub use map::{MappedFileReader, Reader}; pub use parse::{KernelDumpParser, VirtTranslationDetails}; diff --git a/src/parse.rs b/src/parse.rs index 6e1a488..74a030c 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -13,7 +13,7 @@ use std::path::Path; use std::{io, mem}; use crate::bits::Bits; -use crate::error::{PxeNotPresent, Result}; +use crate::error::{MemoryReadError, PageReadError, PxeKind, Result}; use crate::gxa::Gxa; use crate::map::{MappedFileReader, Reader}; use crate::structs::{ @@ -22,7 +22,7 @@ use crate::structs::{ LdrDataTableEntry, ListEntry, PageKind, PfnRange, PhysmemDesc, PhysmemMap, PhysmemRun, UnicodeString, read_struct, }; -use crate::{AddrTranslationError, Gpa, Gva, KdmpParserError, Pfn, Pxe}; +use crate::{Gpa, Gva, KdmpParserError, Pfn, Pxe}; /// The details related to a virtual to physical address translation. /// @@ -122,7 +122,7 @@ where P: PtrSize, { let mut modules = ModuleMap::new(); - let Some(entry) = parser.try_virt_read_struct::>(head)? else { + let Some(entry) = parser.virt_read_struct::>(head)? else { return Ok(None); }; @@ -130,18 +130,18 @@ where // We'll walk it until we hit the starting point (it is circular). while entry_addr != head { // Read the table entry.. - let Some(data) = parser.try_virt_read_struct::>(entry_addr)? else { + let Some(data) = parser.virt_read_struct::>(entry_addr)? else { return Ok(None); }; // ..and read it. We first try to read `full_dll_name` but will try // `base_dll_name` is we couldn't read the former. let Some(dll_name) = parser - .try_virt_read_unicode_string::

(&data.full_dll_name) + .virt_read_unicode_string::

(&data.full_dll_name) .and_then(|s| { if s.is_none() { // If we failed to read the `full_dll_name`, give `base_dll_name` a shot. - parser.try_virt_read_unicode_string::

(&data.base_dll_name) + parser.virt_read_unicode_string::

(&data.base_dll_name) } else { Ok(s) } @@ -181,7 +181,7 @@ fn try_find_prcb( let mut processor_block = kd_debugger_data_block.ki_processor_block; for _ in 0..parser.headers().number_processors { // Read the KPRCB pointer. - let Some(kprcb_addr) = parser.try_virt_read_struct::(processor_block.into())? else { + let Some(kprcb_addr) = parser.virt_read_struct::(processor_block.into())? else { return Ok(None); }; @@ -191,15 +191,13 @@ fn try_find_prcb( .ok_or(KdmpParserError::Overflow("offset_prcb"))?; // ..and read it. - let Some(kprcb_context_addr) = - parser.try_virt_read_struct::(kprcb_context_addr.into())? + let Some(kprcb_context_addr) = parser.virt_read_struct::(kprcb_context_addr.into())? else { return Ok(None); }; // Read the context.. - let Some(kprcb_context) = - parser.try_virt_read_struct::(kprcb_context_addr.into())? + let Some(kprcb_context) = parser.virt_read_struct::(kprcb_context_addr.into())? else { return Ok(None); }; @@ -233,7 +231,7 @@ fn try_extract_user_modules( .u64() .checked_add(kd_debugger_data_block.offset_prcb_current_thread.into()) .ok_or(KdmpParserError::Overflow("offset prcb current thread"))?; - let Some(kthread_addr) = parser.try_virt_read_struct::(kthread_addr.into())? else { + let Some(kthread_addr) = parser.virt_read_struct::(kthread_addr.into())? else { return Ok(None); }; @@ -241,7 +239,7 @@ fn try_extract_user_modules( let teb_addr = kthread_addr .checked_add(kd_debugger_data_block.offset_kthread_teb.into()) .ok_or(KdmpParserError::Overflow("offset kthread teb"))?; - let Some(teb_addr) = parser.try_virt_read_struct::(teb_addr.into())? else { + let Some(teb_addr) = parser.virt_read_struct::(teb_addr.into())? else { return Ok(None); }; @@ -259,7 +257,7 @@ fn try_extract_user_modules( let peb_addr = teb_addr .checked_add(peb_offset) .ok_or(KdmpParserError::Overflow("peb offset"))?; - let Some(peb_addr) = parser.try_virt_read_struct::(peb_addr.into())? else { + let Some(peb_addr) = parser.virt_read_struct::(peb_addr.into())? else { return Ok(None); }; @@ -272,7 +270,7 @@ fn try_extract_user_modules( let peb_ldr_addr = peb_addr .checked_add(ldr_offset) .ok_or(KdmpParserError::Overflow("ldr offset"))?; - let Some(peb_ldr_addr) = parser.try_virt_read_struct::(peb_ldr_addr.into())? else { + let Some(peb_ldr_addr) = parser.virt_read_struct::(peb_ldr_addr.into())? else { return Ok(None); }; @@ -310,7 +308,7 @@ fn try_extract_user_modules( let peb32_addr = teb32_addr .checked_add(peb32_offset) .ok_or(KdmpParserError::Overflow("peb32 offset"))?; - let Some(peb32_addr) = parser.try_virt_read_struct::(peb32_addr.into())? else { + let Some(peb32_addr) = parser.virt_read_struct::(peb32_addr.into())? else { return Ok(Some(modules)); }; @@ -323,8 +321,7 @@ fn try_extract_user_modules( let peb32_ldr_addr = peb32_addr .checked_add(ldr_offset) .ok_or(KdmpParserError::Overflow("ldr32 offset"))?; - let Some(peb32_ldr_addr) = - parser.try_virt_read_struct::(Gva::new(peb32_ldr_addr.into()))? + let Some(peb32_ldr_addr) = parser.virt_read_struct::(Gva::new(peb32_ldr_addr.into()))? else { return Ok(Some(modules)); }; @@ -355,15 +352,15 @@ fn try_extract_user_modules( Ok(Some(modules)) } -/// Filter out [`AddrTranslationError`] errors and turn them into `None`. This +/// Filter out [`MemoryReadError`] errors and turn them into `None`. This /// makes it easier for caller code to write logic that can recover from a /// memory read failure by bailing out for example, and not bubbling up an /// error. -fn filter_addr_translation_err(res: Result) -> Result> { +fn filter_memory_err(res: Result) -> Result> { match res { Ok(o) => Ok(Some(o)), // If we encountered a memory reading error, we won't consider this as a failure. - Err(KdmpParserError::AddrTranslation(..)) => Ok(None), + Err(KdmpParserError::MemoryRead(..)) => Ok(None), Err(e) => Err(e), } } @@ -454,9 +451,8 @@ impl KernelDumpParser { // `KDDEBUGGER_DATA_BLOCK` structure to know where a bunch of things are. // If we can't read the block, we'll have to stop the adventure here as we won't // be able to read the things we need to keep going. - let Some(kd_debugger_data_block) = parser.try_virt_read_struct::( - parser.headers().kd_debugger_data_block.into(), - )? + let Some(kd_debugger_data_block) = parser + .virt_read_struct::(parser.headers().kd_debugger_data_block.into())? else { return Ok(parser); }; @@ -551,7 +547,7 @@ impl KernelDumpParser { let offset = *self .physmem .get(&gpa.page_align()) - .ok_or(AddrTranslationError::Phys(gpa))?; + .ok_or(PageReadError::NotInDump { gva: None, gpa })?; offset .checked_add(gpa.offset()) @@ -611,7 +607,14 @@ impl KernelDumpParser { } // ..otherwise, we call it quits. else { - Err(KdmpParserError::PartialPhysRead) + let gpa = Gpa::new(gpa.u64() + u64::try_from(len).unwrap()); + + Err(MemoryReadError::PartialRead { + expected_amount: buf.len(), + actual_amount: len, + reason: PageReadError::NotInDump { gva: None, gpa }, + } + .into()) } } @@ -639,16 +642,52 @@ impl KernelDumpParser { // Aligning in case PCID bits are set (bits 11:0) let pml4_base = dtb.page_align(); let pml4e_gpa = Gpa::new(pml4_base.u64() + (gva.pml4e_idx() * 8)); - let pml4e = Pxe::from(self.phys_read_struct::(pml4e_gpa)?); + let pml4e = Pxe::from(self.phys_read_struct::(pml4e_gpa).map_err(|e| { + // If reading the PML4E failed due to a memory error, wrap it appropriately + if let KdmpParserError::MemoryRead(MemoryReadError::PageRead( + PageReadError::NotInDump { gpa, .. }, + )) = e + { + PageReadError::NotInDump { + gva: Some((gva, Some(PxeKind::Pml4e))), + gpa, + } + .into() + } else { + e + } + })?); + if !pml4e.present() { - return Err(AddrTranslationError::Virt(gva, PxeNotPresent::Pml4e).into()); + return Err(PageReadError::NotPresent { + gva, + which_pxe: PxeKind::Pml4e, + } + .into()); } let pdpt_base = pml4e.pfn.gpa(); let pdpte_gpa = Gpa::new(pdpt_base.u64() + (gva.pdpe_idx() * 8)); - let pdpte = Pxe::from(self.phys_read_struct::(pdpte_gpa)?); + let pdpte = Pxe::from(self.phys_read_struct::(pdpte_gpa).map_err(|e| { + if let KdmpParserError::MemoryRead(MemoryReadError::PageRead( + PageReadError::NotInDump { gpa, .. }, + )) = e + { + PageReadError::NotInDump { + gpa, + gva: Some((gva, Some(PxeKind::Pdpte))), + } + .into() + } else { + e + } + })?); if !pdpte.present() { - return Err(AddrTranslationError::Virt(gva, PxeNotPresent::Pdpte).into()); + return Err(PageReadError::NotPresent { + gva, + which_pxe: PxeKind::Pdpte, + } + .into()); } // huge pages: @@ -660,9 +699,27 @@ impl KernelDumpParser { } let pde_gpa = Gpa::new(pd_base.u64() + (gva.pde_idx() * 8)); - let pde = Pxe::from(self.phys_read_struct::(pde_gpa)?); + let pde = Pxe::from(self.phys_read_struct::(pde_gpa).map_err(|e| { + if let KdmpParserError::MemoryRead(MemoryReadError::PageRead( + PageReadError::NotInDump { gpa, .. }, + )) = e + { + PageReadError::NotInDump { + gva: Some((gva, Some(PxeKind::Pde))), + gpa, + } + .into() + } else { + e + } + })?); + if !pde.present() { - return Err(AddrTranslationError::Virt(gva, PxeNotPresent::Pde).into()); + return Err(PageReadError::NotPresent { + gva, + which_pxe: PxeKind::Pde, + } + .into()); } // large pages: @@ -674,26 +731,59 @@ impl KernelDumpParser { } let pte_gpa = Gpa::new(pt_base.u64() + (gva.pte_idx() * 8)); - let pte = Pxe::from(self.phys_read_struct::(pte_gpa)?); + let pte = Pxe::from(self.phys_read_struct::(pte_gpa).map_err(|e| { + if let KdmpParserError::MemoryRead(MemoryReadError::PageRead( + PageReadError::NotInDump { gpa, .. }, + )) = e + { + PageReadError::NotInDump { + gva: Some((gva, Some(PxeKind::Pte))), + gpa, + } + .into() + } else { + e + } + })?); + if !pte.present() { // We'll allow reading from a transition PTE, so return an error only if it's // not one, otherwise we'll carry on. if !pte.transition() { - return Err(AddrTranslationError::Virt(gva, PxeNotPresent::Pte).into()); + return Err(PageReadError::NotPresent { + gva, + which_pxe: PxeKind::Pte, + } + .into()); } } Ok(VirtTranslationDetails::new(&[pml4e, pdpte, pde, pte], gva)) } - /// Read virtual memory starting at `gva` into a `buffer`. - pub fn virt_read(&self, gva: Gva, buf: &mut [u8]) -> Result { + /// Read virtual memory starting at `gva` into a `buffer`. Returns `None` if + /// a memory error occurs (page not present, page not in dump, etc.). + pub fn virt_read(&self, gva: Gva, buf: &mut [u8]) -> Result> { self.virt_read_with_dtb(gva, buf, Gpa::new(self.headers.directory_table_base)) } /// Read virtual memory starting at `gva` into a `buffer` using a specific - /// directory table base / set of page tables. - pub fn virt_read_with_dtb(&self, gva: Gva, buf: &mut [u8], dtb: Gpa) -> Result { + /// directory table base / set of page tables. Returns `None` if a memory + /// error occurs (page not present, page not in dump, etc.). + pub fn virt_read_with_dtb(&self, gva: Gva, buf: &mut [u8], dtb: Gpa) -> Result> { + filter_memory_err(self.virt_read_strict_with_dtb(gva, buf, dtb)) + } + + /// Read virtual memory starting at `gva` into a `buffer`, propagating all + /// errors including memory errors. + pub fn virt_read_strict(&self, gva: Gva, buf: &mut [u8]) -> Result { + self.virt_read_strict_with_dtb(gva, buf, Gpa::new(self.headers.directory_table_base)) + } + + /// Read virtual memory starting at `gva` into a `buffer` using a specific + /// directory table base / set of page tables, propagating all errors + /// including memory errors. + pub fn virt_read_strict_with_dtb(&self, gva: Gva, buf: &mut [u8], dtb: Gpa) -> Result { // Amount of bytes left to read. let mut amount_left = buf.len(); // Total amount of bytes that we have successfully read. @@ -706,8 +796,17 @@ impl KernelDumpParser { // occured if we already have read some bytes. let translation = match self.virt_translate_with_dtb(addr, dtb) { Ok(tr) => tr, - // If we already read some bytes, return how many we read.. - Err(_) if total_read > 0 => return Ok(total_read), + // If we already read some bytes, convert the error to a PartialRead.. + Err(KdmpParserError::MemoryRead(MemoryReadError::PageRead(reason))) + if total_read > 0 => + { + return Err(MemoryReadError::PartialRead { + expected_amount: buf.len(), + actual_amount: total_read, + reason, + } + .into()); + } // ..otherwise this is an error. Err(e) => return Err(e), }; @@ -723,7 +822,40 @@ impl KernelDumpParser { let slice = &mut buf[total_read..total_read + amount_wanted]; // Read the physical memory! - let amount_read = self.phys_read(translation.gpa(), slice)?; + let gpa = translation.gpa(); + let amount_read = match self.phys_read(gpa, slice) { + Ok(n) => n, + Err(KdmpParserError::MemoryRead(MemoryReadError::PageRead(reason))) + if total_read > 0 => + { + // Convert physical read error to include virtual address context + let reason = match reason { + PageReadError::NotInDump { gpa, .. } => PageReadError::NotInDump { + gva: Some((addr, None)), + gpa, + }, + other => other, + }; + + return Err(MemoryReadError::PartialRead { + expected_amount: buf.len(), + actual_amount: total_read, + reason, + } + .into()); + } + Err(KdmpParserError::MemoryRead(MemoryReadError::PageRead( + PageReadError::NotInDump { gpa, .. }, + ))) => { + // First read failed, convert to BackingPageNotInDump + return Err(PageReadError::NotInDump { + gva: Some((addr, None)), + gpa, + } + .into()); + } + Err(e) => return Err(e), + }; // Update the total amount of read bytes and how much work we have left. total_read += amount_read; amount_left -= amount_read; @@ -740,98 +872,100 @@ impl KernelDumpParser { Ok(total_read) } - /// Try to read virtual memory starting at `gva` into a `buffer`. If a - /// memory translation error occurs, it'll return `None` instead of an - /// error. - pub fn try_virt_read(&self, gva: Gva, buf: &mut [u8]) -> Result> { - filter_addr_translation_err(self.virt_read(gva, buf)) + /// Read an exact amount of virtual memory starting at `gva`. Returns `None` + /// if a memory error occurs (page not present, page not in dump, etc.). + pub fn virt_read_exact(&self, gva: Gva, buf: &mut [u8]) -> Result> { + self.virt_read_exact_with_dtb(gva, buf, Gpa::new(self.headers.directory_table_base)) } - /// Try to read virtual memory starting at `gva` into a `buffer` using a - /// specific directory table base / set of page tables. If a - /// memory translation error occurs, it'll return `None` instead of an - /// error. - pub fn try_virt_read_with_dtb( + /// Read an exact amount of virtual memory starting at `gva` using a + /// specific directory table base / set of page tables. Returns `None` if a + /// memory error occurs (page not present, page not in dump, etc.). + pub fn virt_read_exact_with_dtb( &self, gva: Gva, buf: &mut [u8], dtb: Gpa, - ) -> Result> { - filter_addr_translation_err(self.virt_read_with_dtb(gva, buf, dtb)) + ) -> Result> { + filter_memory_err(self.virt_read_exact_strict_with_dtb(gva, buf, dtb)) } - /// Read an exact amount of virtual memory starting at `gva`. - pub fn virt_read_exact(&self, gva: Gva, buf: &mut [u8]) -> Result<()> { - self.virt_read_exact_with_dtb(gva, buf, Gpa::new(self.headers.directory_table_base)) + /// Read an exact amount of virtual memory starting at `gva`, propagating + /// all errors including memory errors. + pub fn virt_read_exact_strict(&self, gva: Gva, buf: &mut [u8]) -> Result<()> { + self.virt_read_exact_strict_with_dtb(gva, buf, Gpa::new(self.headers.directory_table_base)) } /// Read an exact amount of virtual memory starting at `gva` using a - /// specific directory table base / set of page tables. - pub fn virt_read_exact_with_dtb(&self, gva: Gva, buf: &mut [u8], dtb: Gpa) -> Result<()> { + /// specific directory table base / set of page tables, propagating all + /// errors including memory errors. + pub fn virt_read_exact_strict_with_dtb( + &self, + gva: Gva, + buf: &mut [u8], + dtb: Gpa, + ) -> Result<()> { // Read virtual memory. - let len = self.virt_read_with_dtb(gva, buf, dtb)?; + let len = self.virt_read_strict_with_dtb(gva, buf, dtb)?; // If we read as many bytes as we wanted, then it's a win.. if len == buf.len() { Ok(()) } - // ..otherwise, we call it quits. + // ..otherwise, we call it quits. The failure should have been reported + // as a PartialRead by virt_read_strict_with_dtb already, but this handles + // the case where we read some bytes but not all without hitting an error. else { - Err(KdmpParserError::PartialVirtRead) + let failed_gva = gva + .u64() + .checked_add(len.try_into().unwrap()) + .map_or(gva, Gva::new); + // This shouldn't normally happen as virt_read_strict_with_dtb should report + // the specific error, but we provide a generic error just in case. + Err(MemoryReadError::PartialRead { + expected_amount: buf.len(), + actual_amount: len, + reason: PageReadError::NotInDump { + gva: Some((failed_gva, None)), + gpa: Gpa::new(0), // Unknown GPA + }, + } + .into()) } } - /// Try to read an exact amount of virtual memory starting at `gva`. If a - /// memory translation error occurs, it'll return `None` instead of an - /// error. - pub fn try_virt_read_exact(&self, gva: Gva, buf: &mut [u8]) -> Result> { - self.try_virt_read_exact_with_dtb(gva, buf, Gpa::new(self.headers.directory_table_base)) + /// Read a `T` from virtual memory. Returns `None` if a memory error occurs + /// (page not present, page not in dump, etc.). + pub fn virt_read_struct(&self, gva: Gva) -> Result> { + self.virt_read_struct_with_dtb(gva, Gpa::new(self.headers.directory_table_base)) } - /// Try to read an exact amount of virtual memory starting at `gva` using a - /// specific directory table base / set of page tables. If a - /// memory translation error occurs, it'll return `None` instead of an - /// error. - pub fn try_virt_read_exact_with_dtb( - &self, - gva: Gva, - buf: &mut [u8], - dtb: Gpa, - ) -> Result> { - filter_addr_translation_err(self.virt_read_exact_with_dtb(gva, buf, dtb)) + /// Read a `T` from virtual memory using a specific directory table base / + /// set of page tables. Returns `None` if a memory error occurs (page not + /// present, page not in dump, etc.). + pub fn virt_read_struct_with_dtb(&self, gva: Gva, dtb: Gpa) -> Result> { + filter_memory_err(self.virt_read_struct_strict_with_dtb::(gva, dtb)) } - /// Read a `T` from virtual memory. - pub fn virt_read_struct(&self, gva: Gva) -> Result { - self.virt_read_struct_with_dtb(gva, Gpa::new(self.headers.directory_table_base)) + /// Read a `T` from virtual memory, propagating all errors including memory + /// errors. + pub fn virt_read_struct_strict(&self, gva: Gva) -> Result { + self.virt_read_struct_strict_with_dtb(gva, Gpa::new(self.headers.directory_table_base)) } /// Read a `T` from virtual memory using a specific directory table base / - /// set of page tables. - pub fn virt_read_struct_with_dtb(&self, gva: Gva, dtb: Gpa) -> Result { + /// set of page tables, propagating all errors including memory errors. + pub fn virt_read_struct_strict_with_dtb(&self, gva: Gva, dtb: Gpa) -> Result { let mut t: MaybeUninit = MaybeUninit::uninit(); let size_of_t = size_of_val(&t); let slice_over_t = unsafe { slice::from_raw_parts_mut(t.as_mut_ptr().cast::(), size_of_t) }; - self.virt_read_exact_with_dtb(gva, slice_over_t, dtb)?; + self.virt_read_exact_strict_with_dtb(gva, slice_over_t, dtb)?; Ok(unsafe { t.assume_init() }) } - /// Try to read a `T` from virtual memory. If a memory translation error - /// occurs, it'll return `None` instead of an error. - pub fn try_virt_read_struct(&self, gva: Gva) -> Result> { - self.try_virt_read_struct_with_dtb::(gva, Gpa::new(self.headers.directory_table_base)) - } - - /// Try to read a `T` from virtual memory using a specific directory table - /// base / set of page tables. If a memory translation error occurs, it' - /// ll return `None` instead of an error. - pub fn try_virt_read_struct_with_dtb(&self, gva: Gva, dtb: Gpa) -> Result> { - filter_addr_translation_err(self.virt_read_struct_with_dtb::(gva, dtb)) - } - /// Seek to `pos`. /// /// # Errors @@ -850,23 +984,20 @@ impl KernelDumpParser { Ok(self.reader.borrow_mut().read(buf)?) } - /// Try to read a `UNICODE_STRING`. - fn try_virt_read_unicode_string

( - &self, - unicode_str: &UnicodeString

, - ) -> Result> + /// Read a `UNICODE_STRING`. Returns `None` if a memory error occurs. + fn virt_read_unicode_string

(&self, unicode_str: &UnicodeString

) -> Result> where P: PtrSize, { - self.try_virt_read_unicode_string_with_dtb( + self.virt_read_unicode_string_with_dtb( unicode_str, Gpa::new(self.headers.directory_table_base), ) } - /// Try to read a `UNICODE_STRING` using a specific directory table base / - /// set of page tables. - fn try_virt_read_unicode_string_with_dtb

( + /// Read a `UNICODE_STRING` using a specific directory table base / set of + /// page tables. Returns `None` if a memory error occurs. + fn virt_read_unicode_string_with_dtb

( &self, unicode_str: &UnicodeString

, dtb: Gpa, @@ -879,12 +1010,11 @@ impl KernelDumpParser { } let mut buffer = vec![0; unicode_str.length.into()]; - match self.virt_read_exact_with_dtb(Gva::new(unicode_str.buffer.into()), &mut buffer, dtb) { - Ok(()) => {} - // If we encountered a memory translation error, we don't consider this a failure. - Err(KdmpParserError::AddrTranslation(_)) => return Ok(None), - Err(e) => return Err(e), - } + let Some(()) = + self.virt_read_exact_with_dtb(Gva::new(unicode_str.buffer.into()), &mut buffer, dtb)? + else { + return Ok(None); + }; let n = unicode_str.length / 2; diff --git a/src/pxe.rs b/src/pxe.rs index c45ae5a..bdadb72 100644 --- a/src/pxe.rs +++ b/src/pxe.rs @@ -8,12 +8,12 @@ //! # use kdmp_parser::{Pxe, PxeFlags, Pfn}; //! let pxe = Pxe::new( //! Pfn::new(0x6d600), -//! PxeFlags::UserAccessible | PxeFlags::Accessed | PxeFlags::Present +//! PxeFlags::USER_ACCESSIBLE | PxeFlags::ACCESSED | PxeFlags::PRESENT //! ); //! let encoded = u64::from(pxe); //! let decoded = Pxe::from(encoded); //! ``` -use std::ops::Deref; +use std::ops::{BitOr, Deref}; use crate::{Bits, Gpa}; @@ -22,6 +22,17 @@ use crate::{Bits, Gpa}; pub struct PxeFlags(u64); impl PxeFlags { + pub const PRESENT: Self = Self(1 << 0); + pub const WRITABLE: Self = Self(1 << 1); + pub const USER_ACCESSIBLE: Self = Self(1 << 2); + pub const WRITE_THROUGH: Self = Self(1 << 3); + pub const CACHE_DISABLED: Self = Self(1 << 4); + pub const ACCESSED: Self = Self(1 << 5); + pub const DIRTY: Self = Self(1 << 6); + pub const LARGE_PAGE: Self = Self(1 << 7); + pub const TRANSITION: Self = Self(1 << 11); + pub const NO_EXECUTE: Self = Self(1 << 63); + #[must_use] pub fn new(bits: u64) -> Self { Self(bits) @@ -78,6 +89,14 @@ impl PxeFlags { } } +impl BitOr for PxeFlags { + type Output = Self; + + fn bitor(self, rhs: Self) -> Self::Output { + Self::new(*self | *rhs) + } +} + impl Deref for PxeFlags { type Target = u64; @@ -135,9 +154,6 @@ impl From for u64 { } /// A [`Pxe`] is a set of flags ([`PxeFlags`]) and a Page Frame Number (PFN). -/// This representation takes more space than a regular `PXE` but it is more -/// convenient to split the flags / the pfn as [`bitflags!`] doesn't seem to -/// support bitfields. #[derive(Debug, Clone, Copy, Eq, Hash, PartialEq, Default, PartialOrd, Ord)] pub struct Pxe { /// The PFN of the next table or the final page. @@ -156,7 +172,7 @@ impl Pxe { /// # fn main() { /// let pxe = Pxe::new( /// Pfn::new(0x6d600), - /// PxeFlags::UserAccessible | PxeFlags::Accessed | PxeFlags::Present + /// PxeFlags::USER_ACCESSIBLE | PxeFlags::ACCESSED | PxeFlags::PRESENT /// ); /// assert_eq!(pxe.pfn.u64(), 0x6d600); /// # } @@ -175,12 +191,12 @@ impl Pxe { /// # fn main() { /// let p = Pxe::new( /// Pfn::new(0x6d600), - /// PxeFlags::Present + /// PxeFlags::PRESENT /// ); /// assert!(p.present()); /// let np = Pxe::new( /// Pfn::new(0x1337), - /// PxeFlags::UserAccessible + /// PxeFlags::USER_ACCESSIBLE /// ); /// assert!(!np.present()); /// # } @@ -199,12 +215,12 @@ impl Pxe { /// # fn main() { /// let p = Pxe::new( /// Pfn::new(0x6d600), - /// PxeFlags::LargePage + /// PxeFlags::LARGE_PAGE /// ); /// assert!(p.large_page()); /// let np = Pxe::new( /// Pfn::new(0x1337), - /// PxeFlags::UserAccessible + /// PxeFlags::USER_ACCESSIBLE /// ); /// assert!(!np.large_page()); /// # } @@ -298,12 +314,14 @@ impl From for Pxe { /// # fn main() { /// let pxe = Pxe::from(0x6D_60_00_25); /// assert_eq!(pxe.pfn.u64(), 0x6d600); - /// assert_eq!(pxe.flags, PxeFlags::UserAccessible | PxeFlags::Accessed | PxeFlags::Present); + /// assert_eq!(pxe.flags, PxeFlags::USER_ACCESSIBLE | PxeFlags::ACCESSED | PxeFlags::PRESENT); /// # } /// ``` fn from(value: u64) -> Self { - let pfn = Pfn::new((value >> 12) & 0xf_ffff_ffff); - let flags = PxeFlags::new(value); + const PFN_MASK: u64 = 0xffff_ffff_f000; + const FLAGS_MASK: u64 = !PFN_MASK; + let pfn = Pfn::new((value & PFN_MASK) >> 12); + let flags = PxeFlags::new(value & FLAGS_MASK); Self::new(pfn, flags) } @@ -320,7 +338,7 @@ impl From for u64 { /// # fn main() { /// let pxe = Pxe::new( /// Pfn::new(0x6d600), - /// PxeFlags::UserAccessible | PxeFlags::Accessed | PxeFlags::Present, + /// PxeFlags::USER_ACCESSIBLE | PxeFlags::ACCESSED | PxeFlags::PRESENT, /// ); /// assert_eq!(u64::from(pxe), 0x6D_60_00_25); /// # } diff --git a/tests/regression.rs b/tests/regression.rs index 7b475fa..26711a1 100644 --- a/tests/regression.rs +++ b/tests/regression.rs @@ -6,7 +6,7 @@ use std::ops::Range; use std::path::PathBuf; use kdmp_parser::{ - AddrTranslationError, Gpa, Gva, KdmpParserError, KernelDumpParser, PageKind, PxeNotPresent, + Gpa, Gva, KdmpParserError, KernelDumpParser, MemoryReadError, PageKind, PageReadError, PxeKind, }; use serde::Deserialize; @@ -472,17 +472,17 @@ fn regressions() { let parser = KernelDumpParser::new(&kernel_dump.file).unwrap(); let mut buffer = [0]; assert!(matches!( - parser.virt_read(0x1a42ea30240.into(), &mut buffer), - Err(KdmpParserError::AddrTranslation( - AddrTranslationError::Phys(gpa) - )) if gpa == 0x166b7240.into() + parser.virt_read_strict(0x1a42ea30240.into(), &mut buffer), + Err(KdmpParserError::MemoryRead(MemoryReadError::PageRead( + PageReadError::NotInDump { gpa, .. } + ))) if gpa == 0x166b7240.into() )); assert!(matches!( - parser.virt_read(0x16e23fa060.into(), &mut buffer), - Err(KdmpParserError::AddrTranslation( - AddrTranslationError::Phys(gpa) - )) if gpa == 0x1bc4060.into() + parser.virt_read_strict(0x16e23fa060.into(), &mut buffer), + Err(KdmpParserError::MemoryRead(MemoryReadError::PageRead( + PageReadError::NotInDump { gpa, .. } + ))) if gpa == 0x1bc4060.into() )); // BUG: https://github.com/0vercl0k/kdmp-parser-rs/issues/10 @@ -510,12 +510,16 @@ fn regressions() { // fffff803`f308700f ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ?? ???????????????? // ``` let mut buffer = [0; 32]; - assert_eq!( - parser - .virt_read(0xfffff803f3086fef.into(), &mut buffer) - .unwrap(), - 17 - ); + match parser.virt_read_strict(0xfffff803f3086fef.into(), &mut buffer) { + Err(KdmpParserError::MemoryRead(MemoryReadError::PartialRead { + expected_amount: 32, + actual_amount: 17, + .. + })) => { + // This is correct - we read 17 bytes before hitting unmapped memory + } + other => panic!("Expected PartialRead with actual_amount 17, got: {other:?}"), + } // ```text // kd> !process 0 0 @@ -678,16 +682,16 @@ fn regressions() { let gva = 0.into(); assert!(matches!( parser.virt_translate(gva), - Err(KdmpParserError::AddrTranslation( - AddrTranslationError::Virt(fault_gva, PxeNotPresent::Pde) - )) if fault_gva == gva + Err(KdmpParserError::MemoryRead(MemoryReadError::PageRead( + PageReadError::NotPresent { gva: fault_gva, which_pxe: PxeKind::Pde } + ))) if fault_gva == gva )); let gva = 0xffffffff_ffffffff.into(); assert!(matches!( parser.virt_translate(gva), - Err(KdmpParserError::AddrTranslation( - AddrTranslationError::Virt(fault_gva, PxeNotPresent::Pte) - )) if fault_gva == gva + Err(KdmpParserError::MemoryRead(MemoryReadError::PageRead( + PageReadError::NotPresent { gva: fault_gva, which_pxe: PxeKind::Pte } + ))) if fault_gva == gva )); } From 79f130e1214e8765612d37c61294ea9e93416162 Mon Sep 17 00:00:00 2001 From: 0vercl0k <1476421+0vercl0k@users.noreply.github.com> Date: Tue, 21 Oct 2025 18:22:55 -0700 Subject: [PATCH 6/8] cleaning up a bit --- src/gxa.rs | 2 +- src/parse.rs | 91 +++++++++++++-------------------------------- src/pxe.rs | 12 +++--- tests/regression.rs | 40 +++++++++++--------- 4 files changed, 56 insertions(+), 89 deletions(-) diff --git a/src/gxa.rs b/src/gxa.rs index eb033d8..58a7ccc 100644 --- a/src/gxa.rs +++ b/src/gxa.rs @@ -13,7 +13,7 @@ //! let page_aligned_gva = gva.page_align(); //! let page_offset = gva.offset(); //! ``` -use std::fmt::{self, Display}; +use std::fmt::{self, Debug, Display}; use std::num::ParseIntError; use std::ops::AddAssign; use std::str::FromStr; diff --git a/src/parse.rs b/src/parse.rs index 74a030c..6bfe25a 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -356,7 +356,7 @@ fn try_extract_user_modules( /// makes it easier for caller code to write logic that can recover from a /// memory read failure by bailing out for example, and not bubbling up an /// error. -fn filter_memory_err(res: Result) -> Result> { +fn filter_memory_read_err(res: Result) -> Result> { match res { Ok(o) => Ok(Some(o)), // If we encountered a memory reading error, we won't consider this as a failure. @@ -639,25 +639,27 @@ impl KernelDumpParser { /// / set of page tables. #[allow(clippy::similar_names)] pub fn virt_translate_with_dtb(&self, gva: Gva, dtb: Gpa) -> Result { + let read_pxe = |gpa: Gpa, pxe_kind: PxeKind| { + self.phys_read_struct::(gpa) + .map_err(|e| match e { + // If the physical page isn't in the dump, enrich the error by adding the gva + // that was getting translated as well as the pxe level. + KdmpParserError::MemoryRead(MemoryReadError::PageRead( + PageReadError::NotInDump { gpa, .. }, + )) => PageReadError::NotInDump { + gva: Some((gva, Some(pxe_kind))), + gpa, + } + .into(), + e => e, + }) + .map(Pxe::from) + }; + // Aligning in case PCID bits are set (bits 11:0) let pml4_base = dtb.page_align(); let pml4e_gpa = Gpa::new(pml4_base.u64() + (gva.pml4e_idx() * 8)); - let pml4e = Pxe::from(self.phys_read_struct::(pml4e_gpa).map_err(|e| { - // If reading the PML4E failed due to a memory error, wrap it appropriately - if let KdmpParserError::MemoryRead(MemoryReadError::PageRead( - PageReadError::NotInDump { gpa, .. }, - )) = e - { - PageReadError::NotInDump { - gva: Some((gva, Some(PxeKind::Pml4e))), - gpa, - } - .into() - } else { - e - } - })?); - + let pml4e = read_pxe(pml4e_gpa, PxeKind::Pml4e)?; if !pml4e.present() { return Err(PageReadError::NotPresent { gva, @@ -668,20 +670,7 @@ impl KernelDumpParser { let pdpt_base = pml4e.pfn.gpa(); let pdpte_gpa = Gpa::new(pdpt_base.u64() + (gva.pdpe_idx() * 8)); - let pdpte = Pxe::from(self.phys_read_struct::(pdpte_gpa).map_err(|e| { - if let KdmpParserError::MemoryRead(MemoryReadError::PageRead( - PageReadError::NotInDump { gpa, .. }, - )) = e - { - PageReadError::NotInDump { - gpa, - gva: Some((gva, Some(PxeKind::Pdpte))), - } - .into() - } else { - e - } - })?); + let pdpte = read_pxe(pdpte_gpa, PxeKind::Pdpte)?; if !pdpte.present() { return Err(PageReadError::NotPresent { gva, @@ -699,21 +688,7 @@ impl KernelDumpParser { } let pde_gpa = Gpa::new(pd_base.u64() + (gva.pde_idx() * 8)); - let pde = Pxe::from(self.phys_read_struct::(pde_gpa).map_err(|e| { - if let KdmpParserError::MemoryRead(MemoryReadError::PageRead( - PageReadError::NotInDump { gpa, .. }, - )) = e - { - PageReadError::NotInDump { - gva: Some((gva, Some(PxeKind::Pde))), - gpa, - } - .into() - } else { - e - } - })?); - + let pde = read_pxe(pde_gpa, PxeKind::Pde)?; if !pde.present() { return Err(PageReadError::NotPresent { gva, @@ -731,21 +706,7 @@ impl KernelDumpParser { } let pte_gpa = Gpa::new(pt_base.u64() + (gva.pte_idx() * 8)); - let pte = Pxe::from(self.phys_read_struct::(pte_gpa).map_err(|e| { - if let KdmpParserError::MemoryRead(MemoryReadError::PageRead( - PageReadError::NotInDump { gpa, .. }, - )) = e - { - PageReadError::NotInDump { - gva: Some((gva, Some(PxeKind::Pte))), - gpa, - } - .into() - } else { - e - } - })?); - + let pte = read_pxe(pte_gpa, PxeKind::Pte)?; if !pte.present() { // We'll allow reading from a transition PTE, so return an error only if it's // not one, otherwise we'll carry on. @@ -771,7 +732,7 @@ impl KernelDumpParser { /// directory table base / set of page tables. Returns `None` if a memory /// error occurs (page not present, page not in dump, etc.). pub fn virt_read_with_dtb(&self, gva: Gva, buf: &mut [u8], dtb: Gpa) -> Result> { - filter_memory_err(self.virt_read_strict_with_dtb(gva, buf, dtb)) + filter_memory_read_err(self.virt_read_strict_with_dtb(gva, buf, dtb)) } /// Read virtual memory starting at `gva` into a `buffer`, propagating all @@ -834,7 +795,7 @@ impl KernelDumpParser { gva: Some((addr, None)), gpa, }, - other => other, + e @ PageReadError::NotPresent { .. } => e, }; return Err(MemoryReadError::PartialRead { @@ -887,7 +848,7 @@ impl KernelDumpParser { buf: &mut [u8], dtb: Gpa, ) -> Result> { - filter_memory_err(self.virt_read_exact_strict_with_dtb(gva, buf, dtb)) + filter_memory_read_err(self.virt_read_exact_strict_with_dtb(gva, buf, dtb)) } /// Read an exact amount of virtual memory starting at `gva`, propagating @@ -944,7 +905,7 @@ impl KernelDumpParser { /// set of page tables. Returns `None` if a memory error occurs (page not /// present, page not in dump, etc.). pub fn virt_read_struct_with_dtb(&self, gva: Gva, dtb: Gpa) -> Result> { - filter_memory_err(self.virt_read_struct_strict_with_dtb::(gva, dtb)) + filter_memory_read_err(self.virt_read_struct_strict_with_dtb::(gva, dtb)) } /// Read a `T` from virtual memory, propagating all errors including memory diff --git a/src/pxe.rs b/src/pxe.rs index bdadb72..cb21f5d 100644 --- a/src/pxe.rs +++ b/src/pxe.rs @@ -22,16 +22,16 @@ use crate::{Bits, Gpa}; pub struct PxeFlags(u64); impl PxeFlags { - pub const PRESENT: Self = Self(1 << 0); - pub const WRITABLE: Self = Self(1 << 1); - pub const USER_ACCESSIBLE: Self = Self(1 << 2); - pub const WRITE_THROUGH: Self = Self(1 << 3); - pub const CACHE_DISABLED: Self = Self(1 << 4); pub const ACCESSED: Self = Self(1 << 5); + pub const CACHE_DISABLED: Self = Self(1 << 4); pub const DIRTY: Self = Self(1 << 6); pub const LARGE_PAGE: Self = Self(1 << 7); - pub const TRANSITION: Self = Self(1 << 11); pub const NO_EXECUTE: Self = Self(1 << 63); + pub const PRESENT: Self = Self(1 << 0); + pub const TRANSITION: Self = Self(1 << 11); + pub const USER_ACCESSIBLE: Self = Self(1 << 2); + pub const WRITABLE: Self = Self(1 << 1); + pub const WRITE_THROUGH: Self = Self(1 << 3); #[must_use] pub fn new(bits: u64) -> Self { diff --git a/tests/regression.rs b/tests/regression.rs index 26711a1..d4c7789 100644 --- a/tests/regression.rs +++ b/tests/regression.rs @@ -83,7 +83,7 @@ fn compare_modules(parser: &KernelDumpParser, modules: &[Module]) -> bool { continue; } - eprintln!("{name} {found_mod:?}"); + eprintln!("name: {name} filename: {filename} found_mod: {found_mod:#x?}"); return false; } } @@ -456,8 +456,8 @@ fn regressions() { // PXE at FFFFECF67B3D9018 PPE at FFFFECF67B203480 PDE at FFFFECF640690BA8 PTE at FFFFEC80D2175180 // contains 0A0000000ECC0867 contains 0A00000013341867 contains 0A000000077AF867 contains 00000000166B7880 // pfn ecc0 ---DA--UWEV pfn 13341 ---DA--UWEV pfn 77af ---DA--UWEV not valid - // Transition: 166b7 - // Protect: 4 - ReadWrite + // Transition: 166b7 + // Protect: 4 - ReadWrite // kd> !db 166b7240 // Physical memory read at 166b7240 failed // @@ -474,15 +474,15 @@ fn regressions() { assert!(matches!( parser.virt_read_strict(0x1a42ea30240.into(), &mut buffer), Err(KdmpParserError::MemoryRead(MemoryReadError::PageRead( - PageReadError::NotInDump { gpa, .. } - ))) if gpa == 0x166b7240.into() + PageReadError::NotInDump { gva: Some((gva, None)), gpa } + ))) if gpa == 0x166b7240.into() && gva == 0x1a42ea30240.into() )); assert!(matches!( parser.virt_read_strict(0x16e23fa060.into(), &mut buffer), Err(KdmpParserError::MemoryRead(MemoryReadError::PageRead( - PageReadError::NotInDump { gpa, .. } - ))) if gpa == 0x1bc4060.into() + PageReadError::NotInDump { gva: Some((gva, None)), gpa } + ))) if gpa == 0x1bc4060.into() && gva == 0x16e23fa060.into() )); // BUG: https://github.com/0vercl0k/kdmp-parser-rs/issues/10 @@ -497,12 +497,15 @@ fn regressions() { // ```text // kd> db 00007ff7`ab766ff7 // 00007ff7`ab766ff7 00 00 00 00 00 00 00 00-00 ?? ?? ?? ?? ?? ?? ?? .........??????? - // ... + // ``` + // + // ```text // kdmp-parser-rs>cargo r --example parser -- mem.dmp --mem 00007ff7`ab766ff7 --virt --len 10 // Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.09s // Running `target\debug\examples\parser.exe mem.dmp --mem 00007ff7`ab766ff7 --virt --len 10` // There is no virtual memory available at 0x7ff7ab766ff7 // ``` + // // ```text // kd> db fffff803`f3086fef // fffff803`f3086fef 9d f5 de ff 48 85 c0 74-0a 40 8a cf e8 80 ee ba ....H..t.@...... @@ -510,16 +513,15 @@ fn regressions() { // fffff803`f308700f ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ?? ???????????????? // ``` let mut buffer = [0; 32]; - match parser.virt_read_strict(0xfffff803f3086fef.into(), &mut buffer) { + assert!( + matches!(parser.virt_read_strict(0xfffff803f3086fef.into(), &mut buffer), Err(KdmpParserError::MemoryRead(MemoryReadError::PartialRead { expected_amount: 32, actual_amount: 17, - .. - })) => { - // This is correct - we read 17 bytes before hitting unmapped memory - } - other => panic!("Expected PartialRead with actual_amount 17, got: {other:?}"), - } + reason: PageReadError::NotPresent { gva, which_pxe } + })) if gva == 0xfffff803f3087000.into() && which_pxe == PxeKind::Pte + ) + ); // ```text // kd> !process 0 0 @@ -570,8 +572,10 @@ fn regressions() { assert!( parser .virt_read_exact(Gva::new(0xfffff80122800000 + 0x100000 - 8), &mut buffer) - .is_ok() + .unwrap() + .is_some() ); + assert_eq!(buffer, [ 0x70, 0x72, 0x05, 0x00, 0x04, 0x3a, 0x65, 0x00, 0x54, 0x3a, 0x65, 0x00, 0xbc, 0x82, 0x0c, 0x00 @@ -597,8 +601,10 @@ fn regressions() { assert!( parser .virt_read_exact(Gva::new(0xfffff80122800000 + 0x200000 - 0x8), &mut buffer) - .is_ok() + .unwrap() + .is_some() ); + assert_eq!(buffer, [ 0x63, 0x00, 0x72, 0x00, 0x6f, 0x00, 0x73, 0x00, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc From 022046fac530075d64333c2cae96cee75f508d98 Mon Sep 17 00:00:00 2001 From: 0vercl0k <1476421+0vercl0k@users.noreply.github.com> Date: Tue, 21 Oct 2025 19:31:19 -0700 Subject: [PATCH 7/8] fix logic to be able to see partial reads when it makes sense --- examples/parser.rs | 6 ++--- src/error.rs | 25 ++++++++++----------- src/parse.rs | 54 +++++++++++++++++++++------------------------ tests/regression.rs | 30 +++++++++++++++++++++---- 4 files changed, 66 insertions(+), 49 deletions(-) diff --git a/examples/parser.rs b/examples/parser.rs index 3871ad0..6bbd558 100644 --- a/examples/parser.rs +++ b/examples/parser.rs @@ -183,10 +183,10 @@ fn main() -> Result<()> { .unwrap_or(Gpa::new(parser.headers().directory_table_base)), ) } else { - parser.phys_read(Gpa::new(addr), &mut buffer) - }; + parser.phys_read(Gpa::new(addr), &mut buffer).map(Some) + }?; - if let Ok(amount) = amount { + if let Some(amount) = amount { hexdump(addr, &buffer[..amount], args.len); } else { println!( diff --git a/src/error.rs b/src/error.rs index ef9d801..d26b626 100644 --- a/src/error.rs +++ b/src/error.rs @@ -24,7 +24,7 @@ pub enum PageReadError { /// Virtual address translation failed because a page table entry is not /// present (it exists in the dump but is marked as not present). NotPresent { gva: Gva, which_pxe: PxeKind }, - /// A physical page is missing from the dump. XXX: + /// A physical page is missing from the dump. NotInDump { gva: Option<(Gva, Option)>, gpa: Gpa, @@ -41,26 +41,25 @@ impl Error for PageReadError {} /// For example, consider reading two 4K pages from the virtual address /// `0x1337_000`; it can fail because: /// - The virtual address (the first 4K page) isn't present in the address space -/// at the `Pde` level: `MemoryError::PageRead(PageReadError::PageNotPresent { +/// at the `Pde` level: `MemoryReadError::PageRead(PageReadError::NotPresent { /// gva: 0x1337_000, which_pxe: PxeKind::Pde })` -/// - The `Pde` that needs reading as part of the translation (of the first -/// page) isn't part of the crash-dump: -/// `MemoryError::PageRead(PageReadError::TranslationPageNotInDump { gva: -/// 0x1337_000, gpa: .., which_pxe: PxeKind::Pde })` +/// - The `Pde` that needs reading as part of the address translation (of the +/// first page) isn't part of the crash-dump: +/// `MemoryReadError::PageRead(PageReadError::NotInDump { gva: +/// Some((0x1337_000, PxeKind::Pde)), gpa: .. })` /// - The physical page backing that virtual address isn't included in the -/// crash-dump: `MemoryError::PageRead(PageReadError::BackingPageNotInDump { -/// gva: 0x1337_000, gpa: .. })` +/// crash-dump: `MemoryReadError::PageRead(PageReadError::NotInDump { gva: +/// Some((0x1337_000, None)), gpa: .. })` /// - Reading the second (and only the second) page failed because of any of the -/// previous reasons: `MemoryError::PartialRead { expected_amount: 8_192, -/// actual_amount: 4_096, reason: PageReadError::PageNotPresent { .. } }` +/// previous reasons: `MemoryReadError::PartialRead { expected_amount: 8_192, +/// actual_amount: 4_096, reason: PageReadError::.. }` /// /// Similarly, for physical memory reads starting at `0x1337_000`: /// - A direct physical page isn't in the crash-dump: -/// `MemoryError::PageRead(PageReadError::PhysicalPageNotInDump { gpa: -/// 0x1337_000 })` +/// `MemoryError::PageRead(PageReadError::NotInDump { gpa: 0x1337_000 })` /// - Reading the second page failed: `MemoryError::PartialRead { /// expected_amount: 8_192, actual_amount: 4_096, reason: -/// PageReadError::PhysicalPageNotInDump { gpa: 0x1338_000 } }` +/// PageReadError::NotInDump { gva: None, gpa: 0x1338_000 } }` /// /// We consider any of those errors 'recoverable' which means that we won't even /// bubble those up to the callers with the regular APIs. Only the `strict` diff --git a/src/parse.rs b/src/parse.rs index 6bfe25a..febc0ed 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -352,19 +352,6 @@ fn try_extract_user_modules( Ok(Some(modules)) } -/// Filter out [`MemoryReadError`] errors and turn them into `None`. This -/// makes it easier for caller code to write logic that can recover from a -/// memory read failure by bailing out for example, and not bubbling up an -/// error. -fn filter_memory_read_err(res: Result) -> Result> { - match res { - Ok(o) => Ok(Some(o)), - // If we encountered a memory reading error, we won't consider this as a failure. - Err(KdmpParserError::MemoryRead(..)) => Ok(None), - Err(e) => Err(e), - } -} - /// A module map. The key is the range of where the module lives at and the /// value is a path to the module or it's name if no path is available. pub type ModuleMap = HashMap, String>; @@ -639,7 +626,7 @@ impl KernelDumpParser { /// / set of page tables. #[allow(clippy::similar_names)] pub fn virt_translate_with_dtb(&self, gva: Gva, dtb: Gpa) -> Result { - let read_pxe = |gpa: Gpa, pxe_kind: PxeKind| { + let read_pxe = |gpa: Gpa, pxe: PxeKind| { self.phys_read_struct::(gpa) .map_err(|e| match e { // If the physical page isn't in the dump, enrich the error by adding the gva @@ -647,7 +634,7 @@ impl KernelDumpParser { KdmpParserError::MemoryRead(MemoryReadError::PageRead( PageReadError::NotInDump { gpa, .. }, )) => PageReadError::NotInDump { - gva: Some((gva, Some(pxe_kind))), + gva: Some((gva, Some(pxe))), gpa, } .into(), @@ -732,7 +719,15 @@ impl KernelDumpParser { /// directory table base / set of page tables. Returns `None` if a memory /// error occurs (page not present, page not in dump, etc.). pub fn virt_read_with_dtb(&self, gva: Gva, buf: &mut [u8], dtb: Gpa) -> Result> { - filter_memory_read_err(self.virt_read_strict_with_dtb(gva, buf, dtb)) + match self.virt_read_strict_with_dtb(gva, buf, dtb) { + Ok(n) => Ok(Some(n)), + Err(KdmpParserError::MemoryRead(MemoryReadError::PartialRead { + actual_amount, + .. + })) => Ok(Some(actual_amount)), + Err(KdmpParserError::MemoryRead(_)) => Ok(None), + Err(e) => Err(e), + } } /// Read virtual memory starting at `gva` into a `buffer`, propagating all @@ -835,20 +830,19 @@ impl KernelDumpParser { /// Read an exact amount of virtual memory starting at `gva`. Returns `None` /// if a memory error occurs (page not present, page not in dump, etc.). - pub fn virt_read_exact(&self, gva: Gva, buf: &mut [u8]) -> Result> { + pub fn virt_read_exact(&self, gva: Gva, buf: &mut [u8]) -> Result { self.virt_read_exact_with_dtb(gva, buf, Gpa::new(self.headers.directory_table_base)) } /// Read an exact amount of virtual memory starting at `gva` using a /// specific directory table base / set of page tables. Returns `None` if a /// memory error occurs (page not present, page not in dump, etc.). - pub fn virt_read_exact_with_dtb( - &self, - gva: Gva, - buf: &mut [u8], - dtb: Gpa, - ) -> Result> { - filter_memory_read_err(self.virt_read_exact_strict_with_dtb(gva, buf, dtb)) + pub fn virt_read_exact_with_dtb(&self, gva: Gva, buf: &mut [u8], dtb: Gpa) -> Result { + match self.virt_read_exact_strict_with_dtb(gva, buf, dtb) { + Ok(()) => Ok(true), + Err(KdmpParserError::MemoryRead(_)) => Ok(false), + Err(e) => Err(e), + } } /// Read an exact amount of virtual memory starting at `gva`, propagating @@ -905,7 +899,11 @@ impl KernelDumpParser { /// set of page tables. Returns `None` if a memory error occurs (page not /// present, page not in dump, etc.). pub fn virt_read_struct_with_dtb(&self, gva: Gva, dtb: Gpa) -> Result> { - filter_memory_read_err(self.virt_read_struct_strict_with_dtb::(gva, dtb)) + match self.virt_read_struct_strict_with_dtb::(gva, dtb) { + Ok(t) => Ok(Some(t)), + Err(KdmpParserError::MemoryRead(_)) => Ok(None), + Err(e) => Err(e), + } } /// Read a `T` from virtual memory, propagating all errors including memory @@ -971,11 +969,9 @@ impl KernelDumpParser { } let mut buffer = vec![0; unicode_str.length.into()]; - let Some(()) = - self.virt_read_exact_with_dtb(Gva::new(unicode_str.buffer.into()), &mut buffer, dtb)? - else { + if !self.virt_read_exact_with_dtb(Gva::new(unicode_str.buffer.into()), &mut buffer, dtb)? { return Ok(None); - }; + } let n = unicode_str.length / 2; diff --git a/tests/regression.rs b/tests/regression.rs index d4c7789..9ae6b5c 100644 --- a/tests/regression.rs +++ b/tests/regression.rs @@ -436,8 +436,8 @@ fn regressions() { // PXE at FFFFECF67B3D9018 PPE at FFFFECF67B203480 PDE at FFFFECF640690BA8 PTE at FFFFEC80D2175180 // contains 0A0000000ECC0867 contains 0A00000013341867 contains 0A000000077AF867 contains 00000000166B7880 // pfn ecc0 ---DA--UWEV pfn 13341 ---DA--UWEV pfn 77af ---DA--UWEV not valid - // Transition: 166b7 - // Protect: 4 - ReadWrite + // Transition: 166b7 + // Protect: 4 - ReadWrite // ``` let parser = KernelDumpParser::new(&kernel_user_dump.file).unwrap(); let mut buffer = [0; 16]; @@ -478,6 +478,18 @@ fn regressions() { ))) if gpa == 0x166b7240.into() && gva == 0x1a42ea30240.into() )); + assert!(matches!( + parser.virt_read(0x1a42ea30240.into(), &mut buffer), + Ok(None) + )); + + assert!(matches!( + parser.phys_read(0x166b7240.into(), &mut buffer), + Err(KdmpParserError::MemoryRead(MemoryReadError::PageRead( + PageReadError::NotInDump { gva: None, gpa } + ))) if gpa == 0x166b7240.into() + )); + assert!(matches!( parser.virt_read_strict(0x16e23fa060.into(), &mut buffer), Err(KdmpParserError::MemoryRead(MemoryReadError::PageRead( @@ -485,6 +497,11 @@ fn regressions() { ))) if gpa == 0x1bc4060.into() && gva == 0x16e23fa060.into() )); + assert!(matches!( + parser.virt_read(0x16e23fa060.into(), &mut buffer), + Ok(None) + )); + // BUG: https://github.com/0vercl0k/kdmp-parser-rs/issues/10 // When reading the end of a virtual memory page that has no available // memory behind, there was an issue in the virtual read algorithm. The @@ -506,6 +523,8 @@ fn regressions() { // There is no virtual memory available at 0x7ff7ab766ff7 // ``` // + // The below address mirrors the same behavior than in the issue's dump: + // // ```text // kd> db fffff803`f3086fef // fffff803`f3086fef 9d f5 de ff 48 85 c0 74-0a 40 8a cf e8 80 ee ba ....H..t.@...... @@ -523,6 +542,11 @@ fn regressions() { ) ); + assert!(matches!( + parser.virt_read(0xfffff803f3086fef.into(), &mut buffer), + Ok(Some(17)) + )); + // ```text // kd> !process 0 0 // PROCESS ffffc00c5120d580 @@ -573,7 +597,6 @@ fn regressions() { parser .virt_read_exact(Gva::new(0xfffff80122800000 + 0x100000 - 8), &mut buffer) .unwrap() - .is_some() ); assert_eq!(buffer, [ @@ -602,7 +625,6 @@ fn regressions() { parser .virt_read_exact(Gva::new(0xfffff80122800000 + 0x200000 - 0x8), &mut buffer) .unwrap() - .is_some() ); assert_eq!(buffer, [ From 81d1ca88ead7570d4ce524533f285cb9dea79d79 Mon Sep 17 00:00:00 2001 From: 0vercl0k <1476421+0vercl0k@users.noreply.github.com> Date: Tue, 21 Oct 2025 20:56:23 -0700 Subject: [PATCH 8/8] consistency --- src/parse.rs | 19 +++++++++++++------ tests/regression.rs | 2 ++ 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/parse.rs b/src/parse.rs index febc0ed..198eea8 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -830,17 +830,22 @@ impl KernelDumpParser { /// Read an exact amount of virtual memory starting at `gva`. Returns `None` /// if a memory error occurs (page not present, page not in dump, etc.). - pub fn virt_read_exact(&self, gva: Gva, buf: &mut [u8]) -> Result { + pub fn virt_read_exact(&self, gva: Gva, buf: &mut [u8]) -> Result> { self.virt_read_exact_with_dtb(gva, buf, Gpa::new(self.headers.directory_table_base)) } /// Read an exact amount of virtual memory starting at `gva` using a /// specific directory table base / set of page tables. Returns `None` if a /// memory error occurs (page not present, page not in dump, etc.). - pub fn virt_read_exact_with_dtb(&self, gva: Gva, buf: &mut [u8], dtb: Gpa) -> Result { + pub fn virt_read_exact_with_dtb( + &self, + gva: Gva, + buf: &mut [u8], + dtb: Gpa, + ) -> Result> { match self.virt_read_exact_strict_with_dtb(gva, buf, dtb) { - Ok(()) => Ok(true), - Err(KdmpParserError::MemoryRead(_)) => Ok(false), + Ok(()) => Ok(Some(())), + Err(KdmpParserError::MemoryRead(_)) => Ok(None), Err(e) => Err(e), } } @@ -969,9 +974,11 @@ impl KernelDumpParser { } let mut buffer = vec![0; unicode_str.length.into()]; - if !self.virt_read_exact_with_dtb(Gva::new(unicode_str.buffer.into()), &mut buffer, dtb)? { + let Some(()) = + self.virt_read_exact_with_dtb(Gva::new(unicode_str.buffer.into()), &mut buffer, dtb)? + else { return Ok(None); - } + }; let n = unicode_str.length / 2; diff --git a/tests/regression.rs b/tests/regression.rs index 9ae6b5c..0e93521 100644 --- a/tests/regression.rs +++ b/tests/regression.rs @@ -597,6 +597,7 @@ fn regressions() { parser .virt_read_exact(Gva::new(0xfffff80122800000 + 0x100000 - 8), &mut buffer) .unwrap() + .is_some() ); assert_eq!(buffer, [ @@ -625,6 +626,7 @@ fn regressions() { parser .virt_read_exact(Gva::new(0xfffff80122800000 + 0x200000 - 0x8), &mut buffer) .unwrap() + .is_some() ); assert_eq!(buffer, [