Skip to content
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
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/fspy/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
5 changes: 3 additions & 2 deletions crates/fspy/src/ipc.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use allocator_api2::alloc::Global;
use fspy_shared::ipc::{
PathAccess,
channel::{FrameReader, Receiver},
Expand Down Expand Up @@ -37,7 +38,7 @@ pub struct ChannelAccesses {
frames: FrameReader,
}

impl TryFrom<Receiver> for ChannelAccesses {
impl TryFrom<Receiver<Global>> for ChannelAccesses {
type Error = TrackingIncomplete;

/// Closes the channel and takes every record it collected.
Expand All @@ -52,7 +53,7 @@ impl TryFrom<Receiver> 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<Self, TrackingIncomplete> {
fn try_from(receiver: Receiver<Global>) -> Result<Self, TrackingIncomplete> {
Ok(Self { frames: receiver.close().map_err(|_| TrackingIncomplete)? })
}
}
Expand Down
3 changes: 2 additions & 1 deletion crates/fspy/src/unix/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"))]
Expand Down
3 changes: 2 additions & 1 deletion crates/fspy/src/windows/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
8 changes: 6 additions & 2 deletions crates/fspy_client_unix/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -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<Item = fspy_nostd::env::Entry>) -> Self {
pub fn from_env(
envs: impl Iterator<Item = fspy_nostd::env::Entry>,
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 }
}
Expand Down
2 changes: 1 addition & 1 deletion crates/fspy_preload_unix/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
2 changes: 2 additions & 0 deletions crates/fspy_preload_windows/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
5 changes: 3 additions & 2 deletions crates/fspy_preload_windows/src/windows/client.rs
Original file line number Diff line number Diff line change
@@ -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},
Expand All @@ -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 }
}
Expand Down
2 changes: 1 addition & 1 deletion crates/fspy_preload_windows/src/windows/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) };

Expand Down
78 changes: 40 additions & 38 deletions crates/fspy_shared/src/ipc/channel/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<A: Allocator>(
capacity: usize,
allocator: A,
) -> io::Result<(ChannelConf, Receiver<A>)> {
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.
Expand All @@ -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<OsCString<Fat, Global>> {
let mut units = os_units(path);
fn os_c_string<A: Allocator>(path: &OsStr, allocator: A) -> io::Result<OsCString<Fat, A>> {
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<u8> {
fn os_units<A: Allocator>(path: &OsStr, allocator: A) -> allocator_api2::vec::Vec<u8, A> {
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<u16> {
fn os_units<A: Allocator>(path: &OsStr, allocator: A) -> allocator_api2::vec::Vec<u16, A> {
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);
}
Expand Down Expand Up @@ -148,11 +151,11 @@ fn to_verbatim_if_long(path: PathBuf) -> io::Result<PathBuf> {
///
/// 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<Fat, Global>,
struct ShmKeeper<A: Allocator> {
path: OsCString<Fat, A>,
}

impl Drop for ShmKeeper {
impl<A: Allocator> Drop for ShmKeeper<A> {
fn drop(&mut self) {
let _ = fspy_shm::remove(self.path.as_c_str().as_thin());
}
Expand All @@ -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<Sender> {
// 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<A: Allocator>(&self, allocator: A) -> Option<Sender> {
// 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"),
Expand Down Expand Up @@ -256,23 +257,23 @@ 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<A: Allocator> {
/// Keeps the shared memory's backing file alive for as long as senders
/// may attach.
_keeper: ShmKeeper,
_keeper: ShmKeeper<A>,
mapping: Mapping,
}

// SAFETY: `Receiver` only holds the mapping; it accesses it exclusively
// 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<A: Allocator + Send> Send for Receiver<A> {}

// SAFETY: see the `Send` impl.
unsafe impl Sync for Receiver {}
unsafe impl<A: Allocator + Sync> Sync for Receiver<A> {}

impl Receiver {
impl<A: Allocator> Receiver<A> {
/// Closes the channel and returns every committed frame, borrowed from
/// the shared mapping that moves into the returned [`FrameReader`].
///
Expand Down Expand Up @@ -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;
Expand All @@ -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);
Expand All @@ -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]);
Expand All @@ -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)]
Expand Down Expand Up @@ -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]);
Expand All @@ -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"));
Expand All @@ -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"));
Expand All @@ -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]);
Expand All @@ -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
Expand Down
Loading
Loading