diff --git a/Cargo.lock b/Cargo.lock index 6959d0881..a820eef4e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1448,6 +1448,7 @@ dependencies = [ name = "fspy_shared_unix" version = "0.0.0" dependencies = [ + "allocator-api2", "anyhow", "base64", "bstr", diff --git a/crates/fspy/src/unix/mod.rs b/crates/fspy/src/unix/mod.rs index aca972420..e1c9eb92d 100644 --- a/crates/fspy/src/unix/mod.rs +++ b/crates/fspy/src/unix/mod.rs @@ -11,11 +11,9 @@ use fspy_seccomp_unotify::supervisor::supervise; use fspy_shared::ipc::PathAccess; #[cfg(not(target_env = "musl"))] use fspy_shared::ipc::{IpcStr, channel::channel}; -#[cfg(target_os = "macos")] -use fspy_shared_unix::payload::Artifacts; use fspy_shared_unix::{ exec::ExecResolveConfig, - payload::{Payload, encode_payload}, + payload::{EncodedPayload, Payload, encode_payload}, spawn::handle_exec, }; use futures_util::FutureExt; @@ -29,9 +27,15 @@ use crate::ipc::{OwnedReceiverLockGuard, SHM_CAPACITY}; use crate::{ChildTermination, Command, TrackedChild, arena::PathAccessArena, error::SpawnError}; #[derive(Debug)] +#[cfg_attr( + target_os = "macos", + expect(clippy::struct_field_names, reason = "each field names a distinct injected path") +)] pub struct SpyImpl { #[cfg(target_os = "macos")] - artifacts: Artifacts, + bash_path: Box, + #[cfg(target_os = "macos")] + coreutils_path: Box, #[cfg(not(target_env = "musl"))] preload_path: Box, @@ -58,15 +62,19 @@ impl SpyImpl { #[cfg(not(target_env = "musl"))] preload_path, #[cfg(target_os = "macos")] - artifacts: { - let coreutils_path = - macos_artifacts::COREUTILS_BINARY.materialize().executable().at(dir)?; - let bash_path = macos_artifacts::OILS_BINARY.materialize().executable().at(dir)?; - Artifacts { - bash_path: bash_path.as_path().into(), - coreutils_path: coreutils_path.as_path().into(), - } - }, + bash_path: macos_artifacts::OILS_BINARY + .materialize() + .executable() + .at(dir)? + .as_path() + .into(), + #[cfg(target_os = "macos")] + coreutils_path: macos_artifacts::COREUTILS_BINARY + .materialize() + .executable() + .at(dir)? + .as_path() + .into(), }) } @@ -79,24 +87,30 @@ impl SpyImpl { let supervisor = supervise::().map_err(SpawnError::Supervisor)?; #[cfg(not(target_env = "musl"))] - let (ipc_channel_conf, ipc_receiver) = channel(SHM_CAPACITY, allocator_api2::alloc::Global) + let ipc_receiver = channel(SHM_CAPACITY, allocator_api2::alloc::Global) .map_err(SpawnError::ChannelCreation)?; let payload = Payload { #[cfg(not(target_env = "musl"))] - ipc_channel_conf, - - #[cfg(target_os = "macos")] - artifacts: self.artifacts.clone(), + ipc_channel_conf: ipc_receiver.conf(), + #[cfg(target_env = "musl")] + ipc_channel_conf: core::marker::PhantomData, #[cfg(not(target_env = "musl"))] - preload_path: self.preload_path.clone(), + preload_path: &self.preload_path, + + #[cfg(target_os = "macos")] + artifacts: fspy_shared_unix::payload::Artifacts { + bash_path: &self.bash_path, + coreutils_path: &self.coreutils_path, + }, #[cfg(target_os = "linux")] seccomp_payload: supervisor.payload().clone(), }; - let encoded_payload = encode_payload(payload); + let encoded_string = encode_payload(&payload); + let encoded_payload = EncodedPayload { payload, encoded_string: encoded_string.as_ref() }; let mut exec = command.get_exec(); let mut exec_resolve_accesses = PathAccessArena::default(); diff --git a/crates/fspy/src/windows/mod.rs b/crates/fspy/src/windows/mod.rs index 395975890..dd403429a 100644 --- a/crates/fspy/src/windows/mod.rs +++ b/crates/fspy/src/windows/mod.rs @@ -86,7 +86,7 @@ impl SpyImpl { command.creation_flags(CREATE_SUSPENDED); - let (channel_conf, receiver) = channel(SHM_CAPACITY, allocator_api2::alloc::Global) + let receiver = channel(SHM_CAPACITY, allocator_api2::alloc::Global) .map_err(SpawnError::ChannelCreation)?; let mut spawn_success = false; @@ -107,7 +107,7 @@ impl SpyImpl { } let payload = Payload { - channel_conf: channel_conf.clone(), + channel_conf: receiver.conf(), ansi_dll_path_with_nul: ansi_dll_path_with_nul.to_bytes(), }; let payload_bytes = wincode::serialize(&payload).unwrap(); diff --git a/crates/fspy_client_unix/src/lib.rs b/crates/fspy_client_unix/src/lib.rs index 493b87a39..f4ea39679 100644 --- a/crates/fspy_client_unix/src/lib.rs +++ b/crates/fspy_client_unix/src/lib.rs @@ -11,6 +11,7 @@ pub mod raw_exec; use std::{ffi::OsStr, fmt::Debug, num::NonZeroUsize, os::unix::ffi::OsStrExt as _, path::Path}; use convert::{ToAbsolutePath, ToAccessMode}; +use fspy_nostd_alloc::BumpAllocator as _; use fspy_shared::ipc::{PathAccess, channel::Sender}; use fspy_shared_unix::{ exec::ExecResolveConfig, @@ -21,7 +22,7 @@ use raw_exec::RawExec; use wincode::Serialize as _; pub struct Client { - encoded_payload: EncodedPayload, + encoded_payload: EncodedPayload<'static>, ipc_sender: Option, } @@ -51,21 +52,28 @@ impl Client { reason = "the client intentionally reports an unavailable supervisor channel" )] pub fn from_env(envs: impl Iterator) -> Self { - let encoded_payload = decode_payload_from_env(envs).unwrap(); - - let ipc_sender = match encoded_payload - .payload - .ipc_channel_conf - .sender(fspy_nostd_alloc::pooled_bump()) - { - Ok(sender) => Some(sender), - Err(err) => { - // This can happen if the process starts after the root target - // has exited and the receiver has closed the channel. - eprintln!("fspy: failed to create ipc sender: {err}"); - None + // One page-backed bump serves the whole attach: the payload's + // process-lifetime storage first, then the scoped temporaries below — + // one mapping unless the payload outgrows the first chunk. The + // `ManuallyDrop` is never taken apart, so the bump is never dropped. + let mut bump = core::mem::ManuallyDrop::new(fspy_nostd_alloc::page_bump()); + let encoded_payload = decode_payload_from_env(envs, &*bump).unwrap(); + // SAFETY: `bump` is `ManuallyDrop` and never dropped, so the storage + // the payload borrows from is never freed, and the scoped temporaries + // below reset only down to this point. + let encoded_payload = unsafe { encoded_payload.assume_process_lifetime() }; + + let ipc_sender = bump.scoped(|scope| { + match encoded_payload.payload.ipc_channel_conf.sender(&*scope) { + Ok(sender) => Some(sender), + Err(err) => { + // This can happen if the process starts after the root target + // has exited and the receiver has closed the channel. + eprintln!("fspy: failed to create ipc sender: {err}"); + None + } } - }; + }); Self { encoded_payload, ipc_sender } } diff --git a/crates/fspy_nostd_alloc/src/lib.rs b/crates/fspy_nostd_alloc/src/lib.rs index 868de9818..b2450e3bd 100644 --- a/crates/fspy_nostd_alloc/src/lib.rs +++ b/crates/fspy_nostd_alloc/src/lib.rs @@ -9,7 +9,8 @@ //! fixed-size chunks; and `bump_scope::Bump`s on top. [`pooled_bump`] //! creates a bump that draws its chunks from the pool and returns them on //! drop, so frequent short-lived bumps reuse memory instead of paying two -//! syscalls each. +//! syscalls each. [`page_bump`] bypasses the pool for bumps whose chunks +//! must never be recycled into other bumps. #![cfg_attr(not(test), no_std)] @@ -23,7 +24,8 @@ mod pool; mod virtual_alloc; use allocator_api2::alloc::Allocator; -/// The bump interface [`pooled_bump`] returns, re-exported so callers can +/// The bump interface [`page_bump`] and [`pooled_bump`] return, +/// re-exported so callers can /// name the bound and call its methods without a direct bump-scope /// dependency. pub use bump_scope::traits::BumpAllocator; @@ -87,6 +89,23 @@ impl Default for &'static ChunkPool::WithGuaranteedAllocated; + +/// Creates an empty bump allocator that draws whole pages straight from +/// the kernel — never from the chunk pool — so its memory is never +/// recycled into other bumps. +/// +/// Creating it allocates nothing; the first allocation maps one chunk, and +/// further chunks are mapped only if the data outgrows it. Dropping the +/// bump frees its chunks; leaking it instead makes its allocations +/// permanent. +#[must_use] +pub const fn page_bump() -> impl BumpAllocator + Allocator { + Bump::, PageBumpSettings>::unallocated() +} + /// Creates a fresh bump backed by the process-wide chunk pool. /// /// Creating the bump allocates nothing; the first allocation grabs a whole diff --git a/crates/fspy_preload_windows/src/windows/client.rs b/crates/fspy_preload_windows/src/windows/client.rs index c115e057f..b649a0204 100644 --- a/crates/fspy_preload_windows/src/windows/client.rs +++ b/crates/fspy_preload_windows/src/windows/client.rs @@ -9,6 +9,7 @@ use winapi::{shared::minwindef::BOOL, um::winnt::HANDLE}; pub struct Client<'a> { payload: Payload<'a>, + payload_bytes: &'a [u8], ipc_sender: Option, } @@ -33,7 +34,7 @@ impl<'a> Client<'a> { } }; - Self { payload, ipc_sender } + Self { payload, payload_bytes, ipc_sender } } pub fn send(&self, access: PathAccess<'_>) { @@ -44,14 +45,15 @@ impl<'a> Client<'a> { } pub unsafe fn prepare_child_process(&self, child_handle: HANDLE) -> BOOL { - let payload_bytes = wincode::serialize(&self.payload).unwrap(); + // The payload propagates to children unchanged, so forward the bytes + // this process was given instead of re-serializing. // SAFETY: FFI call to DetourCopyPayloadToProcess with valid handle and payload buffer unsafe { DetourCopyPayloadToProcess( child_handle, &PAYLOAD_ID, - payload_bytes.as_ptr().cast(), - payload_bytes.len().try_into().unwrap(), + self.payload_bytes.as_ptr().cast(), + self.payload_bytes.len().try_into().unwrap(), ) } } diff --git a/crates/fspy_shared/src/ipc/channel/mod.rs b/crates/fspy_shared/src/ipc/channel/mod.rs index ba7d32e02..748824b64 100644 --- a/crates/fspy_shared/src/ipc/channel/mod.rs +++ b/crates/fspy_shared/src/ipc/channel/mod.rs @@ -25,23 +25,27 @@ use super::IpcStr; const SHM_BACKING_PREFIX: &str = "vite-task-fspy-"; /// Serializable configuration to create channel senders. -#[derive(SchemaWrite, SchemaRead, Clone, Debug)] -pub struct ChannelConf { - lock_file_path: Box, - shm_id: Box, +/// +/// A conf is a view: it borrows the paths the [`Receiver`] owns (or, in a +/// receiving process, the payload bytes it was deserialized from), so +/// materializing and serializing one allocates nothing. +#[derive(SchemaWrite, SchemaRead, Clone, Copy, Debug)] +pub struct ChannelConf<'a> { + lock_file_path: &'a IpcStr, + shm_id: &'a IpcStr, } -/// Creates a mpsc IPC channel with one receiver and a `ChannelConf` that can be passed around processes and used to create multiple senders +/// Creates a mpsc IPC channel and returns its receiver. [`Receiver::conf`] +/// derives the serializable configuration that other processes use to create +/// senders. #[expect(clippy::missing_errors_doc, reason = "non-vt crate: cannot use vt_str/vt_path types")] -pub fn channel( - capacity: usize, - allocator: A, -) -> io::Result<(ChannelConf, Receiver)> { +pub fn channel(capacity: usize, allocator: A) -> io::Result> { // Initialize the lock file with a unique name. `temp_dir` reflects // `TMPDIR` verbatim, which may be relative; the path travels to processes // with other working directories, so resolve it first. let lock_file_path = std::path::absolute(temp_dir())?.join(format!("fspy_ipc_{}.lock", Uuid::new_v4())); + let lock_c_path = os_c_string(lock_file_path.as_os_str(), allocator.clone())?; let shm_c_path = os_c_string(shm_backing_path()?.as_os_str(), allocator)?; let handle = @@ -50,13 +54,7 @@ pub fn channel( let keeper = ShmKeeper { path: shm_c_path }; let mapping = handle.map().map_err(shm_error_to_io)?; - let conf = ChannelConf { - lock_file_path: lock_file_path.as_os_str().into(), - shm_id: IpcStr::from_os_c_str(keeper.path.as_c_str()).to_boxed(), - }; - - let receiver = Receiver::new(lock_file_path, keeper, mapping)?; - Ok((conf, receiver)) + Receiver::new(lock_file_path, lock_c_path, keeper, mapping) } /// Encodes `path` as an owned NUL-terminated platform C string. @@ -144,7 +142,7 @@ impl Drop for ShmKeeper { } } -impl ChannelConf { +impl ChannelConf<'_> { /// Creates a sender. /// /// This doesn't block on the file lock. Instead it returns immediately with error if the receiver is locked or dropped. @@ -169,7 +167,7 @@ impl ChannelConf { // pointer and size. Exclusive write access is ensured by the shared // file lock held by this sender. let writer = unsafe { ShmWriter::new(mapping) }; - Ok(Sender { writer, lock_file, lock_file_path: self.lock_file_path.clone() }) + Ok(Sender { writer, lock_file, lock_file_path: self.lock_file_path.to_boxed() }) } } @@ -206,10 +204,12 @@ unsafe impl Sync for Sender {} /// Owns the lock file and removes it on drop. pub struct Receiver { lock_file_path: PathBuf, + /// The lock file's path in the form [`Self::conf`] serializes. + lock_c_path: OsCString, lock_file: File, /// Keeps the shared memory's backing file alive for as long as senders /// may attach. - _keeper: ShmKeeper, + keeper: ShmKeeper, mapping: Mapping, } @@ -228,9 +228,24 @@ impl Drop for Receiver { } impl Receiver { - fn new(lock_file_path: PathBuf, keeper: ShmKeeper, mapping: Mapping) -> io::Result { + fn new( + lock_file_path: PathBuf, + lock_c_path: OsCString, + keeper: ShmKeeper, + mapping: Mapping, + ) -> io::Result { let lock_file = File::create(&lock_file_path)?; - Ok(Self { lock_file_path, lock_file, _keeper: keeper, mapping }) + Ok(Self { lock_file_path, lock_c_path, lock_file, keeper, mapping }) + } + + /// Returns the serializable configuration other processes pass to + /// [`ChannelConf::sender`], borrowing this receiver's storage. + #[must_use] + pub fn conf(&self) -> ChannelConf<'_> { + ChannelConf { + lock_file_path: IpcStr::from_os_c_str(self.lock_c_path.as_c_str()), + shm_id: IpcStr::from_os_c_str(self.keeper.path.as_c_str()), + } } /// Lock the shared memory for unique read access. @@ -279,17 +294,24 @@ mod tests { use super::*; + /// Deserializes a [`ChannelConf`] view borrowed from serialized bytes, as + /// a child process would from its payload. + fn conf_from(bytes: &[u8]) -> wincode::ReadResult> { + wincode::deserialize(bytes) + } + /// The shared-memory path is generated absolute, so a sender in a process /// with a different working directory and a relative temporary directory /// must still attach. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn sender_ignores_changed_temp_and_working_directory() { - let (conf, receiver) = channel(100, Global).unwrap(); + let receiver = channel(100, Global).unwrap(); + let conf = wincode::serialize(&receiver.conf()).unwrap(); let changed_cwd = temp_dir().join(format!("fspy-ipc-changed-cwd-{}", Uuid::new_v4())); fs::create_dir(&changed_cwd).unwrap(); - let mut command = command_for_fn!(conf, |conf: ChannelConf| { - let sender = conf.sender(Global).unwrap(); + let mut command = command_for_fn!(conf, |conf: Vec| { + let sender = conf_from(&conf).unwrap().sender(Global).unwrap(); let frame_size = NonZeroUsize::new(2).unwrap(); let mut frame = sender.claim_frame(frame_size).unwrap(); frame.copy_from_slice(&[4, 2]); @@ -308,9 +330,10 @@ mod tests { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn smoke() { - let (conf, receiver) = channel(100, Global).unwrap(); - let cmd = command_for_fn!(conf, |conf: ChannelConf| { - let sender = conf.sender(Global).unwrap(); + let receiver = channel(100, Global).unwrap(); + let conf = wincode::serialize(&receiver.conf()).unwrap(); + let cmd = command_for_fn!(conf, |conf: Vec| { + let sender = conf_from(&conf).unwrap().sender(Global).unwrap(); let frame_size = NonZeroUsize::new(2).unwrap(); let mut frame = sender.claim_frame(frame_size).unwrap(); frame.copy_from_slice(&[4, 2]); @@ -329,11 +352,12 @@ mod tests { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[expect(clippy::print_stdout, reason = "test diagnostics")] async fn forbid_new_senders_after_locked() { - let (conf, receiver) = channel(42, Global).unwrap(); + let receiver = channel(42, Global).unwrap(); + let conf = wincode::serialize(&receiver.conf()).unwrap(); let _lock = receiver.lock().unwrap(); - let cmd = command_for_fn!(conf, |conf: ChannelConf| { - print!("{}", conf.sender(Global).is_ok()); + let cmd = command_for_fn!(conf, |conf: Vec| { + print!("{}", conf_from(&conf).unwrap().sender(Global).is_ok()); }); let output = std::process::Command::from(cmd).output().unwrap(); assert_eq!(B(&output.stdout), B("false")); @@ -342,11 +366,12 @@ mod tests { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[expect(clippy::print_stdout, reason = "test diagnostics")] async fn forbid_new_senders_after_receiver_dropped() { - let (conf, receiver) = channel(42, Global).unwrap(); + let receiver = channel(42, Global).unwrap(); + let conf = wincode::serialize(&receiver.conf()).unwrap(); drop(receiver); - let cmd = command_for_fn!(conf, |conf: ChannelConf| { - print!("{}", conf.sender(Global).is_ok()); + let cmd = command_for_fn!(conf, |conf: Vec| { + print!("{}", conf_from(&conf).unwrap().sender(Global).is_ok()); }); let output = std::process::Command::from(cmd).output().unwrap(); assert_eq!(B(&output.stdout), B("false")); @@ -354,10 +379,11 @@ mod tests { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn concurrent_senders() { - let (conf, receiver) = channel(8192, Global).unwrap(); + let receiver = channel(8192, Global).unwrap(); + let conf = wincode::serialize(&receiver.conf()).unwrap(); for i in 0u16..200 { - let cmd = command_for_fn!((conf.clone(), i), |(conf, i): (ChannelConf, u16)| { - let sender = conf.sender(Global).unwrap(); + let cmd = command_for_fn!((conf.clone(), i), |(conf, i): (Vec, u16)| { + let sender = conf_from(&conf).unwrap().sender(Global).unwrap(); let data_to_send = i.to_string(); sender .claim_frame(NonZeroUsize::new(data_to_send.len()).unwrap()) diff --git a/crates/fspy_shared/src/windows/mod.rs b/crates/fspy_shared/src/windows/mod.rs index cf7c536be..b2700c122 100644 --- a/crates/fspy_shared/src/windows/mod.rs +++ b/crates/fspy_shared/src/windows/mod.rs @@ -22,6 +22,6 @@ DEFINE_GUID!( #[derive(SchemaWrite, SchemaRead, Debug, Clone)] pub struct Payload<'a> { - pub channel_conf: ChannelConf, + pub channel_conf: ChannelConf<'a>, pub ansi_dll_path_with_nul: &'a [u8], } diff --git a/crates/fspy_shared_unix/Cargo.toml b/crates/fspy_shared_unix/Cargo.toml index 9cf964474..7ae6990b0 100644 --- a/crates/fspy_shared_unix/Cargo.toml +++ b/crates/fspy_shared_unix/Cargo.toml @@ -6,6 +6,7 @@ license.workspace = true publish = false [target.'cfg(unix)'.dependencies] +allocator-api2 = { workspace = true, features = ["alloc"] } anyhow = { workspace = true } base64 = { workspace = true } wincode = { workspace = true, features = ["derive"] } diff --git a/crates/fspy_shared_unix/src/payload.rs b/crates/fspy_shared_unix/src/payload.rs index 14f30229a..b0c06abf8 100644 --- a/crates/fspy_shared_unix/src/payload.rs +++ b/crates/fspy_shared_unix/src/payload.rs @@ -1,21 +1,31 @@ +use allocator_api2::alloc::Allocator; use base64::{Engine as _, prelude::BASE64_STANDARD_NO_PAD}; -use bstr::BString; +use bstr::{BStr, BString}; #[cfg(not(target_env = "musl"))] use fspy_shared::ipc::IpcStr; #[cfg(not(target_env = "musl"))] use fspy_shared::ipc::channel::ChannelConf; use wincode::{SchemaRead, SchemaWrite}; +/// The payload as it travels between processes. +/// +/// A payload is a view: every path in it borrows from storage its producer +/// owns — the supervisor's session state, or the storage a +/// [`decode_payload_from_env`] caller supplies — so serializing one, and +/// deserializing one in the preload, allocates nothing for the borrowed +/// fields. #[derive(Debug, SchemaWrite, SchemaRead)] -pub struct Payload { +pub struct Payload<'a> { #[cfg(not(target_env = "musl"))] - pub ipc_channel_conf: ChannelConf, + pub ipc_channel_conf: ChannelConf<'a>, + #[cfg(target_env = "musl")] + pub ipc_channel_conf: core::marker::PhantomData<&'a ()>, #[cfg(not(target_env = "musl"))] - pub preload_path: Box, + pub preload_path: &'a IpcStr, #[cfg(target_os = "macos")] - pub artifacts: Artifacts, + pub artifacts: Artifacts<'a>, #[cfg(target_os = "linux")] #[cfg_attr( @@ -26,17 +36,39 @@ pub struct Payload { } #[cfg(target_os = "macos")] -#[derive(Debug, SchemaWrite, SchemaRead, Clone)] -pub struct Artifacts { - pub bash_path: Box, - pub coreutils_path: Box, +#[derive(Debug, SchemaWrite, SchemaRead, Clone, Copy)] +pub struct Artifacts<'a> { + pub bash_path: &'a IpcStr, + pub coreutils_path: &'a IpcStr, } pub(crate) const PAYLOAD_ENV_NAME: &str = "FSPY_PAYLOAD"; -pub struct EncodedPayload { - pub payload: Payload, - pub encoded_string: BString, +/// A payload together with its encoded form, for handing to child processes. +/// +/// Like [`Payload`], this is strictly a view: it lives as long as the +/// storage [`decode_payload_from_env`] allocated into. A caller that leaks +/// that storage for the rest of the process extends the view accordingly +/// with [`assume_process_lifetime`](Self::assume_process_lifetime). +pub struct EncodedPayload<'a> { + pub payload: Payload<'a>, + pub encoded_string: &'a BStr, +} + +impl EncodedPayload<'_> { + /// Extends this payload's borrows to the process lifetime. + /// + /// # Safety + /// + /// The storage this payload borrows from must never be freed for the + /// rest of the process — for example, allocations from a bump allocator + /// that is leaked instead of dropped. + #[must_use] + pub unsafe fn assume_process_lifetime(self) -> EncodedPayload<'static> { + // SAFETY: the types differ only in their lifetime parameter, and the + // caller guarantees the backing storage is never freed. + unsafe { core::mem::transmute(self) } + } } /// Encodes the fspy payload into a base64 string for transmission via environment variable @@ -45,24 +77,42 @@ pub struct EncodedPayload { /// /// Panics if serialization fails, which should never happen for valid `Payload` structs. #[must_use] -pub fn encode_payload(payload: Payload) -> EncodedPayload { - let bytes = wincode::serialize(&payload).unwrap(); - let encoded_string = BASE64_STANDARD_NO_PAD.encode(&bytes); - EncodedPayload { payload, encoded_string: encoded_string.into() } +pub fn encode_payload(payload: &Payload<'_>) -> BString { + let bytes = wincode::serialize(payload).unwrap(); + BASE64_STANDARD_NO_PAD.encode(&bytes).into() } /// Decodes the fspy payload from an iterator over environment entries. /// +/// The returned payload borrows allocations made from `allocator` that are +/// never individually freed, so the allocator's lifetime bounds the +/// payload's: a borrowed bump reclaims the storage wholesale when it drops, +/// and a caller that instead leaks the bump can extend the payload to the +/// process lifetime with +/// [`assume_process_lifetime`](EncodedPayload::assume_process_lifetime). +/// +/// The payload never borrows the process environment itself: the +/// environment is not stable storage (any `setenv` may move or rewrite it), +/// so the encoded value is copied out before it is used. +/// /// # Errors /// /// Returns an error if the payload environment variable is missing, base64 /// decoding fails, or deserialization fails. -pub fn decode_payload_from_env( +pub fn decode_payload_from_env<'a, A: Allocator + Clone + 'a>( mut envs: impl Iterator, -) -> anyhow::Result { + allocator: A, +) -> anyhow::Result> { let Some(encoded_string) = envs.find_map(|(name, value)| { if AsRef::<[u8]>::as_ref(name) == PAYLOAD_ENV_NAME.as_bytes() { - value.map(|value| BString::from(value.as_units())) + value.map(|value| { + let mut encoded = allocator_api2::vec::Vec::with_capacity_in( + value.as_units().len(), + allocator.clone(), + ); + encoded.extend_from_slice(value.as_units()); + BStr::new(encoded.leak()) + }) } else { None } @@ -70,11 +120,12 @@ pub fn decode_payload_from_env( anyhow::bail!("Environment variable '{PAYLOAD_ENV_NAME}' not found"); }; - decode_payload(encoded_string) -} + let decoded_len_estimate = base64::decoded_len_estimate(encoded_string.len()); + let mut buffer = allocator_api2::vec::Vec::with_capacity_in(decoded_len_estimate, allocator); + buffer.resize(decoded_len_estimate, 0); + let decoded_len = BASE64_STANDARD_NO_PAD.decode_slice(encoded_string, &mut buffer)?; + buffer.truncate(decoded_len); + let payload: Payload<'a> = wincode::deserialize_exact(buffer.leak())?; -fn decode_payload(encoded_string: BString) -> anyhow::Result { - let bytes = BASE64_STANDARD_NO_PAD.decode(&encoded_string)?; - let payload: Payload = wincode::deserialize_exact(&bytes)?; Ok(EncodedPayload { payload, encoded_string }) } diff --git a/crates/fspy_shared_unix/src/spawn/linux/mod.rs b/crates/fspy_shared_unix/src/spawn/linux/mod.rs index d3197da00..1bac0529e 100644 --- a/crates/fspy_shared_unix/src/spawn/linux/mod.rs +++ b/crates/fspy_shared_unix/src/spawn/linux/mod.rs @@ -53,7 +53,7 @@ pub fn handle_exec( LD_PRELOAD, encoded_payload.payload.preload_path.as_os_str().as_bytes(), ); - ensure_env(&mut command.envs, PAYLOAD_ENV_NAME, &encoded_payload.encoded_string)?; + ensure_env(&mut command.envs, PAYLOAD_ENV_NAME, encoded_payload.encoded_string)?; return Ok(None); } } diff --git a/crates/fspy_shared_unix/src/spawn/macos.rs b/crates/fspy_shared_unix/src/spawn/macos.rs index 88bd7d0a4..eeab53e41 100644 --- a/crates/fspy_shared_unix/src/spawn/macos.rs +++ b/crates/fspy_shared_unix/src/spawn/macos.rs @@ -69,7 +69,7 @@ pub fn handle_exec( DYLD_INSERT_LIBRARIES, encoded_payload.payload.preload_path.as_os_str().as_bytes(), ); - ensure_env(&mut command.envs, PAYLOAD_ENV_NAME, &encoded_payload.encoded_string)?; + ensure_env(&mut command.envs, PAYLOAD_ENV_NAME, encoded_payload.encoded_string)?; } else { command.envs.retain(|(name, _)| { name != DYLD_INSERT_LIBRARIES && name != PAYLOAD_ENV_NAME.as_bytes()