diff --git a/Cargo.lock b/Cargo.lock index 33a1437f..ad9c0b27 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1685,6 +1685,7 @@ version = "0.1.0" dependencies = [ "libc", "postcard", + "process-backend", "process-reader", "serde", "thiserror 2.0.19", diff --git a/crates/linux/process-backend/Cargo.toml b/crates/linux/process-backend/Cargo.toml index 36e7b4b1..9c8cc8bf 100644 --- a/crates/linux/process-backend/Cargo.toml +++ b/crates/linux/process-backend/Cargo.toml @@ -19,3 +19,6 @@ postcard = { version = "1.1.3", default-features = false, optional = true } process-reader = { version = "0.1.0", path = "../../process-reader", features = ["serde"] } serde.workspace = true thiserror.workspace = true + +[dev-dependencies] +process-backend = { path = ".", features = ["postcard", "testing", "unix_stream"] } \ No newline at end of file diff --git a/crates/linux/process-backend/src/lib.rs b/crates/linux/process-backend/src/lib.rs index 32d9e639..af5aeea6 100644 --- a/crates/linux/process-backend/src/lib.rs +++ b/crates/linux/process-backend/src/lib.rs @@ -1,5 +1,4 @@ #![no_std] -#![allow(dead_code)] #![cfg(any(target_os = "linux", target_os = "android"))] use core::ffi::CStr; @@ -98,6 +97,8 @@ pub enum Error { Local(#[source] local::Error), #[error("the remote executor returned an error")] Executor(#[source] remote::executor::Error), + #[error("there was an error transporting the request to/from the remote executor")] + Transport(#[source] remote::transport::BackendError), #[error("buffer was too small to contain the result")] BufferTooSmall, } diff --git a/crates/linux/process-backend/src/remote/backend.rs b/crates/linux/process-backend/src/remote/backend.rs new file mode 100644 index 00000000..b77b6c9a --- /dev/null +++ b/crates/linux/process-backend/src/remote/backend.rs @@ -0,0 +1,347 @@ +use super as remote; +use crate::wrapper::Stat; +use crate::{Error, ProcessReaderKind, Result}; +use core::cell::{Cell, RefCell}; +use remote::{transport, wire}; +use wire::{Operation, ops}; + +#[derive(Debug)] +pub struct Backend { + transport_state: RefCell>, + pid: Cell>, + max_response_output_len: usize, +} + +#[allow(clippy::large_enum_variant)] +#[derive(Debug)] +enum TransportState { + Ready(T), + Poisoned, +} + +impl TransportState { + fn expect_ready(&mut self) -> &mut T { + match self { + TransportState::Ready(transport) => transport, + TransportState::Poisoned => panic!("attempted to use poisoned transport"), + } + } +} + +impl Backend { + pub fn new(transport: T) -> Self { + let max_response_output_len = transport.max_response_output_len(); + Self { + transport_state: RefCell::new(TransportState::Ready(transport)), + pid: Cell::new(None), + max_response_output_len, + } + } + + fn empty_request(&self) -> Result + where + Op: for<'args, 'output> Operation = (), Output<'output> = Out>, + Out: 'static, + { + self.simple_request::(()) + } + + fn simple_request(&self, args: Op::Args<'_>) -> Result + where + Op: for<'output> Operation = Out>, + Out: 'static, + { + self.mapped_request::(args, Ok) + } + + fn mapped_request(&self, args: Op::Args<'_>, map_fn: F) -> Result + where + Op: Operation, + F: for<'output> FnOnce(Op::Output<'output>) -> Result, + { + let mut transport_state = self.transport_state.borrow_mut(); + let transport = transport_state.expect_ready(); + + let e = match Op::request(transport, args) { + Ok(response) => { + let output = response.map_err(Error::Executor)?; + return map_fn(output); + } + Err(e) => e, + }; + + *transport_state = TransportState::Poisoned; + Err(Error::Transport(e)) + } +} + +impl crate::Backend for Backend { + type ProcessReader<'a> + = ProcessReader<'a, T> + where + Self: 'a; + + type FileReader<'a> + = FileReader<'a, T> + where + Self: 'a; + + type DirReader<'a> + = DirReader<'a, T> + where + Self: 'a; + + type MappedModuleMemoryReader<'a> + = MappedModuleMemoryReader<'a, T> + where + Self: 'a; + + fn pid(&self) -> Result { + match self.pid.get() { + Some(pid) => Ok(pid), + None => { + let pid = self.empty_request::()?; + self.pid.set(Some(pid)); + Ok(pid) + } + } + } + + fn process_reader<'a>(&'a self) -> Self::ProcessReader<'a> { + ProcessReader { backend: self } + } + + fn stop_process(&self) -> Result<()> { + self.empty_request::() + } + + fn continue_process(&self) -> Result<()> { + self.empty_request::() + } + + fn suspend_thread(&self, tid: libc::pid_t) -> Result<()> { + self.simple_request::(tid) + } + + fn resume_thread(&self, tid: libc::pid_t) -> Result<()> { + self.simple_request::(tid) + } + + fn map_module_into_memory<'a>( + &'a self, + path: &core::ffi::CStr, + offset: u64, + ) -> Result> { + self.mapped_request::( + wire::MappedModuleMemoryReaderOpenArgs { + path: path.into(), + offset, + }, + |handle| { + Ok(MappedModuleMemoryReader { + backend: self, + handle, + }) + }, + ) + } + + fn stat_file(&self, path: &core::ffi::CStr) -> Result { + self.simple_request::(path.into()) + } + + fn read_file<'a>(&'a self, path: &core::ffi::CStr) -> Result> { + self.mapped_request::(path.into(), |handle| { + Ok(FileReader { + backend: self, + handle, + }) + }) + } + + fn read_dir<'a>(&'a self, path: &core::ffi::CStr) -> Result> { + self.mapped_request::(path.into(), |handle| { + Ok(DirReader { + backend: self, + handle, + }) + }) + } + + fn read_link(&self, path: &core::ffi::CStr, buf: &mut [u8]) -> Result { + self.mapped_request::( + wire::ReadLinkArgs { + path: path.into(), + requested_len: usize::min(buf.len(), self.max_response_output_len), + }, + |bytes| { + buf[0..bytes.len()].copy_from_slice(bytes); + Ok(bytes.len()) + }, + ) + } + + fn get_gen_regs(&self, tid: libc::pid_t) -> Result { + self.simple_request::(tid) + } + + fn get_fp_regs(&self, tid: libc::pid_t) -> Result { + self.simple_request::(tid) + } + + #[cfg(target_arch = "x86")] + fn get_fpx_regs(&self, tid: libc::pid_t) -> Result { + self.simple_request::(tid) + } + + #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] + fn ptrace_peekuser(&self, addr: usize) -> Result<[u8; crate::PTRACE_DATA_LEN]> { + self.simple_request::(addr) + } + + fn force_process_reader_kind(&mut self, kind: ProcessReaderKind) -> Result<()> { + self.simple_request::(kind) + } + + #[cfg(feature = "testing")] + fn fail_one_syscall_with(&self, errno: core::ffi::c_int) { + let _result = self.simple_request::(errno); + } +} + +impl Drop for Backend { + fn drop(&mut self) { + if let Err(e) = self.empty_request::() { + report_drop_failed!("failed to drop backend: {e:#?}"); + } + } +} + +#[derive(Debug)] +pub struct ProcessReader<'a, T: transport::Backend> { + backend: &'a Backend, +} + +impl<'a, T: transport::Backend> crate::ProcessReader for ProcessReader<'a, T> { + fn read_at(&self, address: usize, buf: &mut [u8]) -> Result { + self.backend + .mapped_request::( + wire::ProcessReaderReadAtArgs { + address, + requested_len: usize::min(buf.len(), self.backend.max_response_output_len), + }, + |bytes| { + buf[0..bytes.len()].copy_from_slice(bytes); + Ok(bytes.len()) + }, + ) + } +} + +#[derive(Debug)] +pub struct FileReader<'a, T: transport::Backend> { + backend: &'a Backend, + handle: usize, +} + +impl<'a, T: transport::Backend> crate::FileReader for FileReader<'a, T> { + fn read(&mut self, buf: &mut [u8]) -> Result { + self.backend.mapped_request::( + wire::FileReaderArgs { + handle: self.handle, + requested_len: usize::min(buf.len(), self.backend.max_response_output_len), + }, + |bytes| { + buf[0..bytes.len()].copy_from_slice(bytes); + Ok(bytes.len()) + }, + ) + } +} + +impl<'a, T: transport::Backend> Drop for FileReader<'a, T> { + fn drop(&mut self) { + if let Err(e) = self + .backend + .simple_request::(self.handle) + { + report_drop_failed!("failed to close FileReader: {e:#?}"); + } + } +} + +#[derive(Debug)] +pub struct DirReader<'a, T: transport::Backend> { + backend: &'a Backend, + handle: usize, +} + +impl<'a, T: transport::Backend> crate::DirReader for DirReader<'a, T> { + fn read_next_name(&mut self, buf: &mut [u8]) -> Result { + self.backend + .mapped_request::(self.handle, |maybe_bytes| { + if let Some(bytes) = maybe_bytes { + buf[0..bytes.len()].copy_from_slice(bytes); + Ok(bytes.len()) + } else { + Ok(0) + } + }) + } +} + +impl<'a, T: transport::Backend> Drop for DirReader<'a, T> { + fn drop(&mut self) { + if let Err(e) = self + .backend + .simple_request::(self.handle) + { + report_drop_failed!("failed to close DirReader: {e:#?}"); + } + } +} + +#[derive(Debug)] +pub struct MappedModuleMemoryReader<'a, T: transport::Backend> { + backend: &'a Backend, + handle: usize, +} + +impl<'a, T: transport::Backend> crate::MappedModuleMemoryReader + for MappedModuleMemoryReader<'a, T> +{ + fn read_at(&self, offset: usize, buf: &mut [u8]) -> Result { + self.backend + .mapped_request::( + wire::MappedModuleMemoryReaderReadAtArgs { + handle: self.handle, + offset, + requested_len: usize::min(buf.len(), self.backend.max_response_output_len), + }, + |bytes| { + buf[0..bytes.len()].copy_from_slice(bytes); + Ok(bytes.len()) + }, + ) + } + + fn len(&self) -> Result { + self.backend + .simple_request::(self.handle) + } + + fn is_empty(&self) -> Result { + self.len().map(|len| len == 0) + } +} + +impl<'a, T: transport::Backend> Drop for MappedModuleMemoryReader<'a, T> { + fn drop(&mut self) { + if let Err(e) = self + .backend + .simple_request::(self.handle) + { + report_drop_failed!("failed to close MappedModuleMemoryReader: {e:#?}"); + } + } +} diff --git a/crates/linux/process-backend/src/remote/mod.rs b/crates/linux/process-backend/src/remote/mod.rs index 221b710d..b1afd99e 100644 --- a/crates/linux/process-backend/src/remote/mod.rs +++ b/crates/linux/process-backend/src/remote/mod.rs @@ -1,3 +1,4 @@ +pub mod backend; pub mod executor; pub mod io; pub mod transport; diff --git a/crates/linux/process-backend/tests/basic.rs b/crates/linux/process-backend/tests/basic.rs new file mode 100644 index 00000000..b135d984 --- /dev/null +++ b/crates/linux/process-backend/tests/basic.rs @@ -0,0 +1,99 @@ +#![cfg(any(target_os = "linux", target_os = "android"))] + +use process_backend::{Backend as _, DirReader, remote}; +use remote::{backend::Backend, executor, io::UnixStream, transport}; +use std::{ + collections::HashSet, + ffi::CString, + fs::remove_file, + os::unix::{ffi::OsStringExt as _, fs::symlink}, + thread, +}; + +const MAX_INPUT_LEN: usize = 32; +const MAX_OUTPUT_LEN: usize = 65536; +const MAX_FILE_READERS: usize = 5; +const MAX_DIR_READERS: usize = 5; +const MAX_MAPPED_MODULE_MEMORY_READERS: usize = 5; + +#[test] +fn basic() { + let (backend_io, executor_io) = UnixStream::pair().unwrap(); + + let executor_thread = thread::spawn(move || { + let pid = unsafe { libc::getpid() }; + let mut args_buf = [0u8; MAX_INPUT_LEN]; + let executor_transport = transport::postcard::Executor::new(executor_io, &mut args_buf); + let mut file_readers = executor::resources::FileReader::new_array::(); + let mut dir_readers = executor::resources::DirReader::new_array::(); + let mut mapped_module_memory_readers = + executor::resources::MappedModuleMemoryReader::new_array::< + MAX_MAPPED_MODULE_MEMORY_READERS, + >(); + + let mut scratch_buf = [0u8; MAX_OUTPUT_LEN]; + let resources = executor::resources::Resources { + file_readers: &mut file_readers, + dir_readers: &mut dir_readers, + mapped_module_memory_readers: &mut mapped_module_memory_readers, + scratch_buf: &mut scratch_buf, + }; + executor::run(pid, executor_transport, resources) + }); + + const FAKE_TARGET: &str = "/foo/bar"; + + let temp_dir = std::env::temp_dir(); + + let link_path = temp_dir.join("process-backend.test.link"); + + if link_path.symlink_metadata().is_ok_and(|m| m.is_symlink()) { + remove_file(&link_path).unwrap(); + } + + symlink(FAKE_TARGET, &link_path).unwrap(); + + let output_buf = vec![0u8; MAX_OUTPUT_LEN]; + let backend_transport = transport::postcard::Backend::new(backend_io, output_buf); + let backend = Backend::new(backend_transport); + + let mut link = vec![0u8; MAX_OUTPUT_LEN]; + + let link_path_cstr = CString::new(link_path.into_os_string().into_vec()).unwrap(); + + let len = backend.read_link(&link_path_cstr, &mut link).unwrap(); + link.truncate(len); + let link = String::from_utf8(link).unwrap(); + + assert_eq!(link, FAKE_TARGET); + + let stat = backend.stat_file(c"/").unwrap(); + + // Should be pretty safe for testing -- The root directory should definitely have rwx for + // owner and rx for everyone else. Group should at-least have r+x. + assert!(stat.st_mode & 0o757 == 0o755); + + // A set of names we'd generally expect to find in the root directory + let mut unencountered_dirs = HashSet::from(["bin", "dev", "etc", "lib", "opt", "tmp", "var"]); + + let mut output_buf = vec![0u8; MAX_OUTPUT_LEN]; + let mut dir_reader = backend.read_dir(c"/").unwrap(); + loop { + let len = dir_reader.read_next_name(&mut output_buf).unwrap(); + let name = &output_buf[0..len]; + if name.is_empty() { + break; + } + let Ok(name_str) = str::from_utf8(&name) else { + continue; + }; + unencountered_dirs.remove(name_str); + } + + assert!(unencountered_dirs.is_empty()); + + drop(dir_reader); + drop(backend); + + executor_thread.join().unwrap().unwrap(); +}