From 39d0a7e438a702fa03ac5c46583716bdbad0c05f Mon Sep 17 00:00:00 2001 From: Chris Martin Date: Tue, 12 May 2026 11:38:37 -0400 Subject: [PATCH] Isolated Processes 5: Redirect remaining process inspection usage --- src/linux/auxv/mod.rs | 9 +- src/linux/auxv/reader.rs | 17 +- src/linux/dumper_cpu_info/arm.rs | 21 +-- src/linux/dumper_cpu_info/mod.rs | 1 + src/linux/dumper_cpu_info/x86.rs | 14 +- src/linux/minidump_writer/errors.rs | 22 ++- .../minidump_writer/handle_data_stream.rs | 37 ++--- src/linux/minidump_writer/mappings.rs | 20 +-- .../memory_info_list_stream.rs | 16 +- src/linux/minidump_writer/mod.rs | 143 ++++++----------- .../minidump_writer/systeminfo_stream.rs | 5 +- src/linux/process_inspection/mod.rs | 151 +++++++++++++++++- src/serializers.rs | 14 +- 13 files changed, 295 insertions(+), 175 deletions(-) diff --git a/src/linux/auxv/mod.rs b/src/linux/auxv/mod.rs index 01654798..ade5448c 100644 --- a/src/linux/auxv/mod.rs +++ b/src/linux/auxv/mod.rs @@ -1,10 +1,10 @@ use { self::reader::ProcfsAuxvIter, - super::Pid, + super::{Pid, process_inspection::ProcessInspector}, crate::serializers::*, error_graph::WriteErrorList, failspot::failspot, - std::{fs::File, io::BufReader}, + std::io::BufReader, thiserror::Error, }; @@ -84,6 +84,7 @@ pub struct AuxvDumpInfo { impl AuxvDumpInfo { pub fn try_filling_missing_info( &mut self, + process_inspector: &ProcessInspector, pid: Pid, mut soft_errors: impl WriteErrorList, ) -> Result<(), AuxvError> { @@ -92,7 +93,9 @@ impl AuxvDumpInfo { } let auxv_path = format!("/proc/{pid}/auxv"); - let auxv_file = File::open(&auxv_path).map_err(|e| AuxvError::OpenError(auxv_path, e))?; + let auxv_file = process_inspector + .read_file(&auxv_path) + .map_err(|e| AuxvError::OpenError(auxv_path, e))?; for pair_result in ProcfsAuxvIter::new(BufReader::new(auxv_file)) { let AuxvPair { key, value } = match pair_result { diff --git a/src/linux/auxv/reader.rs b/src/linux/auxv/reader.rs index e36ba93a..0c35e73a 100644 --- a/src/linux/auxv/reader.rs +++ b/src/linux/auxv/reader.rs @@ -8,22 +8,19 @@ use { super::{AuxvError, AuxvPair, AuxvType}, byteorder::{NativeEndian, ReadBytesExt}, - std::{ - fs::File, - io::{BufReader, Read}, - }, + std::io::Read, }; /// An iterator across auxv pairs from procfs. -pub struct ProcfsAuxvIter { +pub struct ProcfsAuxvIter { pair_size: usize, buf: Vec, - input: BufReader, + input: R, keep_going: bool, } -impl ProcfsAuxvIter { - pub fn new(input: BufReader) -> Self { +impl ProcfsAuxvIter { + pub fn new(input: R) -> Self { let pair_size = 2 * std::mem::size_of::(); let buf: Vec = Vec::with_capacity(pair_size); @@ -36,7 +33,7 @@ impl ProcfsAuxvIter { } } -impl Iterator for ProcfsAuxvIter { +impl Iterator for ProcfsAuxvIter { type Item = Result; fn next(&mut self) -> Option { if !self.keep_going { @@ -95,7 +92,7 @@ impl Iterator for ProcfsAuxvIter { } } -fn read_long(reader: &mut dyn Read) -> std::io::Result { +fn read_long(mut reader: R) -> std::io::Result { match std::mem::size_of::() { 4 => reader.read_u32::().map(|u| u as AuxvType), 8 => reader.read_u64::().map(|u| u as AuxvType), diff --git a/src/linux/dumper_cpu_info/arm.rs b/src/linux/dumper_cpu_info/arm.rs index 712c43c6..fedc9fb5 100644 --- a/src/linux/dumper_cpu_info/arm.rs +++ b/src/linux/dumper_cpu_info/arm.rs @@ -1,18 +1,16 @@ use { - super::CpuInfoError, + super::{CpuInfoError, ProcessInspector}, crate::minidump_format::*, scroll::Pwrite, std::{ collections::HashSet, - fs::File, io::{BufRead, BufReader, Read}, - path, }, }; type Result = std::result::Result; -pub fn parse_cpus_from_sysfile(file: &mut File) -> Result> { +fn parse_cpus_from_sysfile(mut file: R) -> Result> { let mut res = HashSet::new(); let mut content = String::new(); file.read_to_string(&mut content)?; @@ -135,7 +133,10 @@ fn parse_features(_val: &str) -> u32 { 0 } -pub fn write_cpu_information(sys_info: &mut MDRawSystemInfo) -> Result<()> { +pub fn write_cpu_information( + process_inspector: &ProcessInspector, + sys_info: &mut MDRawSystemInfo, +) -> Result<()> { // The CPUID value is broken up in several entries in /proc/cpuinfo. // This table is used to rebuild it from the entries. let cpu_id_entries = [ @@ -168,11 +169,13 @@ pub fn write_cpu_information(sys_info: &mut MDRawSystemInfo) -> Result<()> { // because the content of /proc/cpuinfo will only mirror the number // of 'online' cores, and thus will vary with time. // See http://www.kernel.org/doc/Documentation/cputopology.txt - if let Ok(mut present_file) = File::open("/sys/devices/system/cpu/present") { + if let Ok(mut present_file) = process_inspector.read_file("/sys/devices/system/cpu/present") { // Ignore unparsable content let cpus_present = parse_cpus_from_sysfile(&mut present_file).unwrap_or_default(); - if let Ok(mut possible_file) = File::open("/sys/devices/system/cpu/possible") { + if let Ok(mut possible_file) = + process_inspector.read_file("/sys/devices/system/cpu/possible") + { // Ignore unparsable content let cpus_possible = parse_cpus_from_sysfile(&mut possible_file).unwrap_or_default(); let intersection = cpus_present.intersection(&cpus_possible).count(); @@ -187,7 +190,7 @@ pub fn write_cpu_information(sys_info: &mut MDRawSystemInfo) -> Result<()> { // readable from regular Android applications on later versions // (>= 4.1) of the Android platform. - let cpuinfo_file = match File::open(path::PathBuf::from("/proc/cpuinfo")) { + let cpuinfo_file = match process_inspector.read_file("/proc/cpuinfo") { Ok(x) => x, Err(_) => { // Do not return Error here to allow the minidump generation @@ -307,7 +310,7 @@ mod tests { extern crate std; use std::io::Write; - fn new_file(content: &str) -> File { + fn new_file(content: &str) -> std::fs::File { let mut file = tempfile::Builder::new() .prefix("cpu_sets") .tempfile() diff --git a/src/linux/dumper_cpu_info/mod.rs b/src/linux/dumper_cpu_info/mod.rs index 6f898390..6ef4993f 100644 --- a/src/linux/dumper_cpu_info/mod.rs +++ b/src/linux/dumper_cpu_info/mod.rs @@ -1,4 +1,5 @@ use { + super::process_inspection::ProcessInspector, crate::{minidump_format::PlatformId, serializers::*}, nix::sys::utsname::uname, }; diff --git a/src/linux/dumper_cpu_info/x86.rs b/src/linux/dumper_cpu_info/x86.rs index 013bef1d..e4b55a65 100644 --- a/src/linux/dumper_cpu_info/x86.rs +++ b/src/linux/dumper_cpu_info/x86.rs @@ -1,11 +1,8 @@ use { - super::CpuInfoError, + super::{CpuInfoError, ProcessInspector}, crate::minidump_format::*, failspot::failspot, - std::{ - io::{BufRead, BufReader}, - path, - }, + std::io::{BufRead, BufReader}, }; type Result = std::result::Result; @@ -26,7 +23,10 @@ impl CpuInfoEntry { } } -pub fn write_cpu_information(sys_info: &mut MDRawSystemInfo) -> Result<()> { +pub fn write_cpu_information( + process_inspector: &ProcessInspector, + sys_info: &mut MDRawSystemInfo, +) -> Result<()> { let vendor_id_name = "vendor_id"; let mut cpu_info_table = [ CpuInfoEntry::new("processor", -1, false), @@ -50,7 +50,7 @@ pub fn write_cpu_information(sys_info: &mut MDRawSystemInfo) -> Result<()> { bail(std::io::Error::other("test requested cpuinfo file failure")) ); - let cpuinfo_file = std::fs::File::open(path::PathBuf::from("/proc/cpuinfo"))?; + let cpuinfo_file = process_inspector.read_file("/proc/cpuinfo")?; let mut vendor_id = String::new(); for line in BufReader::new(cpuinfo_file).lines() { diff --git a/src/linux/minidump_writer/errors.rs b/src/linux/minidump_writer/errors.rs index f77f13b0..2c44f24a 100644 --- a/src/linux/minidump_writer/errors.rs +++ b/src/linux/minidump_writer/errors.rs @@ -13,6 +13,7 @@ use { thread_names_stream::SectionThreadNamesError, }, module_reader::ModuleReaderError, + process_inspection, serializers::*, }, crate::{dir_section::FileWriterError, mem_writer::MemoryWriterError, serializers::*}, @@ -150,15 +151,18 @@ pub enum WriterError { #[serde(skip)] std::num::TryFromIntError, ), + #[error("failed to suspend thread")] + SuspendThreadFailed(#[source] process_inspection::SuspendResumeThreadError), + #[error("failed to resume thread")] + ResumeThreadFailed(#[source] process_inspection::SuspendResumeThreadError), } #[derive(Debug, Error, serde::Serialize)] pub enum InitError { #[error("failed to read auxv")] ReadAuxvFailed(#[source] super::super::auxv::AuxvError), - #[error("IO error for file {0}")] - IOError( - String, + #[error("IO error reading /proc//task")] + ReadProcTaskFailed( #[source] #[serde(serialize_with = "serialize_io_error")] std::io::Error, @@ -220,6 +224,12 @@ pub enum StopProcessError { #[serde(serialize_with = "serialize_nix_error")] nix::Error, ), + #[error("failed to open process file")] + ReadFileFailed( + #[source] + #[serde(serialize_with = "serialize_io_error")] + std::io::Error, + ), #[error("Failed to get the process state")] State( #[from] @@ -231,7 +241,5 @@ pub enum StopProcessError { } #[derive(Debug, thiserror::Error)] -pub enum ContinueProcessError { - #[error("Failed to continue the process")] - Continue(#[from] Errno), -} +#[error("Failed to continue the process")] +pub struct ContinueProcessError(#[source] pub Errno); diff --git a/src/linux/minidump_writer/handle_data_stream.rs b/src/linux/minidump_writer/handle_data_stream.rs index e8e44774..80e2655b 100644 --- a/src/linux/minidump_writer/handle_data_stream.rs +++ b/src/linux/minidump_writer/handle_data_stream.rs @@ -2,27 +2,21 @@ use { super::*, crate::mem_writer::MemoryWriter, std::{ - ffi::{CString, OsString}, - fs::{self, DirEntry}, - mem::{self}, - os::unix::prelude::OsStrExt, + ffi::OsStr, + mem, path::{Path, PathBuf}, }, }; -fn file_stat(path: &Path) -> Option { - let c_path = CString::new(path.as_os_str().as_bytes()).ok()?; - let mut stat = unsafe { std::mem::zeroed::() }; - let result = unsafe { libc::stat(c_path.as_ptr(), &mut stat) }; - - if result == 0 { Some(stat) } else { None } -} - -fn direntry_to_descriptor(buffer: &mut DumpBuf, entry: &DirEntry) -> Option { - let handle = filename_to_fd(&entry.file_name())?; - let realpath = fs::read_link(entry.path()).ok()?; +fn descriptor_from_path( + process_inspector: &ProcessInspector, + buffer: &mut DumpBuf, + path: &Path, +) -> Option { + let handle = filename_to_fd(path.file_name().unwrap())?; + let realpath = process_inspector.read_link(path).ok()?; let path_rva = write_string_to_location(buffer, realpath.to_string_lossy().as_ref()).ok()?; - let stat = file_stat(&entry.path())?; + let stat = process_inspector.stat_file(path).ok()?; // TODO: We store the contents of `st_mode` into the `attributes` field, but // we could also store a human-readable string of the file type inside @@ -40,7 +34,7 @@ fn direntry_to_descriptor(buffer: &mut DumpBuf, entry: &DirEntry) -> Option Option { +fn filename_to_fd(filename: &OsStr) -> Option { let filename = filename.to_string_lossy(); filename.parse::().ok() } @@ -69,10 +63,13 @@ impl MinidumpWriter { buffer: &mut DumpBuf, ) -> Result { let proc_fd_path = PathBuf::from(format!("/proc/{}/fd", self.process_id)); - let proc_fd_iter = fs::read_dir(proc_fd_path)?; + let proc_fd_iter = self.process_inspector.read_dir(&proc_fd_path)?; let descriptors: Vec<_> = proc_fd_iter - .filter_map(|entry| entry.ok()) - .filter_map(|entry| direntry_to_descriptor(buffer, &entry)) + .filter_map(|filename| filename.ok()) + .filter_map(|filename| { + let path = proc_fd_path.join(filename); + descriptor_from_path(&self.process_inspector, buffer, &path) + }) .collect(); let number_of_descriptors = descriptors.len() as u32; diff --git a/src/linux/minidump_writer/mappings.rs b/src/linux/minidump_writer/mappings.rs index a31c69e2..d7f9d619 100644 --- a/src/linux/minidump_writer/mappings.rs +++ b/src/linux/minidump_writer/mappings.rs @@ -38,19 +38,13 @@ impl MinidumpWriter { // from the file. If there is no note segment with the build id in // the program headers, we can't get to the note section if the section header // table isn't loaded. - if let Some(path) = &self.mappings[map_idx].name { - let path = std::path::Path::new(&path); - if path.exists() { - log::debug!("failed to get build id from process memory ({e}), attempting to retrieve from {}", path.display()); - return module_reader::read_build_id_from_file(&self.process_inspector, path) - .map_err(errors::WriterError::ModuleReaderError); - } - log::debug!( - "not attempting to get build id from {}: path does not exist", - path.display() - ); - } - Err(e) + let Some(path) = &self.mappings[map_idx].name else { + return Err(e); + }; + + log::debug!("failed to get build id from process memory ({e}), attempting to retrieve from {}", path.display()); + + module_reader::read_build_id_from_file(&self.process_inspector, path.as_ref()).map_err(errors::WriterError::ModuleReaderError) }) .unwrap_or_else(|e| { log::warn!("failed to get build id for mapping: {e}"); diff --git a/src/linux/minidump_writer/memory_info_list_stream.rs b/src/linux/minidump_writer/memory_info_list_stream.rs index 2b4694a4..94cf1c69 100644 --- a/src/linux/minidump_writer/memory_info_list_stream.rs +++ b/src/linux/minidump_writer/memory_info_list_stream.rs @@ -8,6 +8,12 @@ use { pub enum SectionMemInfoListError { #[error("Failed to write to memory")] MemoryWriterError(#[from] MemoryWriterError), + #[error("failed to open /proc//maps file")] + ReadFileFailed( + #[source] + #[serde(serialize_with = "serialize_io_error")] + std::io::Error, + ), #[error("Failed to read from procfs")] ProcfsError( #[from] @@ -22,10 +28,12 @@ impl MinidumpWriter { &mut self, buffer: &mut DumpBuf, ) -> Result { - let maps = procfs_core::process::MemoryMaps::from_file(std::path::PathBuf::from(format!( - "/proc/{}/maps", - self.blamed_thread - )))?; + let path = format!("/proc/{}/maps", self.blamed_thread); + let reader = self + .process_inspector + .read_file(&path) + .map_err(SectionMemInfoListError::ReadFileFailed)?; + let maps = procfs_core::process::MemoryMaps::from_read(reader)?; let list_header = MemoryWriter::alloc_with_val( buffer, diff --git a/src/linux/minidump_writer/mod.rs b/src/linux/minidump_writer/mod.rs index 8206c4cb..fe426191 100644 --- a/src/linux/minidump_writer/mod.rs +++ b/src/linux/minidump_writer/mod.rs @@ -23,17 +23,12 @@ use { error_graph::{ErrorList, WriteErrorList}, errors::{ContinueProcessError, InitError, StopProcessError, WriterError}, failspot::failspot, - nix::{ - errno::Errno, - sys::{ptrace, signal, wait}, - }, procfs_core::{ FromRead, process::{MMPermissions, ProcState, Stat}, }, std::{ - io::{Seek, Write}, - path, + io::{Read, Seek, Write}, time::{Duration, Instant}, }, thiserror::Error, @@ -264,6 +259,7 @@ impl MinidumpWriter { // Even if we completely fail to fill in any additional Auxv info, we can still press // forward. if let Err(e) = self.auxv.try_filling_missing_info( + &self.process_inspector, self.process_id, soft_errors.subwriter(InitError::FillMissingAuxvInfoErrors), ) { @@ -363,6 +359,7 @@ impl MinidumpWriter { dir_section.write_to_file(buffer, Some(dirent))?; let dirent = systeminfo_stream::write( + &self.process_inspector, buffer, soft_errors.subwriter(WriterError::WriteSystemInfoErrors), )?; @@ -396,14 +393,14 @@ impl MinidumpWriter { let trunc = proc_root.len(); proc_root.push_str($fname); - file_entry!(res write_file(buffer, &proc_root), $kind, $err); + file_entry!(res write_file(&self.process_inspector, buffer, &proc_root), $kind, $err); proc_root.truncate(trunc); }; } file_entry!( - res write_file(buffer, "/proc/cpuinfo"), + res write_file(&self.process_inspector, buffer, "/proc/cpuinfo"), LinuxCpuInfo, WriteCpuInfoFailed ); @@ -414,8 +411,8 @@ impl MinidumpWriter { #[cfg(not(target_os = "android"))] { file_entry!( - res write_file(buffer, "/etc/lsb-release") - .or_else(|_| write_file(buffer, "/etc/os-release")), + res write_file(&self.process_inspector, buffer, "/etc/lsb-release") + .or_else(|_| write_file(&self.process_inspector, buffer, "/etc/os-release")), LinuxLsbRelease, WriteOsReleaseInfoFailed ); @@ -523,45 +520,10 @@ impl MinidumpWriter { } /// Suspends a thread by attaching to it. - fn suspend_thread( - _process_inspector: &ProcessInspector, - child: Pid, - ) -> Result<(), WriterError> { - use WriterError::PtraceAttachError as AttachErr; - - let pid = nix::unistd::Pid::from_raw(child); - // This may fail if the thread has just died or debugged. - ptrace::attach(pid).map_err(|e| AttachErr(child, e))?; - loop { - match wait::waitpid(pid, Some(wait::WaitPidFlag::__WALL)) { - Ok(status) => { - let wait::WaitStatus::Stopped(_, status) = status else { - return Err(WriterError::WaitPidError( - child, - nix::errno::Errno::UnknownErrno, - )); - }; - - // Any signal will stop the thread, make sure it is SIGSTOP. Otherwise, this - // signal will be delivered after PTRACE_DETACH, and the thread will enter - // the "T (stopped)" state. - if status == nix::sys::signal::SIGSTOP { - break; - } - - // Signals other than SIGSTOP that are received need to be reinjected, - // or they will otherwise get lost. - if let Err(err) = ptrace::cont(pid, status) { - return Err(WriterError::WaitPidError(child, err)); - } - } - Err(Errno::EINTR) => continue, - Err(e) => { - ptrace_detach(child)?; - return Err(WriterError::WaitPidError(child, e)); - } - } - } + fn suspend_thread(process_inspector: &ProcessInspector, tid: Pid) -> Result<(), WriterError> { + process_inspector + .suspend_thread(tid) + .map_err(WriterError::SuspendThreadFailed)?; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] { // On x86, the stack pointer is NULL or -1, when executing trusted code in @@ -572,7 +534,7 @@ impl MinidumpWriter { // We thus test the stack pointer and exclude any threads that are part of // the seccomp sandbox's trusted code. let skip_thread; - let regs = _process_inspector.get_gen_regs(pid.into()); + let regs = process_inspector.get_gen_regs(tid); if let Ok(regs) = regs { #[cfg(target_arch = "x86_64")] { @@ -586,16 +548,20 @@ impl MinidumpWriter { skip_thread = true; } if skip_thread { - ptrace_detach(child)?; - return Err(WriterError::DetachSkippedThread(child)); + process_inspector + .resume_thread(tid) + .map_err(WriterError::ResumeThreadFailed)?; + return Err(WriterError::DetachSkippedThread(tid)); } } Ok(()) } /// Resumes a thread by detaching from it. - fn resume_thread(child: Pid) -> Result<(), WriterError> { - ptrace_detach(child) + fn resume_thread(process_inspector: &ProcessInspector, tid: Pid) -> Result<(), WriterError> { + process_inspector + .resume_thread(tid) + .map_err(WriterError::ResumeThreadFailed) } fn suspend_threads(&mut self, mut soft_errors: impl WriteErrorList) { @@ -621,7 +587,7 @@ impl MinidumpWriter { fn resume_threads(&mut self, mut soft_errors: impl WriteErrorList) { if self.threads_suspended { for thread in &self.threads { - match Self::resume_thread(thread.tid) { + match Self::resume_thread(&self.process_inspector, thread.tid) { Ok(()) => (), Err(e) => { soft_errors.push(e); @@ -638,10 +604,7 @@ impl MinidumpWriter { fn stop_process(&mut self, timeout: Duration) -> Result<(), StopProcessError> { failspot!(StopProcess bail(nix::Error::EPERM)); - signal::kill( - nix::unistd::Pid::from_raw(self.process_id), - Some(signal::SIGSTOP), - )?; + self.process_inspector.stop_process()?; // Something like waitpid for non-child processes would be better, but we have no such // tool, so we poll the status. @@ -650,7 +613,11 @@ impl MinidumpWriter { let end = Instant::now() + timeout; loop { - if let Ok(ProcState::Stopped) = Stat::from_file(&proc_file)?.state() { + let stat_file = self + .process_inspector + .read_file(&proc_file) + .map_err(StopProcessError::ReadFileFailed)?; + if let Ok(ProcState::Stopped) = Stat::from_read(stat_file)?.state() { return Ok(()); } @@ -665,11 +632,9 @@ impl MinidumpWriter { /// /// Unlike `stop_process`, this function does not wait for the process to continue. fn continue_process(&mut self) -> Result<(), ContinueProcessError> { - signal::kill( - nix::unistd::Pid::from_raw(self.process_id), - Some(signal::SIGCONT), - )?; - Ok(()) + self.process_inspector + .continue_process() + .map_err(ContinueProcessError) } /// Parse /proc/$pid/task to list all the threads of the process identified by @@ -679,21 +644,20 @@ impl MinidumpWriter { mut soft_errors: impl WriteErrorList, ) -> Result<(), InitError> { let pid = self.process_id; - let filename = format!("/proc/{pid}/task"); - let task_path = path::PathBuf::from(&filename); - if !task_path.is_dir() { - return Err(InitError::ProcPidTaskNotDirectory(filename)); - } + let task_path = format!("/proc/{pid}/task"); - for entry in std::fs::read_dir(task_path).map_err(|e| InitError::IOError(filename, e))? { - let entry = match entry { - Ok(entry) => entry, + for file_name in self + .process_inspector + .read_dir(&task_path) + .map_err(InitError::ReadProcTaskFailed)? + { + let file_name = match file_name { + Ok(file_name) => file_name, Err(e) => { soft_errors.push(InitError::ReadProcessThreadEntryFailed(e)); continue; } }; - let file_name = entry.file_name(); let tid = match file_name.to_str().and_then(|name| name.parse::().ok()) { Some(tid) => tid, None => { @@ -708,7 +672,13 @@ impl MinidumpWriter { "testing requested failure reading thread name", )) } else { - std::fs::read_to_string(format!("/proc/{pid}/task/{tid}/comm")) + self.process_inspector + .read_file(format!("/proc/{pid}/task/{tid}/comm")) + .and_then(|mut file| { + let mut s = String::new(); + file.read_to_string(&mut s)?; + Ok(s) + }) }); let name = match name_result { @@ -1007,27 +977,16 @@ impl Drop for MinidumpWriter { } } -/// PTRACE_DETACH the given pid. -/// -/// This handles special errno cases (ESRCH) which we won't consider errors. -fn ptrace_detach(child: Pid) -> Result<(), WriterError> { - let pid = nix::unistd::Pid::from_raw(child); - ptrace::detach(pid, None).or_else(|e| { - // errno is set to ESRCH if the pid no longer exists, but we don't want to error in that - // case. - if e == nix::Error::ESRCH { - Ok(()) - } else { - Err(WriterError::PtraceDetachError(child, e)) - } - }) -} - fn write_file( + process_inspector: &ProcessInspector, buffer: &mut DumpBuf, filename: &str, ) -> std::result::Result { - let content = std::fs::read(filename)?; + let content = process_inspector.read_file(filename).and_then(|mut file| { + let mut v = Vec::new(); + file.read_to_end(&mut v)?; + Ok(v) + })?; let section = MemoryArrayWriter::write_bytes(buffer, &content); Ok(section.location()) diff --git a/src/linux/minidump_writer/systeminfo_stream.rs b/src/linux/minidump_writer/systeminfo_stream.rs index 4e077701..52cbc768 100644 --- a/src/linux/minidump_writer/systeminfo_stream.rs +++ b/src/linux/minidump_writer/systeminfo_stream.rs @@ -1,5 +1,5 @@ use { - super::{super::dumper_cpu_info as dci, *}, + super::{super::dumper_cpu_info as dci, ProcessInspector, *}, error_graph::WriteErrorList, }; @@ -14,6 +14,7 @@ pub enum SectionSystemInfoError { } pub fn write( + process_inspector: &ProcessInspector, buffer: &mut DumpBuf, mut soft_errors: impl WriteErrorList, ) -> Result { @@ -31,7 +32,7 @@ pub fn write( info.platform_id = platform_id as u32; info.csd_version_rva = os_version_loc.rva; - if let Err(e) = dci::write_cpu_information(&mut info) { + if let Err(e) = dci::write_cpu_information(process_inspector, &mut info) { soft_errors.push(SectionSystemInfoError::WriteCpuInformationFailed(e)); } diff --git a/src/linux/process_inspection/mod.rs b/src/linux/process_inspection/mod.rs index a190a6c7..1099fc4c 100644 --- a/src/linux/process_inspection/mod.rs +++ b/src/linux/process_inspection/mod.rs @@ -2,17 +2,24 @@ use { super::{ Pid, maps_reader, module_reader::{ModuleReaderError, ReadModuleMemory}, - serializers, + serializers::{self, *}, }, + crate::serializers::*, core::{ffi::c_void, mem}, module_reader::MappedModuleMemoryReader, - nix::errno::Errno, + nix::{ + errno::Errno, + sys::{ptrace, signal, wait}, + unistd::Pid as NixPid, + }, process_reader::ProcessReader, regs::*, std::{ - fs::File, + ffi::{CString, OsString}, + fs::{self, File}, io::{self, Read}, - path::Path, + os::unix::ffi::OsStringExt, + path::{Path, PathBuf}, }, }; @@ -45,6 +52,51 @@ impl ProcessInspector { &self.process_reader } + pub fn stop_process(&self) -> Result<(), Errno> { + signal::kill(NixPid::from_raw(self.pid), Some(signal::SIGSTOP)) + } + + pub fn continue_process(&self) -> Result<(), Errno> { + signal::kill(NixPid::from_raw(self.pid), Some(signal::SIGCONT)) + } + + pub fn suspend_thread(&self, tid: libc::pid_t) -> Result<(), SuspendResumeThreadError> { + let tid = NixPid::from_raw(tid); + ptrace::attach(tid).map_err(SuspendResumeThreadError::PtraceAttachFailed)?; + loop { + match wait::waitpid(tid, Some(wait::WaitPidFlag::__WALL)) { + Ok(status) => { + let wait::WaitStatus::Stopped(_, signal) = status else { + return Err(SuspendResumeThreadError::UnexpectedStatus(status)); + }; + + // Any signal will stop the thread, make sure it is SIGSTOP. Otherwise, this + // signal will be delivered after PTRACE_DETACH, and the thread will enter + // the "T (stopped)" state. + if signal == signal::SIGSTOP { + break; + } + + // Signals other than SIGSTOP that are received need to be reinjected, + // or they will otherwise get lost. + ptrace::cont(tid, signal) + .map_err(|e| SuspendResumeThreadError::ReinjectFailed(e, signal))?; + } + Err(Errno::EINTR) => (), + Err(e) => { + ptrace_detach(tid).map_err(SuspendResumeThreadError::PtraceDetachFailed)?; + return Err(SuspendResumeThreadError::WaitPidFailed(e)); + } + } + } + Ok(()) + } + + pub fn resume_thread(&self, tid: libc::pid_t) -> Result<(), SuspendResumeThreadError> { + let tid = NixPid::from_raw(tid); + ptrace_detach(tid).map_err(SuspendResumeThreadError::PtraceDetachFailed) + } + pub fn read_memory_mapped_module( &self, path: impl AsRef, @@ -53,8 +105,31 @@ impl ProcessInspector { MappedModuleMemoryReader::new(path.as_ref(), offset) } - pub fn read_file(&self, path: impl AsRef) -> io::Result { - File::open(path) + pub fn stat_file(&self, path: impl Into) -> io::Result { + let c_path = CString::new(path.into().into_os_string().into_vec()).unwrap(); + + let mut output = unsafe { mem::zeroed::() }; + let rv = unsafe { libc::stat(c_path.as_ptr(), &mut output) }; + if rv == -1 { + return Err(io::Error::last_os_error()); + } + Ok(output) + } + + pub fn read_file(&self, path: impl AsRef) -> io::Result { + File::open(path).map(FileReader) + } + + pub fn read_dir(&self, path: impl AsRef) -> io::Result { + fs::read_dir(path).map(DirReader) + } + + pub fn read_link(&self, path: impl AsRef) -> io::Result { + fs::read_link(path) + } + + pub fn path_exists(&self, path: impl AsRef) -> bool { + path.as_ref().exists() } pub fn get_gen_regs(&self, tid: libc::pid_t) -> nix::Result { @@ -90,6 +165,58 @@ impl ProcessInspector { } } +#[derive(Debug)] +pub struct FileReader(File); + +impl Read for FileReader { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + self.0.read(buf) + } +} + +#[derive(Debug)] +pub struct DirReader(fs::ReadDir); + +impl Iterator for DirReader { + type Item = io::Result; + fn next(&mut self) -> Option { + self.0 + .next() + .map(|result| result.map(|entry| entry.file_name())) + } +} + +#[derive(Debug, thiserror::Error, serde::Serialize)] +pub enum SuspendResumeThreadError { + #[error("failed to attach to process")] + PtraceAttachFailed( + #[source] + #[serde(serialize_with = "serialize_nix_error")] + Errno, + ), + #[error("failed to detach from process")] + PtraceDetachFailed( + #[source] + #[serde(serialize_with = "serialize_nix_error")] + Errno, + ), + #[error("received an unexpected status: {0:?}")] + UnexpectedStatus(#[serde(serialize_with = "serialize_debug_string")] wait::WaitStatus), + #[error("failed to reinject irrelevant signal: {1:?}")] + ReinjectFailed( + #[source] + #[serde(serialize_with = "serialize_nix_error")] + Errno, + #[serde(serialize_with = "serialize_debug_string")] signal::Signal, + ), + #[error("failed waiting for process state to change")] + WaitPidFailed( + #[source] + #[serde(serialize_with = "serialize_nix_error")] + Errno, + ), +} + fn getregset(_pid: libc::pid_t) -> nix::Result { #[cfg(target_arch = "arm")] { @@ -166,6 +293,18 @@ fn ptrace_getregset(regset_type: usize, pid: libc::pid_t) -> nix::Result { Ok(unsafe { output.assume_init() }) } +fn ptrace_detach(tid: NixPid) -> Result<(), Errno> { + ptrace::detach(tid, None).or_else(|e| { + // errno is set to ESRCH if the pid no longer exists, but we don't want to error in that + // case. + if e == nix::Error::ESRCH { + Ok(()) + } else { + Err(e) + } + }) +} + #[doc(hidden)] impl ProcessInspector { pub fn force_pr_reset(&mut self) { diff --git a/src/serializers.rs b/src/serializers.rs index a42313da..2d1b592a 100644 --- a/src/serializers.rs +++ b/src/serializers.rs @@ -2,6 +2,17 @@ //! [Serialize] for) use serde::Serializer; + +/// Good for types where the value of the thing doesn't have any programmatic use and +/// it mostly just matters than a human can read it +pub fn serialize_debug_string( + d: &D, + serializer: S, +) -> Result { + let dbg = format!("{d:#?}"); + serializer.serialize_str(&dbg) +} + /// Useful for types that implement [Error][std::error::Error] and don't need any special /// treatment. pub fn serialize_generic_error( @@ -11,8 +22,7 @@ pub fn serialize_generic_error( // I guess we'll have to see if it's more useful to store the debug representation of a // foreign error type or something else (like maybe iterating its error chain into a // list?) - let dbg = format!("{error:#?}"); - serializer.serialize_str(&dbg) + serialize_debug_string(error, serializer) } /// Serialize [std::io::Error] pub fn serialize_io_error(