From 7249fef5e61777b030b9204a25fe47da3fa188ca Mon Sep 17 00:00:00 2001 From: Chris Martin Date: Mon, 10 Aug 2026 16:05:11 -0400 Subject: [PATCH 1/2] Isolated Processes 16: Lay out default remote IO and wire format This defines the first IO implementation (AF_UNIX + SOCK_STREAM), the serialization structures, and the first serializer (Postcard) for the remote executor. Note that these are just defaults, as traits are used to allow for more IO implements (for example, AF_UNIX + SOCK_SEQPACKET) and more serializers (for example, serde_json). --- Cargo.lock | 20 ++ Cargo.toml | 2 +- crates/linux/process-backend/Cargo.toml | 3 + crates/linux/process-backend/src/lib.rs | 2 + .../process-backend/src/remote/io/mod.rs | 22 ++ .../src/remote/io/unix_stream.rs | 67 ++++ .../linux/process-backend/src/remote/mod.rs | 4 + .../src/remote/transport/mod.rs | 55 ++++ .../src/remote/transport/postcard.rs | 300 ++++++++++++++++++ .../src/remote/wire/cstr_with_null.rs | 72 +++++ .../process-backend/src/remote/wire/mod.rs | 190 +++++++++++ 11 files changed, 736 insertions(+), 1 deletion(-) create mode 100644 crates/linux/process-backend/src/remote/io/mod.rs create mode 100644 crates/linux/process-backend/src/remote/io/unix_stream.rs create mode 100644 crates/linux/process-backend/src/remote/mod.rs create mode 100644 crates/linux/process-backend/src/remote/transport/mod.rs create mode 100644 crates/linux/process-backend/src/remote/transport/postcard.rs create mode 100644 crates/linux/process-backend/src/remote/wire/cstr_with_null.rs create mode 100644 crates/linux/process-backend/src/remote/wire/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 59a43810..33a1437f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -268,6 +268,15 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b0fc239e0f6cb375d2402d48afb92f76f5404fd1df208a41930ec81eda078bea" +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror 2.0.19", +] + [[package]] name = "compression-codecs" version = "0.4.38" @@ -1630,6 +1639,16 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "serde", +] + [[package]] name = "potential_utf" version = "0.1.5" @@ -1665,6 +1684,7 @@ name = "process-backend" version = "0.1.0" dependencies = [ "libc", + "postcard", "process-reader", "serde", "thiserror 2.0.19", diff --git a/Cargo.toml b/Cargo.toml index f936a1d9..9e08d2fb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -59,7 +59,7 @@ byteorder = "1.4" error-graph = { version = "0.1.1", features = ["serde"] } failspot = "0.2.0" plain = { version = "0.2.3", default-features = false } -process-backend = { version = "0.1.0", path = "crates/linux/process-backend", features = ["testing"] } +process-backend = { version = "0.1.0", path = "crates/linux/process-backend", features = ["postcard", "unix_stream", "testing"] } # Used for parsing procfs info. # default-features is disabled since it pulls in chrono diff --git a/crates/linux/process-backend/Cargo.toml b/crates/linux/process-backend/Cargo.toml index d2329ec2..36e7b4b1 100644 --- a/crates/linux/process-backend/Cargo.toml +++ b/crates/linux/process-backend/Cargo.toml @@ -9,10 +9,13 @@ rust-version.workspace = true license.workspace = true [features] +postcard = ["dep:postcard"] testing = [] +unix_stream = [] [target.'cfg(any(target_os = "linux", target_os = "android"))'.dependencies] libc.workspace = true +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 diff --git a/crates/linux/process-backend/src/lib.rs b/crates/linux/process-backend/src/lib.rs index 38b513e2..29636379 100644 --- a/crates/linux/process-backend/src/lib.rs +++ b/crates/linux/process-backend/src/lib.rs @@ -1,4 +1,5 @@ #![no_std] +#![allow(dead_code)] #![cfg(any(target_os = "linux", target_os = "android"))] use core::ffi::CStr; @@ -12,6 +13,7 @@ mod drop_fail_handler; pub mod local; pub mod regs; +pub mod remote; mod wrapper; diff --git a/crates/linux/process-backend/src/remote/io/mod.rs b/crates/linux/process-backend/src/remote/io/mod.rs new file mode 100644 index 00000000..b43b4f4c --- /dev/null +++ b/crates/linux/process-backend/src/remote/io/mod.rs @@ -0,0 +1,22 @@ +use core::ffi::c_int; + +#[cfg(feature = "unix_stream")] +pub use unix_stream::UnixStream; + +#[cfg(feature = "unix_stream")] +mod unix_stream; + +pub type Result = core::result::Result; + +pub trait Io: core::fmt::Debug { + fn read_exact(&mut self, buf: &mut [u8]) -> Result<()>; + fn write_all(&mut self, buf: &[u8]) -> Result<()>; +} + +#[derive(Debug, serde::Deserialize, serde::Serialize, thiserror::Error)] +pub enum Error { + #[error("an OS error occurred. Errno: {0}")] + OsError(c_int), + #[error("unexpected end-of-file reached")] + UnexpectedEof, +} diff --git a/crates/linux/process-backend/src/remote/io/unix_stream.rs b/crates/linux/process-backend/src/remote/io/unix_stream.rs new file mode 100644 index 00000000..0bc373da --- /dev/null +++ b/crates/linux/process-backend/src/remote/io/unix_stream.rs @@ -0,0 +1,67 @@ +use super as io; +use crate::wrapper::{OwnedFd, errno}; +use core::ffi::c_int; +use io::{Error, Result}; + +#[derive(Debug)] +pub struct UnixStream(OwnedFd); + +impl UnixStream { + pub fn pair() -> Result<(UnixStream, UnixStream)> { + let mut fds = [0; 2]; + let rv = unsafe { libc::socketpair(libc::AF_UNIX, libc::SOCK_STREAM, 0, fds.as_mut_ptr()) }; + if rv == -1 { + return Err(Error::OsError(errno())); + } + let stream0 = unsafe { Self::from_raw_fd(fds[0]) }; + let stream1 = unsafe { Self::from_raw_fd(fds[1]) }; + Ok((stream0, stream1)) + } + /// # Safety + /// + /// 'fd' must be a valid UNIX stream socket + pub unsafe fn from_raw_fd(fd: c_int) -> Self { + unsafe { Self(OwnedFd::new(fd)) } + } + pub fn as_raw_fd(&self) -> c_int { + self.0.as_raw_fd() + } + fn read(&mut self, buf: &mut [u8]) -> Result { + let rv = unsafe { libc::read(self.0.as_raw_fd(), buf.as_mut_ptr().cast(), buf.len()) }; + if rv == -1 { + return Err(Error::OsError(errno())); + } + let bytes_read = usize::try_from(rv).unwrap(); + Ok(bytes_read) + } + fn write(&mut self, buf: &[u8]) -> Result { + let rv = unsafe { libc::write(self.0.as_raw_fd(), buf.as_ptr().cast(), buf.len()) }; + if rv == -1 { + return Err(Error::OsError(errno())); + } + let bytes_written = usize::try_from(rv).unwrap(); + Ok(bytes_written) + } +} + +impl io::Io for UnixStream { + fn read_exact(&mut self, mut buf: &mut [u8]) -> Result<()> { + while !buf.is_empty() { + let bytes_read = self.read(buf)?; + if bytes_read == 0 { + return Err(Error::UnexpectedEof); + } + buf = &mut buf[bytes_read..]; + } + Ok(()) + } + + fn write_all(&mut self, mut buf: &[u8]) -> Result<()> { + while !buf.is_empty() { + let bytes_written = self.write(buf)?; + assert_ne!(bytes_written, 0); + buf = &buf[bytes_written..]; + } + Ok(()) + } +} diff --git a/crates/linux/process-backend/src/remote/mod.rs b/crates/linux/process-backend/src/remote/mod.rs new file mode 100644 index 00000000..e3c47574 --- /dev/null +++ b/crates/linux/process-backend/src/remote/mod.rs @@ -0,0 +1,4 @@ +pub mod io; +pub mod transport; + +mod wire; diff --git a/crates/linux/process-backend/src/remote/transport/mod.rs b/crates/linux/process-backend/src/remote/transport/mod.rs new file mode 100644 index 00000000..2343fb88 --- /dev/null +++ b/crates/linux/process-backend/src/remote/transport/mod.rs @@ -0,0 +1,55 @@ +use super as remote; +use remote::io; +use serde::{Deserialize, Serialize}; + +#[cfg(feature = "postcard")] +pub mod postcard; + +pub trait Backend: core::fmt::Debug { + fn max_response_output_len(&self) -> usize; + fn send_request<'output, Req: Serialize, Resp: Deserialize<'output>>( + &'output mut self, + req: Req, + ) -> Result; +} + +pub trait Executor: core::fmt::Debug { + fn read_request<'args, D: Deserialize<'args>>(&'args mut self) -> Result; + fn write_response(&mut self, resp: S) -> Result<(), ExecutorError>; +} + +#[derive(Debug, thiserror::Error, Deserialize, Serialize)] +pub enum BackendError { + #[error("an error occurred serializing the request")] + SerializeRequest(#[source] SerializeError), + #[error("an error occurred deserializing the response")] + DeserializeResponse(#[source] DeserializeError), +} + +#[derive(Debug, thiserror::Error, Deserialize, Serialize)] +pub enum ExecutorError { + #[error("an error occurred deserializing the request")] + DeserializeRequest(#[source] DeserializeError), + #[error("an error occurred serializing the response")] + SerializeResponse(#[source] SerializeError), +} + +#[derive(Debug, thiserror::Error, serde::Deserialize, serde::Serialize)] +pub enum SerializeError { + #[cfg(feature = "postcard")] + #[error("a Postcard-specific serialize error occurred")] + Postcard(#[source] postcard::Error), + #[error("an I/O occurred during serialization")] + Io(#[source] io::Error), +} + +#[derive(Debug, thiserror::Error, serde::Deserialize, serde::Serialize)] +pub enum DeserializeError { + #[cfg(feature = "postcard")] + #[error("a Postcard-specific deserialize error occurred")] + Postcard(#[source] postcard::Error), + #[error("an I/O occurred during deserialization")] + Io(#[source] io::Error), + #[error("the I/O buffer was too small")] + BufferTooSmall, +} diff --git a/crates/linux/process-backend/src/remote/transport/postcard.rs b/crates/linux/process-backend/src/remote/transport/postcard.rs new file mode 100644 index 00000000..d9fdd056 --- /dev/null +++ b/crates/linux/process-backend/src/remote/transport/postcard.rs @@ -0,0 +1,300 @@ +use super as transport; +use core::{marker::PhantomData, ops::Range}; +use remote::io; +use serde::{Deserialize, Serialize}; +use transport::remote; + +pub use postcard::Error; + +// Postcard requires an extra scratch buffer for deserializing a few fixed-size types. +// f64 is the largest type it does this for. +const MAX_TEMP_LEN: usize = size_of::(); + +/// The backend side of a Postcard transport +/// +/// See remote/wire.rs `operations!()` macro for notes on buffer sizing +#[derive(Debug)] +pub struct Backend { + io: Io, + output_buf: Buf, +} + +impl Backend { + pub fn new(io: Io, output_buf: Buf) -> Self { + Self { io, output_buf } + } +} + +impl transport::Backend for Backend +where + Io: io::Io, + Buf: AsRef<[u8]> + AsMut<[u8]> + core::fmt::Debug, +{ + fn max_response_output_len(&self) -> usize { + self.output_buf.as_ref().len() + } + fn send_request<'output, Req: Serialize, Resp: Deserialize<'output>>( + &'output mut self, + req: Req, + ) -> Result { + serialize_to_io(&mut self.io, req).map_err(transport::BackendError::SerializeRequest)?; + deserialize_from_io(&mut self.io, self.output_buf.as_mut()) + .map_err(transport::BackendError::DeserializeResponse) + } +} + +/// The executor side of a Postcard transport +/// +/// See remote/wire.rs `operations!()` macro for notes on buffer sizing +#[derive(Debug)] +pub struct Executor { + io: Io, + args_buf: Buf, +} + +impl Executor { + pub fn new(io: Io, args_buf: Buf) -> Self { + Self { io, args_buf } + } +} + +impl transport::Executor for Executor +where + Io: io::Io, + Buf: AsMut<[u8]> + core::fmt::Debug, +{ + fn read_request<'args, D: Deserialize<'args>>( + &'args mut self, + ) -> Result { + deserialize_from_io(&mut self.io, self.args_buf.as_mut()) + .map_err(transport::ExecutorError::DeserializeRequest) + } + + fn write_response(&mut self, resp: S) -> Result<(), transport::ExecutorError> { + serialize_to_io(&mut self.io, resp).map_err(transport::ExecutorError::SerializeResponse) + } +} + +fn serialize_to_io( + io: &mut Io, + s: S, +) -> Result<(), transport::SerializeError> { + let mut serializer = postcard::Serializer { + output: SerFlavor::new(io), + }; + match s.serialize(&mut serializer) { + Ok(()) => Ok(()), + Err(e) => { + let e = postcard::ser_flavors::Flavor::finalize(serializer.output) + .unwrap() + .unwrap_or(transport::SerializeError::Postcard(e)); + Err(e) + } + } +} + +fn deserialize_from_io<'output, Io: io::Io, D: Deserialize<'output>>( + io: &'output mut Io, + buf: &'output mut [u8], +) -> Result { + let mut deserializer = postcard::Deserializer::from_flavor(DeFlavor::new(io, buf)); + match D::deserialize(&mut deserializer) { + Ok(d) => Ok(d), + Err(e) => { + let e = deserializer + .finalize() + .unwrap() + .unwrap_or(transport::DeserializeError::Postcard(e)); + Err(e) + } + } +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// +// So... What's the deal with us returning `postcard::Error::NotYetImplemented` below? +// =================================================================================== +// +// The Postcard wire format is awesome, but the way it propagates errors is... Less awesome. +// (To be fair - the authors are aware and are trying to fix this in v2). +// +// We need a way to smuggle errors past Postcard's `Error` unit enum, so when an error +// occurs we record the real cause in a field of our flavor structs and return +// `NotYetImplemented` because it's an error Postcard would never actually return (since we don't +// use any of its unimplemented functionality). +// +// On the other end, after the Serde call completes, we call `finalize()` to unwrap our flavor +// object, allowing us to read the real error that occurred (if any). +// +/////////////////////////////////////////////////////////////////////////////////////////////////// + +struct SerFlavor<'io, Io> { + io: &'io mut Io, + error: Option, +} + +impl<'io, Io> SerFlavor<'io, Io> { + pub fn new(io: &'io mut Io) -> Self { + Self { io, error: None } + } +} + +impl<'io, Io: io::Io> postcard::ser_flavors::Flavor for SerFlavor<'io, Io> { + type Output = Option; + + fn try_push(&mut self, data: u8) -> postcard::Result<()> { + match self.io.write_all(&[data]) { + Ok(()) => Ok(()), + Err(e) => { + self.error = Some(transport::SerializeError::Io(e)); + Err(postcard::Error::NotYetImplemented) + } + } + } + + fn finalize(self) -> postcard::Result { + Ok(self.error) + } + + fn try_extend(&mut self, data: &[u8]) -> postcard::Result<()> { + match self.io.write_all(data) { + Ok(()) => Ok(()), + Err(e) => { + self.error = Some(transport::SerializeError::Io(e)); + Err(postcard::Error::NotYetImplemented) + } + } + } +} + +struct DeFlavor<'io, 'buf, Io> { + inner: DeFlavorInner<'io, 'buf, Io>, + error: Option, +} + +impl<'io, 'buf, Io> DeFlavor<'io, 'buf, Io> { + fn new(io: &'io mut Io, buf: &'buf mut [u8]) -> Self { + Self { + inner: DeFlavorInner { + io, + alloc_buf: BumpAllocator::new(buf), + temp_buf: [0u8; MAX_TEMP_LEN], + }, + error: None, + } + } + fn with_inner<'a, F, T>(&'a mut self, f: F) -> postcard::Result + where + F: FnOnce(&'a mut DeFlavorInner<'io, 'buf, Io>) -> Result, + { + let (inner, error) = (&mut self.inner, &mut self.error); + + match f(inner) { + Ok(t) => Ok(t), + Err(e) => { + *error = Some(e); + Err(Error::NotYetImplemented) + } + } + } +} + +impl<'io, 'buf, Io: io::Io> postcard::de_flavors::Flavor<'buf> for DeFlavor<'io, 'buf, Io> +where + Self: 'buf, +{ + type Remainder = Option; + + type Source = (); + + fn pop(&mut self) -> postcard::Result { + self.with_inner(|inner| inner.pop()) + } + + fn try_take_n(&mut self, ct: usize) -> postcard::Result<&'buf [u8]> { + self.with_inner(|inner| inner.try_take_n(ct)) + } + + fn finalize(self) -> postcard::Result { + Ok(self.error) + } + + fn try_take_n_temp<'a>(&'a mut self, ct: usize) -> postcard::Result<&'a [u8]> + where + 'buf: 'a, + { + self.with_inner(|inner| inner.try_take_n_temp(ct)) + } +} + +struct DeFlavorInner<'io, 'buf, Io> { + io: &'io mut Io, + alloc_buf: BumpAllocator<'buf>, + temp_buf: [u8; MAX_TEMP_LEN], +} + +impl<'io, 'buf, Io: io::Io> DeFlavorInner<'io, 'buf, Io> +where + Self: 'buf, +{ + fn pop(&mut self) -> Result { + let mut buf = [0u8]; + self.io + .read_exact(&mut buf) + .map_err(transport::DeserializeError::Io)?; + Ok(buf[0]) + } + + fn try_take_n(&mut self, ct: usize) -> Result<&'buf [u8], transport::DeserializeError> { + let buf = self + .alloc_buf + .allocate(ct) + .ok_or(transport::DeserializeError::BufferTooSmall)?; + self.io + .read_exact(buf) + .map_err(transport::DeserializeError::Io)?; + Ok(buf) + } + + fn try_take_n_temp<'a>(&'a mut self, ct: usize) -> Result<&'a [u8], transport::DeserializeError> + where + 'buf: 'a, + { + let buf = self + .temp_buf + .get_mut(0..ct) + .ok_or(transport::DeserializeError::BufferTooSmall)?; + self.io + .read_exact(buf) + .map_err(transport::DeserializeError::Io)?; + Ok(buf) + } +} + +struct BumpAllocator<'buf> { + start: *mut u8, + end: *mut u8, + phantom: PhantomData<&'buf mut [u8]>, +} + +impl<'buf> BumpAllocator<'buf> { + pub fn new(buf: &'buf mut [u8]) -> Self { + let Range { start, end } = buf.as_mut_ptr_range(); + Self { + start, + end, + phantom: PhantomData, + } + } + pub fn allocate(&mut self, n: usize) -> Option<&'buf mut [u8]> { + let bytes_left = (self.end as usize) - (self.start as usize); + if bytes_left < n { + return None; + } + unsafe { + let buf = core::slice::from_raw_parts_mut(self.start, n); + self.start = self.start.add(n); + Some(buf) + } + } +} diff --git a/crates/linux/process-backend/src/remote/wire/cstr_with_null.rs b/crates/linux/process-backend/src/remote/wire/cstr_with_null.rs new file mode 100644 index 00000000..d6179e37 --- /dev/null +++ b/crates/linux/process-backend/src/remote/wire/cstr_with_null.rs @@ -0,0 +1,72 @@ +use core::ffi::CStr; + +/// A version of &CStr that is serialized and deserialized with null terminator +/// +/// The default Serde `Serialize` for this serializes it without the NULL terminator, and as a +/// result `Deserialize` is only implemented for `CString` but not `&CStr` (since you can't have +/// a `&CStr` with no null terminator. +/// +/// This is a wrapper around &CStr that serializes and deserializes the null terminator - which is +/// what we need for directly making syscalls. +#[derive(Debug)] +#[repr(transparent)] +pub(crate) struct CStrWithNull<'a>(&'a CStr); + +impl<'a> From> for &'a CStr { + fn from(value: CStrWithNull<'a>) -> Self { + value.0 + } +} + +impl<'a> From<&'a CStr> for CStrWithNull<'a> { + fn from(value: &'a CStr) -> Self { + Self(value) + } +} + +impl<'a> core::ops::Deref for CStrWithNull<'a> { + type Target = CStr; + fn deref(&self) -> &Self::Target { + self.0 + } +} + +impl<'a> serde::Serialize for CStrWithNull<'a> { + fn serialize(&self, serializer: S) -> core::result::Result + where + S: serde::Serializer, + { + serializer.serialize_bytes(self.0.to_bytes_with_nul()) + } +} + +impl<'de: 'a, 'a> serde::Deserialize<'de> for CStrWithNull<'a> { + fn deserialize(deserializer: D) -> core::result::Result + where + D: serde::Deserializer<'de>, + { + struct Visitor<'a>(core::marker::PhantomData<&'a [u8]>); + + impl<'de: 'a, 'a> serde::de::Visitor<'de> for Visitor<'a> { + type Value = CStrWithNull<'a>; + + fn expecting(&self, formatter: &mut core::fmt::Formatter) -> core::fmt::Result { + write!( + formatter, + "a null-terminated byte slice borrowed from the Deserializer" + ) + } + + fn visit_borrowed_bytes(self, v: &'de [u8]) -> core::result::Result + where + E: serde::de::Error, + { + CStr::from_bytes_with_nul(v).map(CStrWithNull).map_err(|_| { + serde::de::Error::invalid_value(serde::de::Unexpected::Bytes(v), &self) + }) + } + } + + deserializer.deserialize_bytes(Visitor(core::marker::PhantomData)) + } +} diff --git a/crates/linux/process-backend/src/remote/wire/mod.rs b/crates/linux/process-backend/src/remote/wire/mod.rs new file mode 100644 index 00000000..b039a60f --- /dev/null +++ b/crates/linux/process-backend/src/remote/wire/mod.rs @@ -0,0 +1,190 @@ +use super as remote; +use crate::{ProcessReaderKind, regs::*, wrapper::Stat}; +use remote::transport; +use serde::{Deserialize, Serialize}; + +pub(crate) use cstr_with_null::CStrWithNull; + +mod cstr_with_null; + +pub(crate) trait Operation { + type Args<'args>: Deserialize<'args> + Serialize; + type Output<'output>: Deserialize<'output> + Serialize; + fn request<'output, T>( + transport: &'output mut T, + args: Self::Args<'_>, + ) -> Result, RemoteError>, transport::BackendError> + where + T: transport::Backend; +} + +// TEMPORARY - Will be the executor::Error type when the executor is implemented +pub(crate) type RemoteError = (); + +macro_rules! operations({ + $( + $(#[cfg($($cfg_tt: tt)*)])? + $name: ident($arg: ty) -> $output: ty, + )* +} => { + pub(crate) mod ops { + use super::*; + + $( + + $(#[cfg($($cfg_tt)*)])? + pub(crate) enum $name {} + + $(#[cfg($($cfg_tt)*)])? + impl Operation for $name { + type Args<'args> = $arg; + type Output<'output> = $output; + fn request<'output, T>( + transport: &'output mut T, + args: Self::Args<'_>, + ) -> Result, RemoteError>, transport::BackendError> + where + T: transport::Backend, + { + transport.send_request::<'output, Request, Result, RemoteError>>( + Request::$name(args), + ) + } + } + + )* + } + + pub(crate) trait HandlesRequests { + $( + $(#[cfg($($cfg_tt)*)])? + #[allow(non_snake_case)] + fn $name<'args, 'output>(self, args: $arg) -> Result<$output, RemoteError> + where + Self: 'output; + )* + } + + pub(crate) fn serve_request( + transport: &mut T, + handler: H, + ) -> Result<(), transport::ExecutorError> + where + T: transport::Executor, + H: HandlesRequests, + { + let request = transport.read_request::<'_, Request<'_>>()?; + match request { + $( + $(#[cfg($($cfg_tt)*)])? + Request::$name(args) => { + let response = HandlesRequests::$name(handler, args); + transport.write_response::::Output<'_>, RemoteError>>( + response, + ) + } + )* + } + } + + #[derive(Debug, Deserialize, Serialize)] + enum Request<'args> { + $( + $(#[cfg($($cfg_tt)*)])? + $name(#[serde(borrow)] ::Args<'args>), + )* + } +}); + +#[derive(Debug, Deserialize, Serialize)] +pub(crate) struct FileReaderArgs { + pub(crate) handle: usize, + pub(crate) requested_len: usize, +} + +#[derive(Debug, Deserialize, Serialize)] +pub(crate) struct ProcessReaderReadAtArgs { + pub(crate) address: usize, + pub(crate) requested_len: usize, +} + +#[derive(Debug, Deserialize, Serialize)] +pub(crate) struct MappedModuleMemoryReaderOpenArgs<'args> { + #[serde(borrow)] + pub(crate) path: CStrWithNull<'args>, + pub(crate) offset: u64, +} + +#[derive(Debug, Deserialize, Serialize)] +pub(crate) struct MappedModuleMemoryReaderReadAtArgs { + pub(crate) handle: usize, + pub(crate) offset: usize, + pub(crate) requested_len: usize, +} + +#[derive(Debug, Deserialize, Serialize)] +pub(crate) struct ReadLinkArgs<'args> { + #[serde(borrow)] + pub(crate) path: CStrWithNull<'args>, + pub(crate) requested_len: usize, +} + +// Notes on buffer sizing: +// +// The size of buffer-based arguments in these operations are limited by the +// `executor::Transport` argument buffer, the `backend::Transport` output buffer, and +// the `Executor` scratch buffer. +// +// Currently: +// +// The length of the argument buffer limits the maximum path length for: +// read_file(), read_dir(), map_module_into_memory(), stat_file(), read_link() +// +// Sizing the Executor scratch buffer larger than the Backend output buffer is a bad idea - it +// means the Executor could request more bytes than the Backend is capable of holding, causing a +// `BufferTooSmall` error. Sizing the scratch buffer smaller is fine, but then the output buffer +// would never be totally utilized. +// +// So, in practice, the scratch buffer and output buffer should be the same size. +// +// The length of both limits the maximum readable path length for: +// DirReader::read(), read_link() +// +// TL;DR +// +// The argument buffer should be the maximum length of a path you'd ever request for any operation +// that takes a path. +// +// The output buffer should be the maximum length of any directory name you'd expect returned +// by read_dir() and any path you'd ever expect returned by read_link(). + +operations! { + GetPid(()) -> libc::pid_t, + ProcessReaderReadAt(ProcessReaderReadAtArgs) -> &'output [u8], + StopProcess(()) -> (), + ContinueProcess(()) -> (), + SuspendThread(libc::pid_t) -> (), + ResumeThread(libc::pid_t) -> (), + MappedModuleMemoryReaderOpen(MappedModuleMemoryReaderOpenArgs<'args>) -> usize, + MappedModuleMemoryReaderReadAt(MappedModuleMemoryReaderReadAtArgs) -> &'output [u8], + MappedModuleMemoryReaderLen(usize) -> usize, + MappedModuleMemoryReaderClose(usize) -> (), + StatFile(CStrWithNull<'args>) -> Stat, + FileReaderOpen(CStrWithNull<'args>) -> usize, + FileReaderRead(FileReaderArgs) -> &'output [u8], + FileReaderClose(usize) -> (), + DirReaderOpen(CStrWithNull<'args>) -> usize, + DirReaderRead(usize) -> Option<&'output [u8]>, + DirReaderClose(usize) -> (), + ReadLink(ReadLinkArgs<'args>) -> &'output [u8], + GetGenRegs(libc::pid_t) -> GenRegs, + GetFpRegs(libc::pid_t) -> FpRegs, + #[cfg(target_arch = "x86")] + GetFpxRegs(libc::pid_t) -> FpxRegs, + #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] + PtracePeekUser(usize) -> [u8; crate::PTRACE_DATA_LEN], + ForceProcessReaderKind(ProcessReaderKind) -> (), + #[cfg(feature = "testing")] + FailOneSyscallWith(core::ffi::c_int) -> (), + Quit(()) -> (), +} From b362d07d990aa6d0864a0af66965deb6e1d78070 Mon Sep 17 00:00:00 2001 From: Chris Martin Date: Tue, 1 Sep 2026 16:10:16 -0400 Subject: [PATCH 2/2] Address review feedback from Gabriele --- crates/linux/process-backend/src/remote/io/unix_stream.rs | 4 ++++ crates/linux/process-backend/src/remote/transport/postcard.rs | 3 +++ 2 files changed, 7 insertions(+) diff --git a/crates/linux/process-backend/src/remote/io/unix_stream.rs b/crates/linux/process-backend/src/remote/io/unix_stream.rs index 0bc373da..8c9793d6 100644 --- a/crates/linux/process-backend/src/remote/io/unix_stream.rs +++ b/crates/linux/process-backend/src/remote/io/unix_stream.rs @@ -17,15 +17,18 @@ impl UnixStream { let stream1 = unsafe { Self::from_raw_fd(fds[1]) }; Ok((stream0, stream1)) } + /// # Safety /// /// 'fd' must be a valid UNIX stream socket pub unsafe fn from_raw_fd(fd: c_int) -> Self { unsafe { Self(OwnedFd::new(fd)) } } + pub fn as_raw_fd(&self) -> c_int { self.0.as_raw_fd() } + fn read(&mut self, buf: &mut [u8]) -> Result { let rv = unsafe { libc::read(self.0.as_raw_fd(), buf.as_mut_ptr().cast(), buf.len()) }; if rv == -1 { @@ -34,6 +37,7 @@ impl UnixStream { let bytes_read = usize::try_from(rv).unwrap(); Ok(bytes_read) } + fn write(&mut self, buf: &[u8]) -> Result { let rv = unsafe { libc::write(self.0.as_raw_fd(), buf.as_ptr().cast(), buf.len()) }; if rv == -1 { diff --git a/crates/linux/process-backend/src/remote/transport/postcard.rs b/crates/linux/process-backend/src/remote/transport/postcard.rs index d9fdd056..7aefc7c1 100644 --- a/crates/linux/process-backend/src/remote/transport/postcard.rs +++ b/crates/linux/process-backend/src/remote/transport/postcard.rs @@ -33,6 +33,7 @@ where fn max_response_output_len(&self) -> usize { self.output_buf.as_ref().len() } + fn send_request<'output, Req: Serialize, Resp: Deserialize<'output>>( &'output mut self, req: Req, @@ -183,6 +184,7 @@ impl<'io, 'buf, Io> DeFlavor<'io, 'buf, Io> { error: None, } } + fn with_inner<'a, F, T>(&'a mut self, f: F) -> postcard::Result where F: FnOnce(&'a mut DeFlavorInner<'io, 'buf, Io>) -> Result, @@ -286,6 +288,7 @@ impl<'buf> BumpAllocator<'buf> { phantom: PhantomData, } } + pub fn allocate(&mut self, n: usize) -> Option<&'buf mut [u8]> { let bytes_left = (self.end as usize) - (self.start as usize); if bytes_left < n {