├── .github └── workflows │ └── udmp-parser-rs.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── examples └── parser.rs ├── pics ├── parser-usage.gif └── parser.gif ├── rustfmt.toml └── src ├── lib.rs ├── map.rs ├── structs.rs └── udmp_parser.rs /.github/workflows/udmp-parser-rs.yml: -------------------------------------------------------------------------------- 1 | name: Builds 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | fmt: 7 | runs-on: ubuntu-latest 8 | name: fmt 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v4 12 | 13 | - name: Set up rust 14 | run: rustup default nightly 15 | 16 | - name: Install rustfmt 17 | run: rustup component add rustfmt 18 | 19 | - name: cargo fmt 20 | run: cargo +nightly fmt --check 21 | 22 | clippy: 23 | name: clippy 24 | runs-on: ubuntu-latest 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v4 28 | 29 | - name: Set up rust 30 | run: rustup default stable 31 | 32 | - name: cargo clippy 33 | env: 34 | RUSTFLAGS: "-Dwarnings" 35 | run: cargo clippy --example parser 36 | 37 | doc: 38 | name: doc 39 | runs-on: ubuntu-latest 40 | steps: 41 | - name: Checkout 42 | uses: actions/checkout@v4 43 | 44 | - name: Set up rust 45 | run: rustup default stable 46 | 47 | - name: cargo doc 48 | env: 49 | RUSTDOCFLAGS: "-Dwarnings" 50 | run: cargo doc 51 | 52 | build: 53 | strategy: 54 | fail-fast: false 55 | matrix: 56 | os: [ubuntu-latest, windows-latest, macos-latest] 57 | 58 | runs-on: ${{ matrix.os }} 59 | name: build & test / ${{ matrix.os }} 60 | steps: 61 | - name: Checkout 62 | uses: actions/checkout@v4 63 | 64 | - name: Set up rust 65 | run: rustup default stable 66 | 67 | - name: cargo test 68 | run: cargo test 69 | 70 | - name: cargo test release 71 | run: cargo test --release 72 | 73 | - name: cargo build 74 | run: cargo build --release --example parser 75 | 76 | - name: Upload artifacts 77 | uses: actions/upload-artifact@v4 78 | with: 79 | name: parser-${{ matrix.os }} 80 | path: | 81 | target/release/examples/parser.exe 82 | target/release/examples/parser.pdb 83 | target/release/examples/parser 84 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 7 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 8 | Cargo.lock 9 | 10 | # These are backup files generated by rustfmt 11 | **/*.rs.bk 12 | 13 | # MSVC Windows builds of rustc generate these, which store debugging information 14 | *.pdb 15 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "udmp-parser" 3 | version = "0.2.0" 4 | edition = "2021" 5 | authors = ["Axel '0vercl0k' Souchet"] 6 | categories = ["parser-implementations"] 7 | description = "A Rust crate for parsing Windows user minidumps" 8 | include = [ 9 | "/Cargo.toml", 10 | "/LICENSE", 11 | "/src/**", 12 | "/examples/**", 13 | "README.md", 14 | ] 15 | keywords = ["windows", "minidump", "crashdump"] 16 | license = "MIT" 17 | repository = "https://github.com/0vercl0k/udmp-parser-rs" 18 | rust-version = "1.65" 19 | 20 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 21 | [[example]] 22 | name = "parser" 23 | [profile.release] 24 | panic = "abort" 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Axel Souchet 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # udmp-parser: A Rust crate for parsing Windows user minidumps 2 | [![Crates.io](https://img.shields.io/crates/v/udmp-parser.svg)](https://crates.io/crates/udmp-parser) 3 | [![Documentation](https://docs.rs/udmp-parser/badge.svg)](https://docs.rs/udmp-parser/) 4 | ![Build status](https://github.com/0vercl0k/udmp-parser-rs/workflows/Builds/badge.svg) 5 | 6 | This is a cross-platform crate that parses Windows user [minidump](https://docs.microsoft.com/en-us/windows/win32/debug/minidump-files) dumps that you can generate via WinDbg or via right-click **Create memory dump file** in the Windows task manager. 7 | 8 | ![parser](https://github.com/0vercl0k/udmp-parser-rs/raw/main/pics/parser.gif) 9 | 10 | The library supports Intel 32-bit / 64-bit dumps and provides read access to things like: 11 | 12 | - The thread list and their context records, 13 | - The virtual memory, 14 | - The loaded modules. 15 | 16 | Compiled binaries are available in the [releases](https://github.com/0vercl0k/udmp-parser-rs/releases) section. 17 | 18 | ## Parser 19 | The [parser](src/examples/parser.rs) application is a small utility to show-case how to use the library and demonstrate its features. You can use it to dump memory, list the loaded modules, dump thread contexts, dump a memory map various, etc. 20 | 21 | ![parser-usage](https://github.com/0vercl0k/udmp-parser-rs/raw/main/pics/parser-usage.gif) 22 | 23 | Here are the options supported: 24 | ```text 25 | parser.exe [-a] [-mods] [-mem] [-t [|main]] [-dump ] 26 | 27 | Examples: 28 | Show all: 29 | parser.exe -a user.dmp 30 | Show loaded modules: 31 | parser.exe -mods user.dmp 32 | Show memory map: 33 | parser.exe -mem user.dmp 34 | Show all threads: 35 | parser.exe -t user.dmp 36 | Show thread w/ specific TID: 37 | parser.exe -t 1337 user.dmp 38 | Show foreground thread: 39 | parser.exe -t main user.dmp 40 | Show a memory page at a specific address: 41 | parser.exe -dump 0x7ff00 user.dmp 42 | ``` 43 | 44 | # Authors 45 | 46 | * Axel '[@0vercl0k](https://twitter.com/0vercl0k)' Souchet 47 | 48 | # Contributors 49 | 50 | [ ![contributors-img](https://contrib.rocks/image?repo=0vercl0k/udmp-parser-rs) ](https://github.com/0vercl0k/udmp-parser-rs/graphs/contributors) 51 | -------------------------------------------------------------------------------- /examples/parser.rs: -------------------------------------------------------------------------------- 1 | // Axel '0vercl0k' Souchet - July 20 2023 2 | use std::env; 3 | use std::result::Result; 4 | 5 | use udmp_parser::UserDumpParser; 6 | 7 | /// Command line argument. 8 | struct Cli { 9 | dump_path: String, 10 | show_all: bool, 11 | show_mods: bool, 12 | show_memmap: bool, 13 | show_threads: bool, 14 | show_foreground_thread: bool, 15 | thread: Option, 16 | address: Option, 17 | } 18 | 19 | /// Convert an hexadecimal string to a `u64`. 20 | fn string_to_hex(s: &str) -> Result { 21 | u64::from_str_radix(s.trim_start_matches("0x"), 16).map_err(|e| e.to_string()) 22 | } 23 | 24 | /// Parse the command line arguments. 25 | fn parse_args() -> Result { 26 | let mut dump_path = None; 27 | let mut show_all = false; 28 | let mut show_mods = false; 29 | let mut show_memmap = false; 30 | let mut show_threads = false; 31 | let mut show_foreground_thread = false; 32 | let mut thread = None; 33 | let mut address = None; 34 | 35 | let args = env::args().collect::>(); 36 | let mut idx = 1; 37 | while idx < args.len() { 38 | let cur = &args[idx]; 39 | let is_final = (idx + 1) >= args.len(); 40 | if is_final { 41 | dump_path = Some(cur.clone()); 42 | break; 43 | } 44 | 45 | let next = if is_final { None } else { Some(&args[idx + 1]) }; 46 | match cur.as_str() { 47 | "-a" => { 48 | show_all = true; 49 | } 50 | "-mods" => { 51 | show_mods = true; 52 | } 53 | "-mem" => { 54 | show_memmap = true; 55 | } 56 | "-t" => { 57 | show_threads = true; 58 | let Some(next) = next else { 59 | break; 60 | }; 61 | 62 | if next == "main" { 63 | show_foreground_thread = true; 64 | } else { 65 | thread = next.parse().map(Some).unwrap_or(None); 66 | } 67 | 68 | if show_foreground_thread || thread.is_some() { 69 | idx += 1; 70 | } 71 | } 72 | "-dump" => { 73 | let Some(next) = next else { 74 | return Err("-dump needs to be followed by an address".into()); 75 | }; 76 | 77 | address = Some(string_to_hex(next)?); 78 | idx += 1; 79 | } 80 | rest => { 81 | return Err(format!("{} is not a valid option", rest)); 82 | } 83 | }; 84 | 85 | idx += 1; 86 | } 87 | 88 | let Some(dump_path) = dump_path else { 89 | return Err("You didn't specify a dump path".into()); 90 | }; 91 | 92 | Ok(Cli { 93 | dump_path, 94 | show_all, 95 | show_mods, 96 | show_memmap, 97 | show_threads, 98 | show_foreground_thread, 99 | thread, 100 | address, 101 | }) 102 | } 103 | 104 | /// Print a hexdump of data that started at `address`. 105 | fn hexdump(address: u64, mut data_iter: impl ExactSizeIterator) { 106 | let len = data_iter.len(); 107 | for i in (0..len).step_by(16) { 108 | print!("{:016x}: ", address + (i as u64 * 16)); 109 | let mut row = [None; 16]; 110 | for item in row.iter_mut() { 111 | if let Some(c) = data_iter.next() { 112 | *item = Some(c); 113 | print!("{:02x}", c); 114 | } else { 115 | print!(" "); 116 | } 117 | } 118 | print!(" |"); 119 | for item in &row { 120 | if let Some(c) = item { 121 | let c = char::from(*c); 122 | print!("{}", if c.is_ascii_graphic() { c } else { '.' }); 123 | } else { 124 | print!(" "); 125 | } 126 | } 127 | println!("|"); 128 | } 129 | } 130 | 131 | /// Display help. 132 | fn help() { 133 | println!("parser.exe [-a] [-mods] [-mem] [-t []] [-dump ] "); 134 | println!(); 135 | println!("Examples:"); 136 | println!(" Show all:"); 137 | println!(" parser.exe -a user.dmp"); 138 | println!(" Show loaded modules:"); 139 | println!(" parser.exe -mods user.dmp"); 140 | println!(" Show memory map:"); 141 | println!(" parser.exe -mem user.dmp"); 142 | println!(" Show all threads:"); 143 | println!(" parser.exe -t user.dmp"); 144 | println!(" Show thread w/ specific TID:"); 145 | println!(" parser.exe -t 1337 user.dmp"); 146 | println!(" Show foreground thread:"); 147 | println!(" parser.exe -t main user.dmp"); 148 | println!(" Dump a memory page at a specific address:"); 149 | println!(" parser.exe -dump 0x7ff00 user.dmp"); 150 | } 151 | 152 | fn main() -> Result<(), String> { 153 | // If we don't have any arguments, display the help. 154 | if env::args().len() == 1 { 155 | help(); 156 | return Ok(()); 157 | } 158 | 159 | // Parse the command line arguments. 160 | let cli = parse_args()?; 161 | 162 | // Let's try to parse the dump file specified by the user. 163 | let dump = UserDumpParser::new(cli.dump_path).map_err(|e| e.to_string())?; 164 | 165 | // Do we want to display modules? 166 | if cli.show_mods || cli.show_all { 167 | println!("Loaded modules:"); 168 | 169 | // Iterate through the module and display their base address and path. 170 | for (base, module) in dump.modules() { 171 | println!("{:016x}: {}", base, module.path.display()); 172 | } 173 | } 174 | 175 | // Do we want the memory map? 176 | if cli.show_memmap || cli.show_all { 177 | println!("Memory map:"); 178 | 179 | // Iterate over the memory blocks. 180 | for block in dump.mem_blocks().values() { 181 | // Grab the string representation about its state, type, protection. 182 | let state = block.state_as_str(); 183 | let type_ = block.type_as_str(); 184 | let protect = block.protect_as_str(); 185 | 186 | // Print it all out. 187 | print!( 188 | "{:016x} {:016x} {:016x} {:11} {:11} {:22}", 189 | block.range.start, 190 | block.range.end, 191 | block.len(), 192 | type_, 193 | state, 194 | protect 195 | ); 196 | 197 | // Do we have a module that exists at this address? 198 | let module = dump.get_module(block.range.start); 199 | 200 | // If we do, then display its name / path. 201 | if let Some(module) = module { 202 | print!( 203 | " [{}; \"{}\"]", 204 | module.file_name().unwrap(), 205 | module.path.display() 206 | ); 207 | } 208 | 209 | // Do we have data with this block? If so display the first few 210 | // bytes. 211 | if block.data.len() >= 4 { 212 | print!( 213 | " {:02x} {:02x} {:02x} {:02x}...", 214 | block.data[0], block.data[1], block.data[2], block.data[3] 215 | ); 216 | } 217 | 218 | println!(); 219 | } 220 | } 221 | 222 | // Do we want threads? 223 | if cli.show_threads || cli.show_all { 224 | println!("Threads:"); 225 | 226 | // Grab the foreground tid. 227 | let foreground_tid = dump.foreground_tid; 228 | 229 | // Iterate through all the threads. 230 | for (tid, thread) in dump.threads() { 231 | // If the user specified a pid.. 232 | if let Some(wanted_tid) = cli.thread { 233 | // .. skip an threads that don't match what the user wants.. 234 | if *tid != wanted_tid { 235 | continue; 236 | } 237 | 238 | // Otherwise we keep going. 239 | } 240 | 241 | // If the user only wants the main thread, and we haven't found it, 242 | // skip this thread until we find it. 243 | if cli.show_foreground_thread 244 | && *tid != foreground_tid.expect("no foreground thread id in dump") 245 | { 246 | continue; 247 | } 248 | 249 | // Print out the thread info. 250 | println!("TID {}, TEB {:016x}", tid, thread.teb); 251 | println!("Context:"); 252 | println!("{}", thread.context()); 253 | } 254 | } 255 | 256 | // Do we want to dump memory? 257 | if let Some(address) = cli.address { 258 | println!("Memory:"); 259 | 260 | // Try to find a block that contains `address`. 261 | let block = dump.get_mem_block(address); 262 | 263 | // If we have one.. 264 | if let Some(block) = block { 265 | // .. and it has data, dump it.. 266 | if let Some(data) = block.data_from(address) { 267 | println!("{:016x} -> {:016x}", address, block.end_addr()); 268 | hexdump(address, data.iter().take(0x1_00).copied()); 269 | } 270 | // .. otherwise, inform the user.. 271 | else { 272 | println!( 273 | "The memory at {:016x} (from block {:016x} -> {:016x}) has no backing data", 274 | address, block.range.start, block.range.end 275 | ); 276 | } 277 | } 278 | // .. otherwise, inform he user. 279 | else { 280 | println!("No memory block were found for {:016x}", address); 281 | } 282 | } 283 | 284 | // All right, enough for today. 285 | Ok(()) 286 | } 287 | -------------------------------------------------------------------------------- /pics/parser-usage.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0vercl0k/udmp-parser-rs/bc194cf89b4af0a4c6c3b10a2bf3f9fc26f21d0c/pics/parser-usage.gif -------------------------------------------------------------------------------- /pics/parser.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0vercl0k/udmp-parser-rs/bc194cf89b4af0a4c6c3b10a2bf3f9fc26f21d0c/pics/parser.gif -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | reorder_modules = true 2 | use_field_init_shorthand = true 3 | 4 | unstable_features = true 5 | indent_style = "Block" 6 | reorder_imports = true 7 | imports_granularity = "Module" 8 | normalize_comments = true 9 | normalize_doc_attributes = true 10 | overflow_delimited_expr = true 11 | reorder_impl_items = true 12 | group_imports = "StdExternalCrate" 13 | wrap_comments = true -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | // Axel '0vercl0k' Souchet - July 29 2023 2 | #![doc = include_str!("../README.md")] 3 | mod udmp_parser; 4 | pub use udmp_parser::{ 5 | PAGE_EXECUTE, PAGE_EXECUTE_READ, PAGE_EXECUTE_READWRITE, PAGE_EXECUTE_WRITECOPY, PAGE_GUARD, 6 | PAGE_NOACCESS, PAGE_NOCACHE, PAGE_READONLY, PAGE_READWRITE, PAGE_WRITECOMBINE, PAGE_WRITECOPY, 7 | *, 8 | }; 9 | 10 | mod map; 11 | 12 | mod structs; 13 | pub use structs::{FloatingSaveArea32, ThreadContextX64, ThreadContextX86}; 14 | -------------------------------------------------------------------------------- /src/map.rs: -------------------------------------------------------------------------------- 1 | // Axel '0vercl0k' Souchet - July 18 2023 2 | //! This module implements the logic that allows to memory map a file on both 3 | //! Unix and Windows (cf [`memory_map_file`] / [`unmap_memory_mapped_file`]). 4 | use std::{convert, fs, io, path, ptr, slice}; 5 | 6 | /// A cursor over a slice of bytes. This is used to seek / read from the 7 | /// mapping. 8 | pub type Cursor<'a> = io::Cursor<&'a [u8]>; 9 | 10 | /// A memory mapped is basically either a slice over a memory mapping or a 11 | /// regular slice. The main difference is that the former needs special handling 12 | /// when dropped ([`unmap_memory_mapped_file`]). This means that the type 13 | /// actually owns the memory mapped region although it's not obvious from its 14 | /// definition. 15 | #[derive(Debug)] 16 | pub enum MappedFile<'a> { 17 | /// This means the underlying slice is over a memory mapping that needs 18 | /// special handling to be dropped. It is owned by this type. 19 | Mmaped(&'a [u8]), 20 | /// This gives users flexibility when they don't want to necessarily memory 21 | /// map a file and want to parse directly from a vector, or a string. It 22 | /// could also be because they already have mapped a file themselves in 23 | /// which case it wouldn't make sense to map it again. 24 | Slice(&'a [u8]), 25 | } 26 | 27 | impl<'a> MappedFile<'a> { 28 | /// Create a new [`MappedFile`] from a path. This memory maps the file and 29 | /// as a result, the instance will known that mapping. 30 | pub fn new

