8000 Isolated Processes 19: Integrate remote backend by marti4d · Pull Request #219 · 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
91 changes: 74 additions & 17 deletions src/bin/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,13 @@ pub type Result<T> = std::result::Result<T, Error>;

#[cfg(any(target_os = "linux", target_os = "android"))]
mod linux {
use {
super::*,
error_graph::ErrorList,
minidump_writer::{
LINUX_GATE_LIBRARY_NAME, ProcessReaderKind,
minidump_writer::{MinidumpWriter, MinidumpWriterConfig},
},
std::ptr,
};
use super::*;
use error_graph::ErrorList;
use minidump_writer::Pid;
use minidump_writer::minidump_writer::{MinidumpWriter, MinidumpWriterConfig};
use minidump_writer::remote_process_inspection as remote;
use minidump_writer::{LINUX_GATE_LIBRARY_NAME, ProcessReaderKind};
use std::{ptr, thread};

macro_rules! test {
($x:expr, $errmsg:expr) => {
Expand All @@ -41,7 +39,7 @@ mod linux {
let ppid = getppid();
fail_on_soft_error!(
soft_errors,
MinidumpWriterConfig::new(ppid, ppid).build_for_testing(&mut soft_errors)?
remote_mw_config(ppid, ppid).build_for_testing(&mut soft_errors)?
);
Ok(())
}
Expand All @@ -50,7 +48,7 @@ mod linux {
let ppid = getppid();
let dumper = fail_on_soft_error!(
soft_errors,
MinidumpWriterConfig::new(ppid, ppid).build_for_testing(&mut soft_errors)?
remote_mw_config(ppid, ppid).build_for_testing(&mut soft_errors)?
);
test!(!dumper.threads.is_empty(), "No threads");
test!(
Expand All @@ -75,7 +73,7 @@ mod linux {
let ppid = getppid();
let mut dumper = fail_on_soft_error!(
soft_errors,
MinidumpWriterConfig::new(ppid, ppid).build_for_testing(&mut soft_errors)?
remote_mw_config(ppid, ppid).build_for_testing(&mut soft_errors)?
);

// We support 3 different methods of reading memory from another
Expand Down Expand Up @@ -158,7 +156,7 @@ mod linux {

let dumper = fail_on_soft_error!(
soft_errors,
MinidumpWriterConfig::new(ppid, ppid).build_for_testing(&mut soft_errors)?
remote_mw_config(ppid, ppid).build_for_testing(&mut soft_errors)?
);
dumper
.find_mapping(addr1)
Expand All @@ -179,7 +177,7 @@ mod linux {

let dumper = fail_on_soft_error!(
soft_errors,
MinidumpWriterConfig::new(ppid, ppid).build_for_testing(&mut soft_errors)?
remote_mw_config(ppid, ppid).build_for_testing(&mut soft_errors)?
);

let exe_mapping = dumper
Expand All @@ -200,7 +198,7 @@ mod linux {
// Now check that PtraceDumper interpreted the mappings properly.
let dumper = fail_on_soft_error!(
soft_errors,
MinidumpWriterConfig::new(getppid(), getppid()).build_for_testing(&mut soft_errors)?
remote_mw_config(getppid(), getppid()).build_for_testing(&mut soft_errors)?
);
let mut mapping_count = 0;
for map in &dumper.mappings {
Expand All @@ -225,7 +223,7 @@ mod linux {
let ppid = getppid();
let dumper = fail_on_soft_error!(
soft_errors,
MinidumpWriterConfig::new(ppid, ppid).build_for_testing(&mut soft_errors)?
remote_mw_config(ppid, ppid).build_for_testing(&mut soft_errors)?
);
let linux_gate = dumper
.mappings
Expand All @@ -243,7 +241,7 @@ mod linux {
let ppid = getppid();
let dumper = fail_on_soft_error!(
soft_errors,
MinidumpWriterConfig::new(ppid, ppid).build_for_testing(&mut soft_errors)?
remote_mw_config(ppid, ppid).build_for_testing(&mut soft_errors)?
);
let linux_gate_loc = dumper.auxv.get_linux_gate_address().unwrap();
test!(linux_gate_loc != 0, "linux_gate_loc == 0");
Expand Down Expand Up @@ -443,6 +441,65 @@ mod linux {
_ => Err("Unknown test option".into()),
}
}

fn remote_mw_config(process_id: Pid, blamed_thread: Pid) -> MinidumpWriterConfig {
let mut config = MinidumpWriterConfig::new(process_id, blamed_thread);
config.set_remote_transport(TransportForTestExecutor::new(process_id));
config
}

#[derive(Debug)]
struct TransportForTestExecutor {
transport: Option<remote::transport::postcard::Backend<remote::io::UnixStream, Vec<u8>>>,
thread_handle: Option<std::thread::JoinHandle<()>>,
}

impl TransportForTestExecutor {
pub fn new(pid: Pid) -> Self {
let (backend_io, executor_io) =
remote::io::UnixStream::pair().expect("failed to create unix stream");

let thread_handle = thread::spawn(move || {
let args_buf = vec![0u8; remote::ARGS_BUFFER_LEN];
let transport = remote::transport::postcard::Executor::new(executor_io, args_buf);
let mut executor_resources = remote::ExecutorResources::const_new();
remote::executor::run(pid, transport, executor_resources.as_mut())
.expect("an error occurred running the executor");
});

let output_buf = vec![0u8; remote::OUTPUT_BUFFER_LEN];
let transport = remote::transport::postcard::Backend::new(backend_io, output_buf);

TransportForTestExecutor {
transport: Some(transport),
thread_handle: Some(thread_handle),
}
}
}

impl Drop for TransportForTestExecutor {
fn drop(&mut self) {
drop(self.transport.take().unwrap());
if let Err(payload) = self.thread_handle.take().unwrap().join()
&& !thread::panicking()
{
std::panic::resume_unwind(payload);
}
}
}

impl remote::transport::Backend for TransportForTestExecutor {
fn max_response_output_len(&self) -> usize {
self.transport.as_ref().unwrap().max_response_output_len()
}

fn send_request<'output, Req: serde::Serialize, Resp: serde::Deserialize<'output>>(
&'output mut self,
req: Req,
) -> std::result::Result<Resp, remote::transport::BackendError> {
self.transport.as_mut().unwrap().send_request(req)
}
}
}

#[cfg(target_os = "windows")]
Expand Down
18 changes: 15 additions & 3 deletions src/linux/minidump_writer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use {
maps_reader::{MappingInfo, MappingList},
module_list,
process_inspection::{self, ProcessInspector, process_reader::CopyFromProcessError},
remote_process_inspection,
serializers::*,
thread_info::{ThreadInfo, ThreadInfoError},
},
Expand Down Expand Up @@ -202,6 +203,19 @@ impl MinidumpWriterConfig {
self
}

/// Set the transport used to communicate with the remote executor
///
/// The [`MinidumpWriter`] is created in local mode by default. Specifying a remote Transport
/// changes it to remote mode, where it will forward all process inspection syscalls to a
/// remote process for execution.
pub fn set_remote_transport<T: remote_process_inspection::transport::Backend + 'static>(
&mut self,
transport: T,
) -> &mut Self {
self.process_inspector = process_inspection::remote(transport);
self
}

/// Generates a minidump and writes to the destination provided. Returns the in-memory
/// version of the minidump as well.
pub fn write(self, destination: &mut (impl Write + Seek)) -> Result<Vec<u8>, WriterError> {
Expand Down Expand Up @@ -955,10 +969,8 @@ impl MinidumpWriter {
src: usize,
length: usize,
) -> Result<Vec<u8>, CopyFromProcessError> {
let length =
std::num::NonZeroUsize::new(length).ok_or(CopyFromProcessError::InvalidArgument)?;
let mem = process_inspector.process_reader();
mem.read_to_vec(src, length)
mem.read_all_to_vec(src, length)
Comment on lines 971 to +973
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this may have spilled over from another commit (part 9 maybe?), anyway no big deal.

@marti4d marti4d Sep 9, 2026 •
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You'd think, but no -- As it turns out, this is actually a bug! There are several pieces of code that call copy_from_process() that basically don't work if it only returns a partial read -- It seems that the assumed contract for copy_for_process() is that it's all-or-nothing -- It's just that partial reads never practically occurred until IO buffering entered the fold.

}
}

Expand Down
4 changes: 3 additions & 1 deletion src/linux/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@

pub use {
maps_reader::LINUX_GATE_LIBRARY_NAME,
process_inspection::{Error as BackendError, ProcessReaderKind, process_reader},
process_inspection::{
Error as BackendError, ProcessReaderKind, process_reader, remote_process_inspection,
},
};

pub mod app_memory;
Expand Down
100 changes: 92 additions & 8 deletions src/linux/process_inspection/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use super as linux;
use crate::module_reader::{ModuleMemoryReadError, ReadError, ReadModuleMemory};
use linux::maps_reader;
use process_backend::{ProcessReader as _, Stat, local, regs::*};
use process_backend::{ProcessReader as _, Stat, local, regs::*, remote};
use process_reader::{CopyFromProcessError, ProcessReader, ProcessReaderBackend};
use std::{
borrow::Cow,
Expand All @@ -20,17 +20,85 @@ pub use process_backend::ProcessReaderKind;

pub mod process_reader;

pub mod remote_process_inspection {
use process_backend::remote::executor::resources::*;

pub mod executor {
pub use process_backend::remote::executor::{Error, Resources, run};
}

pub use process_backend::remote::{io, transport};

pub const ARGS_BUFFER_LEN: usize = super::MAX_INPUT_PATH_LEN;
pub const OUTPUT_BUFFER_LEN: usize = SCRATCH_BUFFER_LEN;

// All of these are arbitrary and may need to be adjusted
const MAX_SIMULTANEOUS_OPEN_FILES: usize = 5;
const MAX_SIMULTANEOUS_OPEN_DIRS: usize = 5;
const MAX_SIMULTANEOUS_OPEN_MODULE_MEMORY_READERS: usize = 5;
const SCRATCH_BUFFER_LEN: usize =
if super::MAX_SYMLINK_TARGET_LEN > super::MAX_DIRECTORY_NAME_LENGTH {
super::MAX_SYMLINK_TARGET_LEN
} else {
super::MAX_DIRECTORY_NAME_LENGTH
};

pub struct ExecutorResources {
file_readers: [FileReader; MAX_SIMULTANEOUS_OPEN_FILES],
dir_readers: [DirReader; MAX_SIMULTANEOUS_OPEN_DIRS],
mapped_module_memory_readers:
[MappedModuleMemoryReader; MAX_SIMULTANEOUS_OPEN_MODULE_MEMORY_READERS],
scratch_buf: [u8; SCRATCH_BUFFER_LEN],
}

impl ExecutorResources {
pub const fn const_new() -> ExecutorResources {
ExecutorResources {
file_readers: FileReader::new_array(),
dir_readers: DirReader::new_array(),
mapped_module_memory_readers: MappedModuleMemoryReader::new_array(),
scratch_buf: [0; SCRATCH_BUFFER_LEN],
}
}

pub const fn as_mut(&mut self) -> Resources<'_> {
Resources {
file_readers: &mut self.file_readers,
dir_readers: &mut self.dir_readers,
mapped_module_memory_readers: &mut self.mapped_module_memory_readers,
scratch_buf: &mut self.scratch_buf,
}
}
}
}

pub(crate) type Result<T> = core::result::Result<T, Error>;

// These are both arbitrary choices and may need to be tweaked
const MAX_PATH_LEN: usize = 65536;
// These are arbitrary choices and are probably way bigger than they need to be.
// They probably could be optimized further...

// The longest path that may be passed as an argument to any of the APIs that
// take a PathBuf arg. Realistically, even this is a fairly conservative number.
const MAX_INPUT_PATH_LEN: usize = 1024;

// The longest symlink we'd ever expect to get from a call to read_link()
const MAX_SYMLINK_TARGET_LEN: usize = 1024;

/// The longest directory name we'd expect to see from a read_dir() iterator
const MAX_DIRECTORY_NAME_LENGTH: usize = 256;

pub(crate) fn local(pid: libc::pid_t) -> Box<dyn ProcessInspector> {
set_process_backend_drop_fail_handler();
Box::new(local::Backend::new(pid))
}

pub(crate) fn remote<'t, T: remote_process_inspection::transport::Backend + 't>(
transport: T,
) -> Box<dyn ProcessInspector + 't> {
set_process_backend_drop_fail_handler();
Box::new(remote::backend::Backend::new(transport))
}

pub trait ProcessInspector: core::fmt::Debug {
fn process_reader<'a>(&'a self) -> ProcessReader<'a>;
fn pid(&self) -> Result<libc::pid_t>;
Expand Down Expand Up @@ -61,6 +129,15 @@ pub trait ProcessInspector: core::fmt::Debug {
fn fail_one_syscall_with(&self, errno: core::ffi::c_int);
}

fn path_to_cstring(path: PathBuf) -> Result<CString> {
let c_path = CString::new(path.into_os_string().into_vec()).unwrap();
if c_path.as_bytes_with_nul().len() <= MAX_INPUT_PATH_LEN {
Ok(c_path)
} else {
Err(Error::InputPathTooLong)
}
}

impl<B: process_backend::Backend> ProcessInspector for B {
fn process_reader<'a>(&'a self) -> ProcessReader<'a> {
ProcessReader::new(self)
Expand Down Expand Up @@ -97,25 +174,25 @@ impl<B: process_backend::Backend> ProcessInspector for B {
}

fn stat_file(&self, path: PathBuf) -> Result<Stat> {
let c_path = CString::new(path.into_os_string().into_vec()).unwrap();
let c_path = path_to_cstring(path)?;
B::stat_file(self, &c_path).map_err(Error::Backend)
}

fn read_file<'a>(&'a self, path: PathBuf) -> Result<FileReader<'a>> {
let c_path = CString::new(path.into_os_string().into_vec()).unwrap();
let c_path = path_to_cstring(path)?;
let reader = B::read_file(self, &c_path).map_err(Error::Backend)?;
Ok(FileReader(Box::new(reader)))
}

fn read_dir<'a>(&'a self, path: PathBuf) -> Result<DirReader<'a>> {
let c_path = CString::new(path.into_os_string().into_vec()).unwrap();
let c_path = path_to_cstring(path)?;
let reader = B::read_dir(self, &c_path).map_err(Error::Backend)?;
Ok(DirReader(Box::new(reader)))
}

fn read_link(&self, path: PathBuf) -> Result<PathBuf> {
let c_path = CString::new(path.into_os_string().into_vec()).unwrap();
let mut buf = vec![0u8; MAX_PATH_LEN];
let c_path = path_to_cstring(path)?;
let mut buf = vec![0u8; MAX_SYMLINK_TARGET_LEN];
let len = B::read_link(self, &c_path, &mut buf).map_err(Error::Backend)?;
buf.truncate(len);
Ok(PathBuf::from(OsString::from_vec(buf)))
Expand Down Expand Up @@ -205,9 +282,14 @@ impl<'a> MappedModuleMemoryReader<'a> {

loop {
let bytes_read = self.0.read_at(offset, buf).map_err(Error::Backend)?;
if bytes_read == 0 {
return Err(Error::UnexpectedEndOfBuffer);
}

if bytes_read == buf.len() {
return Ok(());
}

offset = offset
.checked_add(bytes_read)
.ok_or(Error::AddressOverflowed)?;
Expand Down Expand Up @@ -270,6 +352,8 @@ pub enum Error {
UnexpectedEndOfBuffer,
#[error("got an unexpected end of file")]
UnexpectedEndOfFile,
#[error("a path argument was too long")]
InputPathTooLong,
}

fn set_process_backend_drop_fail_handler() {
Expand Down
Loading
Loading
0