From 6747c8886fa6d84c2f21c464bae1c905b7bced6b Mon Sep 17 00:00:00 2001 From: Chris Martin Date: Mon, 1 Jun 2026 11:50:20 -0400 Subject: [PATCH 1/2] Isolated Processes 6: Add a basic libc backend --- Cargo.lock | 11 +++ Cargo.toml | 38 +++++++--- crates/linux/process-backend/Cargo.toml | 17 +++++ crates/linux/process-backend/src/lib.rs | 8 +++ .../linux/process-backend/src/local/error.rs | 51 +++++++++++++ crates/linux/process-backend/src/local/mod.rs | 72 +++++++++++++++++++ .../src/local/syscall_runner.rs | 46 ++++++++++++ src/linux/minidump_writer/errors.rs | 9 +-- src/linux/minidump_writer/mod.rs | 6 +- src/linux/process_inspection/mod.rs | 34 +++++++-- tests/linux_minidump_writer_soft_error.rs | 2 +- 11 files changed, 270 insertions(+), 24 deletions(-) create mode 100644 crates/linux/process-backend/Cargo.toml create mode 100644 crates/linux/process-backend/src/lib.rs create mode 100644 crates/linux/process-backend/src/local/error.rs create mode 100644 crates/linux/process-backend/src/local/mod.rs create mode 100644 crates/linux/process-backend/src/local/syscall_runner.rs diff --git a/Cargo.lock b/Cargo.lock index 55f6b343..1ea525c3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1376,6 +1376,7 @@ dependencies = [ "minidump-common", "minidump-unwind", "nix", + "process-backend", "procfs-core 0.18.0", "scroll 0.12.0", "serde", @@ -1687,6 +1688,16 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "process-backend" +version = "0.0.0" +dependencies = [ + "libc", + "log", + "serde", + "thiserror 2.0.18", +] + [[package]] name = "procfs-core" version = "0.17.0" diff --git a/Cargo.toml b/Cargo.toml index 88ccd48d..b9a9af08 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,14 +1,33 @@ -[package] -name = "minidump-writer" -version = "0.12.0" -authors = ["Martin Sirringhaus"] -description = "Rust rewrite of Breakpad's minidump_writer" +[workspace] +resolver = "3" +members = [ + "crates/linux/process-backend", +] + +[workspace.package] repository = "https://github.com/rust-minidump/minidump-writer" homepage = "https://github.com/rust-minidump/minidump-writer" edition = "2024" rust-version = "1.88.0" license = "MIT" +[workspace.dependencies] +libc = "0.2" +log = "0.4" +serde = { version = "1.0.208", default-features = false, features = ["derive"] } +thiserror = "2.0" + +[package] +name = "minidump-writer" +version = "0.12.0" +authors = ["Martin Sirringhaus"] +description = "Rust rewrite of Breakpad's minidump_writer" +repository.workspace = true +homepage.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true + [dependencies] bitflags = "2.10" cfg-if = "1.0" @@ -17,17 +36,17 @@ error-graph = { version = "0.1.1", features = ["serde"] } failspot = "0.2.0" # goblin >= 0.10 uses scroll 0.13 goblin = "0.9.2" -log = "0.4" +log.workspace = true # Type definitions and utilities for working with the Minidump format minidump-common = "0.26" # Reading and writing to memory. Keep aligned with version used by minidump-common and goblin scroll = "0.12" -serde = { version = "1.0.208", features = ["derive"] } +serde.workspace = true serde_json = "1.0.116" -thiserror = "2.0" +thiserror.workspace = true [target.'cfg(unix)'.dependencies] -libc = "0.2" +libc.workspace = true memmap2 = "0.9" [target.'cfg(any(target_os = "linux", target_os = "android"))'.dependencies] @@ -42,6 +61,7 @@ nix = { version = "0.30", default-features = false, features = [ "uio", "user", ] } +process-backend = { path = "crates/linux/process-backend", features = ["testing"] } # Used for parsing procfs info. # default-features is disabled since it pulls in chrono procfs-core = { version = "0.18", default-features = false, features = ["serde1"] } diff --git a/crates/linux/process-backend/Cargo.toml b/crates/linux/process-backend/Cargo.toml new file mode 100644 index 00000000..ba920c99 --- /dev/null +++ b/crates/linux/process-backend/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "process-backend" +version = "0.0.0" +repository.workspace = true +homepage.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true + +[dependencies] +libc.workspace = true +log.workspace = true +serde.workspace = true +thiserror.workspace = true + +[features] +testing = [] diff --git a/crates/linux/process-backend/src/lib.rs b/crates/linux/process-backend/src/lib.rs new file mode 100644 index 00000000..df95a2e5 --- /dev/null +++ b/crates/linux/process-backend/src/lib.rs @@ -0,0 +1,8 @@ +#![no_std] +#![cfg(any(target_os = "linux", target_os = "android"))] + +pub mod local; + +/// This is the longest path length we guarantee we can handle, since we won't be able to allocate +/// in the fork of the crashed process. We can increase if necessary. +pub const MAX_PATH_LEN: usize = 256; diff --git a/crates/linux/process-backend/src/local/error.rs b/crates/linux/process-backend/src/local/error.rs new file mode 100644 index 00000000..c01c682b --- /dev/null +++ b/crates/linux/process-backend/src/local/error.rs @@ -0,0 +1,51 @@ +use core::ffi::c_int; + +#[derive(Debug, thiserror::Error, serde::Serialize, serde::Deserialize)] +pub enum Error { + #[error("failed to send SIGSTOP to process: {0}")] + SigStopFailed(c_int), + #[error("failed to send SIGCONT to process: {0}")] + SigContFailed(c_int), + #[error("failed to attach to process: {0}")] + PtraceAttachFailed(c_int), + #[error("failed to detach from process: {0}")] + PtraceDetachFailed(c_int), + #[error("failed to peek at process user section: {0}")] + PtracePeekUserFailed(c_int), + #[error("failed waiting for thread to stop: {0}")] + WaitPidFailed(c_int), + #[error("unexpected status returned waiting for thread to stop: {0}")] + UnexpectedStatus(c_int), + #[error("failed to reinject signal {0} into stopped process: {1}")] + ReinjectFailed(c_int, c_int), + #[error("failed to stat file: {0}")] + StatFailed(c_int), + #[error("failed to open file: {0}")] + OpenFileFailed(c_int), + #[error("failed to read file: {0}")] + ReadFileFailed(c_int), + #[error("failed to open directory: {0}")] + OpenDirFailed(c_int), + #[error("I/O error reading directory: {0}")] + ReadDirFailed(c_int), + #[error("failed to read link: {0}")] + ReadLinkFailed(c_int), + #[error("buffer too small")] + BufferTooSmall, + #[error("not supported")] + NotSupported, + #[error("failed to get registers: {0}")] + GetRegistersFailed(c_int), + #[error("failed to map memory: {0}")] + MMapfailed(c_int), + #[error("the start position of a mapping is past its end position")] + StartPositionPastEnd, + #[error("mapping too large to fit in memory")] + MappingTooLarge, + #[error("index was out of bounds")] + IndexOutOfBounds, + #[error("process_vm_readv failed: {0}")] + ProcessVmReadvFailed(c_int), + #[error("failed to peek at process data: {0}")] + PtracePeekDataFailed(c_int), +} diff --git a/crates/linux/process-backend/src/local/mod.rs b/crates/linux/process-backend/src/local/mod.rs new file mode 100644 index 00000000..59417eb2 --- /dev/null +++ b/crates/linux/process-backend/src/local/mod.rs @@ -0,0 +1,72 @@ +use { + core::{cell::RefCell, ffi::c_int}, + libc::pid_t, + syscall_runner::SyscallRunner, +}; + +pub use error::Error; + +mod error; +mod syscall_runner; + +#[derive(Debug)] +pub struct Backend { + pid: pid_t, + syscall_runner: RefCell, +} + +impl Backend { + pub fn new(pid: libc::pid_t) -> Self { + Self { + pid, + syscall_runner: Default::default(), + } + } + pub fn stop_process(&self) -> Result<(), Error> { + self.standard_syscall(|| unsafe { libc::kill(self.pid, libc::SIGSTOP) }) + .map_err(Error::SigStopFailed)?; + Ok(()) + } + + pub fn continue_process(&self) -> Result<(), Error> { + self.standard_syscall(|| unsafe { libc::kill(self.pid, libc::SIGCONT) }) + .map_err(Error::SigContFailed)?; + Ok(()) + } + + pub fn standard_syscall(&self, f: F) -> Result + where + F: FnOnce() -> T, + T: From + core::cmp::PartialEq, + { + self.syscall_runner.borrow_mut().standard(f) + } + + pub fn special_syscall(&self, f: F) -> Result + where + F: FnOnce() -> Result, + { + self.syscall_runner.borrow_mut().run(f) + } + + #[cfg(feature = "testing")] + pub fn fail_one_syscall_with(&self, errno: c_int) { + self.syscall_runner + .borrow_mut() + .fail_one_syscall_with(errno); + } +} + +fn errno() -> c_int { + unsafe { *errno_location() } +} + +#[cfg(target_os = "android")] +fn errno_location() -> *mut c_int { + unsafe { libc::__errno() } +} + +#[cfg(not(target_os = "android"))] +fn errno_location() -> *mut c_int { + unsafe { libc::__errno_location() } +} diff --git a/crates/linux/process-backend/src/local/syscall_runner.rs b/crates/linux/process-backend/src/local/syscall_runner.rs new file mode 100644 index 00000000..da312018 --- /dev/null +++ b/crates/linux/process-backend/src/local/syscall_runner.rs @@ -0,0 +1,46 @@ +use {super::errno, core::ffi::c_int}; + +#[derive(Debug, Default)] +pub struct SyscallRunner(Option); + +impl SyscallRunner { + /// Helper function to run a syscall and capture errno if it fails + /// + /// The given function's job is simply to run the syscall and determine whether the return + /// value was success (`Ok(t)`) or failure (`Err(())`). On success, this wrapper will return + /// whatever value the inner function returned. On failure, it will return `Err(errno())`. + /// + /// If testing requests a failure, will never actually make the syscall and just returns + /// the errno requested by testing + pub fn run(&mut self, f: F) -> Result + where + F: FnOnce() -> Result, + { + if let Some(errno) = self.0.take() { + Err(errno) + } else { + f().map_err(|()| errno()) + } + } + + /// Ergonomics for `run` for the standard case where `-1` indicates the syscall failed + pub fn standard(&mut self, f: F) -> Result + where + F: FnOnce() -> T, + T: From + core::cmp::PartialEq, + { + self.run(|| { + let rv = f(); + if rv == T::from(-1) { + return Err(()); + } + Ok(rv) + }) + } + + /// Force the next syscall to fail with the given errno + #[cfg(feature = "testing")] + pub fn fail_one_syscall_with(&mut self, errno: c_int) { + self.0 = Some(errno); + } +} diff --git a/src/linux/minidump_writer/errors.rs b/src/linux/minidump_writer/errors.rs index 2c44f24a..78df0fa4 100644 --- a/src/linux/minidump_writer/errors.rs +++ b/src/linux/minidump_writer/errors.rs @@ -18,7 +18,6 @@ use { }, crate::{dir_section::FileWriterError, mem_writer::MemoryWriterError, serializers::*}, error_graph::ErrorList, - nix::errno::Errno, procfs_core::ProcError, std::ffi::OsString, thiserror::Error, @@ -219,11 +218,7 @@ pub enum InitError { #[derive(Debug, thiserror::Error, serde::Serialize)] pub enum StopProcessError { #[error("Failed to stop the process")] - Stop( - #[from] - #[serde(serialize_with = "serialize_nix_error")] - nix::Error, - ), + Stop(#[source] process_inspection::Error), #[error("failed to open process file")] ReadFileFailed( #[source] @@ -242,4 +237,4 @@ pub enum StopProcessError { #[derive(Debug, thiserror::Error)] #[error("Failed to continue the process")] -pub struct ContinueProcessError(#[source] pub Errno); +pub struct ContinueProcessError(#[source] pub process_inspection::Error); diff --git a/src/linux/minidump_writer/mod.rs b/src/linux/minidump_writer/mod.rs index fe426191..64ad7b4c 100644 --- a/src/linux/minidump_writer/mod.rs +++ b/src/linux/minidump_writer/mod.rs @@ -602,9 +602,9 @@ impl MinidumpWriter { /// /// This will block waiting for the process to stop until `timeout` has passed. fn stop_process(&mut self, timeout: Duration) -> Result<(), StopProcessError> { - failspot!(StopProcess bail(nix::Error::EPERM)); - - self.process_inspector.stop_process()?; + self.process_inspector + .stop_process() + .map_err(StopProcessError::Stop)?; // Something like waitpid for non-child processes would be better, but we have no such // tool, so we poll the status. diff --git a/src/linux/process_inspection/mod.rs b/src/linux/process_inspection/mod.rs index 1099fc4c..a843f373 100644 --- a/src/linux/process_inspection/mod.rs +++ b/src/linux/process_inspection/mod.rs @@ -6,12 +6,14 @@ use { }, crate::serializers::*, core::{ffi::c_void, mem}, + failspot::failspot, module_reader::MappedModuleMemoryReader, nix::{ errno::Errno, sys::{ptrace, signal, wait}, unistd::Pid as NixPid, }, + process_backend::local, process_reader::ProcessReader, regs::*, std::{ @@ -20,6 +22,7 @@ use { io::{self, Read}, os::unix::ffi::OsStringExt, path::{Path, PathBuf}, + rc::Rc, }, }; @@ -38,13 +41,22 @@ type PtraceRequestType = core::ffi::c_int; pub struct ProcessInspector { pid: libc::pid_t, process_reader: ProcessReader, + backend: Rc, +} + +#[derive(Debug)] +pub enum Backend { + Local(local::Backend), } impl ProcessInspector { pub fn local(pid: libc::pid_t) -> Self { + let backend = Rc::new(Backend::Local(local::Backend::new(pid))); + ProcessInspector { pid, process_reader: ProcessReader::new(pid), + backend, } } @@ -52,12 +64,20 @@ impl ProcessInspector { &self.process_reader } - pub fn stop_process(&self) -> Result<(), Errno> { - signal::kill(NixPid::from_raw(self.pid), Some(signal::SIGSTOP)) + pub fn stop_process(&self) -> Result<(), Error> { + failspot!(if StopProcess { + return Err(Error::Local(local::Error::SigStopFailed(libc::EPERM))); + }); + + match &*self.backend { + Backend::Local(l) => l.stop_process().map_err(Error::Local), + } } - pub fn continue_process(&self) -> Result<(), Errno> { - signal::kill(NixPid::from_raw(self.pid), Some(signal::SIGCONT)) + pub fn continue_process(&self) -> Result<(), Error> { + match &*self.backend { + Backend::Local(l) => l.continue_process().map_err(Error::Local), + } } pub fn suspend_thread(&self, tid: libc::pid_t) -> Result<(), SuspendResumeThreadError> { @@ -321,3 +341,9 @@ impl ProcessInspector { self.process_reader = ProcessReader::for_ptrace(self.pid); } } + +#[derive(Debug, thiserror::Error, serde::Serialize)] +pub enum Error { + #[error("an error occurred running a syscall directly")] + Local(#[source] local::Error), +} diff --git a/tests/linux_minidump_writer_soft_error.rs b/tests/linux_minidump_writer_soft_error.rs index ba5ca76b..4a536383 100644 --- a/tests/linux_minidump_writer_soft_error.rs +++ b/tests/linux_minidump_writer_soft_error.rs @@ -38,7 +38,7 @@ fn soft_error_stream() { fn soft_error_stream_content() { let expected_errors = vec![ json!({"InitErrors": [ - {"StopProcessFailed": {"Stop": "EPERM"}}, + {"StopProcessFailed": {"Stop": { "Local": { "SigStopFailed": 1 }}}}, {"FillMissingAuxvInfoErrors": ["InvalidFormat"]}, {"EnumerateThreadsErrors": [ {"ReadThreadNameFailed": "\ From 4b605a199e03cb17f635b9897f6559947909452e Mon Sep 17 00:00:00 2001 From: Chris Martin Date: Thu, 4 Jun 2026 09:54:43 -0400 Subject: [PATCH 2/2] Address review feedback from Gabriele --- crates/linux/process-backend/src/local/mod.rs | 14 +++++++------- .../{syscall_runner.rs => syscall_invoker.rs} | 16 ++++++++-------- 2 files changed, 15 insertions(+), 15 deletions(-) rename crates/linux/process-backend/src/local/{syscall_runner.rs => syscall_invoker.rs} (67%) diff --git a/crates/linux/process-backend/src/local/mod.rs b/crates/linux/process-backend/src/local/mod.rs index 59417eb2..84fdc907 100644 --- a/crates/linux/process-backend/src/local/mod.rs +++ b/crates/linux/process-backend/src/local/mod.rs @@ -1,25 +1,25 @@ use { core::{cell::RefCell, ffi::c_int}, libc::pid_t, - syscall_runner::SyscallRunner, + syscall_invoker::SyscallInvoker, }; pub use error::Error; mod error; -mod syscall_runner; +mod syscall_invoker; #[derive(Debug)] pub struct Backend { pid: pid_t, - syscall_runner: RefCell, + syscall_invoker: RefCell, } impl Backend { pub fn new(pid: libc::pid_t) -> Self { Self { pid, - syscall_runner: Default::default(), + syscall_invoker: Default::default(), } } pub fn stop_process(&self) -> Result<(), Error> { @@ -39,19 +39,19 @@ impl Backend { F: FnOnce() -> T, T: From + core::cmp::PartialEq, { - self.syscall_runner.borrow_mut().standard(f) + self.syscall_invoker.borrow_mut().invoke_standard(f) } pub fn special_syscall(&self, f: F) -> Result where F: FnOnce() -> Result, { - self.syscall_runner.borrow_mut().run(f) + self.syscall_invoker.borrow_mut().invoke(f) } #[cfg(feature = "testing")] pub fn fail_one_syscall_with(&self, errno: c_int) { - self.syscall_runner + self.syscall_invoker .borrow_mut() .fail_one_syscall_with(errno); } diff --git a/crates/linux/process-backend/src/local/syscall_runner.rs b/crates/linux/process-backend/src/local/syscall_invoker.rs similarity index 67% rename from crates/linux/process-backend/src/local/syscall_runner.rs rename to crates/linux/process-backend/src/local/syscall_invoker.rs index da312018..f0e7596f 100644 --- a/crates/linux/process-backend/src/local/syscall_runner.rs +++ b/crates/linux/process-backend/src/local/syscall_invoker.rs @@ -1,18 +1,18 @@ use {super::errno, core::ffi::c_int}; #[derive(Debug, Default)] -pub struct SyscallRunner(Option); +pub struct SyscallInvoker(Option); -impl SyscallRunner { - /// Helper function to run a syscall and capture errno if it fails +impl SyscallInvoker { + /// Helper function to invoke a syscall and capture errno if it fails /// - /// The given function's job is simply to run the syscall and determine whether the return + /// The given function's job is simply to invoke the syscall and determine whether the return /// value was success (`Ok(t)`) or failure (`Err(())`). On success, this wrapper will return /// whatever value the inner function returned. On failure, it will return `Err(errno())`. /// /// If testing requests a failure, will never actually make the syscall and just returns /// the errno requested by testing - pub fn run(&mut self, f: F) -> Result + pub fn invoke(&mut self, f: F) -> Result where F: FnOnce() -> Result, { @@ -23,13 +23,13 @@ impl SyscallRunner { } } - /// Ergonomics for `run` for the standard case where `-1` indicates the syscall failed - pub fn standard(&mut self, f: F) -> Result + /// Ergonomics for `invoke` for the standard case where `-1` indicates the syscall failed + pub fn invoke_standard(&mut self, f: F) -> Result where F: FnOnce() -> T, T: From + core::cmp::PartialEq, { - self.run(|| { + self.invoke(|| { let rv = f(); if rv == T::from(-1) { return Err(());