(path: P) -> io::Result> 31 | where 32 | P: convert::AsRef, 33 | { 34 | // Open the file.. 35 | let file = fs::File::open(path)?; 36 | 37 | // ..and memory map it using the underlying OS-provided APIs. 38 | memory_map_file(file) 39 | } 40 | 41 | /// Create a [`io::Cursor`] over the underlying byte slice. This is used 42 | /// extensively to parse the minidump like it is reading / seeking from a 43 | /// file. 44 | pub fn cursor(&self) -> Cursor<'a> { 45 | // Grab the slice off of it. 46 | let slice = match self { 47 | Self::Mmaped(mmaped) => mmaped, 48 | Self::Slice(slice_) => slice_, 49 | }; 50 | 51 | // Create the cursor over the slice. 52 | io::Cursor::new(slice) 53 | } 54 | } 55 | 56 | /// Convert a byte slice reference into a [`MappedFile`]. This is useful for 57 | /// users that want to create a [`MappedFile`] instance from a slice that came 58 | /// from somewhere and that isn't owned by the instance. You can imagine this 59 | /// slice coming from a [`Vec`] or a [`String`] or another mapped file 60 | /// maybe. 61 | impl<'a> From<&'a [u8]> for MappedFile<'a> { 62 | fn from(value: &'a [u8]) -> Self { 63 | Self::Slice(value) 64 | } 65 | } 66 | 67 | /// Drop the [`MappedFile`]. In the case we memory mapped the file, we need to 68 | /// drop the mapping using OS-provided APIs. Otherwise, we have nothing to do! 69 | impl<'a> Drop for MappedFile<'a> { 70 | fn drop(&mut self) { 71 | match self { 72 | Self::Mmaped(mmap) => unmap_memory_mapped_file(mmap).expect("failed to unmap"), 73 | Self::Slice(_) => {} 74 | } 75 | } 76 | } 77 | 78 | #[cfg(windows)] 79 | #[allow(non_camel_case_types, clippy::upper_case_acronyms)] 80 | /// Module that implements memory mapping on Windows using CreateFileMappingA / 81 | /// MapViewOfFile. 82 | mod windows { 83 | use std::os::windows::prelude::AsRawHandle; 84 | use std::os::windows::raw::HANDLE; 85 | 86 | use super::*; 87 | 88 | const PAGE_READONLY: DWORD = 2; 89 | const FILE_MAP_READ: DWORD = 4; 90 | 91 | type DWORD = u32; 92 | type BOOL = u32; 93 | type SIZE_T = usize; 94 | type LPCSTR = *mut u8; 95 | type LPVOID = *const u8; 96 | 97 | extern "system" { 98 | /// Creates or opens a named or unnamed file mapping object for a 99 | /// specified file. 100 | /// 101 | /// 102 | fn CreateFileMappingA( 103 | h: HANDLE, 104 | file_mapping_attrs: *const u8, 105 | protect: DWORD, 106 | max_size_high: DWORD, 107 | max_size_low: DWORD, 108 | name: LPCSTR, 109 | ) -> HANDLE; 110 | 111 | /// Maps a view of a file mapping into the address space of a calling 112 | /// process. 113 | /// 114 | /// 115 | fn MapViewOfFile( 116 | file_mapping_object: HANDLE, 117 | desired_access: DWORD, 118 | file_offset_high: DWORD, 119 | file_offset_low: DWORD, 120 | number_of_bytes_to_map: SIZE_T, 121 | ) -> LPVOID; 122 | 123 | /// Closes an open object handle. 124 | /// 125 | /// 126 | fn CloseHandle(h: HANDLE) -> BOOL; 127 | 128 | /// Unmaps a mapped view of a file from the calling process's address 129 | /// space. 130 | /// 131 | /// 132 | fn UnmapViewOfFile(base_address: LPVOID) -> BOOL; 133 | } 134 | 135 | /// Memory map a file into memory. 136 | pub fn memory_map_file<'a>(file: fs::File) -> Result, io::Error> { 137 | // Grab the underlying HANDLE. 138 | let file_handle = file.as_raw_handle(); 139 | 140 | // Create the mapping. 141 | let mapping_handle = unsafe { 142 | CreateFileMappingA( 143 | file_handle, 144 | ptr::null_mut(), 145 | PAGE_READONLY, 146 | 0, 147 | 0, 148 | ptr::null_mut(), 149 | ) 150 | }; 151 | 152 | // If the mapping is NULL, it failed so let's bail. 153 | if mapping_handle.is_null() { 154 | return Err(io::Error::last_os_error()); 155 | } 156 | 157 | // Grab the size of the underlying file, this will be the size of the 158 | // view. 159 | let size = file.metadata()?.len().try_into().unwrap(); 160 | 161 | // Map the view in the address space. 162 | let base = unsafe { MapViewOfFile(mapping_handle, FILE_MAP_READ, 0, 0, size) }; 163 | 164 | // If the base address is NULL, it failed so let's bail. 165 | if base.is_null() { 166 | // Don't forget to close the HANDLE we created for the mapping. 167 | unsafe { 168 | CloseHandle(mapping_handle); 169 | } 170 | return Err(io::Error::last_os_error()); 171 | } 172 | 173 | // Now we materialized a view in the address space, we can get rid of 174 | // the mapping handle. 175 | unsafe { 176 | CloseHandle(mapping_handle); 177 | } 178 | 179 | // Make sure the size is not bigger than what [`slice::from_raw_parts`] wants. 180 | if size > isize::MAX.try_into().unwrap() { 181 | panic!("slice is too large"); 182 | } 183 | 184 | // Create the slice over the mapping. 185 | // SAFETY: This is safe because: 186 | // - It is a byte slice, so we don't need to care about the alignment. 187 | // - The base is not NULL as we've verified that it is the case above. 188 | // - The underlying is owned by the type and the lifetime. 189 | // - We asked the OS to map `size` bytes, so we have a guarantee that there's 190 | // `size` consecutive bytes. 191 | // - We never give a mutable reference to this slice, so it can't get mutated. 192 | // - The total len of the slice is guaranteed to be smaller than 193 | // [`isize::MAX`]. 194 | // - The underlying mapping, the type and the slice have the same lifetime 195 | // which guarantees that we can't access the underlying once it has been 196 | // unmapped (use-after-unmap). 197 | Ok(MappedFile::Mmaped(unsafe { 198 | slice::from_raw_parts(base, size) 199 | })) 200 | } 201 | 202 | /// Unmap the memory mapped file. 203 | pub fn unmap_memory_mapped_file(data: &[u8]) -> Result<(), io::Error> { 204 | match unsafe { UnmapViewOfFile(data.as_ptr()) } { 205 | 0 => Err(io::Error::last_os_error()), 206 | _ => Ok(()), 207 | } 208 | } 209 | } 210 | 211 | #[cfg(windows)] 212 | use windows::*; 213 | 214 | #[cfg(unix)] 215 | /// Module that implements memory mapping on Unix using the mmap syscall. 216 | mod unix { 217 | use std::os::fd::AsRawFd; 218 | 219 | use super::*; 220 | 221 | const PROT_READ: i32 = 1; 222 | const MAP_SHARED: i32 = 1; 223 | const MAP_FAILED: *const u8 = usize::MAX as _; 224 | 225 | extern "system" { 226 | fn mmap( 227 | addr: *const u8, 228 | length: usize, 229 | prot: i32, 230 | flags: i32, 231 | fd: i32, 232 | offset: i32, 233 | ) -> *const u8; 234 | 235 | fn munmap(addr: *const u8, length: usize) -> i32; 236 | } 237 | 238 | pub fn memory_map_file<'a>(file: fs::File) -> Result, io::Error> { 239 | // Grab the underlying file descriptor. 240 | let file_fd = file.as_raw_fd(); 241 | 242 | // Grab the size of the underlying file. This will be the size of the 243 | // memory mapped region. 244 | let size = file.metadata()?.len().try_into().unwrap(); 245 | 246 | // Mmap the file. 247 | let ret = unsafe { mmap(ptr::null_mut(), size, PROT_READ, MAP_SHARED, file_fd, 0) }; 248 | 249 | // If the system call failed, bail. 250 | if ret == MAP_FAILED { 251 | return Err(io::Error::last_os_error()); 252 | } 253 | 254 | // Make sure the size is not bigger than what [`slice::from_raw_parts`] wants. 255 | if size > isize::MAX.try_into().unwrap() { 256 | panic!("slice is too large"); 257 | } 258 | 259 | // Create the slice over the mapping. 260 | // SAFETY: This is safe because: 261 | // - It is a byte slice, so we don't need to care about the alignment. 262 | // - The base is not NULL as we've verified that it is the case above. 263 | // - The underlying is owned by the type and the lifetime. 264 | // - We asked the OS to map `size` bytes, so we have a guarantee that there's 265 | // `size` consecutive bytes. 266 | // - We never give a mutable reference to this slice, so it can't get mutated. 267 | // - The total len of the slice is guaranteed to be smaller than 268 | // [`isize::MAX`]. 269 | // - The underlying mapping, the type and the slice have the same lifetime 270 | // which guarantees that we can't access the underlying once it has been 271 | // unmapped (use-after-unmap). 272 | Ok(MappedFile::Mmaped(unsafe { 273 | slice::from_raw_parts(ret, size) 274 | })) 275 | } 276 | 277 | // Unmap a memory mapped file. 278 | pub fn unmap_memory_mapped_file(data: &[u8]) -> Result<(), io::Error> { 279 | match unsafe { munmap(data.as_ptr(), data.len()) } { 280 | 0 => Ok(()), 281 | _ => Err(io::Error::last_os_error()), 282 | } 283 | } 284 | } 285 | 286 | #[cfg(unix)] 287 | use unix::*; 288 | 289 | #[cfg(not(any(windows, unix)))] 290 | /// Your system hasn't been implemented; if you do it, send a PR! 291 | fn unimplemented() -> u32 {} 292 | -------------------------------------------------------------------------------- /src/structs.rs: -------------------------------------------------------------------------------- 1 | // Axel '0vercl0k' Souchet - July 29 2023 2 | //! This is where all the raw Windows user-dump structures are stored in. 3 | use std::fmt; 4 | 5 | pub const STREAM_TYPE_UNUSED: u32 = 0; 6 | pub const STREAM_TYPE_THREAD_LIST: u32 = 3; 7 | pub const STREAM_TYPE_MODULE_LIST: u32 = 4; 8 | pub const STREAM_TYPE_EXCEPTION: u32 = 6; 9 | pub const STREAM_TYPE_SYSTEM_INFO: u32 = 7; 10 | pub const STREAM_TYPE_MEMORY64_LIST: u32 = 9; 11 | pub const STREAM_TYPE_MEMORY_INFO_LIST: u32 = 16; 12 | 13 | pub const EXCEPTION_MAXIMUM_PARAMETERS: usize = 15; 14 | 15 | pub const EXPECTED_DUMP_SIGNATURE: u32 = 0x504d_444d; 16 | 17 | pub const VALID_DUMP_FLAGS: u32 = 0x001f_ffff; 18 | 19 | pub const WOW64_MAXIMUM_SUPPORTED_EXTENSION: usize = 512; 20 | 21 | pub const WOW64_SIZE_OF_80387_REGISTERS: usize = 80; 22 | 23 | pub const ARCH_X86: u16 = 0; 24 | pub const ARCH_X64: u16 = 9; 25 | 26 | #[derive(Debug, Default)] 27 | #[repr(C)] 28 | pub struct Header { 29 | pub signature: u32, 30 | pub version: u16, 31 | pub implementation_version: u16, 32 | pub number_of_streams: u32, 33 | pub stream_directory_rva: u32, 34 | pub checksum: u32, 35 | pub reserved: u32, 36 | pub timedatestamp: u32, 37 | pub flags: u32, 38 | } 39 | 40 | #[derive(Debug, Default, Clone, Copy)] 41 | #[repr(C)] 42 | pub struct LocationDescriptor32 { 43 | pub data_size: u32, 44 | pub rva: u32, 45 | } 46 | 47 | #[derive(Debug, Default)] 48 | #[repr(C)] 49 | pub struct Directory { 50 | pub stream_type: u32, 51 | pub location: LocationDescriptor32, 52 | } 53 | 54 | #[derive(Debug, Default)] 55 | #[repr(C)] 56 | pub struct SystemInfoStream { 57 | pub processor_arch: u16, 58 | pub processor_level: u16, 59 | pub processor_revision: u16, 60 | pub number_of_processors: u8, 61 | pub product_type: u8, 62 | pub major_version: u32, 63 | pub minor_version: u32, 64 | pub build_number: u32, 65 | pub platform_id: u32, 66 | pub csd_version_rva: u32, 67 | pub suite_mask: u16, 68 | pub reserverd2: u16, 69 | } 70 | 71 | #[derive(Default, Debug)] 72 | #[repr(C)] 73 | pub struct ExceptionRecord { 74 | pub exception_code: u32, 75 | pub exception_flags: u32, 76 | pub exception_record: u64, 77 | pub exception_address: u64, 78 | pub number_parameters: u32, 79 | pub unused_alignment: u32, 80 | pub exception_information: [u64; EXCEPTION_MAXIMUM_PARAMETERS], 81 | } 82 | 83 | #[derive(Default, Debug)] 84 | #[repr(C)] 85 | pub struct ExceptionStream { 86 | pub thread_id: u32, 87 | pub alignment: u32, 88 | pub exception_record: ExceptionRecord, 89 | pub thread_context: LocationDescriptor32, 90 | } 91 | 92 | #[derive(Default, Debug)] 93 | #[repr(C)] 94 | pub struct MemoryInfo { 95 | pub base_address: u64, 96 | pub allocation_base: u64, 97 | pub allocation_protect: u32, 98 | pub alignment1: u32, 99 | pub region_size: u64, 100 | pub state: u32, 101 | pub protect: u32, 102 | pub type_: u32, 103 | pub alignment2: u32, 104 | } 105 | 106 | #[derive(Default, Debug)] 107 | #[repr(C)] 108 | pub struct MemoryInfoListStream { 109 | pub size_of_header: u32, 110 | pub size_of_entry: u32, 111 | pub number_of_entries: u64, 112 | } 113 | 114 | #[derive(Default, Debug)] 115 | #[repr(C)] 116 | pub struct Memory64ListStream { 117 | pub number_of_memory_ranges: u64, 118 | pub base_rva: u64, 119 | } 120 | 121 | #[derive(Default, Debug)] 122 | #[repr(C)] 123 | pub struct MemoryDescriptor64 { 124 | pub start_of_memory_range: u64, 125 | pub data_size: u64, 126 | } 127 | 128 | #[derive(Default, Debug)] 129 | #[repr(C)] 130 | pub struct ThreadList { 131 | pub number_of_threads: u32, 132 | } 133 | 134 | #[derive(Default, Debug)] 135 | #[repr(C)] 136 | pub struct MemoryDescriptor { 137 | pub start_of_memory_range: u64, 138 | pub memory: LocationDescriptor32, 139 | } 140 | 141 | #[derive(Default, Debug)] 142 | #[repr(C)] 143 | pub struct ThreadEntry { 144 | pub thread_id: u32, 145 | pub suspend_count: u32, 146 | pub priority_class: u32, 147 | pub priority: u32, 148 | pub teb: u64, 149 | pub stack: MemoryDescriptor, 150 | pub thread_context: LocationDescriptor32, 151 | } 152 | 153 | #[derive(Default, Debug)] 154 | #[repr(C)] 155 | pub struct ModuleList { 156 | pub number_of_modules: u32, 157 | } 158 | 159 | #[derive(Default, Debug, Clone, Copy)] 160 | #[repr(C)] 161 | pub struct FixedFileInfo { 162 | pub signature: u32, 163 | pub struc_version: u32, 164 | pub file_version_ms: u32, 165 | pub file_version_ls: u32, 166 | pub product_version_ms: u32, 167 | pub product_version_ls: u32, 168 | pub file_flags_mask: u32, 169 | pub file_flags: u32, 170 | pub file_os: u32, 171 | pub file_type: u32, 172 | pub file_subtype: u32, 173 | pub file_date_ms: u32, 174 | pub file_date_ls: u32, 175 | } 176 | 177 | #[derive(Default, Debug)] 178 | #[repr(packed(1))] 179 | pub struct ModuleEntry { 180 | pub base_of_image: u64, 181 | pub size_of_image: u32, 182 | pub checksum: u32, 183 | pub time_date_stamp: u32, 184 | pub module_name_rva: u32, 185 | pub version_info: FixedFileInfo, 186 | pub cv_record: LocationDescriptor32, 187 | pub misc_record: LocationDescriptor32, 188 | _reserved0: u64, 189 | _reserved1: u64, 190 | } 191 | 192 | #[derive(Debug, PartialEq)] 193 | #[repr(C)] 194 | pub struct FloatingSaveArea32 { 195 | pub control_word: u32, 196 | pub status_word: u32, 197 | pub tag_word: u32, 198 | pub error_offset: u32, 199 | pub error_selector: u32, 200 | pub data_offset: u32, 201 | pub data_selector: u32, 202 | pub register_area: [u8; WOW64_SIZE_OF_80387_REGISTERS], 203 | pub cr0_npx_state: u32, 204 | } 205 | 206 | impl Default for FloatingSaveArea32 { 207 | fn default() -> Self { 208 | // SAFETY: All zero values are fine for every types used by 209 | // [`FloatingSaveArea32`]. 210 | unsafe { std::mem::zeroed() } 211 | } 212 | } 213 | 214 | /// The context of an Intel X86 thread. 215 | #[derive(Debug)] 216 | #[repr(C)] 217 | pub struct ThreadContextX86 { 218 | pub context_flags: u32, 219 | pub dr0: u32, 220 | pub dr1: u32, 221 | pub dr2: u32, 222 | pub dr3: u32, 223 | pub dr6: u32, 224 | pub dr7: u32, 225 | pub float_save: FloatingSaveArea32, 226 | pub seg_gs: u32, 227 | pub seg_fs: u32, 228 | pub seg_es: u32, 229 | pub seg_ds: u32, 230 | pub edi: u32, 231 | pub esi: u32, 232 | pub ebx: u32, 233 | pub edx: u32, 234 | pub ecx: u32, 235 | pub eax: u32, 236 | pub ebp: u32, 237 | pub eip: u32, 238 | pub seg_cs: u32, 239 | pub eflags: u32, 240 | pub esp: u32, 241 | pub seg_ss: u32, 242 | pub extended_registers: [u8; WOW64_MAXIMUM_SUPPORTED_EXTENSION], 243 | } 244 | 245 | impl fmt::Display for ThreadContextX86 { 246 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 247 | writeln!( 248 | f, 249 | "eax={:08x} ebx={:08x} ecx={:08x} edx={:08x} esi={:08x} edi={:08x}", 250 | self.eax, self.ebx, self.ecx, self.edx, self.esi, self.edi 251 | )?; 252 | writeln!( 253 | f, 254 | "eip={:08x} esp={:08x} ebp={:08x}", 255 | self.eip, self.esp, self.ebp 256 | )?; 257 | write!( 258 | f, 259 | "cs={:04x} ss={:04x} ds={:04x} es={:04x} fs={:04x} gs={:04x} efl={:08x}", 260 | self.seg_cs, 261 | self.seg_ss, 262 | self.seg_ds, 263 | self.seg_es, 264 | self.seg_fs, 265 | self.seg_gs, 266 | self.eflags 267 | ) 268 | } 269 | } 270 | 271 | impl Default for ThreadContextX86 { 272 | fn default() -> Self { 273 | // SAFETY: All zero values are fine for every types used by 274 | // [`ThreadContextX86`]. 275 | unsafe { std::mem::zeroed() } 276 | } 277 | } 278 | 279 | /// The context of an Intel X64 thread. 280 | #[derive(Debug)] 281 | #[repr(C)] 282 | pub struct ThreadContextX64 { 283 | pub p1_home: u64, 284 | pub p2_home: u64, 285 | pub p3_home: u64, 286 | pub p4_home: u64, 287 | pub p5_home: u64, 288 | pub p6_home: u64, 289 | pub context_flags: u32, 290 | pub mxcsr: u32, 291 | pub seg_cs: u16, 292 | pub seg_ds: u16, 293 | pub seg_es: u16, 294 | pub seg_fs: u16, 295 | pub seg_gs: u16, 296 | pub seg_ss: u16, 297 | pub eflags: u32, 298 | pub dr0: u64, 299 | pub dr1: u64, 300 | pub dr2: u64, 301 | pub dr3: u64, 302 | pub dr6: u64, 303 | pub dr7: u64, 304 | pub rax: u64, 305 | pub rcx: u64, 306 | pub rdx: u64, 307 | pub rbx: u64, 308 | pub rsp: u64, 309 | pub rbp: u64, 310 | pub rsi: u64, 311 | pub rdi: u64, 312 | pub r8: u64, 313 | pub r9: u64, 314 | pub r10: u64, 315 | pub r11: u64, 316 | pub r12: u64, 317 | pub r13: u64, 318 | pub r14: u64, 319 | pub r15: u64, 320 | pub rip: u64, 321 | pub control_word: u16, 322 | pub status_word: u16, 323 | pub tag_word: u8, 324 | pub reserved1: u8, 325 | pub error_opcode: u16, 326 | pub error_offset: u32, 327 | pub error_selector: u16, 328 | pub reserved2: u16, 329 | pub data_offset: u32, 330 | pub data_selector: u16, 331 | pub reserved3: u16, 332 | pub mxcsr2: u32, 333 | pub mxcsr_mask: u32, 334 | pub float_registers: [u128; 8], 335 | pub xmm0: u128, 336 | pub xmm1: u128, 337 | pub xmm2: u128, 338 | pub xmm3: u128, 339 | pub xmm4: u128, 340 | pub xmm5: u128, 341 | pub xmm6: u128, 342 | pub xmm7: u128, 343 | pub xmm8: u128, 344 | pub xmm9: u128, 345 | pub xmm10: u128, 346 | pub xmm11: u128, 347 | pub xmm12: u128, 348 | pub xmm13: u128, 349 | pub xmm14: u128, 350 | pub xmm15: u128, 351 | pub padding: [u8; 0x60], 352 | pub vector_registers: [u128; 26], 353 | pub vector_control: u64, 354 | pub debug_control: u64, 355 | pub last_branch_to_rip: u64, 356 | pub last_branch_from_rip: u64, 357 | pub last_exception_to_rip: u64, 358 | pub last_exception_from_rip: u64, 359 | } 360 | 361 | impl fmt::Display for ThreadContextX64 { 362 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 363 | writeln!( 364 | f, 365 | "rax={:016x} rbx={:016x} rcx={:016x}", 366 | self.rax, self.rbx, self.rcx 367 | )?; 368 | writeln!( 369 | f, 370 | "rdx={:016x} rsi={:016x} rdi={:016x}", 371 | self.rdx, self.rsi, self.rdi 372 | )?; 373 | writeln!( 374 | f, 375 | "rip={:016x} rsp={:016x} rbp={:016x}", 376 | self.rip, self.rsp, self.rbp 377 | )?; 378 | writeln!( 379 | f, 380 | " r8={:016x} r9={:016x} r10={:016x}", 381 | self.r8, self.r9, self.r10 382 | )?; 383 | writeln!( 384 | f, 385 | "r11={:016x} r12={:016x} r13={:016x}", 386 | self.r11, self.r12, self.r13 387 | )?; 388 | writeln!(f, "r14={:016x} r15={:016x}", self.r14, self.r15)?; 389 | writeln!(f, "cs={:04x} ss={:04x} ds={:04x} es={:04x} fs={:04x} gs={:04x} efl={:08x}", 390 | self.seg_cs, self.seg_ss, self.seg_ds, self.seg_es, self.seg_fs, self.seg_gs, 391 | self.eflags)?; 392 | writeln!( 393 | f, 394 | "fpcw={:04x} fpsw={:04x} fptw={:04x}", 395 | self.control_word, self.status_word, self.tag_word 396 | )?; 397 | writeln!( 398 | f, 399 | " st0={:032x} st1={:032x}", 400 | self.float_registers[0], self.float_registers[1] 401 | )?; 402 | writeln!( 403 | f, 404 | " st2={:032x} st3={:032x}", 405 | self.float_registers[2], self.float_registers[3] 406 | )?; 407 | writeln!( 408 | f, 409 | " st4={:032x} st5={:032x}", 410 | self.float_registers[4], self.float_registers[5] 411 | )?; 412 | writeln!( 413 | f, 414 | " st6={:032x} st7={:032x}", 415 | self.float_registers[6], self.float_registers[7] 416 | )?; 417 | writeln!(f, " xmm0={:032x} xmm1={:032x}", self.xmm0, self.xmm1)?; 418 | writeln!(f, " xmm2={:032x} xmm3={:032x}", self.xmm2, self.xmm3)?; 419 | writeln!(f, " xmm4={:032x} xmm5={:032x}", self.xmm4, self.xmm5)?; 420 | writeln!(f, " xmm6={:032x} xmm7={:032x}", self.xmm6, self.xmm7)?; 421 | writeln!(f, " xmm8={:032x} xmm9={:032x}", self.xmm8, self.xmm9)?; 422 | writeln!(f, "xmm10={:032x} xmm11={:032x}", self.xmm10, self.xmm11)?; 423 | writeln!(f, "xmm12={:032x} xmm13={:032x}", self.xmm12, self.xmm13)?; 424 | write!(f, "xmm14={:032x} xmm15={:032x}", self.xmm14, self.xmm15) 425 | } 426 | } 427 | 428 | impl Default for ThreadContextX64 { 429 | fn default() -> Self { 430 | // SAFETY: All zero values are fine for every types used by 431 | // [`ThreadContextX64`]. 432 | unsafe { std::mem::zeroed() } 433 | } 434 | } 435 | 436 | #[cfg(test)] 437 | mod tests { 438 | use std::mem; 439 | 440 | use super::*; 441 | 442 | /// Ensure that the sizes of key structures are right. 443 | #[test] 444 | fn sizeofs() { 445 | assert_eq!(mem::size_of::(), 0x70); 446 | assert_eq!(mem::size_of::(), 0x2cc); 447 | // assert_eq!(mem::offset_of!(ThreadContextX64, Xmm0), 0x1a0); 448 | // assert_eq!(mem::offset_of!(ThreadContextX64, VectorRegister), 0x300); 449 | assert_eq!(mem::size_of::(), 0x4d0); 450 | assert_eq!(mem::size_of::

(), 0x20); 451 | assert_eq!(mem::size_of::(), 0x8); 452 | assert_eq!(mem::size_of::(), 0xC); 453 | assert_eq!(mem::size_of::(), 0x10); 454 | assert_eq!(mem::size_of::(), 0x10); 455 | assert_eq!(mem::size_of::(), 0x34); 456 | assert_eq!(mem::size_of::(), 0x6c); 457 | assert_eq!(mem::size_of::(), 0x10); 458 | assert_eq!(mem::size_of::(), 0x30); 459 | assert_eq!(mem::size_of::(), 0x10); 460 | assert_eq!(mem::size_of::(), 0x30); 461 | assert_eq!(mem::size_of::(), 32); 462 | assert_eq!(mem::size_of::(), 0x98); 463 | assert_eq!(mem::size_of::(), 0xa8); 464 | } 465 | } 466 | -------------------------------------------------------------------------------- /src/udmp_parser.rs: -------------------------------------------------------------------------------- 1 | // Axel '0vercl0k' Souchet - July 29 2023 2 | //! This module is where the parsing logic is implemented. The 3 | //! [`UserDumpParser`] can memory map a file by default but users can also build 4 | //! an instance from a slice they got from somewhere else. 5 | use std::io::{Read, Seek}; 6 | use std::{collections, fmt, io, mem, ops, path, slice, vec}; 7 | 8 | use crate::map::{Cursor, MappedFile}; 9 | use crate::structs::*; 10 | 11 | /// Disables all access to the committed region of pages. An attempt to read 12 | /// from, write to, or execute the committed region results in an access 13 | /// violation. 14 | pub const PAGE_NOACCESS: u32 = 1; 15 | /// Enables read-only access to the committed region of pages. An attempt to 16 | /// write to the committed region results in an access violation. If Data 17 | /// Execution Prevention is enabled, an attempt to execute code in the committed 18 | /// region results in an access violation. 19 | pub const PAGE_READONLY: u32 = 2; 20 | /// Enables read-only or read/write access to the committed region of pages. If 21 | /// Data Execution Prevention is enabled, attempting to execute code in the 22 | /// committed region results in an access violation. 23 | pub const PAGE_READWRITE: u32 = 4; 24 | /// Enables read-only or copy-on-write access to a mapped view of a file mapping 25 | /// object. An attempt to write to a committed copy-on-write page results in a 26 | /// private copy of the page being made for the process. The private page is 27 | /// marked as PAGE_READWRITE, and the change is written to the new page. If Data 28 | /// Execution Prevention is enabled, attempting to execute code in the committed 29 | /// region results in an access violation. 30 | pub const PAGE_WRITECOPY: u32 = 8; 31 | /// Enables execute access to the committed region of pages. An attempt to write 32 | /// to the committed region results in an access violation. 33 | pub const PAGE_EXECUTE: u32 = 16; 34 | /// Enables execute or read-only access to the committed region of pages. An 35 | /// attempt to write to the committed region results in an access violation. 36 | pub const PAGE_EXECUTE_READ: u32 = 32; 37 | /// Enables execute, read-only, or read/write access to the committed region of 38 | /// pages. 39 | pub const PAGE_EXECUTE_READWRITE: u32 = 64; 40 | /// Enables execute, read-only, or copy-on-write access to a mapped view of a 41 | /// file mapping object. An attempt to write to a committed copy-on-write page 42 | /// results in a private copy of the page being made for the process. The 43 | /// private page is marked as PAGE_EXECUTE_READWRITE, and the change is written 44 | /// to the new page. 45 | pub const PAGE_EXECUTE_WRITECOPY: u32 = 128; 46 | /// Pages in the region become guard pages. Any attempt to access a guard page 47 | /// causes the system to raise a STATUS_GUARD_PAGE_VIOLATION exception and turn 48 | /// off the guard page status. Guard pages thus act as a one-time access alarm. 49 | pub const PAGE_GUARD: u32 = 0x1_00; 50 | /// Sets all pages to be non-cachable. Applications should not use this 51 | /// attribute except when explicitly required for a device. Using the 52 | /// interlocked functions with memory that is mapped with SEC_NOCACHE can result 53 | /// in an EXCEPTION_ILLEGAL_INSTRUCTION exception. 54 | pub const PAGE_NOCACHE: u32 = 0x2_00; 55 | /// Sets all pages to be write-combined. Applications should not use this 56 | /// attribute except when explicitly required for a device. Using the 57 | /// interlocked functions with memory that is mapped as write-combined can 58 | /// result in an EXCEPTION_ILLEGAL_INSTRUCTION exception. 59 | pub const PAGE_WRITECOMBINE: u32 = 0x4_00; 60 | 61 | /// The memory rights constants on Windows make it annoying to know if the page 62 | /// is readable / writable / executable, so we have to create our own masks. 63 | /// A page is readable if it is protected with any of the below rights. 64 | const READABLE: u32 = PAGE_READONLY 65 | | PAGE_READWRITE 66 | | PAGE_EXECUTE_READ 67 | | PAGE_EXECUTE_READWRITE 68 | | PAGE_EXECUTE_WRITECOPY 69 | | PAGE_WRITECOPY; 70 | 71 | /// A page is writable if it is protected with any of the below rights. 72 | const WRITABLE: u32 = PAGE_READWRITE | PAGE_EXECUTE_READWRITE | PAGE_WRITECOPY; 73 | /// A page is executable if it is protected with any of the below rights. 74 | const EXECUTABLE: u32 = 75 | PAGE_EXECUTE | PAGE_EXECUTE_READ | PAGE_EXECUTE_READWRITE | PAGE_EXECUTE_WRITECOPY; 76 | 77 | /// A DLL loaded in the virtual address space. 78 | #[allow(clippy::len_without_is_empty)] 79 | #[derive(Default, Debug)] 80 | pub struct Module<'a> { 81 | /// The range of where the module is loaded in memory at. 82 | pub range: ops::Range, 83 | /// PE checksum of the module. 84 | pub checksum: u32, 85 | /// Timestamp. 86 | pub time_date_stamp: u32, 87 | /// The module path on the file system. 88 | pub path: path::PathBuf, 89 | pub version_info: FixedFileInfo, 90 | pub cv_record: &'a [u8], 91 | pub misc_record: &'a [u8], 92 | } 93 | 94 | impl<'a> Module<'a> { 95 | /// Build a new [`Module`] instance. 96 | fn new( 97 | entry: ModuleEntry, 98 | module_name: String, 99 | cv_record: &'a [u8], 100 | misc_record: &'a [u8], 101 | ) -> Self { 102 | let start = entry.base_of_image; 103 | let end = entry.base_of_image + entry.size_of_image as u64; 104 | let range = ops::Range { start, end }; 105 | if range.is_empty() { 106 | panic!("range is malformed"); 107 | } 108 | 109 | Self { 110 | range, 111 | checksum: entry.checksum, 112 | time_date_stamp: entry.time_date_stamp, 113 | path: module_name.into(), 114 | version_info: entry.version_info, 115 | cv_record, 116 | misc_record, 117 | } 118 | } 119 | 120 | /// Get the file name of the module. This returns [`None`] if the file name 121 | /// can't be converted to a Rust string. 122 | pub fn file_name(&self) -> Option<&str> { 123 | self.path.file_name().unwrap().to_str() 124 | } 125 | 126 | /// Get the address of where the module was loaded at. 127 | pub fn start_addr(&self) -> u64 { 128 | self.range.start 129 | } 130 | 131 | /// Get the address of where the last byte of the module was loaded at. 132 | pub fn end_addr(&self) -> u64 { 133 | self.range.end - 1 134 | } 135 | 136 | /// Get the length of the range of memory the module was loaded at. 137 | pub fn len(&self) -> u64 { 138 | self.range.end - self.range.start 139 | } 140 | } 141 | 142 | /// A [`ThreadContext`] stores the thread contexts for the architecture that are 143 | /// supported by the library. 144 | #[derive(Debug)] 145 | pub enum ThreadContext { 146 | /// The Intel x86 thread context. 147 | X86(Box), 148 | /// The Intel x64 thread context. 149 | X64(Box), 150 | } 151 | 152 | /// Display the [`ThreadContext`] like WinDbg would. 153 | impl fmt::Display for ThreadContext { 154 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 155 | match self { 156 | Self::X86(ctx) => ctx.fmt(f), 157 | Self::X64(ctx) => ctx.fmt(f), 158 | } 159 | } 160 | } 161 | 162 | /// A thread that was running when the dump was generated. 163 | #[derive(Debug)] 164 | pub struct Thread { 165 | /// The thread ID. 166 | pub id: u32, 167 | /// The suspend count counter cf [Freezing and Suspending Threads](https://learn.microsoft.com/en-us/windows-hardware/drivers/debugger/controlling-processes-and-threads). 168 | pub suspend_count: u32, 169 | /// The priority class cf [Priority Class](https://learn.microsoft.com/en-us/windows/win32/procthread/scheduling-priorities). 170 | pub priority_class: u32, 171 | /// Thread priority cf [Priority level](https://learn.microsoft.com/en-us/windows/win32/procthread/scheduling-priorities). 172 | pub priority: u32, 173 | /// The thread environment block address. 174 | pub teb: u64, 175 | /// The thread context. 176 | context: ThreadContext, 177 | } 178 | 179 | impl Thread { 180 | /// Build a new [`Thread`] instance. 181 | fn new(entry: ThreadEntry, context: ThreadContext) -> Self { 182 | Self { 183 | id: entry.thread_id, 184 | suspend_count: entry.suspend_count, 185 | priority_class: entry.priority_class, 186 | priority: entry.priority, 187 | teb: entry.teb, 188 | context, 189 | } 190 | } 191 | 192 | /// Get a reference to the [`ThreadContext`]. 193 | pub fn context(&self) -> &ThreadContext { 194 | &self.context 195 | } 196 | } 197 | 198 | /// A block of memory in the address space that isn't a [`Module`]. [`MemBlock`] 199 | /// can have `data` associated with it but isn't a guarantee (think about a 200 | /// memory region that is mapped as `PAGE_NOACCESS`). 201 | #[derive(Default, Debug)] 202 | #[allow(clippy::len_without_is_empty)] 203 | pub struct MemBlock<'a> { 204 | /// Range over the start/end address of the memory region. 205 | pub range: ops::Range, 206 | /// The base of the allocation that gave life to this memory region. 207 | pub allocation_base: u64, 208 | /// The page protection used at allocation time. 209 | pub allocation_protect: u32, 210 | /// The state of the memory region. See [State](https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-memory_basic_information). 211 | pub state: u32, 212 | /// The page protection currently applied to the memory region. 213 | pub protect: u32, 214 | /// The type of memory region. See [Type](https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-memory_basic_information). 215 | pub type_: u32, 216 | /// The [`MemBlock`]'s data. 217 | pub data: &'a [u8], 218 | } 219 | 220 | impl<'a> MemBlock<'a> { 221 | /// Is the memory region readable? 222 | pub fn is_readable(&self) -> bool { 223 | (self.protect & READABLE) != 0 224 | } 225 | 226 | /// Is the memory region writable? 227 | pub fn is_writable(&self) -> bool { 228 | (self.protect & WRITABLE) != 0 229 | } 230 | 231 | /// Is the memory region executable? 232 | pub fn is_executable(&self) -> bool { 233 | (self.protect & EXECUTABLE) != 0 234 | } 235 | 236 | /// Stringify the memory region state. 237 | pub fn state_as_str(&self) -> &str { 238 | match self.state { 239 | 0x10_00 => "MEM_COMMIT", 240 | 0x20_00 => "MEM_RESERVE", 241 | 0x1_00_00 => "MEM_FREE", 242 | _ => "UNKNOWN", 243 | } 244 | } 245 | 246 | /// Stringify the memory region type. 247 | pub fn type_as_str(&self) -> &str { 248 | if self.state == 0x1_00_00 { 249 | return ""; 250 | } 251 | 252 | match self.type_ { 253 | 0x2_00_00 => "MEM_PRIVATE", 254 | 0x4_00_00 => "MEM_MAPPED", 255 | 0x1_00_00_00 => "MEM_IMAGE", 256 | _ => "UNKNOWN", 257 | } 258 | } 259 | 260 | /// Stringify the memory region protection. 261 | pub fn protect_as_str(&self) -> String { 262 | if self.protect == 0 { 263 | return "".into(); 264 | } 265 | 266 | // Those bits are the only ones that can be combined with the page 267 | // protections from below. So strip those first off `protect`. 268 | let bits = collections::HashMap::from([ 269 | (PAGE_GUARD, "PAGE_GUARD"), 270 | (PAGE_NOCACHE, "PAGE_NOCACHE"), 271 | (PAGE_WRITECOMBINE, "PAGE_WRITECOMBINE"), 272 | ]); 273 | 274 | // This is where the parts of the stringified mask are stored in. 275 | let mut parts = vec::Vec::new(); 276 | let mut protect = self.protect; 277 | 278 | // Walk through the bits to check if turned on. 279 | for (mask, str) in bits.iter() { 280 | // If the current bit isn't set, skip. 281 | if (protect & mask) == 0 { 282 | continue; 283 | } 284 | 285 | // If it is set, strip it off from `protect` and push its 286 | // stringified value in the vector. 287 | protect &= !mask; 288 | parts.push(*str); 289 | } 290 | 291 | // Now we can handle the 'normal' page properties. 292 | parts.push(match protect { 293 | PAGE_NOACCESS => "PAGE_NOACCESS", 294 | PAGE_READONLY => "PAGE_READONLY", 295 | PAGE_READWRITE => "PAGE_READWRITE", 296 | PAGE_WRITECOPY => "PAGE_WRITECOPY", 297 | PAGE_EXECUTE => "PAGE_EXECUTE", 298 | PAGE_EXECUTE_READ => "PAGE_EXECUTE_READ", 299 | PAGE_EXECUTE_READWRITE => "PAGE_EXECUTE_READWRITE", 300 | PAGE_EXECUTE_WRITECOPY => "PAGE_EXECUTE_WRITECOPY", 301 | _ => "UNKNOWN", 302 | }); 303 | 304 | parts.join(" | ") 305 | } 306 | 307 | /// Get a slice over the [`MemBlock`]'s data from its absolute address. 308 | /// 309 | /// If the dump had a memory block of size 4 bytes starting at address 310 | /// 0xdead then calling `data_from(0xdead+1)` returns a slice over the 311 | /// last 3 bytes of the memory block. This is useful when you don't need 312 | /// to reason about offsets. 313 | pub fn data_from(&self, addr: u64) -> Option<&[u8]> { 314 | // If the memory block is empty return `None`. Also bail if this 315 | // `MemBlock` doesn't contain the address. 316 | if self.data.is_empty() || !self.range.contains(&addr) { 317 | return None; 318 | } 319 | 320 | // `addr` is contained in the range, so this is safe. 321 | let offset = addr - self.range.start; 322 | 323 | // Return the slice to the user. 324 | Some(&self.data[offset.try_into().unwrap()..]) 325 | } 326 | 327 | /// Get the address of where this [`MemBlock`] was at in memory. 328 | pub fn start_addr(&self) -> u64 { 329 | self.range.start 330 | } 331 | 332 | /// Get the end address of where this [`MemBlock`] was at in memory. 333 | /// 334 | /// Note that the underlying range is not inclusive, so this address is 335 | /// pointing right after the last byte's address. 336 | pub fn end_addr(&self) -> u64 { 337 | self.range.end 338 | } 339 | 340 | /// Get the size of the [`MemBlock`]. 341 | /// 342 | /// Note that a region of memory can exists without having any `data` 343 | /// associated with it. This method returns the range len, not `data`'s len. 344 | /// 345 | /// An example is a memory region mapped as `PAGE_NOACCESS`; it exists in 346 | /// the address space but has no content. 347 | pub fn len(&self) -> u64 { 348 | self.range.end - self.range.start 349 | } 350 | } 351 | 352 | /// Convert a [`MemoryInfo`] into a [`MemBlock`]. 353 | impl<'a> From for MemBlock<'a> { 354 | fn from(value: MemoryInfo) -> Self { 355 | Self { 356 | range: value.base_address..(value.base_address + value.region_size), 357 | allocation_base: value.allocation_base, 358 | allocation_protect: value.allocation_protect, 359 | state: value.state, 360 | protect: value.protect, 361 | type_: value.type_, 362 | ..Default::default() 363 | } 364 | } 365 | } 366 | 367 | /// Map a base address to a [`MemBlock`]. 368 | pub type MemBlocks<'a> = collections::BTreeMap>; 369 | 370 | /// Map a thread id to a [`Thread`]. 371 | pub type Threads = collections::BTreeMap; 372 | 373 | /// Map a base address to a [`Module`]. 374 | pub type Modules<'a> = collections::BTreeMap>; 375 | 376 | /// Architectures supported by the library. 377 | #[derive(Debug, Clone, Copy)] 378 | pub enum Arch { 379 | /// Intel x86. 380 | X86, 381 | /// Intel x64. 382 | X64, 383 | } 384 | 385 | /// This stores useful information fished out of of Windows minidump file: 386 | /// thread contexts and memory blocks. 387 | #[derive(Debug)] 388 | pub struct UserDumpParser<'a> { 389 | /// The thread id of the foreground thread. 390 | pub foreground_tid: Option, 391 | /// The architecture of the dumped process. 392 | arch: Arch, 393 | /// A map of [`MemBlock`]s. 394 | mem_blocks: MemBlocks<'a>, 395 | /// A map of [`Module`]. 396 | modules: Modules<'a>, 397 | /// A map of [`Thread`]. 398 | threads: Threads, 399 | /// This is where we hold the backing data. Either it is a memory mapped 400 | /// file, or a slice that needs to live as long as this. 401 | _mapped_file: MappedFile<'a>, 402 | } 403 | 404 | impl<'a> UserDumpParser<'a> { 405 | /// Create an instance from a filepath. This memory maps the file and parses 406 | /// it. 407 | pub fn new>(path: S) -> io::Result> { 408 | let mapped_file = MappedFile::new(path)?; 409 | Self::with_file(mapped_file) 410 | } 411 | 412 | /// Create an instance from something that dereference to a slice of bytes. 413 | pub fn with_slice( 414 | slice: &'a impl std::ops::Deref, 415 | ) -> io::Result> { 416 | Self::with_file(MappedFile::from(slice.deref())) 417 | } 418 | 419 | /// Is the architeture X64? 420 | pub fn is_arch_x64(&self) -> bool { 421 | matches!(self.arch, Arch::X64) 422 | } 423 | 424 | /// Is the architecture X86? 425 | pub fn is_arch_x86(&self) -> bool { 426 | matches!(self.arch, Arch::X86) 427 | } 428 | 429 | /// Get a reference to the base address -> [`Module`] map. 430 | pub fn modules(&self) -> &Modules { 431 | &self.modules 432 | } 433 | 434 | /// Find a [`Module`] that includes `address` in its range. 435 | pub fn get_module(&self, address: u64) -> Option<&Module> { 436 | self.modules 437 | .values() 438 | .find(|module| module.range.contains(&address)) 439 | } 440 | 441 | /// Get a reference to the TID -> [`Thread`] map. 442 | pub fn threads(&self) -> &Threads { 443 | &self.threads 444 | } 445 | 446 | /// Find a [`Thread`] with a specific TID. 447 | pub fn get_thread(&self, id: u32) -> Option<&Thread> { 448 | self.threads.values().find(|thread| thread.id == id) 449 | } 450 | 451 | /// Get a reference to the base address -> [`MemBlock`] map. 452 | pub fn mem_blocks(&self) -> &MemBlocks { 453 | &self.mem_blocks 454 | } 455 | 456 | /// Find a [`MemBlock`] that includes `address` in its range. 457 | pub fn get_mem_block(&self, address: u64) -> Option<&MemBlock> { 458 | self.mem_blocks 459 | .values() 460 | .find(|block| block.range.contains(&address)) 461 | } 462 | 463 | /// Utility to get a slice from a [`LocationDescriptor32`] safely. 464 | fn slice_from_location_descriptor( 465 | reader: &Cursor, 466 | location: LocationDescriptor32, 467 | ) -> io::Result<&'a [u8]> { 468 | // Grab the offset and the wanted len. 469 | let offset = location.rva.try_into().unwrap(); 470 | let len = location.data_size.try_into().unwrap(); 471 | 472 | // Grab a reference on the underlying slice. 473 | let slice_ref = reader.get_ref(); 474 | 475 | // Split the slice in two. We only care about the tail. 476 | let (_, tail) = slice_ref.split_at(offset); 477 | 478 | // Make sure the tail slice is big enough. 479 | if tail.len() < len { 480 | return Err(io::Error::new( 481 | io::ErrorKind::UnexpectedEof, 482 | "not enough data for slicing", 483 | )); 484 | } 485 | 486 | // Make sure we hold `from_raw_parts`'s contract. 487 | if len > isize::MAX.try_into().unwrap() { 488 | panic!("len > isize::MAX"); 489 | } 490 | 491 | // Build the slice! 492 | Ok(unsafe { slice::from_raw_parts(tail.as_ptr(), len) }) 493 | } 494 | 495 | /// Parse the system info stream to know which architecture is used. 496 | fn parse_system_info(cursor: &mut Cursor) -> io::Result { 497 | // Read the stream info. 498 | let system_info = read_struct::(cursor)?; 499 | 500 | // Build the value of the enum safely. 501 | Ok(match system_info.processor_arch { 502 | ARCH_X86 => Arch::X86, 503 | ARCH_X64 => Arch::X64, 504 | _ => panic!("Unsupported architecture {:x}", system_info.processor_arch), 505 | }) 506 | } 507 | 508 | /// Parse the exception stream to know figure out if there's a foreground 509 | /// TID. 510 | fn parse_exception(cursor: &mut Cursor) -> io::Result { 511 | // Read the exception stream. 512 | let exception = read_struct::(cursor)?; 513 | 514 | // Return the TID. 515 | Ok(exception.thread_id) 516 | } 517 | 518 | /// Parse the memory info list stream to build the [`MemBlocks`] map. 519 | fn parse_mem_info_list(cursor: &mut Cursor) -> io::Result> { 520 | // Create storage for the memory blocks. 521 | let mut mem_blocks = MemBlocks::new(); 522 | 523 | // Read the memory info list stream. 524 | let mem_info_list = read_struct::(cursor)?; 525 | 526 | // Ensure that each entry is at least as big as what we expected. 527 | let mem_info_size = mem::size_of::() as u32; 528 | let size_of_entry = mem_info_list.size_of_entry; 529 | if size_of_entry < mem_info_size { 530 | return Err(io::Error::new( 531 | io::ErrorKind::InvalidData, 532 | format!( 533 | "MemoryInfo's size ({}) doesn't match the dump ({})", 534 | mem_info_size, mem_info_list.size_of_entry 535 | ), 536 | )); 537 | } 538 | 539 | // Iterate through every entries. 540 | for _ in 0..mem_info_list.number_of_entries { 541 | // Read the memory info structure. 542 | let mem_info = peek_struct::(cursor)?; 543 | 544 | // The key in the map is the base address. 545 | let key = mem_info.base_address; 546 | 547 | // If we already inserted this address, there's something wrong so 548 | // bail. 549 | let previous_val = mem_blocks.insert(key, mem_info.into()); 550 | if previous_val.is_some() { 551 | return Err(io::Error::new( 552 | io::ErrorKind::InvalidData, 553 | format!("Address {} already in the mem map", key), 554 | )); 555 | } 556 | 557 | // Move on to the next entry. 558 | cursor.seek(io::SeekFrom::Current(size_of_entry.into()))?; 559 | } 560 | 561 | // We're done. 562 | Ok(mem_blocks) 563 | } 564 | 565 | /// Parse the memory64 list stream to associate data to the MemBlock we 566 | /// parsed from the memory info list stream. That's why we parse the memory 567 | /// info list stream first. 568 | fn parse_mem64_list(cursor: &mut Cursor, mem_blocks: &mut MemBlocks<'a>) -> io::Result<()> { 569 | // Read the memory64 list stream. 570 | let mem_list = read_struct::(cursor)?; 571 | 572 | // Grab the starting offset. 573 | let mut data_offset = mem_list.base_rva; 574 | 575 | // Iterate through every entries. 576 | for _ in 0..mem_list.number_of_memory_ranges { 577 | // Read a descriptor. 578 | let descriptor = read_struct::(cursor)?; 579 | 580 | // Get a reference to the associated MemBlock off `mem_blocks`. 581 | let entry = mem_blocks 582 | .get_mut(&descriptor.start_of_memory_range) 583 | .ok_or(io::Error::new( 584 | io::ErrorKind::InvalidData, 585 | format!( 586 | "Address {} in Memory64ListStream but not in MemoryInfoListStream", 587 | descriptor.start_of_memory_range 588 | ), 589 | ))?; 590 | 591 | // Read the slice of bytes and associate it to the MemBlock instance. 592 | entry.data = Self::slice_from_location_descriptor(cursor, LocationDescriptor32 { 593 | rva: data_offset.try_into().unwrap(), 594 | data_size: descriptor.data_size.try_into().unwrap(), 595 | })?; 596 | 597 | // Bump the offset by the size of this region to find where the next 598 | // data slice is at. 599 | data_offset = data_offset.checked_add(descriptor.data_size).unwrap(); 600 | } 601 | 602 | // We're done! 603 | Ok(()) 604 | } 605 | 606 | /// Parse the tread list and extract their contexts. 607 | fn parse_thread_list(cursor: &mut Cursor, arch: Arch) -> io::Result { 608 | // Create the map of threads. 609 | let mut threads = Threads::new(); 610 | 611 | // Read the thread list. 612 | let thread_list = read_struct::(cursor)?; 613 | 614 | // Iterate through every entries. 615 | for _ in 0..thread_list.number_of_threads { 616 | // Read the entry. 617 | let thread = read_struct::(cursor)?; 618 | 619 | // Save the current position. 620 | let pos = cursor.stream_position()?; 621 | 622 | // Grab the slice of its context. 623 | let thread_context_slice = 624 | Self::slice_from_location_descriptor(cursor, thread.thread_context)?; 625 | 626 | // Let's make sense of this slice based on what architectcure it is. 627 | let thread_context = match arch { 628 | // Read a ThreadContextX86 context if the slice is big enough. 629 | Arch::X86 => { 630 | if thread_context_slice.len() < mem::size_of::() { 631 | return Err(io::Error::new( 632 | io::ErrorKind::InvalidData, 633 | format!( 634 | "The X86 thread context for TID {} has an unexpected size", 635 | thread.thread_id 636 | ), 637 | )); 638 | } 639 | 640 | // Build a reference to a ThreadContextX86 at this address. 641 | let ptr = thread_context_slice.as_ptr() as *const ThreadContextX86; 642 | ThreadContext::X86(Box::new(unsafe { std::ptr::read_unaligned(ptr) })) 643 | } 644 | // Read a ThreadContextX86 context if the slice is big enough. 645 | Arch::X64 => { 646 | if thread_context_slice.len() < mem::size_of::() { 647 | return Err(io::Error::new( 648 | io::ErrorKind::InvalidData, 649 | format!( 650 | "The X64 thread context for TID {} has an unexpected size", 651 | thread.thread_id 652 | ), 653 | )); 654 | } 655 | 656 | // Build a reference to a ThreadContextX64 at this address. 657 | let ptr = thread_context_slice.as_ptr() as *const ThreadContextX64; 658 | ThreadContext::X64(Box::new(unsafe { std::ptr::read_unaligned(ptr) })) 659 | } 660 | }; 661 | 662 | // The key in the map is the thread id. 663 | let key = thread.thread_id; 664 | 665 | // Create a Thread from its context and the descriptor. 666 | let thread = Thread::new(thread, thread_context); 667 | 668 | // If we've already encountered a thread with this id, then let's 669 | // bail. 670 | let previous_val = threads.insert(key, thread); 671 | if previous_val.is_some() { 672 | return Err(io::Error::new( 673 | io::ErrorKind::InvalidData, 674 | format!("Thread {} already in the map", key), 675 | )); 676 | } 677 | 678 | // Restore the position to get ready to parse the next entry. 679 | cursor.seek(io::SeekFrom::Start(pos))?; 680 | } 681 | 682 | Ok(threads) 683 | } 684 | 685 | /// Parse the module list. 686 | fn parse_module_list(cursor: &mut Cursor) -> io::Result> { 687 | // Build the map of modules. 688 | let mut modules = Modules::new(); 689 | 690 | // Read the module list. 691 | let module_list = read_struct::(cursor)?; 692 | 693 | // Iterate through every entries. 694 | for _ in 0..module_list.number_of_modules { 695 | // Read the module entry. 696 | let module = read_struct::(cursor)?; 697 | 698 | // Save the position. 699 | let pos = cursor.stream_position()?; 700 | 701 | // Grab the CV / misc record slices. 702 | let cv_record = Self::slice_from_location_descriptor(cursor, module.cv_record)?; 703 | let misc_record = Self::slice_from_location_descriptor(cursor, module.misc_record)?; 704 | 705 | // Travel to where the module name is stored at. 706 | cursor.seek(io::SeekFrom::Start(module.module_name_rva.into()))?; 707 | 708 | // Read its length. 709 | let module_name_length = read_struct::(cursor)?.try_into().unwrap(); 710 | 711 | // Allocate a backing buffer. 712 | let mut module_name = vec![0; module_name_length]; 713 | 714 | // Read the module name off the slice into the buffer. 715 | cursor.read_exact(module_name.as_mut_slice())?; 716 | 717 | // Convert the module name into a Rust string. 718 | let module_name = utf16_string_from_slice(&module_name).map_err(|e| { 719 | io::Error::new( 720 | io::ErrorKind::InvalidData, 721 | format!("Module name is incorrect utf8: {e}"), 722 | ) 723 | })?; 724 | 725 | // Create a module from its descriptor / name / records. 726 | let module = Module::new(module, module_name, cv_record, misc_record); 727 | 728 | // If there's already a module at this address, something is wrong 729 | // so we bail. 730 | let previous_val = modules.insert(module.range.start, module); 731 | if let Some(previous_val) = previous_val { 732 | return Err(io::Error::new( 733 | io::ErrorKind::InvalidData, 734 | format!("Module {} already in the map", previous_val.path.display()), 735 | )); 736 | } 737 | 738 | // Restore the saved cursor. 739 | cursor.seek(io::SeekFrom::Start(pos))?; 740 | } 741 | 742 | // We're done! 743 | Ok(modules) 744 | } 745 | 746 | pub fn with_file(_mapped_file: MappedFile<'a>) -> io::Result> { 747 | // Grab a cursor to start parsing the bits. 748 | let mut cursor = _mapped_file.cursor(); 749 | 750 | // Read the header. 751 | let hdr = read_struct::
(&mut cursor)?; 752 | 753 | // If we don't see the expected signature, bail. 754 | if hdr.signature != EXPECTED_DUMP_SIGNATURE { 755 | return Err(io::Error::new( 756 | io::ErrorKind::InvalidData, 757 | format!("Header signature {:x} is unexpected", hdr.signature), 758 | )); 759 | } 760 | 761 | // Check if the flags make sense. 762 | if (hdr.flags & VALID_DUMP_FLAGS) != 0 { 763 | return Err(io::Error::new( 764 | io::ErrorKind::InvalidData, 765 | format!("Header signature {:x} is unexpected", hdr.signature), 766 | )); 767 | } 768 | 769 | // Move to the stream directory. 770 | cursor.seek(io::SeekFrom::Start(hdr.stream_directory_rva.into()))?; 771 | 772 | // Create a map to store where directories are stored at. 773 | let mut directory_locations = collections::HashMap::new(); 774 | 775 | // Iterate through every entries. 776 | for _ in 0..hdr.number_of_streams { 777 | // Read the directory.. 778 | let directory = read_struct::(&mut cursor)?; 779 | 780 | // ..if we hit the `STREAM_TYPE_UNUSED`, we'll stop there. 781 | if directory.stream_type == STREAM_TYPE_UNUSED { 782 | break; 783 | } 784 | 785 | // Keep track of this directory. 786 | directory_locations.insert(directory.stream_type, directory.location); 787 | } 788 | 789 | // Parsing directories in a certain orders make things easier, and below 790 | // is the order we want. 791 | let required = true; 792 | let not_required = false; 793 | let directory_parsing_order = [ 794 | // We need the architecture to decode threads. 795 | (STREAM_TYPE_SYSTEM_INFO, required), 796 | (STREAM_TYPE_EXCEPTION, not_required), 797 | // We parse this stream to build MemBlock w/o any data. 798 | (STREAM_TYPE_MEMORY_INFO_LIST, required), 799 | // We associate the data when parsing that stream. 800 | (STREAM_TYPE_MEMORY64_LIST, required), 801 | (STREAM_TYPE_THREAD_LIST, not_required), 802 | (STREAM_TYPE_MODULE_LIST, not_required), 803 | ]; 804 | 805 | // Declare a bunch of state. 806 | let mut arch = None; 807 | let mut foreground_tid = None; 808 | let mut mem_blocks = MemBlocks::new(); 809 | let mut modules = Modules::new(); 810 | let mut threads = Threads::new(); 811 | 812 | // Iterate through the directories in order. 813 | for (directory_type, required) in directory_parsing_order { 814 | // Check if we've encountered this stream directory 815 | let directory_location = directory_locations.get(&directory_type); 816 | 817 | // If we haven't, and that this directory is required, we bail. 818 | // Otherwise we just go to the next. 819 | let Some(directory_location) = directory_location else { 820 | if required { 821 | return Err(io::Error::new( 822 | io::ErrorKind::InvalidData, 823 | format!("The directory {directory_type} is required but not present"), 824 | )); 825 | } 826 | 827 | continue; 828 | }; 829 | 830 | // Move to where the stream is at. 831 | cursor.seek(io::SeekFrom::Start(directory_location.rva.into()))?; 832 | 833 | // Parse the streams we support. 834 | match directory_type { 835 | STREAM_TYPE_SYSTEM_INFO => arch = Some(Self::parse_system_info(&mut cursor)?), 836 | STREAM_TYPE_EXCEPTION => foreground_tid = Some(Self::parse_exception(&mut cursor)?), 837 | STREAM_TYPE_MEMORY_INFO_LIST => { 838 | mem_blocks = Self::parse_mem_info_list(&mut cursor)? 839 | } 840 | STREAM_TYPE_MEMORY64_LIST => Self::parse_mem64_list(&mut cursor, &mut mem_blocks)?, 841 | STREAM_TYPE_THREAD_LIST => { 842 | threads = Self::parse_thread_list(&mut cursor, arch.unwrap())? 843 | } 844 | STREAM_TYPE_MODULE_LIST => modules = Self::parse_module_list(&mut cursor)?, 845 | _ => unreachable!("Only parsing stream types we know about"), 846 | }; 847 | } 848 | 849 | // The system info stream is required to be parsed so we know we have a 850 | // value in arch. 851 | let arch = arch.unwrap(); 852 | 853 | // Phew, we have everything needed to build an instance! 854 | Ok(UserDumpParser { 855 | _mapped_file, 856 | arch, 857 | foreground_tid, 858 | mem_blocks, 859 | modules, 860 | threads, 861 | }) 862 | } 863 | } 864 | 865 | /// Peek for a `T` from the cursor. 866 | fn peek_struct(cursor: &mut Cursor) -> io::Result { 867 | let mut s = mem::MaybeUninit::uninit(); 868 | let size_of_s = mem::size_of_val(&s); 869 | let slice_over_s = unsafe { slice::from_raw_parts_mut(s.as_mut_ptr() as *mut u8, size_of_s) }; 870 | 871 | let pos = cursor.position(); 872 | cursor.read_exact(slice_over_s)?; 873 | cursor.seek(io::SeekFrom::Start(pos))?; 874 | 875 | Ok(unsafe { s.assume_init() }) 876 | } 877 | 878 | /// Read a `T` from the cursor. 879 | fn read_struct(cursor: &mut Cursor) -> io::Result { 880 | let s = peek_struct(cursor)?; 881 | let size_of_s = mem::size_of_val(&s); 882 | 883 | cursor.seek(io::SeekFrom::Current(size_of_s.try_into().unwrap()))?; 884 | 885 | Ok(s) 886 | } 887 | 888 | /// Convert a slice of byte into an UTF16 Rust string. 889 | fn utf16_string_from_slice(slice: &[u8]) -> io::Result { 890 | // Every code point is 2 bytes, so we expect the length to be a multiple of 891 | // 2. 892 | if (slice.len() % 2) != 0 { 893 | return Err(io::Error::new( 894 | io::ErrorKind::InvalidData, 895 | "Slice length needs to be % 2", 896 | )); 897 | } 898 | 899 | // Iterate over chunks of 2 bytes to yield u16's. 900 | let iter = slice.chunks(2).map(|c| u16::from_le_bytes([c[0], c[1]])); 901 | 902 | // Decode the u16's into a String. If one of the u16 can't be decoded into a 903 | // valid code point, then it fails. Otherwise they all get collected into a 904 | // String. 905 | char::decode_utf16(iter) 906 | .collect::>() 907 | .or(Err(io::Error::new( 908 | io::ErrorKind::InvalidData, 909 | "Module name is not UTF16", 910 | ))) 911 | } 912 | 913 | #[cfg(test)] 914 | mod tests { 915 | use core::fmt::Debug; 916 | 917 | use crate::UserDumpParser; 918 | 919 | #[test] 920 | fn assert_traits() { 921 | fn assert_traits_() {} 922 | assert_traits_::(); 923 | } 924 | } 925 | --------------------------------------------------------------------------------