diff --git a/Cargo.lock b/Cargo.lock index 35ce5bdd0..5896568a3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1231,6 +1231,7 @@ dependencies = [ name = "fspy" version = "0.1.0" dependencies = [ + "allocator-api2", "anyhow", "bstr", "bumpalo", @@ -1382,9 +1383,11 @@ dependencies = [ name = "fspy_preload_windows" version = "0.1.0" dependencies = [ + "allocator-api2", "constcat", "fspy_detours_sys", "fspy_nostd", + "fspy_nostd_alloc", "fspy_shared", "ntapi", "smallvec 2.0.0-alpha.12", diff --git a/crates/fspy/Cargo.toml b/crates/fspy/Cargo.toml index 59ce55cc9..5bdc61b5f 100644 --- a/crates/fspy/Cargo.toml +++ b/crates/fspy/Cargo.toml @@ -6,6 +6,7 @@ license.workspace = true publish = false [dependencies] +allocator-api2 = { workspace = true, features = ["alloc"] } wincode = { workspace = true } bstr = { workspace = true, features = ["alloc", "std"] } bumpalo = { workspace = true } diff --git a/crates/fspy/src/ipc.rs b/crates/fspy/src/ipc.rs index da8f7828d..6d15bf436 100644 --- a/crates/fspy/src/ipc.rs +++ b/crates/fspy/src/ipc.rs @@ -1,3 +1,4 @@ +use allocator_api2::alloc::Global; use fspy_shared::ipc::{ PathAccess, channel::{FrameReader, Receiver}, @@ -37,7 +38,7 @@ pub struct ChannelAccesses { frames: FrameReader, } -impl TryFrom for ChannelAccesses { +impl TryFrom> for ChannelAccesses { type Error = TrackingIncomplete; /// Closes the channel and takes every record it collected. @@ -52,7 +53,7 @@ impl TryFrom for ChannelAccesses { /// [`TrackingIncomplete`] when a tracked process could not record /// something it went on to do. What did arrive is then a subset of /// what the run really touched, so none of it is handed back. - fn try_from(receiver: Receiver) -> Result { + fn try_from(receiver: Receiver) -> Result { Ok(Self { frames: receiver.close().map_err(|_| TrackingIncomplete)? }) } } diff --git a/crates/fspy/src/unix/mod.rs b/crates/fspy/src/unix/mod.rs index bc9dbca89..3fb25b55e 100644 --- a/crates/fspy/src/unix/mod.rs +++ b/crates/fspy/src/unix/mod.rs @@ -80,7 +80,8 @@ impl SpyImpl { #[cfg(not(target_env = "musl"))] let (ipc_channel_conf, ipc_receiver) = - channel(crate::ipc::shm_capacity()).map_err(SpawnError::ChannelCreation)?; + channel(crate::ipc::shm_capacity(), allocator_api2::alloc::Global) + .map_err(SpawnError::ChannelCreation)?; let payload = Payload { #[cfg(not(target_env = "musl"))] diff --git a/crates/fspy/src/windows/mod.rs b/crates/fspy/src/windows/mod.rs index d5a884e46..114c796e0 100644 --- a/crates/fspy/src/windows/mod.rs +++ b/crates/fspy/src/windows/mod.rs @@ -84,7 +84,8 @@ impl SpyImpl { command.creation_flags(CREATE_SUSPENDED); let (channel_conf, receiver) = - channel(crate::ipc::shm_capacity()).map_err(SpawnError::ChannelCreation)?; + channel(crate::ipc::shm_capacity(), allocator_api2::alloc::Global) + .map_err(SpawnError::ChannelCreation)?; let mut spawn_success = false; let spawn_success = &mut spawn_success; diff --git a/crates/fspy_client_unix/src/lib.rs b/crates/fspy_client_unix/src/lib.rs index 066e6e07c..25c5c98be 100644 --- a/crates/fspy_client_unix/src/lib.rs +++ b/crates/fspy_client_unix/src/lib.rs @@ -10,6 +10,7 @@ pub mod raw_exec; use std::{ffi::OsStr, fmt::Debug, os::unix::ffi::OsStrExt as _, path::Path}; +use allocator_api2::alloc::Allocator; use convert::{ToAbsolutePath, ToAccessMode}; use fspy_shared::ipc::{PathAccess, channel::Sender}; use fspy_shared_unix::{ @@ -47,14 +48,17 @@ impl Client { /// Panics when the payload is missing, malformed, or cannot be decoded, /// and when the channel is there but cannot be attached to (see /// [`ChannelConf::sender`](fspy_shared::ipc::channel::ChannelConf::sender)). - pub fn from_env(envs: impl Iterator) -> Self { + pub fn from_env( + envs: impl Iterator, + allocator: impl Allocator, + ) -> Self { let encoded_payload = decode_payload_from_env(envs).unwrap(); // `None` when the channel is already over, which happens when this // process starts after the root target exited. Nothing is said // about it: a preload library writing to the traced process's // stderr corrupts whatever that process is printing. - let ipc_sender = encoded_payload.payload.ipc_channel_conf.sender(); + let ipc_sender = encoded_payload.payload.ipc_channel_conf.sender(allocator); Self { encoded_payload, ipc_sender } } diff --git a/crates/fspy_preload_unix/src/client.rs b/crates/fspy_preload_unix/src/client.rs index 4a203af2f..a72800974 100644 --- a/crates/fspy_preload_unix/src/client.rs +++ b/crates/fspy_preload_unix/src/client.rs @@ -45,5 +45,5 @@ fn init_client() { // SAFETY: the ctor only reads the process environment while constructing // the client and does not retain borrowed environment views. let current = unsafe { fspy_nostd::env::current() }.unwrap(); - CLIENT.set(Client::from_env(current.envs())).unwrap(); + CLIENT.set(Client::from_env(current.envs(), fspy_nostd_alloc::pooled_bump())).unwrap(); } diff --git a/crates/fspy_preload_windows/Cargo.toml b/crates/fspy_preload_windows/Cargo.toml index ba4426aca..6afcd2082 100644 --- a/crates/fspy_preload_windows/Cargo.toml +++ b/crates/fspy_preload_windows/Cargo.toml @@ -13,6 +13,8 @@ wincode = { workspace = true } constcat = { workspace = true } fspy_detours_sys = { workspace = true } fspy_nostd = { workspace = true } +allocator-api2 = { workspace = true } +fspy_nostd_alloc = { workspace = true } fspy_shared = { workspace = true } ntapi = { workspace = true } smallvec = { workspace = true } diff --git a/crates/fspy_preload_windows/src/windows/client.rs b/crates/fspy_preload_windows/src/windows/client.rs index bbe4e0c11..29a9771fa 100644 --- a/crates/fspy_preload_windows/src/windows/client.rs +++ b/crates/fspy_preload_windows/src/windows/client.rs @@ -1,5 +1,6 @@ use std::{cell::SyncUnsafeCell, ffi::CStr, mem::MaybeUninit}; +use allocator_api2::alloc::Allocator; use fspy_detours_sys::DetourCopyPayloadToProcess; use fspy_shared::{ ipc::{PathAccess, channel::Sender}, @@ -13,14 +14,14 @@ pub struct Client<'a> { } impl<'a> Client<'a> { - pub fn from_payload_bytes(payload_bytes: &'a [u8]) -> Self { + pub fn from_payload_bytes(payload_bytes: &'a [u8], allocator: impl Allocator) -> Self { let payload: Payload<'a> = wincode::deserialize_exact(payload_bytes).unwrap(); // `None` when the channel is already over, which happens when this // process starts after the root target exited. Nothing is said // about it: a detours DLL writing to the traced process's stderr // corrupts whatever that process is printing. - let ipc_sender = payload.channel_conf.sender(); + let ipc_sender = payload.channel_conf.sender(allocator); Self { payload, ipc_sender } } diff --git a/crates/fspy_preload_windows/src/windows/mod.rs b/crates/fspy_preload_windows/src/windows/mod.rs index d187ed000..a575f8927 100644 --- a/crates/fspy_preload_windows/src/windows/mod.rs +++ b/crates/fspy_preload_windows/src/windows/mod.rs @@ -45,7 +45,7 @@ fn dll_main(_hinstance: HINSTANCE, reason: u32) -> winsafe::SysResult<()> { let payload_bytes = unsafe { slice::from_raw_parts::<'static, u8>(payload_ptr, payload_len.try_into().unwrap()) }; - let client = Client::from_payload_bytes(payload_bytes); + let client = Client::from_payload_bytes(payload_bytes, fspy_nostd_alloc::pooled_bump()); // SAFETY: setting the global client during single-threaded DLL_PROCESS_ATTACH unsafe { set_global_client(client) }; diff --git a/crates/fspy_shared/src/ipc/channel/mod.rs b/crates/fspy_shared/src/ipc/channel/mod.rs index d04c7f8ad..d4541e300 100644 --- a/crates/fspy_shared/src/ipc/channel/mod.rs +++ b/crates/fspy_shared/src/ipc/channel/mod.rs @@ -9,7 +9,7 @@ mod shm_io; use std::{env::temp_dir, ffi::OsStr, io, num::NonZeroUsize, path::PathBuf}; -use allocator_api2::alloc::Global; +use allocator_api2::alloc::Allocator; use fspy_nostd::Fat; use fspy_nostd_alloc::OsCString; use fspy_shm::Mapping; @@ -48,8 +48,11 @@ pub struct ChannelConf { /// Creates a mpsc IPC channel with one receiver and a `ChannelConf` that can be passed around processes and used to create multiple senders. #[expect(clippy::missing_errors_doc, reason = "non-vt crate: cannot use vt_str/vt_path types")] -pub fn channel(capacity: usize) -> io::Result<(ChannelConf, Receiver)> { - let shm_c_path = os_c_string(shm_backing_path()?.as_os_str())?; +pub fn channel( + capacity: usize, + allocator: A, +) -> io::Result<(ChannelConf, Receiver)> { + let shm_c_path = os_c_string(shm_backing_path()?.as_os_str(), allocator)?; let handle = fspy_shm::create(shm_c_path.as_c_str().as_thin(), capacity).map_err(shm_error_to_io)?; // The keeper exists from here on, so every error path below cleans up. @@ -74,27 +77,27 @@ pub fn channel(capacity: usize) -> io::Result<(ChannelConf, Receiver)> { } /// Encodes `path` as an owned NUL-terminated platform C string. -fn os_c_string(path: &OsStr) -> io::Result> { - let mut units = os_units(path); +fn os_c_string(path: &OsStr, allocator: A) -> io::Result> { + let mut units = os_units(path, allocator); units.push(0); OsCString::from_vec_with_nul(units) .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "path contains NUL")) } #[cfg(unix)] -fn os_units(path: &OsStr) -> allocator_api2::vec::Vec { +fn os_units(path: &OsStr, allocator: A) -> allocator_api2::vec::Vec { use std::os::unix::ffi::OsStrExt as _; - let mut units = allocator_api2::vec::Vec::with_capacity(path.len() + 1); + let mut units = allocator_api2::vec::Vec::with_capacity_in(path.len() + 1, allocator); units.extend_from_slice(path.as_bytes()); units } #[cfg(windows)] -fn os_units(path: &OsStr) -> allocator_api2::vec::Vec { +fn os_units(path: &OsStr, allocator: A) -> allocator_api2::vec::Vec { use std::os::windows::ffi::OsStrExt as _; - let mut units = allocator_api2::vec::Vec::with_capacity(path.len() + 1); + let mut units = allocator_api2::vec::Vec::with_capacity_in(path.len() + 1, allocator); for unit in path.encode_wide() { units.push(unit); } @@ -148,11 +151,11 @@ fn to_verbatim_if_long(path: PathBuf) -> io::Result { /// /// Removal is cleanup, not a stop signal: later opens fail, but existing /// handles and mappings keep reading and writing; see [`fspy_shm::remove`]. -struct ShmKeeper { - path: OsCString, +struct ShmKeeper { + path: OsCString, } -impl Drop for ShmKeeper { +impl Drop for ShmKeeper { fn drop(&mut self) { let _ = fspy_shm::remove(self.path.as_c_str().as_thin()); } @@ -174,14 +177,12 @@ impl ChannelConf { /// receiver it recorded nothing, and a trace that silently omits every /// access a process made is worse than no trace, so it stops here. #[must_use] - pub fn sender(&self) -> Option { - // The arena never touches the process heap, so this stays safe in - // the preload contexts that create senders (pre-`main` constructors, - // the Windows loader lock). - let arena = fspy_nostd_alloc::pooled_bump(); + pub fn sender(&self, allocator: A) -> Option { + // The allocation is transient: the decoded path only has to outlive + // the open call below. let shm_path = self .shm_id - .to_os_c_string_in(&arena) + .to_os_c_string_in(allocator) .expect("the channel's shared-memory path is not a valid C string"); let mapping = match fspy_shm::open(shm_path.as_c_str().as_thin()) { Ok(handle) => handle.map().expect("cannot map the shared-memory channel"), @@ -256,10 +257,10 @@ unsafe impl Sync for Sender {} /// /// Holds the shared memory and its backing file alive for as long as senders /// may attach; [`Receiver::close`] (or dropping) removes the backing file. -pub struct Receiver { +pub struct Receiver { /// Keeps the shared memory's backing file alive for as long as senders /// may attach. - _keeper: ShmKeeper, + _keeper: ShmKeeper, mapping: Mapping, } @@ -267,12 +268,12 @@ pub struct Receiver { // through the `shm_io` protocol in `close`, which synchronizes with senders // via atomic operations. The mapping's address is stable and independently // owned. -unsafe impl Send for Receiver {} +unsafe impl Send for Receiver {} // SAFETY: see the `Send` impl. -unsafe impl Sync for Receiver {} +unsafe impl Sync for Receiver {} -impl Receiver { +impl Receiver { /// Closes the channel and returns every committed frame, borrowed from /// the shared mapping that moves into the returned [`FrameReader`]. /// @@ -333,6 +334,7 @@ pub struct RecordsLost; mod tests { use std::{ffi::OsString, fs, num::NonZeroUsize, str::from_utf8}; + use allocator_api2::alloc::Global; use assert2::assert; use bstr::B; use subprocess_test::command_for_fn; @@ -353,7 +355,7 @@ mod tests { fn a_capacity_too_small_for_the_table_fails_the_channel() { // The counters alone need sixteen bytes, and the table needs eight // per slot on top. - let Err(error) = channel(8) else { + let Err(error) = channel(8, Global) else { panic!("a region too small for the protocol made a channel"); }; assert!(error.kind() == io::ErrorKind::InvalidInput); @@ -364,12 +366,12 @@ mod tests { /// must still attach. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn sender_ignores_changed_temp_and_working_directory() { - let (conf, receiver) = channel(CAPACITY).unwrap(); + let (conf, receiver) = channel(CAPACITY, Global).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().unwrap(); + let sender = conf.sender(Global).unwrap(); let frame_size = NonZeroUsize::new(2).unwrap(); let mut frame = sender.writer.claim_frame(frame_size).unwrap(); frame.copy_from_slice(&[4, 2]); @@ -394,8 +396,8 @@ mod tests { /// here rather than in a build. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn sender_round_trips_records() { - let (conf, receiver) = channel(CAPACITY).unwrap(); - let sender = conf.sender().unwrap(); + let (conf, receiver) = channel(CAPACITY, Global).unwrap(); + let sender = conf.sender(Global).unwrap(); // A record path carries the platform's own string form: bytes on // unix, UTF-16 on Windows. #[cfg(unix)] @@ -425,9 +427,9 @@ mod tests { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn smoke() { - let (conf, receiver) = channel(CAPACITY).unwrap(); + let (conf, receiver) = channel(CAPACITY, Global).unwrap(); let cmd = command_for_fn!(conf, |conf: ChannelConf| { - let sender = conf.sender().unwrap(); + let sender = conf.sender(Global).unwrap(); let frame_size = NonZeroUsize::new(2).unwrap(); let mut frame = sender.writer.claim_frame(frame_size).unwrap(); frame.copy_from_slice(&[4, 2]); @@ -447,11 +449,11 @@ mod tests { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[expect(clippy::print_stdout, reason = "test diagnostics")] async fn forbid_new_senders_after_close() { - let (conf, receiver) = channel(CAPACITY).unwrap(); + let (conf, receiver) = channel(CAPACITY, Global).unwrap(); let _frames = receiver.close().unwrap(); let cmd = command_for_fn!(conf, |conf: ChannelConf| { - print!("{}", conf.sender().is_some()); + print!("{}", conf.sender(Global).is_some()); }); let output = std::process::Command::from(cmd).output().unwrap(); assert!(B(&output.stdout) == B("false")); @@ -460,11 +462,11 @@ 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(CAPACITY).unwrap(); + let (conf, receiver) = channel(CAPACITY, Global).unwrap(); drop(receiver); let cmd = command_for_fn!(conf, |conf: ChannelConf| { - print!("{}", conf.sender().is_some()); + print!("{}", conf.sender(Global).is_some()); }); let output = std::process::Command::from(cmd).output().unwrap(); assert!(B(&output.stdout) == B("false")); @@ -474,8 +476,8 @@ mod tests { /// claim any new frame afterwards. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn attached_sender_cannot_claim_after_close() { - let (conf, receiver) = channel(CAPACITY).unwrap(); - let sender = conf.sender().unwrap(); + let (conf, receiver) = channel(CAPACITY, Global).unwrap(); + let sender = conf.sender(Global).unwrap(); let mut frame = sender.writer.claim_frame(NonZeroUsize::new(2).unwrap()).unwrap(); frame.copy_from_slice(&[4, 2]); @@ -492,10 +494,10 @@ mod tests { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn concurrent_senders() { - let (conf, receiver) = channel(CAPACITY).unwrap(); + let (conf, receiver) = channel(CAPACITY, Global).unwrap(); for i in 0u16..200 { let cmd = command_for_fn!((conf.clone(), i), |(conf, i): (ChannelConf, u16)| { - let sender = conf.sender().unwrap(); + let sender = conf.sender(Global).unwrap(); let data_to_send = i.to_string(); let mut frame = sender .writer diff --git a/crates/fspy_shared/src/ipc/channel/shm_io/mod.rs b/crates/fspy_shared/src/ipc/channel/shm_io/mod.rs index f8ed4b2a8..8a592b187 100644 --- a/crates/fspy_shared/src/ipc/channel/shm_io/mod.rs +++ b/crates/fspy_shared/src/ipc/channel/shm_io/mod.rs @@ -567,7 +567,9 @@ mod tests { let shm_path = crate::ipc::channel::shm_backing_path().unwrap(); let shm_name = shm_path.to_str().expect("test temp dir is UTF-8").to_owned(); - let c_path = crate::ipc::channel::os_c_string(shm_path.as_os_str()).unwrap(); + let c_path = + crate::ipc::channel::os_c_string(shm_path.as_os_str(), allocator_api2::alloc::Global) + .unwrap(); let handle = fspy_shm::create(c_path.as_c_str().as_thin(), SHM_SIZE).unwrap(); let _keeper = crate::ipc::channel::ShmKeeper { path: c_path }; // Map before the children run. Windows keeps views coherent while they @@ -580,9 +582,11 @@ mod tests { let cmd = command_for_fn!( (shm_name.clone(), child_index), |(shm_name, child_index): (String, usize)| { - let c_path = - crate::ipc::channel::os_c_string(std::ffi::OsStr::new(&shm_name)) - .unwrap(); + let c_path = crate::ipc::channel::os_c_string( + std::ffi::OsStr::new(&shm_name), + allocator_api2::alloc::Global, + ) + .unwrap(); let mapping = fspy_shm::open(c_path.as_c_str().as_thin()).unwrap().map().unwrap(); // SAFETY: `mapping` is a freshly mapped shared memory @@ -634,13 +638,19 @@ mod tests { let shm_path = crate::ipc::channel::shm_backing_path().unwrap(); let shm_name = shm_path.to_str().expect("test temp dir is UTF-8").to_owned(); - let c_path = crate::ipc::channel::os_c_string(shm_path.as_os_str()).unwrap(); + let c_path = + crate::ipc::channel::os_c_string(shm_path.as_os_str(), allocator_api2::alloc::Global) + .unwrap(); let handle = fspy_shm::create(c_path.as_c_str().as_thin(), SHM_SIZE).unwrap(); let _keeper = crate::ipc::channel::ShmKeeper { path: c_path }; let mapping = handle.map().unwrap(); let cmd = command_for_fn!(shm_name, |shm_name: String| { - let c_path = crate::ipc::channel::os_c_string(std::ffi::OsStr::new(&shm_name)).unwrap(); + let c_path = crate::ipc::channel::os_c_string( + std::ffi::OsStr::new(&shm_name), + allocator_api2::alloc::Global, + ) + .unwrap(); let child_mapping = fspy_shm::open(c_path.as_c_str().as_thin()).unwrap().map().unwrap(); // SAFETY: see `real_shm_across_processes`. let writer = unsafe { ShmWriter::new(child_mapping, S) }.unwrap();