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()
+ ));
+}