8000 Isolated Processes 6: Add a basic libc backend by marti4d · Pull Request #184 · rust-minidump/minidump-writer · GitHub
[go: up one dir, main page]

Skip to content
8000
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

38 changes: 29 additions & 9 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
Comment thread
gabrielesvelto marked this conversation as resolved.
Resolved
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"
Expand All @@ -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]
Expand All @@ -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"] }
Expand Down
17 changes: 17 additions & 0 deletions crates/linux/process-backend/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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 = []
8 changes: 8 additions & 0 deletions crates/linux/process-backend/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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;
51 changes: 51 additions & 0 deletions crates/linux/process-backend/src/local/error.rs
Original file line number Diff line number Diff line change
@@ -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),
}
72 changes: 72 additions & 0 deletions crates/linux/process-backend/src/local/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
use {
core::{cell::RefCell, ffi::c_int},
libc::pid_t,
syscall_invoker::SyscallInvoker,
};

pub use error::Error;

mod error;
mod syscall_invoker;

#[derive(Debug)]
pub struct Backend {
pid: pid_t,
syscall_invoker: RefCell<SyscallInvoker>,
}

impl Backend {
pub fn new(pid: libc::pid_t) -> Self {
Self {
pid,
syscall_invoker: Default::default(),
}
}
pub fn stop_process(&self) -> Result<(), Error> {
8 se 10BC8 lf.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<T, F>(&self, f: F) -> Result<T, c_int>
where
F: FnOnce() -> T,
T: From<i8> + core::cmp::PartialEq,
{
self.syscall_invoker.borrow_mut().invoke_standard(f)
}

pub fn special_syscall<T, F>(&self, f: F) -> Result<T, c_int>
where
F: FnOnce() -> Result<T, ()>,
{
self.syscall_invoker.borrow_mut().invoke(f)
}

#[cfg(feature = "testing")]
pub fn fail_one_syscall_with(&self, errno: c_int) {
self.syscall_invoker
.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() }
}
46 changes: 46 additions & 0 deletions crates/linux/process-backend/src/local/syscall_invoker.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
use {super::errno, core::ffi::c_int};

#[derive(Debug, Default)]
pub struct SyscallInvoker(Option<c_int>);

impl SyscallInvoker {
/// Helper function to invoke a syscall and capture errno if it fails
///
/// 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 invoke<T, F>(&mut self, f: F) -> Result<T, c_int>
where
F: FnOnce() -> Result<T, ()>,
{
if let Some(errno) = self.0.take() {
Err(errno)
} else {
f().map_err(|()| errno())
}
}

/// Ergonomics for `invoke` for the standard case where `-1` indicates the syscall failed
pub fn invoke_standard<T, F>(&mut self, f: F) -> Result<T, c_int>
where
F: FnOnce() -> T,
T: From<i8> + core::cmp::PartialEq,
{
self.invoke(|| {
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);
}
}
9 changes: 2 additions & 7 deletions src/linux/minidump_writer/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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]
Expand All @@ -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);
6 changes: 3 additions & 3 deletions src/linux/minidump_writer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading
Loading
0