diff --git a/Cargo.lock b/Cargo.lock index 1cb46da48977..339810a2623d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4184,6 +4184,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "process_reader" +version = "0.1.0" +dependencies = [ + "goblin", + "memoffset", + "mozilla-central-workspace-hack", + "thiserror", + "winapi", +] + [[package]] name = "processtools" version = "0.1.0" diff --git a/toolkit/crashreporter/process_reader/Cargo.toml b/toolkit/crashreporter/process_reader/Cargo.toml new file mode 100644 index 000000000000..ee85951d5ba8 --- /dev/null +++ b/toolkit/crashreporter/process_reader/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "process_reader" +version = "0.1.0" +authors = ["Gabriele Svelto"] +edition = "2018" +license = "MPL-2.0" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +goblin = { version = "0.6", features = ["elf32", "elf64", "pe32", "pe64"] } +memoffset = "0.8" +mozilla-central-workspace-hack = { path = "../../../build/workspace-hack" } +thiserror = "1.0" + +[target."cfg(target_os = \"windows\")".dependencies] +[dependencies.winapi] +version = "0.3" diff --git a/toolkit/crashreporter/process_reader/src/error.rs b/toolkit/crashreporter/process_reader/src/error.rs new file mode 100644 index 000000000000..af4d803a7a44 --- /dev/null +++ b/toolkit/crashreporter/process_reader/src/error.rs @@ -0,0 +1,30 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum ProcessReaderError { + #[error("Could not convert address {0}")] + ConvertAddressError(#[from] std::num::TryFromIntError), + #[cfg(target_os = "windows")] + #[error("Cannot enumerate the target process's modules")] + EnumProcessModulesError, + #[error("goblin failed to parse a module")] + GoblinError(#[from] goblin::error::Error), + #[error("Address was out of bounds")] + InvalidAddress, + #[error("Could not read from the target process address space")] + ReadFromProcessError(#[from] ReadError), + #[cfg(target_os = "windows")] + #[error("Section was not found")] + SectionNotFound, +} + +#[derive(Debug, Error)] +pub enum ReadError { + #[cfg(target_os = "windows")] + #[error("ReadProcessMemory failed")] + ReadProcessMemoryError, +} diff --git a/toolkit/crashreporter/process_reader/src/lib.rs b/toolkit/crashreporter/process_reader/src/lib.rs new file mode 100644 index 000000000000..20bdad5207e6 --- /dev/null +++ b/toolkit/crashreporter/process_reader/src/lib.rs @@ -0,0 +1,13 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#[cfg(target_os = "windows")] +type ProcessHandle = winapi::um::winnt::HANDLE; + +pub struct ProcessReader { + process: ProcessHandle, +} + +mod error; +mod process_reader; diff --git a/toolkit/crashreporter/process_reader/src/process_reader.rs b/toolkit/crashreporter/process_reader/src/process_reader.rs new file mode 100644 index 000000000000..1473aafa09ba --- /dev/null +++ b/toolkit/crashreporter/process_reader/src/process_reader.rs @@ -0,0 +1,6 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#[cfg(target_os = "windows")] +mod windows; diff --git a/toolkit/crashreporter/process_reader/src/process_reader/windows.rs b/toolkit/crashreporter/process_reader/src/process_reader/windows.rs new file mode 100644 index 000000000000..fdc47f544831 --- /dev/null +++ b/toolkit/crashreporter/process_reader/src/process_reader/windows.rs @@ -0,0 +1,222 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +use std::{ + convert::TryInto, + ffi::OsString, + mem::{size_of, MaybeUninit}, + os::windows::ffi::OsStringExt, + ptr::null_mut, +}; + +use winapi::{ + shared::minwindef::{FALSE, HMODULE, MAX_PATH}, + um::{ + memoryapi::ReadProcessMemory, + psapi::{GetModuleBaseNameW, K32EnumProcessModules, K32GetModuleInformation, MODULEINFO}, + }, +}; + +use crate::{ + error::{ProcessReaderError, ReadError}, + ProcessHandle, ProcessReader, +}; + +impl ProcessReader { + pub fn new(process: ProcessHandle) -> Result { + Ok(ProcessReader { process }) + } + + pub fn find_section( + &self, + module_name: &str, + section_name: &str, + ) -> Result { + let modules = self.get_module_list()?; + + modules + .iter() + .filter(|&&module| { + let name = self.get_module_name(module); + // Crude way of mimicking Windows lower-case comparisons but + // sufficient for our use-cases. + name.is_some_and(|name| name.eq_ignore_ascii_case(module_name)) + }) + .find_map(|&module| { + self.get_module_info(module).and_then(|info| { + self.find_section_in_module( + section_name, + info.lpBaseOfDll as usize, + info.SizeOfImage as usize, + ) + .ok() + }) + }) + .ok_or(ProcessReaderError::InvalidAddress) + } + + fn get_module_list(&self) -> Result, ProcessReaderError> { + let mut module_num: usize = 100; + let mut required_buffer_size: u32 = 0; + let mut module_array = Vec::::with_capacity(module_num); + + loop { + let buffer_size: u32 = (module_num * size_of::()).try_into()?; + let res = unsafe { + K32EnumProcessModules( + self.process, + module_array.as_mut_ptr() as *mut _, + buffer_size, + &mut required_buffer_size as *mut _, + ) + }; + + module_num = required_buffer_size as usize / size_of::(); + + if res == 0 { + if required_buffer_size > buffer_size { + module_array = Vec::::with_capacity(module_num); + } else { + return Err(ProcessReaderError::EnumProcessModulesError); + } + } else { + break; + } + } + + // SAFETY: module_array has been filled by K32EnumProcessModules() + unsafe { + module_array.set_len(module_num); + }; + + Ok(module_array) + } + + fn get_module_name(&self, module: HMODULE) -> Option { + let mut path: [u16; MAX_PATH as usize] = [0; MAX_PATH as usize]; + let res = unsafe { + GetModuleBaseNameW( + self.process, + module, + (&mut path).as_mut_ptr(), + MAX_PATH.try_into().unwrap(), + ) + }; + + if res == 0 { + None + } else { + let name = OsString::from_wide(&path[0..res as usize]); + let name = name.to_str()?; + Some(name.to_string()) + } + } + + fn get_module_info(&self, module: HMODULE) -> Option { + let mut info: MaybeUninit = MaybeUninit::uninit(); + let res = unsafe { + K32GetModuleInformation( + self.process, + module, + info.as_mut_ptr(), + size_of::() as u32, + ) + }; + + if res == 0 { + None + } else { + let info = unsafe { info.assume_init() }; + Some(info) + } + } + + fn find_section_in_module( + &self, + section_name: &str, + module_address: usize, + size: usize, + ) -> Result { + // We read only the first page from the module, this should be more than + // enough to read the header and section list. In the future we might do + // this incrementally but for now goblin requires an array to parse + // so we can't do it just yet. + let page_size = 4096; + if size < page_size { + // Don't try to read from the target module if it's too small + return Err(ProcessReaderError::ReadFromProcessError( + ReadError::ReadProcessMemoryError, + )); + } + + let bytes = self.copy_array(module_address as _, 4096)?; + let header = goblin::pe::header::Header::parse(&bytes)?; + + // Skip the PE header so we can parse the sections + let optional_header_offset = header.dos_header.pe_pointer as usize + + goblin::pe::header::SIZEOF_PE_MAGIC + + goblin::pe::header::SIZEOF_COFF_HEADER; + let offset = + &mut (optional_header_offset + header.coff_header.size_of_optional_header as usize); + + let sections = header.coff_header.sections(&bytes, offset)?; + + for section in sections { + if section.name.eq(section_name.as_bytes()) { + let address = module_address.checked_add(section.virtual_address as usize); + return address.ok_or(ProcessReaderError::InvalidAddress); + } + } + + Err(ProcessReaderError::SectionNotFound) + } + + pub fn copy_object_shallow(&self, src: usize) -> Result, ReadError> { + let mut object = MaybeUninit::::uninit(); + let res = unsafe { + ReadProcessMemory( + self.process, + src as _, + object.as_mut_ptr() as _, + size_of::(), + null_mut(), + ) + }; + + if res != FALSE { + Ok(object) + } else { + Err(ReadError::ReadProcessMemoryError) + } + } + + pub fn copy_object(&self, src: usize) -> Result { + let object = self.copy_object_shallow(src)?; + Ok(unsafe { object.assume_init() }) + } + + pub fn copy_array(&self, src: usize, num: usize) -> Result, ReadError> { + let num_of_bytes = num * size_of::(); + let mut array: Vec = Vec::with_capacity(num); + let res = unsafe { + ReadProcessMemory( + self.process, + src as _, + array.as_mut_ptr() as _, + num_of_bytes, + null_mut(), + ) + }; + + if res != FALSE { + unsafe { + array.set_len(num); + } + + Ok(array) + } else { + Err(ReadError::ReadProcessMemoryError) + } + } +}