diff --git a/Cargo.toml b/Cargo.toml index 9f3e528..73b6a6d 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"] @@ -9,12 +9,7 @@ include = ["/Cargo.toml", "/LICENSE", "/src/**", "/examples/**", "README.md"] keywords = ["windows", "kernel", "crashdump"] 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" +rust-version = "1.91" [dev-dependencies] anyhow = "1.0" @@ -22,5 +17,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" 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/examples/parser.rs b/examples/parser.rs index 3871ad0..c695e4b 100644 --- a/examples/parser.rs +++ b/examples/parser.rs @@ -7,7 +7,10 @@ use std::path::PathBuf; use anyhow::{Context, Result}; use clap::{Parser, ValueEnum}; -use kdmp_parser::{Gpa, Gva, Gxa, KernelDumpParser, MappedFileReader}; +use kdmp_parser::gxa::{Gpa, Gva, Gxa}; +use kdmp_parser::map::MappedFileReader; +use kdmp_parser::parse::KernelDumpParser; +use kdmp_parser::{phys, virt}; #[derive(Debug, Default, Clone, Copy, ValueEnum)] enum ReaderMode { @@ -18,10 +21,12 @@ enum ReaderMode { File, } +#[expect(clippy::struct_excessive_bools)] #[derive(Parser, Debug)] #[command(version, about)] struct Args { /// The dump path. + #[arg(last = true)] dump_path: PathBuf, /// Dump the dump headers. #[arg(long, default_value_t = false)] @@ -65,7 +70,7 @@ fn hexdump(address: u64, data: &[u8], wanted_len: usize) { let wanted_left = wanted_len - i; // Do we need a full row or less? let left_to_display = min(wanted_left, 16); - print!("{:016x}: ", address + (i as u64 * 16)); + print!("{:016x}: ", address + i as u64); // Iterate over the row now and populate it with the data. We do this because // the output first displays the hexadecimal value of every bytes, and then its @@ -73,29 +78,26 @@ fn hexdump(address: u64, data: &[u8], wanted_len: usize) { let mut row_it = row.iter_mut().enumerate().peekable(); while let Some((idx, item)) = row_it.next() { // Drain the data iterator byte by byte and fill the row with the data. - match data_it.next() { - Some(c) => { - // If we have a byte, then easy peasy. - *item = Some(*c); - print!("{:02x}", c); - } - None => { - *item = None; - // If we don't have a byte, then we need to figure out what to do. There are two - // cases to take care of: - let displayed_amount = i + idx; - if displayed_amount >= wanted_len { - // - either what is left to display is not a full row, in which case we need - // to display spaces to padd the output such that the upcoming ASCII - // representation stays aligned. - print!(" "); - } else { - // - either the user asked a larger length than what is mapped in memory, in - // which case we need to display `??` for those bytes. - print!("??"); - } + if let Some(c) = data_it.next() { + // If we have a byte, then easy peasy. + *item = Some(*c); + print!("{c:02x}"); + } else { + *item = None; + // If we don't have a byte, then we need to figure out what to do. There are two + // cases to take care of: + let displayed_amount = i + idx; + if displayed_amount >= wanted_len { + // - either what is left to display is not a full row, in which case we need to + // display spaces to padd the output such that the upcoming ASCII + // representation stays aligned. + print!(" "); + } else { + // - either the user asked a larger length than what is mapped in memory, in + // which case we need to display `??` for those bytes. + print!("??"); } - }; + } // We separate half of the row with a dash. But we only want to display it if // there'll be at least one byte after it (so at least 9 bytes to display in @@ -117,7 +119,7 @@ fn hexdump(address: u64, data: &[u8], wanted_len: usize) { print!("?"); } } - println!() + println!(); } } @@ -169,21 +171,23 @@ fn main() -> Result<()> { if let Some(addr) = args.mem { let mut buffer = vec![0; args.len]; let addr = to_hex(&addr)?; + let phys_reader = phys::Reader::new(&parser); + let virt_reader = virt::Reader::with_dtb( + &parser, + args.dtb + .unwrap_or(Gpa::new(parser.headers().directory_table_base)), + ); + if addr == u64::MAX { for (gpa, _) in parser.physmem() { - parser.phys_read_exact(gpa, &mut buffer)?; - hexdump(gpa.u64(), &buffer, args.len) + phys_reader.read_exact(gpa, &mut buffer)?; + hexdump(gpa.u64(), &buffer, args.len); } } else { let amount = if args.virt { - parser.virt_read_with_dtb( - Gva::new(addr), - &mut buffer, - args.dtb - .unwrap_or(Gpa::new(parser.headers().directory_table_base)), - ) + virt_reader.read(Gva::new(addr), &mut buffer) } else { - parser.phys_read(Gpa::new(addr), &mut buffer) + phys_reader.read(Gpa::new(addr), &mut buffer) }; if let Ok(amount) = amount { diff --git a/src/bits.rs b/src/bits.rs index ac8fcc3..6fdfbd3 100644 --- a/src/bits.rs +++ b/src/bits.rs @@ -5,7 +5,7 @@ //! //! # Examples //! //! ``` -//! # use kdmp_parser::Bits; +//! # use kdmp_parser::bits::Bits; //! let v = 0xAB_CD_EF_01_23_45_67_89u64; //! assert_eq!(v.bits(0..=63), v); //! assert_eq!(v.bits(0..=55), 0xCD_EF_01_23_45_67_89); @@ -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..6975d40 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,71 +1,178 @@ // 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::fmt::{self, Display}; +use std::io; +use std::string::FromUtf16Error; +use crate::gxa::{Gpa, Gva}; use crate::structs::{DUMP_HEADER64_EXPECTED_SIGNATURE, DUMP_HEADER64_EXPECTED_VALID_DUMP}; -use crate::{Gpa, Gva}; -pub type Result = std::result::Result; +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, Error)] -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. + NotInDump { + gva: Option<(Gva, Option)>, + gpa: Gpa, + }, } -impl Display for AddrTranslationError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl std::error::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: `MemoryReadError::PageRead(PageReadError::NotPresent { +/// gva: 0x1337_000, 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: `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: `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::NotInDump { gpa: 0x1337_000 })` +/// - Reading the second page failed: `MemoryError::PartialRead { +/// expected_amount: 8_192, actual_amount: 4_096, reason: +/// 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` +/// versions will. +impl Display for PageReadError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> 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}")) + 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)") + } + }, } } } -#[derive(Error, Debug)] -pub enum KdmpParserError { - #[error("invalid UNICODE_STRING")] +#[derive(Debug)] +pub enum Error { 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), + PartialRead { + expected_amount: usize, + actual_amount: usize, + reason: PageReadError, + }, + PageRead(PageReadError), +} + +impl From for Error { + fn from(value: io::Error) -> Self { + Self::Io(value) + } +} + +impl From for Error { + fn from(value: FromUtf16Error) -> Self { + Self::Utf16(value) + } +} + +impl From for Error { + fn from(value: PageReadError) -> Self { + Self::PageRead(value) + } +} + +impl Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::InvalidUnicodeString => write!(f, "invalid UNICODE_STRING"), + Self::Utf16(_) => write!(f, "utf16"), + Self::Overflow(o) => write!(f, "overflow: {o}"), + Self::Io(_) => write!(f, "io"), + Self::InvalidData(i) => write!(f, "invalid data: {i}"), + Self::UnknownDumpType(u) => write!(f, "unsupported dump type {u:#x}"), + Self::DuplicateGpa(gpa) => { + write!(f, "duplicate gpa found in physmem map for {gpa}") + } + Self::InvalidSignature(sig) => write!( + f, + "header's signature looks wrong: {sig:#x} vs {DUMP_HEADER64_EXPECTED_SIGNATURE:#x}" + ), + Self::InvalidValidDump(dump) => write!( + f, + "header's valid dump looks wrong: {dump:#x} vs {DUMP_HEADER64_EXPECTED_VALID_DUMP:#x}" + ), + Self::PhysAddrOverflow(run, page) => { + write!(f, "overflow for phys addr w/ run {run} page {page}") + } + Self::PageOffsetOverflow(run, page) => { + write!(f, "overflow for page offset w/ run {run} page {page}") + } + Self::BitmapPageOffsetOverflow(bitmap_idx, bit_idx) => write!( + f, + "overflow for page offset w/ bitmap_idx {bitmap_idx} bit_idx {bit_idx}" + ), + Self::PartialRead { + expected_amount, + actual_amount, + reason, + } => write!( + f, + "partially read {actual_amount} bytes out of {expected_amount} because of {reason}" + ), + Self::PageRead(p) => write!(f, "page read: {p}"), + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::Utf16(u) => Some(u), + Self::Io(e) => Some(e), + Self::PartialRead { reason, .. } => Some(reason), + _ => None, + } + } } diff --git a/src/gxa.rs b/src/gxa.rs index 5bd0797..453bef0 100644 --- a/src/gxa.rs +++ b/src/gxa.rs @@ -8,12 +8,12 @@ //! # Examples //! //! ``` -//! use kdmp_parser::{Gxa, Gva}; +//! use kdmp_parser::gxa::{Gxa, Gva}; //! let gva = Gva::new(1337); //! let page_aligned_gva = gva.page_align(); //! let page_offset = gva.offset(); //! ``` -use std::fmt::Display; +use std::fmt::{self, Debug, 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() @@ -58,7 +61,7 @@ pub trait Gxa: Sized + Default + Copy + From { /// # Examples /// /// ``` -/// # use kdmp_parser::{Gxa, Gpa}; +/// # use kdmp_parser::gxa::{Gxa, Gpa}; /// # fn main() { /// let gpa = Gpa::new(0x1337_123); /// assert_eq!(gpa.offset(), 0x123); @@ -79,11 +82,12 @@ impl Gpa { /// # Examples /// /// ``` - /// # use kdmp_parser::{Gxa, Gpa}; + /// # use kdmp_parser::gxa::{Gxa, Gpa}; /// # fn main() { /// let gpa = Gpa::new(1337); /// # } /// ``` + #[must_use] pub const fn new(addr: u64) -> Self { Self(addr) } @@ -93,12 +97,14 @@ impl Gpa { /// # Examples /// /// ``` - /// # use kdmp_parser::{Gxa, Gpa, Pfn}; + /// # use kdmp_parser::pxe::Pfn; + /// # use kdmp_parser::gxa::{Gxa, Gpa}; /// # fn main() { /// let gpa = Gpa::from_pfn(Pfn::new(0x1337)); /// assert_eq!(gpa.u64(), 0x1337_000); /// # } /// ``` + #[must_use] pub const fn from_pfn(pfn: Pfn) -> Self { Self(pfn.u64() << (4 * 3)) } @@ -109,12 +115,14 @@ impl Gpa { /// # Examples /// /// ``` - /// # use kdmp_parser::{Gxa, Gpa, Pfn}; + /// # use kdmp_parser::pxe::Pfn; + /// # use kdmp_parser::gxa::{Gxa, Gpa}; /// # fn main() { /// let gpa = Gpa::from_pfn_with_offset(Pfn::new(0x1337), 0x11); /// 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); @@ -126,12 +134,13 @@ impl Gpa { /// # Examples /// /// ``` - /// # use kdmp_parser::{Gxa, Gpa}; + /// # use kdmp_parser::gxa::{Gxa, Gpa}; /// # fn main() { /// let gpa = Gpa::new(0x1337_337); /// assert_eq!(gpa.pfn(), 0x1337); /// # } /// ``` + #[must_use] pub const fn pfn(&self) -> u64 { self.0 >> (4 * 3) } @@ -140,7 +149,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; } } @@ -150,7 +159,7 @@ impl Gxa for Gpa { /// # Examples /// /// ``` - /// # use kdmp_parser::{Gxa, Gpa}; + /// # use kdmp_parser::gxa::{Gxa, Gpa}; /// # fn main() { /// let gpa = Gpa::new(1337); /// assert_eq!(gpa.u64(), 1337); @@ -168,7 +177,7 @@ impl From for Gpa { /// # Examples /// /// ``` - /// # use kdmp_parser::{Gxa, Gpa}; + /// # use kdmp_parser::gxa::{Gxa, Gpa}; /// # fn main() { /// let gpa = Gpa::from(0xdeadbeef_baadc0de); /// assert_eq!(u64::from(gpa), 0xdeadbeef_baadc0de); @@ -186,7 +195,7 @@ impl From for u64 { /// # Examples /// /// ``` - /// # use kdmp_parser::{Gxa, Gpa}; + /// # use kdmp_parser::gxa::{Gxa, Gpa}; /// # fn main() { /// let gpa = Gpa::new(0xdeadbeef_baadc0de); /// let gpa_u64: u64 = gpa.into(); @@ -206,7 +215,7 @@ impl From<&Gpa> for u64 { /// # Examples /// /// ``` - /// # use kdmp_parser::{Gxa, Gpa}; + /// # use kdmp_parser::gxa::{Gxa, Gpa}; /// # fn main() { /// let gpa = Gpa::new(0xdeadbeef_baadc0de); /// let gpa_p = &gpa; @@ -222,7 +231,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) } } @@ -246,7 +255,7 @@ impl FromStr for Gpa { /// # Examples /// /// ``` -/// # use kdmp_parser::{Gxa, Gva}; +/// # use kdmp_parser::gxa::{Gxa, Gva}; /// # fn main() { /// let gva = Gva::new(0x1337_fff); /// assert_eq!(gva.offset(), 0xfff); @@ -267,11 +276,12 @@ impl Gva { /// # Examples /// /// ``` - /// # use kdmp_parser::{Gxa, Gva}; + /// # use kdmp_parser::gxa::{Gxa, Gva}; /// # fn main() { /// let gva = Gva::new(0xdeadbeef); /// # } /// ``` + #[must_use] pub const fn new(addr: u64) -> Self { Self(addr) } @@ -281,7 +291,7 @@ impl Gva { /// # Examples /// /// ``` - /// # use kdmp_parser::{Gxa, Gva}; + /// # use kdmp_parser::gxa::{Gxa, Gva}; /// # fn main() { /// let first = Gva::new(0xff_ff_b9_dc_ee_77_31_37); /// assert_eq!(first.pte_idx(), 371); @@ -290,6 +300,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 } @@ -299,7 +310,7 @@ impl Gva { /// # Examples /// /// ``` - /// # use kdmp_parser::{Gxa, Gva}; + /// # use kdmp_parser::gxa::{Gxa, Gva}; /// # fn main() { /// let first = Gva::new(0xff_ff_b9_dc_ee_77_31_37); /// assert_eq!(first.pde_idx(), 371); @@ -308,6 +319,7 @@ impl Gva { /// # } /// ``` #[allow(clippy::identity_op)] + #[must_use] pub const fn pde_idx(&self) -> u64 { (self.0 >> (12 + (9 * 1))) & 0b1_1111_1111 } @@ -317,7 +329,7 @@ impl Gva { /// # Examples /// /// ``` - /// # use kdmp_parser::{Gxa, Gva}; + /// # use kdmp_parser::gxa::{Gxa, Gva}; /// # fn main() { /// let first = Gva::new(0xff_ff_b9_dc_ee_77_31_37); /// assert_eq!(first.pdpe_idx(), 371); @@ -325,6 +337,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 } @@ -334,7 +347,7 @@ impl Gva { /// # Examples /// /// ``` - /// # use kdmp_parser::{Gxa, Gva}; + /// # use kdmp_parser::gxa::{Gxa, Gva}; /// # fn main() { /// let first = Gva::new(0xff_ff_b9_dc_ee_77_31_37); /// assert_eq!(first.pml4e_idx(), 371); @@ -342,6 +355,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 +364,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; } } @@ -360,7 +374,7 @@ impl Gxa for Gva { /// # Examples /// /// ``` - /// # use kdmp_parser::{Gxa, Gva}; + /// # use kdmp_parser::gxa::{Gxa, Gva}; /// # fn main() { /// let gva = Gva::new(0xdeadbeef); /// assert_eq!(gva.u64(), 0xdeadbeef); @@ -378,7 +392,7 @@ impl From for Gva { /// # Examples /// /// ``` - /// # use kdmp_parser::{Gxa, Gva}; + /// # use kdmp_parser::gxa::{Gxa, Gva}; /// # fn main() { /// let gva = Gva::from(0xbaadc0de_deadbeef); /// assert_eq!(u64::from(gva), 0xbaadc0de_deadbeef); @@ -396,7 +410,7 @@ impl From for u64 { /// # Examples /// /// ``` - /// # use kdmp_parser::{Gxa, Gva}; + /// # use kdmp_parser::gxa::{Gxa, Gva}; /// # fn main() { /// let gva = Gva::new(0xbaadc0de_deadbeef); /// let gva_u64: u64 = gva.into(); @@ -416,7 +430,7 @@ impl From<&Gva> for u64 { /// # Examples /// /// ``` - /// # use kdmp_parser::{Gxa, Gpa}; + /// # use kdmp_parser::gxa::{Gxa, Gpa}; /// # fn main() { /// let gva = Gpa::new(0xbaadc0de_deadbeef); /// let gva_p = &gva; diff --git a/src/lib.rs b/src/lib.rs index 0c8dd89..1d6887a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,17 +1,14 @@ // Axel '0vercl0k' Souchet - February 25 2024 +#![allow(clippy::doc_markdown)] #![doc = include_str!("../README.md")] -mod bits; -mod error; -mod gxa; -mod map; -mod parse; -mod pxe; -mod structs; - -pub use bits::Bits; -pub use error::{AddrTranslationError, KdmpParserError, PxeNotPresent, Result}; -pub use gxa::{Gpa, Gva, Gxa}; -pub use map::{MappedFileReader, Reader}; -pub use parse::{KernelDumpParser, VirtTranslationDetails}; -pub use pxe::{Pfn, Pxe, PxeFlags}; -pub use structs::{Context, DumpType, Header64, PageKind}; +#![allow(clippy::missing_panics_doc, clippy::missing_errors_doc)] +pub mod bits; +pub mod error; +pub mod gxa; +pub mod map; +pub mod parse; +pub mod phys; +pub mod pxe; +pub mod structs; +pub mod virt; +mod virt_utils; diff --git a/src/map.rs b/src/map.rs index f25176c..f6ee730 100644 --- a/src/map.rs +++ b/src/map.rs @@ -1,10 +1,12 @@ // 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}; +//! Unix and Windows (cf `memory_map_file` / `unmap_memory_mapped_file`). +use std::fmt::{self, Debug}; +use std::fs::File; +use std::io::{self, Cursor, Read, Seek}; use std::path::Path; -use std::{fs, io, ptr, slice}; + +// XXX: use [cfg_select](https://github.com/rust-lang/rust/issues/115585#issue-1882997206) when it's stabilized. pub trait Reader: Read + Seek {} @@ -14,11 +16,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 +29,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 +57,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 +119,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 +163,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 +191,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 +217,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(); @@ -234,9 +234,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: @@ -264,7 +262,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 1f42d06..f18d576 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -2,76 +2,28 @@ //! This has all the parsing logic for parsing kernel crash-dumps. 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}; use crate::bits::Bits; -use crate::error::{PxeNotPresent, Result}; -use crate::gxa::Gxa; +use crate::error::{Error, Result}; +use crate::gxa::{Gpa, Gva}; use crate::map::{MappedFileReader, Reader}; +use crate::pxe::Pfn; use crate::structs::{ BmpHeader64, Context, DUMP_HEADER64_EXPECTED_SIGNATURE, DUMP_HEADER64_EXPECTED_VALID_DUMP, DumpType, ExceptionRecord64, FullRdmpHeader64, Header64, KdDebuggerData64, KernelRdmpHeader64, - LdrDataTableEntry, ListEntry, PageKind, PfnRange, PhysmemDesc, PhysmemMap, PhysmemRun, - UnicodeString, read_struct, + PageKind, PfnRange, PhysmemDesc, PhysmemMap, PhysmemRun, Pod, +}; +use crate::virt; +use crate::virt_utils::{ + ModuleMap, try_extract_kernel_modules, try_extract_user_modules, try_find_prcb, }; -use crate::{AddrTranslationError, Gpa, Gva, KdmpParserError, Pfn, Pxe}; - -/// The details related to a virtual to physical address translation. -/// -/// If you are wondering why there is no 'readable' field, it is because -/// [`KernelDumpParser::virt_translate`] returns an error if one of the PXE is -/// marked as not present. In other words, if the translation succeeds, the page -/// is at least readable. -#[derive(Debug)] -pub struct VirtTranslationDetails { - /// The physical address backing the virtual address that was requested. - pub pfn: Pfn, - /// The byte offset in that physical page. - pub offset: u64, - /// The kind of physical page. - pub page_kind: PageKind, - /// Is the page writable? - pub writable: bool, - /// Is the page executable? - pub executable: bool, - /// Is the page user accessible? - pub user_accessible: bool, -} - -impl VirtTranslationDetails { - pub fn new(pxes: &[Pxe], gva: Gva) -> Self { - let writable = pxes.iter().all(Pxe::writable); - let executable = pxes.iter().all(Pxe::executable); - let user_accessible = pxes.iter().all(Pxe::user_accessible); - let pfn = pxes.last().map(|p| p.pfn).expect("at least one pxe"); - let page_kind = match pxes.len() { - 4 => PageKind::Normal, - 3 => PageKind::Large, - 2 => PageKind::Huge, - _ => unreachable!("pxes len should be between 2 and 4"), - }; - let offset = page_kind.page_offset(gva.u64()); - - Self { - pfn, - offset, - page_kind, - writable, - executable, - user_accessible, - } - } - - pub fn gpa(&self) -> Gpa { - self.pfn.gpa_with_offset(self.offset) - } -} fn gpa_from_bitmap(bitmap_idx: u64, bit_idx: usize) -> Option { let pfn = Pfn::new( @@ -89,281 +41,16 @@ fn gpa_from_pfn_range(pfn_range: &PfnRange, page_idx: u64) -> Option { Some(Pfn::new(pfn_range.page_file_number).gpa_with_offset(offset)) } -/// This trait is used to implement generic behavior for both 32/64-bit. -/// It is implemented for both [`u32`] & [`u64`]. -trait PtrSize: Sized + Copy + Into + From { - fn checked_add(self, rhs: Self) -> Option; -} - -macro_rules! impl_checked_add { - ($($ty:ident),*) => { - $(impl PtrSize for $ty { - fn checked_add(self, rhs: $ty) -> Option { - $ty::checked_add(self, rhs) - } - })* - }; -} - -impl_checked_add!(u32, u64); - -/// 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, -{ - let mut modules = ModuleMap::new(); - let Some(entry) = parser.try_virt_read_struct::>(head)? else { - return Ok(None); - }; - - let mut entry_addr = Gva::new(entry.flink.into()); - // 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 { - 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) - .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) - } else { - Ok(s) - } - })? - else { - return Ok(None); - }; - - // Shove it into the map. - let dll_end_addr = data - .dll_base - .checked_add(data.size_of_image.into()) - .ok_or(KdmpParserError::Overflow("module address"))?; - let at = Gva::new(data.dll_base.into())..Gva::new(dll_end_addr.into()); - let inserted = modules.insert(at, dll_name); - debug_assert!(inserted.is_none()); - - // Go to the next entry. - entry_addr = Gva::new(data.in_load_order_links.flink.into()); - } - - Ok(Some(modules)) -} - -/// Extract the drivers / modules out of the `PsLoadedModuleList`. -fn try_extract_kernel_modules(parser: &mut KernelDumpParser) -> Result> { - // Walk the LIST_ENTRY! - try_read_module_map::(parser, parser.headers().ps_loaded_module_list.into()) -} - -/// Try to find the right `nt!_KPRCB` by walking them and finding one that has -/// the same `Rsp` than in the dump headers' context. -fn try_find_prcb( - parser: &mut KernelDumpParser, - kd_debugger_data_block: &KdDebuggerData64, -) -> Result> { - 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 { - return Ok(None); - }; - - // Calculate the address of where the CONTEXT pointer is at.. - let kprcb_context_addr = kprcb_addr - .checked_add(kd_debugger_data_block.offset_prcb_context.into()) - .ok_or(KdmpParserError::Overflow("offset_prcb"))?; - - // ..and read it. - let Some(kprcb_context_addr) = - parser.try_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())? - else { - return Ok(None); - }; - - // ..and compare it to ours. - let kprcb_context = Box::new(kprcb_context); - if kprcb_context.rsp == parser.context_record().rsp { - // The register match so we'll assume the current KPRCB is the one describing - // the 'foreground' processor in the crash-dump. - return Ok(Some(kprcb_addr.into())); - } - - // Otherwise, let's move on to the next pointer. - processor_block = processor_block - .checked_add(mem::size_of::() as _) - .ok_or(KdmpParserError::Overflow("kprcb ptr"))?; - } - - Ok(None) -} - -/// Extract the user modules list by grabbing the current thread from the KPRCB. -/// Then, walk the `PEB.Ldr.InLoadOrderModuleList`. -fn try_extract_user_modules( - parser: &mut KernelDumpParser, - kd_debugger_data_block: &KdDebuggerData64, - prcb_addr: Gva, -) -> Result> { - // Get the current _KTHREAD.. - let kthread_addr = prcb_addr - .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 { - return Ok(None); - }; - - // ..then its TEB.. - 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 { - return Ok(None); - }; - - if teb_addr == 0 { - return Ok(None); - } - - // ..then its PEB.. - // ``` - // kd> dt nt!_TEB ProcessEnvironmentBlock - // nt!_TEB - // +0x060 ProcessEnvironmentBlock : Ptr64 _PEB - // ``` - let peb_offset = 0x60; - 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 { - return Ok(None); - }; - - // ..then its _PEB_LDR_DATA.. - // ``` - // kd> dt nt!_PEB Ldr - // +0x018 Ldr : Ptr64 _PEB_LDR_DATA - // ``` - let ldr_offset = 0x18; - 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 { - return Ok(None); - }; - - // ..and finally the `InLoadOrderModuleList`. - // ``` - // kd> dt nt!_PEB_LDR_DATA InLoadOrderModuleList - // +0x010 InLoadOrderModuleList : _LIST_ENTRY - // ```` - let in_load_order_module_list_offset = 0x10; - let module_list_entry_addr = peb_ldr_addr - .checked_add(in_load_order_module_list_offset) - .ok_or(KdmpParserError::Overflow( - "in load order module list offset", - ))?; +/// Read a `T` from the cursor. +fn read_struct(reader: &mut impl Reader) -> Result { + 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) }; + reader.read_exact(slice_over_s)?; - // From there, we walk the list! - let Some(mut modules) = try_read_module_map::(parser, module_list_entry_addr.into())? - else { - return Ok(None); - }; - - // Now, it's time to dump the TEB32 if there's one. - // - // TEB32 is at offset 0x2000 from TEB and PEB32 is at +0x30: - // ``` - // kd> dt nt!_TEB32 ProcessEnvironmentBlock - // nt!_TEB32 - // +0x030 ProcessEnvironmentBlock : Uint4B - // ``` - let teb32_offset = 0x2_000; - let teb32_addr = teb_addr - .checked_add(teb32_offset) - .ok_or(KdmpParserError::Overflow("teb32 offset"))?; - let peb32_offset = 0x30; - 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 { - return Ok(Some(modules)); - }; - - // ..then its _PEB_LDR_DATA.. (32-bit) - // ``` - // kd> dt nt!_PEB32 Ldr - // +0x00c Ldr : Uint4B - // ``` - let ldr_offset = 0x0c; - 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()))? - else { - return Ok(Some(modules)); - }; - - // ..and finally the `InLoadOrderModuleList`. - // ``` - // 0:000> dt ntdll!_PEB_LDR_DATA InLoadOrderModuleList - // +0x00c InLoadOrderModuleList : _LIST_ENTRY - // ```` - let in_load_order_module_list_offset = 0xc; - let module_list_entry_addr = peb32_ldr_addr - .checked_add(in_load_order_module_list_offset) - .ok_or(KdmpParserError::Overflow( - "in load order module list offset", - ))?; - - // From there, we walk the list! - let Some(modules32) = - try_read_module_map::(parser, Gva::new(module_list_entry_addr.into()))? - else { - return Ok(Some(modules)); - }; - - // Merge the lists. - modules.extend(modules32); - - // We're done! - Ok(Some(modules)) + Ok(unsafe { s.assume_init() }) } -/// Filter out [`AddrTranslationError`] 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> { - 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(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>; - /// A kernel dump parser that gives access to the physical memory space stored /// in the dump. It also offers virtual to physical memory translation as well /// as a virtual read facility. @@ -376,7 +63,8 @@ pub struct KernelDumpParser { headers: Box, /// This maps a physical address to a file offset. Seeking there gives the /// page content. - physmem: PhysmemMap, + /// XXX: Is this pub(crate) fair? + pub(crate) physmem: PhysmemMap, /// The [`Reader`] object that allows us to seek / read the dump file which /// could be memory mapped, read from a file, etc. reader: RefCell>, @@ -389,25 +77,29 @@ 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() } } 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)?); if headers.signature != DUMP_HEADER64_EXPECTED_SIGNATURE { - return Err(KdmpParserError::InvalidSignature(headers.signature)); + return Err(Error::InvalidSignature(headers.signature)); } if headers.valid_dump != DUMP_HEADER64_EXPECTED_VALID_DUMP { - return Err(KdmpParserError::InvalidValidDump(headers.valid_dump)); + return Err(Error::InvalidValidDump(headers.valid_dump)); } // Grab the dump type and make sure it is one we support. @@ -428,36 +120,36 @@ 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 // translation error we'll keep going, otherwise we'll error out. - if let Some(kernel_modules) = try_extract_kernel_modules(&mut parser)? { + if let Some(kernel_modules) = try_extract_kernel_modules(&parser)? { parser.kernel_modules.extend(kernel_modules); } // 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::( - parser.headers().kd_debugger_data_block.into(), - )? + let virt_reader = virt::Reader::new(&parser); + let Some(kd_debugger_data_block) = virt_reader + .try_read_struct::(parser.headers().kd_debugger_data_block.into())? else { return Ok(parser); }; let kd_debugger_data_block = Box::new(kd_debugger_data_block); // We need to figure out which PRCB is the one that crashed. - let Some(prcb_addr) = try_find_prcb(&mut parser, &kd_debugger_data_block)? else { + let Some(prcb_addr) = try_find_prcb(&parser, &kd_debugger_data_block)? else { return Ok(parser); }; // Finally, we're ready to extract the user modules! let Some(user_modules) = - try_extract_user_modules(&mut parser, &kd_debugger_data_block, prcb_addr)? + try_extract_user_modules(&virt_reader, &kd_debugger_data_block, prcb_addr)? else { return Ok(parser); }; @@ -467,23 +159,26 @@ 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 // 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) } } @@ -524,343 +219,22 @@ impl KernelDumpParser { &self.context } - /// Translate a [`Gpa`] into a file offset of where the content of the page - /// resides in. - pub fn phys_translate(&self, gpa: Gpa) -> Result { - let offset = *self - .physmem - .get(&gpa.page_align()) - .ok_or(AddrTranslationError::Phys(gpa))?; - - offset - .checked_add(gpa.offset()) - .ok_or(KdmpParserError::Overflow("w/ gpa offset")) - } - - /// Read physical memory starting at `gpa` into a `buffer`. - pub fn phys_read(&self, gpa: Gpa, buf: &mut [u8]) -> Result { - // Amount of bytes left to read. - let mut amount_left = buf.len(); - // Total amount of bytes that we have successfully read. - let mut total_read = 0; - // The current gpa we are reading from. - let mut addr = gpa; - // Let's try to read as much as the user wants. - while amount_left > 0 { - // 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))?; - // 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 - // less. - let left_in_page = (PageKind::Normal.size() - gpa.offset()) as usize; - 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]; - // Read the physical memory! - let amount_read = self.read(slice)?; - // Update the total amount of read bytes and how much work we have left. - total_read += amount_read; - amount_left -= amount_read; - // If we couldn't read as much as we wanted, we're done. - if amount_read != amount_wanted { - return Ok(total_read); - } - - // We have more work to do, so let's move to the next page. - addr = addr.next_aligned_page(); - } - - // Yay, we read as much bytes as the user wanted! - Ok(total_read) - } - - /// Read an exact amount of physical memory starting at `gpa` into a - /// `buffer`. - pub fn phys_read_exact(&self, gpa: Gpa, buf: &mut [u8]) -> Result<()> { - // Read physical memory. - let len = self.phys_read(gpa, buf)?; - - // If we read as many bytes as we wanted, then it's a win.. - if len == buf.len() { - Ok(()) - } - // ..otherwise, we call it quits. - else { - Err(KdmpParserError::PartialPhysRead) - } - } - - /// 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 slice_over_t = - unsafe { slice::from_raw_parts_mut(t.as_mut_ptr() as *mut u8, size_of_t) }; - - self.phys_read_exact(gpa, slice_over_t)?; - - Ok(unsafe { t.assume_init() }) - } - - /// Translate a [`Gva`] into a [`Gpa`]. - pub fn virt_translate(&self, gva: Gva) -> Result { - self.virt_translate_with_dtb(gva, Gpa::new(self.headers.directory_table_base)) - } - - /// Translate a [`Gva`] into a [`Gpa`] using a specific directory table base - /// / set of page tables. - 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(); - let pml4e_gpa = Gpa::new(pml4_base.u64() + (gva.pml4e_idx() * 8)); - let pml4e = Pxe::from(self.phys_read_struct::(pml4e_gpa)?); - if !pml4e.present() { - return Err(AddrTranslationError::Virt(gva, PxeNotPresent::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)?); - if !pdpte.present() { - return Err(AddrTranslationError::Virt(gva, PxeNotPresent::Pdpte).into()); - } - - // huge pages: - // 7 (PS) - Page size; must be 1 (otherwise, this entry references a page - // directory; see Table 4-1 - let pd_base = pdpte.pfn.gpa(); - if pdpte.large_page() { - return Ok(VirtTranslationDetails::new(&[pml4e, pdpte], gva)); - } - - let pde_gpa = Gpa::new(pd_base.u64() + (gva.pde_idx() * 8)); - let pde = Pxe::from(self.phys_read_struct::(pde_gpa)?); - if !pde.present() { - return Err(AddrTranslationError::Virt(gva, PxeNotPresent::Pde).into()); - } - - // large pages: - // 7 (PS) - Page size; must be 1 (otherwise, this entry references a page - // table; see Table 4-18 - let pt_base = pde.pfn.gpa(); - if pde.large_page() { - return Ok(VirtTranslationDetails::new(&[pml4e, pdpte, pde], gva)); - } - - let pte_gpa = Gpa::new(pt_base.u64() + (gva.pte_idx() * 8)); - let pte = Pxe::from(self.phys_read_struct::(pte_gpa)?); - 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()); - } - } - - 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 { - 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 { - // Amount of bytes left to read. - let mut amount_left = buf.len(); - // Total amount of bytes that we have successfully read. - let mut total_read = 0; - // The current gva we are reading from. - let mut addr = gva; - // Let's try to read as much as the user wants. - while amount_left > 0 { - // Translate the gva into a gpa. But make sure to not early return if an error - // 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); - } - }; - - // 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; - // 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); - // Figure out where we should read into. - let slice = &mut buf[total_read..total_read + amount_wanted]; - - // Read the physical memory! - let amount_read = self.phys_read(translation.gpa(), slice)?; - // Update the total amount of read bytes and how much work we have left. - total_read += amount_read; - amount_left -= amount_read; - // If we couldn't read as much as we wanted, we're done. - if amount_read != amount_wanted { - return Ok(total_read); - } - - // We have more work to do, so let's move to the next page. - addr = addr.next_aligned_page(); - } - - // Yay, we read as much bytes as the user wanted! - 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)) - } - - /// 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( - &self, - gva: Gva, - buf: &mut [u8], - dtb: Gpa, - ) -> Result> { - filter_addr_translation_err(self.virt_read_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` 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<()> { - // Read virtual memory. - let len = self.virt_read_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. - else { - Err(KdmpParserError::PartialVirtRead) - } - } - - /// 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)) - } - - /// 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. - 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 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 slice_over_t = - unsafe { slice::from_raw_parts_mut(t.as_mut_ptr() as *mut u8, size_of_t) }; - - self.virt_read_exact_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 + /// + /// 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)?) } - pub fn read(&self, buf: &mut [u8]) -> Result { - Ok(self.reader.borrow_mut().read(buf)?) - } - - /// Try to read a `UNICODE_STRING`. - fn try_virt_read_unicode_string

( - &self, - unicode_str: &UnicodeString

, - ) -> Result> - where - P: PtrSize, - { - self.try_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

( - &self, - unicode_str: &UnicodeString

, - dtb: Gpa, - ) -> Result> - where - P: PtrSize, - { - if (unicode_str.length % 2) != 0 { - return Err(KdmpParserError::InvalidUnicodeString); - } - - 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 n = unicode_str.length / 2; - - Ok(Some(String::from_utf16(unsafe { - slice::from_raw_parts(buffer.as_ptr().cast(), n.into()) - })?)) + /// 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_exact(&self, buf: &mut [u8]) -> Result<()> { + Ok(self.reader.borrow_mut().read_exact(buf)?) } /// Build the physical memory map for a [`DumpType::Full`] dump. @@ -869,17 +243,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); @@ -892,17 +266,17 @@ impl KernelDumpParser { // Calculate the physical address. let phys_addr = run .phys_addr(page_idx) - .ok_or(KdmpParserError::PhysAddrOverflow(run_idx, page_idx))?; + .ok_or(Error::PhysAddrOverflow(run_idx, page_idx))?; // We now know where this page lives at, insert it into the physmem map. if physmem.insert(phys_addr, page_offset).is_some() { - return Err(KdmpParserError::DuplicateGpa(phys_addr)); + return Err(Error::DuplicateGpa(phys_addr)); } // Move the page offset along. page_offset = page_offset .checked_add(PageKind::Normal.size()) - .ok_or(KdmpParserError::PageOffsetOverflow(run_idx, page_idx))?; + .ok_or(Error::PageOffsetOverflow(run_idx, page_idx))?; } } @@ -913,9 +287,7 @@ impl KernelDumpParser { fn bmp_physmem(reader: &mut impl Reader) -> Result { let bmp_header = read_struct::(reader)?; if !bmp_header.looks_good() { - return Err(KdmpParserError::InvalidData( - "bmp header doesn't look right", - )); + return Err(Error::InvalidData("bmp header doesn't look right")); } let remaining_bits = bmp_header.pages % 8; @@ -944,14 +316,14 @@ impl KernelDumpParser { } // Calculate where the page is. - let pa = gpa_from_bitmap(bitmap_idx, bit_idx) - .ok_or(KdmpParserError::Overflow("pfn in bitmap"))?; + let pa = + gpa_from_bitmap(bitmap_idx, bit_idx).ok_or(Error::Overflow("pfn in bitmap"))?; let insert = physmem.insert(pa, page_offset); debug_assert!(insert.is_none()); - page_offset = page_offset.checked_add(PageKind::Normal.size()).ok_or( - KdmpParserError::BitmapPageOffsetOverflow(bitmap_idx, bit_idx), - )?; + page_offset = page_offset + .checked_add(PageKind::Normal.size()) + .ok_or(Error::BitmapPageOffsetOverflow(bitmap_idx, bit_idx))?; } } @@ -967,9 +339,7 @@ impl KernelDumpParser { D::KernelMemory | D::KernelAndUserMemory => { let kernel_hdr = read_struct::(reader)?; if !kernel_hdr.hdr.looks_good() { - return Err(KdmpParserError::InvalidData( - "RdmpHeader64 doesn't look right", - )); + return Err(Error::InvalidData("RdmpHeader64 doesn't look right")); } ( @@ -981,9 +351,7 @@ impl KernelDumpParser { D::CompleteMemory => { let full_hdr = read_struct::(reader)?; if !full_hdr.hdr.looks_good() { - return Err(KdmpParserError::InvalidData( - "FullRdmpHeader64 doesn't look right", - )); + return Err(Error::InvalidData("FullRdmpHeader64 doesn't look right")); } ( @@ -996,16 +364,12 @@ impl KernelDumpParser { }; if page_offset == 0 || metadata_size == 0 { - return Err(KdmpParserError::InvalidData( - "no first page or metadata size", - )); + return Err(Error::InvalidData("no first page or metadata size")); } let pfn_range_size = mem::size_of::(); if (metadata_size % pfn_range_size as u64) != 0 { - return Err(KdmpParserError::InvalidData( - "metadata size is not a multiple of 8", - )); + return Err(Error::InvalidData("metadata size is not a multiple of 8")); } let number_pfns = metadata_size / pfn_range_size as u64; @@ -1020,9 +384,7 @@ impl KernelDumpParser { } if page_count > total_number_of_pages { - return Err(KdmpParserError::InvalidData( - "page_count > total_number_of_pages", - )); + return Err(Error::InvalidData("page_count > total_number_of_pages")); } } @@ -1033,17 +395,17 @@ impl KernelDumpParser { for page_idx in 0..pfn_range.number_of_pages { let gpa = gpa_from_pfn_range(&pfn_range, page_idx) - .ok_or(KdmpParserError::Overflow("w/ pfn_range"))?; + .ok_or(Error::Overflow("w/ pfn_range"))?; let insert = physmem.insert(gpa, page_offset); debug_assert!(insert.is_none()); page_offset = page_offset .checked_add(PageKind::Normal.size()) - .ok_or(KdmpParserError::Overflow("w/ page_offset"))?; + .ok_or(Error::Overflow("w/ page_offset"))?; } page_count = page_count .checked_add(pfn_range.number_of_pages) - .ok_or(KdmpParserError::Overflow("w/ page_count"))?; + .ok_or(Error::Overflow("w/ page_count"))?; } Ok(physmem) diff --git a/src/phys.rs b/src/phys.rs new file mode 100644 index 0000000..7e13da2 --- /dev/null +++ b/src/phys.rs @@ -0,0 +1,111 @@ +// Axel '0vercl0k' Souchet - November 9 2025 +//! Everything related to physical memory. +use core::slice; +use std::cmp::min; +use std::io::SeekFrom; +use std::mem::MaybeUninit; + +use crate::error::{Error, PageReadError, Result}; +use crate::gxa::{Gpa, Gxa}; +use crate::parse::KernelDumpParser; +use crate::structs::PageKind; + +/// A reader lets you translate & read physical memory from a dump file. +pub struct Reader<'parser> { + parser: &'parser KernelDumpParser, +} + +impl<'parser> Reader<'parser> { + pub fn new(parser: &'parser KernelDumpParser) -> Self { + Self { parser } + } + + /// Translate a [`Gpa`] into a file offset. At that file offset is where the + /// content of the page resides in. + pub fn translate(&self, gpa: Gpa) -> Result { + let Some(base_offset) = self.parser.physmem.get(&gpa.page_align()) else { + return Err(PageReadError::NotInDump { gva: None, gpa }.into()); + }; + + base_offset + .checked_add(gpa.offset()) + .map(SeekFrom::Start) + .ok_or(Error::Overflow("w/ gpa offset")) + } + + /// Read the exact amount of bytes asked by the user & return a + /// `PartialRead` error if it couldn't read as much as wanted. + pub fn read_exact(&self, gpa: Gpa, buf: &mut [u8]) -> Result<()> { + // Amount of bytes left to read. + let mut amount_left = buf.len(); + // Total amount of bytes that we have successfully read. + let mut total_read = 0; + // The current gpa we are reading from. + let mut addr = gpa; + // Let's try to read as much as the user wants. + while amount_left > 0 { + // Translate the gpa into a file offset.. + let offset = match self.translate(addr) { + Ok(o) => o, + Err(Error::PageRead(PageReadError::NotInDump { gva: None, gpa })) => { + return Err(Error::PartialRead { + expected_amount: buf.len(), + actual_amount: total_read, + reason: PageReadError::NotInDump { gva: None, gpa }, + }); + } + Err(Error::PageRead(_)) => { + // We should never get there; `translate` can only fail with a + // [`PageReadError::NotInDump`] if and only if the gpa + // doesn't exist in the dump. + unreachable!(); + } + Err(e) => return Err(e), + }; + // ..and seek the reader there. + self.parser.seek(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 + // less. + 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]; + // Read the physical memory! + self.parser.read_exact(slice)?; + // Update the total amount of read bytes and how much work we have left. + total_read += amount_wanted; + amount_left -= amount_wanted; + // We have more work to do, so let's move to the next page. + addr = addr.next_aligned_page(); + } + + // Yay, we read as much bytes as the user wanted! + Ok(()) + } + + /// Read the physical memory starting at `gpa` into `buf`. If it cannot read + /// as much as asked by the user because the dump file is missing a physical + /// memory page, the function doesn't error out and return the amount of + /// bytes that was successfully read. + pub fn read(&self, gpa: Gpa, buf: &mut [u8]) -> Result { + match self.read_exact(gpa, buf) { + Ok(()) => Ok(buf.len()), + Err(Error::PartialRead { actual_amount, .. }) => Ok(actual_amount), + Err(e) => Err(e), + } + } + + /// Read a `T` from physical memory. + pub fn read_struct(&self, gpa: 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.read_exact(gpa, slice_over_t)?; + + Ok(unsafe { t.assume_init() }) + } +} diff --git a/src/pxe.rs b/src/pxe.rs index fcc83b6..37a1c78 100644 --- a/src/pxe.rs +++ b/src/pxe.rs @@ -5,32 +5,104 @@ //! # Examples //! //! ``` -//! # use kdmp_parser::{Pxe, PxeFlags, Pfn}; +//! # use kdmp_parser::pxe::{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 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::{BitOr, Deref}; + +use crate::bits::Bits; +use crate::gxa::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 { + 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 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 { + 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 BitOr for PxeFlags { + type Output = Self; + + fn bitor(self, rhs: Self) -> Self::Output { + Self::new(*self | *rhs) + } +} + +impl Deref for PxeFlags { + type Target = u64; + + fn deref(&self) -> &Self::Target { + &self.0 } } @@ -39,7 +111,8 @@ bitflags! { /// # Examples /// /// ``` -/// # use kdmp_parser::{Pfn, Gpa}; +/// # use kdmp_parser::gxa::Gpa; +/// # use kdmp_parser::pxe::Pfn; /// # fn main() { /// let pfn = Pfn::new(0x1337); /// assert_eq!(pfn.gpa(), Gpa::new(0x1337000)); @@ -49,18 +122,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) } @@ -79,9 +156,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. @@ -96,15 +170,16 @@ impl Pxe { /// # Examples /// /// ``` - /// # use kdmp_parser::{Pxe, PxeFlags, Pfn}; + /// # use kdmp_parser::pxe::{Pxe, PxeFlags, Pfn}; /// # 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); /// # } /// ``` + #[must_use] pub fn new(pfn: Pfn, flags: PxeFlags) -> Self { Self { pfn, flags } } @@ -114,22 +189,23 @@ impl Pxe { /// # Examples /// /// ``` - /// # use kdmp_parser::{Pxe, PxeFlags, Pfn}; + /// # use kdmp_parser::pxe::{Pxe, PxeFlags, Pfn}; /// # 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()); /// # } /// ``` + #[must_use] pub fn present(&self) -> bool { - self.flags.contains(PxeFlags::Present) + self.flags.present() } /// Is it a large page? @@ -137,22 +213,23 @@ impl Pxe { /// # Examples /// /// ``` - /// # use kdmp_parser::{Pxe, PxeFlags, Pfn}; + /// # use kdmp_parser::pxe::{Pxe, PxeFlags, Pfn}; /// # 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()); /// # } /// ``` + #[must_use] pub fn large_page(&self) -> bool { - self.flags.contains(PxeFlags::LargePage) + self.flags.large_page() } /// Is it a transition PTE? @@ -160,7 +237,7 @@ impl Pxe { /// # Examples /// /// ``` - /// # use kdmp_parser::{Pxe, PxeFlags, Pfn}; + /// # use kdmp_parser::pxe::{Pxe, PxeFlags, Pfn}; /// # fn main() { /// let p = Pxe::from(0x166B7880); /// let np = Pxe::from(0xA000000077AF867); @@ -168,8 +245,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? @@ -177,7 +255,7 @@ impl Pxe { /// # Examples /// /// ``` - /// # use kdmp_parser::{Pxe, PxeFlags, Pfn}; + /// # use kdmp_parser::pxe::{Pxe, PxeFlags, Pfn}; /// # fn main() { /// let w = Pxe::from(0x2709063); /// let ro = Pxe::from(0x8A00000002C001A1); @@ -185,8 +263,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? @@ -194,7 +273,7 @@ impl Pxe { /// # Examples /// /// ``` - /// # use kdmp_parser::{Pxe, PxeFlags, Pfn}; + /// # use kdmp_parser::pxe::{Pxe, PxeFlags, Pfn}; /// # fn main() { /// let x = Pxe::from(0x270a063); /// let nx = Pxe::from(0x8A00000002C001A1); @@ -202,8 +281,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? @@ -211,7 +291,7 @@ impl Pxe { /// # Examples /// /// ``` - /// # use kdmp_parser::{Pxe, PxeFlags, Pfn}; + /// # use kdmp_parser::pxe::{Pxe, PxeFlags, Pfn}; /// # fn main() { /// let u = Pxe::from(0x8000000F34E5025); /// let s = Pxe::from(0x270A063); @@ -219,8 +299,9 @@ impl Pxe { /// assert!(!s.user_accessible()); /// # } /// ``` + #[must_use] pub fn user_accessible(&self) -> bool { - self.flags.contains(PxeFlags::UserAccessible) + self.flags.user_accessible() } } @@ -231,16 +312,18 @@ impl From for Pxe { /// # Examples /// /// ``` - /// # use kdmp_parser::{Pxe, PxeFlags, Pfn}; + /// # use kdmp_parser::pxe::{Pxe, PxeFlags, Pfn}; /// # 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::from_bits(value & PxeFlags::all().bits()).expect("PxeFlags"); + 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) } @@ -253,11 +336,11 @@ impl From for u64 { /// # Examples /// /// ``` - /// # use kdmp_parser::{Pxe, PxeFlags, Pfn}; + /// # use kdmp_parser::pxe::{Pxe, PxeFlags, Pfn}; /// # 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); /// # } @@ -265,6 +348,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..f15ea76 100644 --- a/src/structs.rs +++ b/src/structs.rs @@ -2,10 +2,31 @@ //! 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 crate::error::Result; -use crate::{Gpa, KdmpParserError, Reader}; +use crate::error::{Error, Result}; +use crate::gxa::Gpa; + +/// We use this `Pod` trait to implement / constraint the `*read_struct` +/// functions. For the functions to work as expected and be safe, here is the +/// rule that a type `T` needs to follow to be `Pod`: +/// - `T` should not contain a field / type that have invalid bit patterns (no +/// `char`, no `bool`, no pointers). We will read bytes from a file and +/// basically `transmute` those bytes to `T` so all possible bit patterns +/// should be 'fine'. +/// +/// # Safety +/// +/// Implementing this trait for a type `T` requires that `T` is safe to +/// initialize from any arbitrary bit pattern. This means: +/// - `T` must not contain types with invalid bit patterns (e.g., `bool`, +/// `char`, references, pointers) +/// - All possible byte patterns must represent valid values of `T` +/// - This is required because bytes are read from a file and transmuted to `T` +pub unsafe trait Pod {} + +unsafe impl Pod for u64 {} +unsafe impl Pod for u32 {} +unsafe impl Pod for u16 {} /// The different kind of physical pages. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -20,6 +41,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 +51,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 +63,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, @@ -59,7 +82,7 @@ pub enum DumpType { pub type PhysmemMap = BTreeMap; impl TryFrom for DumpType { - type Error = KdmpParserError; + type Error = Error; fn try_from(value: u32) -> Result { match value { @@ -69,7 +92,7 @@ impl TryFrom for DumpType { x if x == DumpType::KernelAndUserMemory as u32 => Ok(DumpType::KernelAndUserMemory), x if x == DumpType::CompleteMemory as u32 => Ok(DumpType::CompleteMemory), x if x == DumpType::LiveKernelMemory as u32 => Ok(DumpType::LiveKernelMemory), - _ => Err(KdmpParserError::UnknownDumpType(value)), + _ => Err(Error::UnknownDumpType(value)), } } } @@ -132,6 +155,8 @@ pub struct Header64 { reserved1: [u8; 4008], } +unsafe impl Pod for Header64 {} + impl Debug for Header64 { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Header64") @@ -163,7 +188,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,19 +208,22 @@ 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 } +unsafe impl Pod for BmpHeader64 {} + impl BmpHeader64 { + #[must_use] pub fn looks_good(&self) -> bool { (self.signature == BMPHEADER64_EXPECTED_SIGNATURE || self.signature == BMPHEADER64_EXPECTED_SIGNATURE2) @@ -210,6 +238,8 @@ pub struct PhysmemRun { pub page_count: u64, } +unsafe impl Pod for PhysmemRun {} + impl PhysmemRun { /// Calculate a physical address from a run and an index. /// @@ -233,13 +263,15 @@ pub struct PhysmemDesc { // PHYSMEM_RUN Run[1]; follows } +unsafe impl Pod for PhysmemDesc {} + impl TryFrom<&[u8]> for PhysmemDesc { - type Error = KdmpParserError; + type Error = Error; 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")); + return Err(Error::InvalidData("physmem desc is too small")); } let number_of_runs = u32::from_le_bytes((&slice[0..4]).try_into().unwrap()); @@ -319,6 +351,8 @@ pub struct Context { pub last_exception_from_rip: u64, } +unsafe impl Pod for Context {} + impl Debug for Context { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Context") @@ -379,33 +413,10 @@ 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 pos = reader.stream_position()?; - reader.read_exact(slice_over_s)?; - reader.seek(io::SeekFrom::Start(pos))?; - - Ok(unsafe { s.assume_init() }) -} - -/// 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); - - reader.seek(io::SeekFrom::Current(size_of_s.try_into().unwrap()))?; - - Ok(s) -} - const RDMP_HEADER64_EXPECTED_MARKER: u32 = 0x40; const RDMP_HEADER64_EXPECTED_SIGNATURE: u32 = 0x50_4D_44_52; // 'PMDR' const RDMP_HEADER64_EXPECTED_VALID_DUMP: u32 = 0x50_4D_55_44; // 'PMUD' @@ -423,6 +434,7 @@ pub struct RdmpHeader64 { } impl RdmpHeader64 { + #[must_use] pub fn looks_good(&self) -> bool { if self.marker != RDMP_HEADER64_EXPECTED_MARKER { return false; @@ -453,6 +465,8 @@ pub struct KernelRdmpHeader64 { // Bitmap follows } +unsafe impl Pod for KernelRdmpHeader64 {} + #[repr(C)] #[derive(Debug, Default)] pub struct FullRdmpHeader64 { @@ -464,6 +478,8 @@ pub struct FullRdmpHeader64 { // Bitmap follows } +unsafe impl Pod for FullRdmpHeader64 {} + #[repr(C)] #[derive(Debug, Default)] pub struct PfnRange { @@ -471,24 +487,30 @@ pub struct PfnRange { pub number_of_pages: u64, } +unsafe impl Pod for PfnRange {} + #[repr(C)] #[derive(Debug, Default)] -pub struct ListEntry

{ +pub struct ListEntry { pub flink: P, pub blink: P, } +unsafe impl Pod for ListEntry

{} + #[repr(C)] #[derive(Debug, Default)] -pub struct UnicodeString

{ +pub struct UnicodeString { pub length: u16, pub maximum_length: u16, pub buffer: P, } +unsafe impl Pod for UnicodeString

{} + #[derive(Debug, Default)] #[repr(C)] -pub struct LdrDataTableEntry

{ +pub struct LdrDataTableEntry { pub in_load_order_links: ListEntry

, pub in_memory_order_links: ListEntry

, pub in_initialization_order_links: ListEntry

, @@ -499,6 +521,8 @@ pub struct LdrDataTableEntry

{ pub base_dll_name: UnicodeString

, } +unsafe impl Pod for LdrDataTableEntry

{} + // Copied from `WDBGEXTS.H`. #[repr(C)] #[derive(Debug, Default)] @@ -513,6 +537,8 @@ pub struct DbgKdDebugDataHeader64 { pub size: u32, } +unsafe impl Pod for DbgKdDebugDataHeader64 {} + // https://github.com/tpn/winsdk-10/blob/9b69fd26ac0c7d0b83d378dba01080e93349c2ed/Include/10.0.14393.0/um/WDBGEXTS.H#L1206C16-L1206C34 #[repr(C)] #[derive(Debug, Default)] @@ -520,18 +546,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, @@ -679,18 +705,9 @@ pub struct KdDebuggerData64 { // ... } -#[cfg(test)] -mod tests { - use std::mem; - - use crate::structs::{Context, Header64, PhysmemDesc, PhysmemRun}; +unsafe impl Pod for KdDebuggerData64 {} - /// Ensure that the sizes of key structures are right. - #[test] - fn layout() { - assert_eq!(mem::size_of::(), 0x10); - assert_eq!(mem::size_of::(), 0x10); - assert_eq!(mem::size_of::(), 0x2_000); - assert_eq!(mem::size_of::(), 0x4d0); - } -} +const _: () = assert!(size_of::() == 0x10); +const _: () = assert!(size_of::() == 0x10); +const _: () = assert!(size_of::() == 0x2_000); +const _: () = assert!(size_of::() == 0x4d0); diff --git a/src/virt.rs b/src/virt.rs new file mode 100644 index 0000000..a6efc49 --- /dev/null +++ b/src/virt.rs @@ -0,0 +1,310 @@ +// Axel '0vercl0k' Souchet - November 9 2025 +//! Everything related to virtual memory. +use core::slice; +use std::cmp::min; +use std::mem::MaybeUninit; + +use crate::error::{Error, PageReadError, PxeKind, Result}; +use crate::gxa::{Gpa, Gva, Gxa}; +use crate::parse::KernelDumpParser; +use crate::phys; +use crate::pxe::{Pfn, Pxe}; +use crate::structs::{PageKind, Pod}; + +/// The details related to a virtual to physical address translation. +/// +/// If you are wondering why there is no 'readable' field, it is because +/// [`Reader::translate`] returns an error if one of the PXE is +/// marked as not present. In other words, if the translation succeeds, the page +/// is at least readable. +#[derive(Debug)] +pub struct Translation { + /// The physical address backing the virtual address that was requested. + pub pfn: Pfn, + /// The byte offset in that physical page. + pub offset: u64, + /// The kind of physical page. + pub page_kind: PageKind, + /// Is the page writable? + pub writable: bool, + /// Is the page executable? + pub executable: bool, + /// Is the page user accessible? + pub user_accessible: bool, +} + +impl Translation { + #[must_use] + pub fn huge_page(pxes: &[Pxe; 2], gva: Gva) -> Self { + Self::inner_new(pxes, gva) + } + + #[must_use] + pub fn large_page(pxes: &[Pxe; 3], gva: Gva) -> Self { + Self::inner_new(pxes, gva) + } + + #[must_use] + pub fn new(pxes: &[Pxe; 4], gva: Gva) -> Self { + Self::inner_new(pxes, gva) + } + + fn inner_new(pxes: &[Pxe], gva: Gva) -> Self { + let writable = pxes.iter().all(Pxe::writable); + let executable = pxes.iter().all(Pxe::executable); + let user_accessible = pxes.iter().all(Pxe::user_accessible); + let pfn = pxes.last().map(|p| p.pfn).expect("at least one pxe"); + let page_kind = match pxes.len() { + 4 => PageKind::Normal, + 3 => PageKind::Large, + 2 => PageKind::Huge, + _ => unreachable!("pxes len should be between 2 and 4"), + }; + let offset = page_kind.page_offset(gva.u64()); + + Self { + pfn, + offset, + page_kind, + writable, + executable, + user_accessible, + } + } + + #[must_use] + pub fn gpa(&self) -> Gpa { + self.pfn.gpa_with_offset(self.offset) + } +} + +pub(crate) fn ignore_non_fatal(r: Result) -> Result> { + match r { + Ok(o) => Ok(Some(o)), + Err(Error::PageRead(_) | Error::PartialRead { .. }) => Ok(None), + Err(e) => Err(e), + } +} + +/// A reader lets you translate & read virtual memory from a dump file. +pub struct Reader<'parser> { + parser: &'parser KernelDumpParser, + dtb: Gpa, +} + +impl<'parser> Reader<'parser> { + pub fn new(parser: &'parser KernelDumpParser) -> Self { + Self::with_dtb(parser, Gpa::new(parser.headers().directory_table_base)) + } + + pub fn with_dtb(parser: &'parser KernelDumpParser, dtb: Gpa) -> Self { + Self { parser, dtb } + } + + /// Translate a [`Gva`] into a [`Gpa`]. + #[expect(clippy::similar_names)] + pub fn translate(&self, gva: Gva) -> Result { + let read_pxe = |gpa: Gpa, pxe: PxeKind| -> Result { + let r = phys::Reader::new(self.parser); + let Ok(pxe) = r.read_struct::(gpa).map(Pxe::from) else { + // 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 we were at. + return Err(PageReadError::NotInDump { + gva: Some((gva, Some(pxe))), + gpa, + } + .into()); + }; + + Ok(pxe) + }; + + // Aligning in case PCID bits are set (bits 11:0) + let pml4_base = self.dtb.page_align(); + let pml4e_gpa = Gpa::new(pml4_base.u64() + (gva.pml4e_idx() * 8)); + let pml4e = read_pxe(pml4e_gpa, PxeKind::Pml4e)?; + if !pml4e.present() { + 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 = read_pxe(pdpte_gpa, PxeKind::Pdpte)?; + if !pdpte.present() { + return Err(PageReadError::NotPresent { + gva, + which_pxe: PxeKind::Pdpte, + } + .into()); + } + + // huge pages: + // 7 (PS) - Page size; must be 1 (otherwise, this entry references a page + // directory; see Table 4-1. + let pd_base = pdpte.pfn.gpa(); + if pdpte.large_page() { + return Ok(Translation::huge_page(&[pml4e, pdpte], gva)); + } + + let pde_gpa = Gpa::new(pd_base.u64() + (gva.pde_idx() * 8)); + let pde = read_pxe(pde_gpa, PxeKind::Pde)?; + if !pde.present() { + return Err(PageReadError::NotPresent { + gva, + which_pxe: PxeKind::Pde, + } + .into()); + } + + // large pages: + // 7 (PS) - Page size; must be 1 (otherwise, this entry references a page + // table; see Table 4-18. + let pt_base = pde.pfn.gpa(); + if pde.large_page() { + return Ok(Translation::large_page(&[pml4e, pdpte, pde], gva)); + } + + let pte_gpa = Gpa::new(pt_base.u64() + (gva.pte_idx() * 8)); + 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. + if !pte.transition() { + return Err(PageReadError::NotPresent { + gva, + which_pxe: PxeKind::Pte, + } + .into()); + } + } + + Ok(Translation::new(&[pml4e, pdpte, pde, pte], gva)) + } + + /// Read the exact amount of bytes asked by the user & return a + /// `PartialRead` error if it couldn't read as much as wanted. + pub fn read_exact(&self, gva: Gva, buf: &mut [u8]) -> Result<()> { + // Amount of bytes left to read. + let mut amount_left = buf.len(); + // Total amount of bytes that we have successfully read. + let mut total_read = 0; + // The current gva we are reading from. + let mut addr = gva; + // Let's try to read as much as the user wants. + while amount_left > 0 { + // Translate the gva into a gpa. + let translation = match self.translate(addr) { + Ok(t) => t, + Err(Error::PageRead(reason)) => { + return Err(Error::PartialRead { + expected_amount: buf.len(), + actual_amount: total_read, + reason, + }); + } + // ..otherwise this is an error. + Err(e) => return Err(e), + }; + + // 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 = + 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); + // Figure out where we should read into. + let slice = &mut buf[total_read..total_read + amount_wanted]; + + // Read the physical memory! + let gpa = translation.gpa(); + match phys::Reader::new(self.parser).read_exact(gpa, slice) { + Ok(()) => {} + Err(Error::PartialRead { + actual_amount, + reason: PageReadError::NotInDump { gva: None, gpa }, + .. + }) => { + // Augment `NotInDump` with the `gva` as `phys::Reader::read_exact` doesn't know + // anything about it. + let reason = PageReadError::NotInDump { + gva: Some((addr, None)), + gpa, + }; + + return Err(Error::PartialRead { + expected_amount: buf.len(), + actual_amount: total_read + actual_amount, + reason, + }); + } + Err(Error::PartialRead { .. }) => { + // We should never get there; `phys::Reader::read_exact` can only return a + // [`PageReadError::NotInDump`] error if it cannot read the gpa because it + // isn't in the dump. + unreachable!(); + } + Err(e) => return Err(e), + } + + // Update the total amount of read bytes and how much work we have left. + total_read += amount_wanted; + amount_left -= amount_wanted; + // We have more work to do, so let's move to the next page. + addr = addr.next_aligned_page(); + } + + // Yay, we read as much bytes as the user wanted! + Ok(()) + } + + /// Read the virtual memory starting at `gva` into `buf`. If it cannot read + /// as much as asked by the user because the dump file is missing a physical + /// memory page or because one of the PXE is non present, the function + /// doesn't error out and return the amount of bytes that was + /// successfully read. + pub fn read(&self, gva: Gva, buf: &mut [u8]) -> Result { + match self.read_exact(gva, buf) { + Ok(()) => Ok(buf.len()), + Err(Error::PartialRead { actual_amount, .. }) => Ok(actual_amount), + Err(e) => Err(e), + } + } + + /// Read a `T` from virtual memory. + pub fn read_struct(&self, gva: Gva) -> 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.read_exact(gva, slice_over_t)?; + + Ok(unsafe { t.assume_init() }) + } + + /// Try to translate `gva` into [`Gpa`]. Returns `Some` if the translation + /// is successful, `None` if it fails because a page is not present or that + /// a physical page doesn't exist in the dump. + pub fn try_translate(&self, gva: Gva) -> Result> { + ignore_non_fatal(self.translate(gva)) + } + + /// Try to read the exact amount of bytes asked by the user. Returns `Some` + /// if successful, `None` if it fails because a page is not present or that + /// a physical page doesn't exist in the dump.. + pub fn try_read_exact(&self, gva: Gva, buf: &mut [u8]) -> Result> { + ignore_non_fatal(self.read_exact(gva, buf)) + } + + /// Try to read a `T` from virtual memory. Returns `Some` if the translation + /// is successful, `None` if it fails because a page is not present or that + /// a physical page doesn't exist in the dump. + pub fn try_read_struct(&self, gva: Gva) -> Result> { + ignore_non_fatal(self.read_struct(gva)) + } +} diff --git a/src/virt_utils.rs b/src/virt_utils.rs new file mode 100644 index 0000000..040b51f --- /dev/null +++ b/src/virt_utils.rs @@ -0,0 +1,294 @@ +// Axel '0vercl0k' Souchet - November 11 2025 +use core::slice; +use std::collections::HashMap; +use std::ops::Range; + +use crate::error::{Error, Result}; +use crate::gxa::{Gva, Gxa}; +use crate::parse::KernelDumpParser; +use crate::structs::{Context, KdDebuggerData64, LdrDataTableEntry, ListEntry, Pod, UnicodeString}; +use crate::virt::{self, ignore_non_fatal}; + +/// This trait is used to implement generic behavior when reading pointers that +/// could be either 32/64-bit (think Wow64). +trait HasCheckedAdd: Sized + Copy + Into + From { + fn checked_add(self, rhs: Self) -> Option; +} + +macro_rules! impl_checked_add { + ($($ty:ident),*) => { + $(impl HasCheckedAdd for $ty { + fn checked_add(self, rhs: $ty) -> Option { + $ty::checked_add(self, rhs) + } + })* + }; +} + +impl_checked_add!(u32, u64); + +/// Read a `UNICODE_STRING`. Returns `None` if a memory error occurs. +fn read_unicode_string( + reader: &virt::Reader, + unicode_str: &UnicodeString, +) -> Result { + if !unicode_str.length.is_multiple_of(2) { + return Err(Error::InvalidUnicodeString); + } + + let mut buffer = vec![0; unicode_str.length.into()]; + reader.read_exact(Gva::new(unicode_str.buffer.into()), &mut buffer)?; + + let n = unicode_str.length / 2; + + Ok(String::from_utf16(unsafe { + slice::from_raw_parts(buffer.as_ptr().cast(), n.into()) + })?) +} + +fn try_read_unicode_string( + reader: &virt::Reader, + unicode_str: &UnicodeString, +) -> Result> { + ignore_non_fatal(read_unicode_string(reader, unicode_str)) +} + +/// 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>; + +/// Walk a `LIST_ENTRY` of `LdrDataTableEntry`. It is used to dump both the user +/// & driver / module lists. +fn try_read_module_map( + reader: &virt::Reader, + head: Gva, +) -> Result> { + let mut modules = ModuleMap::new(); + let Some(entry) = reader.try_read_struct::>(head)? else { + return Ok(None); + }; + + let mut entry_addr = Gva::new(entry.flink.into()); + // We'll walk it until we hit the starting point (it is circular). + while entry_addr != head { + // Read the table entry.. + let Some(data) = reader.try_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) = + try_read_unicode_string(reader, &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. + try_read_unicode_string(reader, &data.base_dll_name) + } else { + Ok(s) + } + })? + else { + return Ok(None); + }; + + // Shove it into the map. + let dll_end_addr = data + .dll_base + .checked_add(data.size_of_image.into()) + .ok_or(Error::Overflow("module address"))?; + let at = Gva::new(data.dll_base.into())..Gva::new(dll_end_addr.into()); + let inserted = modules.insert(at, dll_name); + debug_assert!(inserted.is_none()); + + // Go to the next entry. + entry_addr = Gva::new(data.in_load_order_links.flink.into()); + } + + Ok(Some(modules)) +} + +/// Extract the drivers / modules out of the `PsLoadedModuleList`. +pub(crate) fn try_extract_kernel_modules(parser: &KernelDumpParser) -> Result> { + // Walk the LIST_ENTRY! + try_read_module_map::( + &virt::Reader::new(parser), + parser.headers().ps_loaded_module_list.into(), + ) +} + +/// Try to find the right `nt!_KPRCB` by walking them and finding one that has +/// the same `Rsp` than in the dump headers' context. +pub(crate) fn try_find_prcb( + parser: &KernelDumpParser, + kd_debugger_data_block: &KdDebuggerData64, +) -> Result> { + let reader = virt::Reader::new(parser); + 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) = reader.try_read_struct::(processor_block.into())? else { + return Ok(None); + }; + + // Calculate the address of where the CONTEXT pointer is at.. + let kprcb_context_addr = kprcb_addr + .checked_add(kd_debugger_data_block.offset_prcb_context.into()) + .ok_or(Error::Overflow("offset_prcb"))?; + + // ..and read it. + let Some(kprcb_context_addr) = reader.try_read_struct::(kprcb_context_addr.into())? + else { + return Ok(None); + }; + + // Read the context.. + let Some(kprcb_context) = reader.try_read_struct::(kprcb_context_addr.into())? + else { + return Ok(None); + }; + + // ..and compare it to ours. + let kprcb_context = Box::new(kprcb_context); + if kprcb_context.rsp == parser.context_record().rsp { + // The register match so we'll assume the current KPRCB is the one describing + // the 'foreground' processor in the crash-dump. + return Ok(Some(kprcb_addr.into())); + } + + // Otherwise, let's move on to the next pointer. + processor_block = processor_block + .checked_add(size_of::() as _) + .ok_or(Error::Overflow("kprcb ptr"))?; + } + + Ok(None) +} + +/// Extract the user modules list by grabbing the current thread from the KPRCB. +/// Then, walk the `PEB.Ldr.InLoadOrderModuleList`. +pub(crate) fn try_extract_user_modules( + reader: &virt::Reader, + kd_debugger_data_block: &KdDebuggerData64, + prcb_addr: Gva, +) -> Result> { + // Get the current _KTHREAD.. + let kthread_addr = prcb_addr + .u64() + .checked_add(kd_debugger_data_block.offset_prcb_current_thread.into()) + .ok_or(Error::Overflow("offset prcb current thread"))?; + let Some(kthread_addr) = reader.try_read_struct::(kthread_addr.into())? else { + return Ok(None); + }; + + // ..then its TEB.. + let teb_addr = kthread_addr + .checked_add(kd_debugger_data_block.offset_kthread_teb.into()) + .ok_or(Error::Overflow("offset kthread teb"))?; + let Some(teb_addr) = reader.try_read_struct::(teb_addr.into())? else { + return Ok(None); + }; + + if teb_addr == 0 { + return Ok(None); + } + + // ..then its PEB.. + // ``` + // kd> dt nt!_TEB ProcessEnvironmentBlock + // nt!_TEB + // +0x060 ProcessEnvironmentBlock : Ptr64 _PEB + // ``` + let peb_offset = 0x60; + let peb_addr = teb_addr + .checked_add(peb_offset) + .ok_or(Error::Overflow("peb offset"))?; + let Some(peb_addr) = reader.try_read_struct::(peb_addr.into())? else { + return Ok(None); + }; + + // ..then its _PEB_LDR_DATA.. + // ``` + // kd> dt nt!_PEB Ldr + // +0x018 Ldr : Ptr64 _PEB_LDR_DATA + // ``` + let ldr_offset = 0x18; + let peb_ldr_addr = peb_addr + .checked_add(ldr_offset) + .ok_or(Error::Overflow("ldr offset"))?; + let Some(peb_ldr_addr) = reader.try_read_struct::(peb_ldr_addr.into())? else { + return Ok(None); + }; + + // ..and finally the `InLoadOrderModuleList`. + // ``` + // kd> dt nt!_PEB_LDR_DATA InLoadOrderModuleList + // +0x010 InLoadOrderModuleList : _LIST_ENTRY + // ```` + let in_load_order_module_list_offset = 0x10; + let module_list_entry_addr = peb_ldr_addr + .checked_add(in_load_order_module_list_offset) + .ok_or(Error::Overflow("in load order module list offset"))?; + + // From there, we walk the list! + let Some(mut modules) = try_read_module_map::(reader, module_list_entry_addr.into())? + else { + return Ok(None); + }; + + // Now, it's time to dump the TEB32 if there's one. + // + // TEB32 is at offset 0x2000 from TEB and PEB32 is at +0x30: + // ``` + // kd> dt nt!_TEB32 ProcessEnvironmentBlock + // nt!_TEB32 + // +0x030 ProcessEnvironmentBlock : Uint4B + // ``` + let teb32_offset = 0x2_000; + let teb32_addr = teb_addr + .checked_add(teb32_offset) + .ok_or(Error::Overflow("teb32 offset"))?; + let peb32_offset = 0x30; + let peb32_addr = teb32_addr + .checked_add(peb32_offset) + .ok_or(Error::Overflow("peb32 offset"))?; + let Some(peb32_addr) = reader.try_read_struct::(peb32_addr.into())? else { + return Ok(Some(modules)); + }; + + // ..then its _PEB_LDR_DATA.. (32-bit) + // ``` + // kd> dt nt!_PEB32 Ldr + // +0x00c Ldr : Uint4B + // ``` + let ldr_offset = 0x0c; + let peb32_ldr_addr = peb32_addr + .checked_add(ldr_offset) + .ok_or(Error::Overflow("ldr32 offset"))?; + let Some(peb32_ldr_addr) = reader.try_read_struct::(Gva::new(peb32_ldr_addr.into()))? + else { + return Ok(Some(modules)); + }; + + // ..and finally the `InLoadOrderModuleList`. + // ``` + // 0:000> dt ntdll!_PEB_LDR_DATA InLoadOrderModuleList + // +0x00c InLoadOrderModuleList : _LIST_ENTRY + // ```` + let in_load_order_module_list_offset = 0xc; + let module_list_entry_addr = peb32_ldr_addr + .checked_add(in_load_order_module_list_offset) + .ok_or(Error::Overflow("in load order module list offset"))?; + + // From there, we walk the list! + let Some(modules32) = + try_read_module_map::(reader, Gva::new(module_list_entry_addr.into()))? + else { + return Ok(Some(modules)); + }; + + // Merge the lists. + modules.extend(modules32); + + // We're done! + Ok(Some(modules)) +} diff --git a/tests/regression.rs b/tests/regression.rs index 7b475fa..2f85d8c 100644 --- a/tests/regression.rs +++ b/tests/regression.rs @@ -4,14 +4,18 @@ use std::env; use std::fs::File; use std::ops::Range; use std::path::PathBuf; +use std::sync::LazyLock; -use kdmp_parser::{ - AddrTranslationError, Gpa, Gva, KdmpParserError, KernelDumpParser, PageKind, PxeNotPresent, -}; +use kdmp_parser::error::{Error, PageReadError, PxeKind}; +use kdmp_parser::gxa::{Gpa, Gva}; +use kdmp_parser::parse::KernelDumpParser; +use kdmp_parser::structs::{DumpType, PageKind}; +use kdmp_parser::{phys, virt}; use serde::Deserialize; /// Convert an hexadecimal encoded integer string into a `u64`. -pub fn hex_str(s: &str) -> u64 { +#[must_use] +fn hex_str(s: &str) -> u64 { u64::from_str_radix(s.trim_start_matches("0x"), 16).unwrap() } @@ -39,7 +43,7 @@ impl From for Module { struct TestcaseValues<'test> { file: PathBuf, - dump_type: kdmp_parser::DumpType, + dump_type: DumpType, size: u64, phys_addr: u64, phys_bytes: [u8; 16], @@ -77,13 +81,13 @@ fn compare_modules(parser: &KernelDumpParser, modules: &[Module]) -> bool { let found_mod = modules.iter().find(|m| m.at == *r).unwrap(); seen.insert(r.start); - let filename = name.rsplit_once('\\').map(|(_, s)| s).unwrap_or(name); + let filename = name.rsplit_once('\\').map_or(name, |(_, s)| s); if filename.to_lowercase() != found_mod.name.to_lowercase() { if found_mod.name == "nt" && filename == "ntoskrnl.exe" { continue; } - eprintln!("{name} {found_mod:?}"); + eprintln!("name: {name} filename: {filename} found_mod: {found_mod:#x?}"); return false; } } @@ -91,21 +95,42 @@ fn compare_modules(parser: &KernelDumpParser, modules: &[Module]) -> bool { seen.len() == modules.len() } +static BASE_PATH: LazyLock = LazyLock::new(|| { + PathBuf::from(env::var("TESTDATAS").expect("I need the TESTDATAS env var to work")) +}); + +static TEST_DIR: LazyLock = + LazyLock::new(|| PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests")); + // Extract the info with WinDbg w/ the below: // ``` // dx -r2 @$curprocess.Modules.Select(p => new {start=p.BaseAddress, end=p.BaseAddress + p.Size, name=p.Name}) // ``` +static BMP_PATH: LazyLock = LazyLock::new(|| BASE_PATH.join("bmp.dmp")); + +static FULL_PATH: LazyLock = LazyLock::new(|| BASE_PATH.join("full.dmp")); + +static KERNEL_DUMP_PATH: LazyLock = LazyLock::new(|| BASE_PATH.join("kerneldump.dmp")); + +static KERNEL_USER_DUMP_PATH: LazyLock = + LazyLock::new(|| BASE_PATH.join("kerneluserdump.dmp")); + +static COMPLETE_DUMP_PATH: LazyLock = LazyLock::new(|| BASE_PATH.join("completedump.dmp")); + +static LIVE_KERNEL_PATH: LazyLock = + LazyLock::new(|| BASE_PATH.join("fulllivekernelmemory.dmp")); + +static WOW64_DUMP_PATH: LazyLock = + LazyLock::new(|| BASE_PATH.join("wow64_kernelactive.dmp")); + +#[expect(clippy::too_many_lines)] #[test] fn regressions() { - let base_path = - PathBuf::from(env::var("TESTDATAS").expect("I need the TESTDATAS env var to work")); - - let test_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests"); let modules_1: Vec = - serde_json::from_reader(File::open(test_dir.join("modules_1.json")).unwrap()).unwrap(); + serde_json::from_reader(File::open(TEST_DIR.join("modules_1.json")).unwrap()).unwrap(); let modules_1 = modules_1 .into_iter() - .map(|m| m.into()) + .map(Into::into) .collect::>(); // kd> r // rax=0000000000000003 rbx=fffff8050f4e9f70 rcx=0000000000000001 @@ -117,268 +142,268 @@ fn regressions() { // iopl=0 nv up ei pl nz na pe nc // cs=0010 ss=0018 ds=002b es=002b fs=0053 gs=002b efl=00040202 let bmp = TestcaseValues { - file: base_path.join("bmp.dmp"), - dump_type: kdmp_parser::DumpType::Bmp, + file: BMP_PATH.to_path_buf(), + dump_type: DumpType::Bmp, size: 0x54_4b, phys_addr: 0x6d_4d_22, phys_bytes: [ 0x6d, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x63, 0x88, 0x75, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x63, 0x98, ], - virt_addr: 0xfffff805_108776a0, + virt_addr: 0xffff_f805_1087_76a0, virt_bytes: [ 0xcc, 0xc3, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0x0f, 0x1f, 0x84, 0x00, 0x00, 0x00, 0x00, 0x00, ], - rax: 0x00000000_00000003, - rbx: 0xfffff805_0f4e9f70, - rcx: 0x00000000_00000001, - rdx: 0xfffff805_135684d0, - rsi: 0x00000000_00000100, - rdi: 0xfffff805_0f4e9f80, - rip: 0xfffff805_108776a0, - rsp: 0xfffff805_135684f8, - rbp: 0xfffff805_13568600, - r8: 0x00000000_00000003, - r9: 0xfffff805_135684b8, - r10: 0x00000000_00000000, - r11: 0xffffa884_8825e000, - r12: 0xfffff805_0f4e9f80, - r13: 0xfffff805_10c3c958, - r14: 0x00000000_00000000, - r15: 0x00000000_00000052, + rax: 0x0000_0000_0000_0003, + rbx: 0xffff_f805_0f4e_9f70, + rcx: 0x0000_0000_0000_0001, + rdx: 0xffff_f805_1356_84d0, + rsi: 0x0000_0000_0000_0100, + rdi: 0xffff_f805_0f4e_9f80, + rip: 0xffff_f805_1087_76a0, + rsp: 0xffff_f805_1356_84f8, + rbp: 0xffff_f805_1356_8600, + r8: 0x0000_0000_0000_0003, + r9: 0xffff_f805_1356_84b8, + r10: 0x0000_0000_0000_0000, + r11: 0xffff_a884_8825_e000, + r12: 0xffff_f805_0f4e_9f80, + r13: 0xffff_f805_10c3_c958, + r14: 0x0000_0000_0000_0000, + r15: 0x0000_0000_0000_0052, modules: modules_1.as_slice(), }; let full = TestcaseValues { - file: base_path.join("full.dmp"), - dump_type: kdmp_parser::DumpType::Full, + file: FULL_PATH.to_path_buf(), + dump_type: DumpType::Full, size: 0x03_fb_e6, phys_addr: 0x6d_4d_22, phys_bytes: [ 0x6d, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x63, 0x88, 0x75, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x63, 0x98, ], - virt_addr: 0xfffff805_108776a0, + virt_addr: 0xffff_f805_1087_76a0, virt_bytes: [ 0xcc, 0xc3, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0x0f, 0x1f, 0x84, 0x00, 0x00, 0x00, 0x00, 0x00, ], - rax: 0x00000000_00000003, - rbx: 0xfffff805_0f4e9f70, - rcx: 0x00000000_00000001, - rdx: 0xfffff805_135684d0, - rsi: 0x00000000_00000100, - rdi: 0xfffff805_0f4e9f80, - rip: 0xfffff805_108776a0, - rsp: 0xfffff805_135684f8, - rbp: 0xfffff805_13568600, - r8: 0x00000000_00000003, - r9: 0xfffff805_135684b8, - r10: 0x00000000_00000000, - r11: 0xffffa884_8825e000, - r12: 0xfffff805_0f4e9f80, - r13: 0xfffff805_10c3c958, - r14: 0x00000000_00000000, - r15: 0x00000000_00000052, + rax: 0x0000_0000_0000_0003, + rbx: 0xffff_f805_0f4e_9f70, + rcx: 0x0000_0000_0000_0001, + rdx: 0xffff_f805_1356_84d0, + rsi: 0x0000_0000_0000_0100, + rdi: 0xffff_f805_0f4e_9f80, + rip: 0xffff_f805_1087_76a0, + rsp: 0xffff_f805_1356_84f8, + rbp: 0xffff_f805_1356_8600, + r8: 0x0000_0000_0000_0003, + r9: 0xffff_f805_1356_84b8, + r10: 0x0000_0000_0000_0000, + r11: 0xffff_a884_8825_e000, + r12: 0xffff_f805_0f4e_9f80, + r13: 0xffff_f805_10c3_c958, + r14: 0x0000_0000_0000_0000, + r15: 0x0000_0000_0000_0052, modules: &modules_1, }; let modules_2: Vec = - serde_json::from_reader(File::open(test_dir.join("modules_2.json")).unwrap()).unwrap(); + serde_json::from_reader(File::open(TEST_DIR.join("modules_2.json")).unwrap()).unwrap(); let modules_2 = modules_2 .into_iter() - .map(|m| m.into()) + .map(Into::into) .collect::>(); let kernel_dump = TestcaseValues { - file: base_path.join("kerneldump.dmp"), - dump_type: kdmp_parser::DumpType::KernelMemory, + file: KERNEL_DUMP_PATH.to_path_buf(), + dump_type: DumpType::KernelMemory, size: 0xa0_2e, - phys_addr: 0x02_58_92_f0, + phys_addr: 0x0258_92f0, phys_bytes: [ 0x10, 0x8c, 0x24, 0x50, 0x0c, 0xc0, 0xff, 0xff, 0xa0, 0x19, 0x38, 0x51, 0x0c, 0xc0, 0xff, 0xff, ], - virt_addr: 0xfffff803_f2c35470, + virt_addr: 0xffff_f803_f2c3_5470, virt_bytes: [ 0xcc, 0xc3, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0x0f, 0x1f, 0x84, 0x00, 0x00, 0x00, 0x00, 0x00, ], - rax: 0x00000000_00007a01, - rbx: 0xffffc00c_5191e010, - rcx: 0x00000000_00000001, - rdx: 0x00000012_00000000, - rsi: 0xffffc00c_51907bb0, - rdi: 0x00000000_00000002, - rip: 0xfffff803_f2c35470, - rsp: 0xfffff803_f515ec28, - rbp: 0x00000000_0c1c9800, - r8: 0x00000000_000000b0, - r9: 0xffffc00c_502ff000, - r10: 0x00000000_00000057, - r11: 0xfffff803_f3a04500, - r12: 0xfffff803_f515ee60, - r13: 0x00000000_00000003, - r14: 0xfffff803_f1e9a180, - r15: 0x00000000_0000001f, + rax: 0x0000_0000_0000_7a01, + rbx: 0xffff_c00c_5191_e010, + rcx: 0x0000_0000_0000_0001, + rdx: 0x0000_0012_0000_0000, + rsi: 0xffff_c00c_5190_7bb0, + rdi: 0x0000_0000_0000_0002, + rip: 0xffff_f803_f2c3_5470, + rsp: 0xffff_f803_f515_ec28, + rbp: 0x0000_0000_0c1c_9800, + r8: 0x0000_0000_0000_00b0, + r9: 0xffff_c00c_502f_f000, + r10: 0x0000_0000_0000_0057, + r11: 0xffff_f803_f3a0_4500, + r12: 0xffff_f803_f515_ee60, + r13: 0x0000_0000_0000_0003, + r14: 0xffff_f803_f1e9_a180, + r15: 0x0000_0000_0000_001f, modules: &modules_2, }; let modules_3: Vec = - serde_json::from_reader(File::open(test_dir.join("modules_3.json")).unwrap()).unwrap(); + serde_json::from_reader(File::open(TEST_DIR.join("modules_3.json")).unwrap()).unwrap(); let modules_3 = modules_3 .into_iter() - .map(|m| m.into()) + .map(Into::into) .collect::>(); let kernel_user_dump = TestcaseValues { - file: base_path.join("kerneluserdump.dmp"), - dump_type: kdmp_parser::DumpType::KernelAndUserMemory, + file: KERNEL_USER_DUMP_PATH.to_path_buf(), + dump_type: DumpType::KernelAndUserMemory, size: 0x01_f7_c7, - phys_addr: 0x02_58_92_f0, + phys_addr: 0x0258_92f0, phys_bytes: [ 0x10, 0x8c, 0x24, 0x50, 0x0c, 0xc0, 0xff, 0xff, 0xa0, 0x19, 0x38, 0x51, 0x0c, 0xc0, 0xff, 0xff, ], - virt_addr: 0xfffff803_f2c35470, + virt_addr: 0xffff_f803_f2c3_5470, virt_bytes: [ 0xcc, 0xc3, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0x0f, 0x1f, 0x84, 0x00, 0x00, 0x00, 0x00, 0x00, ], - rax: 0x00000000_00007a01, - rbx: 0xffffc00c_5191e010, - rcx: 0x00000000_00000001, - rdx: 0x00000012_00000000, - rsi: 0xffffc00c_51907bb0, - rdi: 0x00000000_00000002, - rip: 0xfffff803_f2c35470, - rsp: 0xfffff803_f515ec28, - rbp: 0x00000000_0c1c9800, - r8: 0x00000000_000000b0, - r9: 0xffffc00c_502ff000, - r10: 0x00000000_00000057, - r11: 0xfffff803_f3a04500, - r12: 0xfffff803_f515ee60, - r13: 0x00000000_00000003, - r14: 0xfffff803_f1e9a180, - r15: 0x00000000_0000001f, + rax: 0x0000_0000_0000_7a01, + rbx: 0xffff_c00c_5191_e010, + rcx: 0x0000_0000_0000_0001, + rdx: 0x0000_0012_0000_0000, + rsi: 0xffff_c00c_5190_7bb0, + rdi: 0x0000_0000_0000_0002, + rip: 0xffff_f803_f2c3_5470, + rsp: 0xffff_f803_f515_ec28, + rbp: 0x0000_0000_0c1c_9800, + r8: 0x0000_0000_0000_00b0, + r9: 0xffff_c00c_502f_f000, + r10: 0x0000_0000_0000_0057, + r11: 0xffff_f803_f3a0_4500, + r12: 0xffff_f803_f515_ee60, + r13: 0x0000_0000_0000_0003, + r14: 0xffff_f803_f1e9_a180, + r15: 0x0000_0000_0000_001f, modules: &modules_3, }; let complete_dump = TestcaseValues { - file: base_path.join("completedump.dmp"), - dump_type: kdmp_parser::DumpType::CompleteMemory, + file: COMPLETE_DUMP_PATH.to_path_buf(), + dump_type: DumpType::CompleteMemory, size: 0x01_fb_f9, - phys_addr: 0x02_58_92_f0, + phys_addr: 0x0258_92f0, phys_bytes: [ 0x10, 0x8c, 0x24, 0x50, 0x0c, 0xc0, 0xff, 0xff, 0xa0, 0x19, 0x38, 0x51, 0x0c, 0xc0, 0xff, 0xff, ], - virt_addr: 0xfffff803_f2c35470, + virt_addr: 0xffff_f803_f2c3_5470, virt_bytes: [ 0xcc, 0xc3, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0x0f, 0x1f, 0x84, 0x00, 0x00, 0x00, 0x00, 0x00, ], - rax: 0x00000000_00007a01, - rbx: 0xffffc00c_5191e010, - rcx: 0x00000000_00000001, - rdx: 0x00000012_00000000, - rsi: 0xffffc00c_51907bb0, - rdi: 0x00000000_00000002, - rip: 0xfffff803_f2c35470, - rsp: 0xfffff803_f515ec28, - rbp: 0x00000000_0c1c9800, - r8: 0x00000000_000000b0, - r9: 0xffffc00c_502ff000, - r10: 0x00000000_00000057, - r11: 0xfffff803_f3a04500, - r12: 0xfffff803_f515ee60, - r13: 0x00000000_00000003, - r14: 0xfffff803_f1e9a180, - r15: 0x00000000_0000001f, + rax: 0x0000_0000_0000_7a01, + rbx: 0xffff_c00c_5191_e010, + rcx: 0x0000_0000_0000_0001, + rdx: 0x0000_0012_0000_0000, + rsi: 0xffff_c00c_5190_7bb0, + rdi: 0x0000_0000_0000_0002, + rip: 0xffff_f803_f2c3_5470, + rsp: 0xffff_f803_f515_ec28, + rbp: 0x0000_0000_0c1c_9800, + r8: 0x0000_0000_0000_00b0, + r9: 0xffff_c00c_502f_f000, + r10: 0x0000_0000_0000_0057, + r11: 0xffff_f803_f3a0_4500, + r12: 0xffff_f803_f515_ee60, + r13: 0x0000_0000_0000_0003, + r14: 0xffff_f803_f1e9_a180, + r15: 0x0000_0000_0000_001f, modules: &modules_3, }; let modules_4: Vec = - serde_json::from_reader(File::open(test_dir.join("modules_4.json")).unwrap()).unwrap(); + serde_json::from_reader(File::open(TEST_DIR.join("modules_4.json")).unwrap()).unwrap(); let modules_4 = modules_4 .into_iter() - .map(|m| m.into()) + .map(Into::into) .collect::>(); let live_kernel = TestcaseValues { - file: base_path.join("fulllivekernelmemory.dmp"), - dump_type: kdmp_parser::DumpType::LiveKernelMemory, + file: LIVE_KERNEL_PATH.to_path_buf(), + dump_type: DumpType::LiveKernelMemory, size: 0x01_54_f5, - phys_addr: 0xd9_6a_90_00, + phys_addr: 0xd96a_9000, phys_bytes: [ 0x67, 0xd8, 0xb6, 0xdd, 0x00, 0x00, 0x00, 0x0a, 0x67, 0xa8, 0x1d, 0xd6, 0x00, 0x00, 0x00, 0x0a, ], - virt_addr: 0xfffff807_50a98b6d, + virt_addr: 0xffff_f807_50a9_8b6d, virt_bytes: [ 0x48, 0x8d, 0x8f, 0x00, 0x01, 0x00, 0x00, 0xe8, 0x17, 0x2a, 0x98, 0xff, 0x48, 0x81, 0xc3, 0x48, ], - rax: 0x00000000_00000004, - rbx: 0xffffd20f_d8553000, - rcx: 0xffffa100_0ed84a00, - rdx: 0x00000000_00000000, - rsi: 0xffffd20f_d3beeae0, - rdi: 0xfffff807_4fb4b180, - rip: 0xfffff807_50a98b6d, - rsp: 0xfffffd8d_6bcaed10, - rbp: 0x00000000_00000000, - r8: 0x00000000_00000b80, - r9: 0xffffd20f_d8553348, - r10: 0x00000000_00000000, - r11: 0xffffd20f_d8553000, - r12: 0x00000000_00000002, - r13: 0x00000000_00000000, - r14: 0xffffd20f_d48d5080, - r15: 0x00000000_00000001, + rax: 0x0000_0000_0000_0004, + rbx: 0xffff_d20f_d855_3000, + rcx: 0xffff_a100_0ed8_4a00, + rdx: 0x0000_0000_0000_0000, + rsi: 0xffff_d20f_d3be_eae0, + rdi: 0xffff_f807_4fb4_b180, + rip: 0xffff_f807_50a9_8b6d, + rsp: 0xffff_fd8d_6bca_ed10, + rbp: 0x0000_0000_0000_0000, + r8: 0x0000_0000_0000_0b80, + r9: 0xffff_d20f_d855_3348, + r10: 0x0000_0000_0000_0000, + r11: 0xffff_d20f_d855_3000, + r12: 0x0000_0000_0000_0002, + r13: 0x0000_0000_0000_0000, + r14: 0xffff_d20f_d48d_5080, + r15: 0x0000_0000_0000_0001, modules: &modules_4, }; let modules_5: Vec = - serde_json::from_reader(File::open(test_dir.join("modules_5.json")).unwrap()).unwrap(); + serde_json::from_reader(File::open(TEST_DIR.join("modules_5.json")).unwrap()).unwrap(); let modules_5 = modules_5 .into_iter() - .map(|m| m.into()) + .map(Into::into) .collect::>(); let wow64 = TestcaseValues { - file: base_path.join("wow64_kernelactive.dmp"), - dump_type: kdmp_parser::DumpType::KernelAndUserMemory, + file: WOW64_DUMP_PATH.to_path_buf(), + dump_type: DumpType::KernelAndUserMemory, size: 0x03_ec_ff, - phys_addr: 0x06_23_50_00, + phys_addr: 0x0623_5000, phys_bytes: [ 0xcc, 0x33, 0xc0, 0xc3, 0x3b, 0x0d, 0x00, 0x50, 0x46, 0x00, 0x75, 0x01, 0xc3, 0xe9, 0x79, 0x02, ], - virt_addr: 0x00451000, + virt_addr: 0x0045_1000, virt_bytes: [ 0xcc, 0x33, 0xc0, 0xc3, 0x3b, 0x0d, 0x00, 0x50, 0x46, 0x00, 0x75, 0x01, 0xc3, 0xe9, 0x79, 0x02, ], - rax: 0x00465e58, - rbx: 0x0062d000, - rcx: 0x00000000, - rdx: 0x420e1d36, - rsi: 0x009ef4c0, - rdi: 0x009f0d30, - rip: 0x00451000, - rsp: 0x0056fbcc, - rbp: 0x0056fc10, - r8: 0x0000002b, - r9: 0x77cb2c0c, - r10: 0x00000000, - r11: 0x0038e450, - r12: 0x0062e000, - r13: 0x0038fda0, - r14: 0x0038ed40, - r15: 0x77c34660, + rax: 0x0046_5e58, + rbx: 0x0062_d000, + rcx: 0x0000_0000, + rdx: 0x420e_1d36, + rsi: 0x009e_f4c0, + rdi: 0x009f_0d30, + rip: 0x0045_1000, + rsp: 0x0056_fbcc, + rbp: 0x0056_fc10, + r8: 0x0000_002b, + r9: 0x77cb_2c0c, + r10: 0x0000_0000, + r11: 0x0038_e450, + r12: 0x0062_e000, + r13: 0x0038_fda0, + r14: 0x0038_ed40, + r15: 0x77c3_4660, modules: &modules_5, }; @@ -394,16 +419,18 @@ fn regressions() { for test in tests { let parser = KernelDumpParser::new(&test.file).unwrap(); + let virt_reader = virt::Reader::new(&parser); + let phys_reader = phys::Reader::new(&parser); eprintln!("{parser:?}"); assert_eq!(parser.dump_type(), test.dump_type); - assert_eq!(parser.physmem().len(), test.size as usize); + assert_eq!(parser.physmem().len(), usize::try_from(test.size).unwrap()); let mut buf = [0; 16]; - parser - .phys_read_exact(Gpa::new(test.phys_addr), &mut buf) + phys_reader + .read_exact(Gpa::new(test.phys_addr), &mut buf) .unwrap(); assert_eq!(buf, test.phys_bytes); - parser - .virt_read_exact(Gva::new(test.virt_addr), &mut buf) + virt_reader + .read_exact(Gva::new(test.virt_addr), &mut buf) .unwrap(); assert_eq!(buf, test.virt_bytes); let ctx = parser.context_record(); @@ -426,9 +453,12 @@ fn regressions() { assert_eq!(ctx.r15, test.r15); assert!(compare_modules(&parser, test.modules)); } +} +#[test] +fn transition_pte() { // Example of a transition PTE readable by WinDbg (in kerneluserdump.dmp): - // ``` + // ```text // kd> db 0x1a42ea30240 l10 // 000001a4`2ea30240 e0 07 a3 2e a4 01 00 00-80 f2 a2 2e a4 01 00 00 ................ // kd> !pte 0x1a42ea30240 @@ -436,31 +466,42 @@ 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 parser = KernelDumpParser::new(KERNEL_USER_DUMP_PATH.as_path()).unwrap(); + let reader = virt::Reader::new(&parser); let mut buffer = [0; 16]; let expected_buffer = [ 0xe0, 0x07, 0xa3, 0x2e, 0xa4, 0x01, 0x00, 0x00, 0x80, 0xf2, 0xa2, 0x2e, 0xa4, 0x01, 0x00, 0x00, ]; - assert!(parser.virt_read(0x1a42ea30240.into(), &mut buffer).is_ok()); + assert!( + reader + .read(0x01a4_2ea3_0240.into(), &mut buffer) + .inspect_err(|e| eprintln!("{e:?}")) + .is_ok() + ); assert_eq!(buffer, expected_buffer); +} - // Example of a valid PTE that don't have a physical page backing it (in +#[test] +fn valid_pte_no_backing() { + // Examples of a valid PTE that don't have a physical page backing it (in // kerneldump.dmp): - // ``` + // ```text // kd> !pte 0x1a42ea30240 // VA 000001a42ea30240 // 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 + // ``` // + // ```text // kd> !pte 0x16e23fa060 // VA 00000016e23fa060 // PXE at FFFFECF67B3D9000 PPE at FFFFECF67B2002D8 PDE at FFFFECF64005B888 PTE at FFFFEC800B711FD0 @@ -469,53 +510,111 @@ fn regressions() { // kd> !db 1bc4000 // Physical memory read at 1bc4000 failed // ``` - let parser = KernelDumpParser::new(&kernel_dump.file).unwrap(); + let parser = KernelDumpParser::new(KERNEL_DUMP_PATH.as_path()).unwrap(); + let virt_reader = virt::Reader::new(&parser); let mut buffer = [0]; assert!(matches!( - parser.virt_read(0x1a42ea30240.into(), &mut buffer), - Err(KdmpParserError::AddrTranslation( - AddrTranslationError::Phys(gpa) - )) if gpa == 0x166b7240.into() + virt_reader + .read(0x01a4_2ea3_0240.into(), &mut buffer) + .inspect_err(|e| eprintln!("{e:?}")), + Ok(0) + )); + + assert!(matches!( + virt_reader.read_exact(0x01a4_2ea3_0240.into(), &mut buffer).inspect_err(|e| eprintln!("{e:?}")), + Err(Error::PartialRead { reason: PageReadError::NotInDump { gva: Some((gva, None)), gpa }, ..} + ) if gpa == 0x166b_7240.into() && gva == 0x01a4_2ea3_0240.into() + )); + + assert!(matches!( + virt_reader + .try_read_exact(0x01a4_2ea3_0240.into(), &mut buffer) + .inspect_err(|e| eprintln!("{e:?}")), + Ok(None) + )); + + let phys_reader = phys::Reader::new(&parser); + assert!(matches!( + phys_reader + .read(Gpa::new(0x166b_7240), &mut buffer) + .inspect_err(|e| eprintln!("{e:?}")), + Ok(0) + )); + + assert!(matches!( + phys_reader.read_exact(Gpa::new(0x166b_7240), &mut buffer).inspect_err(|e| eprintln!("{e:?}")), + Err(Error::PartialRead { reason: PageReadError::NotInDump { gva: None, gpa }, .. + }) if gpa == 0x166b_7240.into() + )); + + assert!(matches!( + virt_reader.read_exact(0x0016_e23f_a060.into(), &mut buffer).inspect_err(|e| eprintln!("{e:?}")), + Err(Error::PartialRead { reason: PageReadError::NotInDump { gva: Some((gva, None)), gpa }, ..} + ) if gpa == 0x01bc_4060.into() && gva == 0x0016_e23f_a060.into() )); assert!(matches!( - parser.virt_read(0x16e23fa060.into(), &mut buffer), - Err(KdmpParserError::AddrTranslation( - AddrTranslationError::Phys(gpa) - )) if gpa == 0x1bc4060.into() + virt_reader + .try_read_exact(0x0016_e23f_a060.into(), &mut buffer) + .inspect_err(|e| eprintln!("{e:?}")), + Ok(None) )); +} +#[test] +fn bug_10() { // 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 - // first time the loop ran, it reads as much as it can and if the user + // first time the loop ran, it reads as much as it could and if the user // wanted more, then the loop runs a second time to virt translate the next // page. However, because there is nothing mapped the virtual to physical // translation fails & bails (because of `?`) which suggests to the user // that the read operation completely failed when it was in fact able to // read some amount of bytes. + // // ```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 // ``` + // + // 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.@...... // fffff803`f3086fff ff ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ?? .??????????????? // fffff803`f308700f ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ?? ???????????????? // ``` + let parser = KernelDumpParser::new(KERNEL_DUMP_PATH.as_path()).unwrap(); + let virt_reader = virt::Reader::new(&parser); + let mut buffer = [0; 32]; - assert_eq!( - parser - .virt_read(0xfffff803f3086fef.into(), &mut buffer) - .unwrap(), - 17 - ); + assert!(matches!( + virt_reader + .read_exact(0xffff_f803_f308_6fef.into(), &mut buffer) + .inspect_err(|e| eprintln!("{e:?}")), + Err( + Error::PartialRead { + expected_amount: 32, + actual_amount: 17, + reason: PageReadError::NotPresent { gva, which_pxe } + }) if gva == 0xffff_f803_f308_7000.into() && which_pxe == PxeKind::Pte, + )); + + assert!(matches!( + virt_reader + .read(0xffff_f803_f308_6fef.into(), &mut buffer) + .inspect_err(|e| eprintln!("{e:?}")), + Ok(17) + )); // ```text // kd> !process 0 0 @@ -530,13 +629,12 @@ fn regressions() { // 0000015c`c6603928 6d 00 33 00 32 00 5c 00-52 00 75 00 6e 00 74 00 m.3.2.\.R.u.n.t. // 0000015c`c6603938 69 00 6d 00 65 00 42 00-72 00 6f 00 6b 00 65 00 i.m.e.B.r.o.k.e. // ``` - let parser = KernelDumpParser::new(&complete_dump.file).unwrap(); + let parser = KernelDumpParser::new(COMPLETE_DUMP_PATH.as_path()).unwrap(); + let virt_reader = virt::Reader::with_dtb(&parser, 0x0ea0_0002.into()); let mut buffer = [0; 64]; - assert!( - parser - .virt_read_exact_with_dtb(0x15cc6603908.into(), &mut buffer, 0xea00002.into()) - .is_ok() - ); + virt_reader + .read_exact(0x015c_c660_3908.into(), &mut buffer) + .unwrap(); assert_eq!(buffer, [ 0x43, 0x00, 0x3a, 0x00, 0x5c, 0x00, 0x57, 0x00, 0x69, 0x00, 0x6e, 0x00, 0x64, 0x00, 0x6f, @@ -545,8 +643,12 @@ fn regressions() { 0x00, 0x74, 0x00, 0x69, 0x00, 0x6d, 0x00, 0x65, 0x00, 0x42, 0x00, 0x72, 0x00, 0x6f, 0x00, 0x6b, 0x00, 0x65, 0x00 ]); +} +#[test] +fn large_page() { // Read from the middle of a large page. + // // ```text // 32.1: kd> !pte nt // VA fffff80122800000 @@ -554,26 +656,30 @@ fn regressions() { // contains 0000000002709063 contains 000000000270A063 contains 8A000000048001A1 contains 0000000000000000 // pfn 2709 ---DA--KWEV pfn 270a ---DA--KWEV pfn 4800 -GL-A--KR-V LARGE PAGE pfn 4800 // ``` - let parser = KernelDumpParser::new(&wow64.file).unwrap(); - let tr = parser.virt_translate(0xfffff80122800000.into()).unwrap(); + let parser = KernelDumpParser::new(WOW64_DUMP_PATH.as_path()).unwrap(); + let virt_reader = virt::Reader::new(&parser); + let tr = virt_reader.translate(0xffff_f801_2280_0000.into()).unwrap(); assert!(matches!(tr.page_kind, PageKind::Large)); - assert!(matches!(tr.pfn.u64(), 0x4800)); + assert!(matches!(tr.pfn.u64(), 0x48_00)); let mut buffer = [0; 0x10]; // ```text // 32.1: kd> db 0xfffff80122800000 + 0x100000 - 8 // 002b:fffff801`228ffff8 70 72 05 00 04 3a 65 00-54 3a 65 00 bc 82 0c 00 pr...:e.T:e..... // ``` - assert!( - parser - .virt_read_exact(Gva::new(0xfffff80122800000 + 0x100000 - 8), &mut buffer) - .is_ok() - ); + virt_reader + .read_exact( + Gva::new(0xffff_f801_2280_0000 + 0x10_00_00 - 8), + &mut buffer, + ) + .unwrap(); + assert_eq!(buffer, [ 0x70, 0x72, 0x05, 0x00, 0x04, 0x3a, 0x65, 0x00, 0x54, 0x3a, 0x65, 0x00, 0xbc, 0x82, 0x0c, 0x00 ]); // Read from two straddling large pages. + // // ```text // 32.1: kd> !pte 0xfffff80122800000 + 0x200000 - 0x8 // VA fffff801229ffff8 @@ -590,11 +696,13 @@ fn regressions() { // 002b:fffff801`229ffff8 63 00 72 00 6f 00 73 00-cc cc cc cc cc cc cc cc c.r.o.s......... // ``` let mut buffer = [0; 0x10]; - assert!( - parser - .virt_read_exact(Gva::new(0xfffff80122800000 + 0x200000 - 0x8), &mut buffer) - .is_ok() - ); + virt_reader + .read_exact( + Gva::new(0xffff_f801_2280_0000 + 0x20_00_00 - 0x8), + &mut buffer, + ) + .unwrap(); + assert_eq!(buffer, [ 0x63, 0x00, 0x72, 0x00, 0x6f, 0x00, 0x73, 0x00, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc @@ -609,7 +717,7 @@ fn regressions() { // contains 0A0000005DC78867 contains 0A0000005DC79867 contains 0A0000005DC7A867 contains 81000000625D5867 // pfn 5dc78 ---DA--UWEV pfn 5dc79 ---DA--UWEV pfn 5dc7a ---DA--UWEV pfn 625d5 ---DA--UW-V // ``` - let tr = parser.virt_translate(0x56fbcc.into()).unwrap(); + let tr = virt_reader.translate(0x56_fb_cc.into()).unwrap(); assert!(tr.writable); assert!(!tr.executable); assert!(tr.user_accessible); @@ -623,7 +731,7 @@ fn regressions() { // contains 0A0000005DC78867 contains 0A0000005DC79867 contains 0A0000005DC7A867 contains 0100000006235025 // pfn 5dc78 ---DA--UWEV pfn 5dc79 ---DA--UWEV pfn 5dc7a ---DA--UWEV pfn 6235 ----A--UREV // ``` - let tr = parser.virt_translate(0x451000.into()).unwrap(); + let tr = virt_reader.translate(0x45_10_00.into()).unwrap(); assert!(!tr.writable); assert!(tr.executable); assert!(tr.user_accessible); @@ -637,7 +745,7 @@ fn regressions() { // contains 0000000002709063 contains 000000000270A063 contains 0A000000050001A1 contains 0000000000000000 // pfn 2709 ---DA--KWEV pfn 270a ---DA--KWEV pfn 5000 -GL-A--KREV LARGE PAGE pfn 5103 // ``` - let tr = parser.virt_translate(0xfffff80123103ba0.into()).unwrap(); + let tr = virt_reader.translate(0xffff_f801_2310_3ba0.into()).unwrap(); assert!(!tr.writable); assert!(tr.executable); assert!(!tr.user_accessible); @@ -651,7 +759,7 @@ fn regressions() { // contains 0A00000104B61863 contains 0A00000104B62863 contains 0A000000EA030863 contains 8A000000408FF963 // pfn 104b61 ---DA--KWEV pfn 104b62 ---DA--KWEV pfn ea030 ---DA--KWEV pfn 408ff -G-DA--KW-V // ``` - let tr = parser.virt_translate(0xffffa587dcc2f650.into()).unwrap(); + let tr = virt_reader.translate(0xffff_a587_dcc2_f650.into()).unwrap(); assert!(tr.writable); assert!(!tr.executable); assert!(!tr.user_accessible); @@ -677,17 +785,60 @@ fn regressions() { // ``` let gva = 0.into(); assert!(matches!( - parser.virt_translate(gva), - Err(KdmpParserError::AddrTranslation( - AddrTranslationError::Virt(fault_gva, PxeNotPresent::Pde) + virt_reader.translate(gva), + Err(Error::PageRead( + PageReadError::NotPresent { gva: fault_gva, which_pxe: PxeKind::Pde } )) if fault_gva == gva )); - let gva = 0xffffffff_ffffffff.into(); + let gva = 0xffff_ffff_ffff_ffff.into(); assert!(matches!( - parser.virt_translate(gva), - Err(KdmpParserError::AddrTranslation( - AddrTranslationError::Virt(fault_gva, PxeNotPresent::Pte) + virt_reader.translate(gva), + Err(Error::PageRead( + PageReadError::NotPresent { gva: fault_gva, which_pxe: PxeKind::Pte } )) if fault_gva == gva )); } + +#[test] +fn partial_phys() { + let parser = KernelDumpParser::new(WOW64_DUMP_PATH.as_path()).unwrap(); + let phys_reader = phys::Reader::new(&parser); + + let mut buffer = [0; 0x11]; + // ```text + // kd> !db 0x14ff0 l10 + // # 14ff0 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ + // kd> !db 0x15000 l1 + // Physical memory read at 15000 failed + // ``` + assert!(matches!( + phys_reader.read(0x01_4f_f0.into(), &mut buffer), + Ok(0x10) + )); + + assert!(matches!( + phys_reader.read_exact(0x01_4f_f0.into(), &mut buffer).inspect(|e| eprintln!("{e:?}")), + Err(Error::PartialRead { + expected_amount, + actual_amount: 0x10, + reason: PageReadError::NotInDump { gva: None, gpa } + }) if expected_amount == buffer.len() && gpa == 0x01_50_00.into() + )); + + // ```text + // kd> !db 0000000000016000 - 10 l10 + // Physical memory read at 15ff0 failed + // kd> !db 16000 l10 + // # 16000 00 04 04 03 50 6e 70 5a-00 00 00 00 00 00 00 00 ....PnpZ........ + // ``` + assert!(matches!( + phys_reader.read(0x01_5f_f0.into(), &mut buffer), + Ok(0) + )); + + assert!(matches!( + phys_reader.read_exact(0x01_5f_f0.into(), &mut buffer).inspect(|e| eprintln!("{e:?}")), + Err(Error::PartialRead { reason: PageReadError::NotInDump { gva: None, gpa }, .. }) if gpa == 0x01_5f_f0.into() + )); +}