Skip to content
Draft
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
1 change: 1 addition & 0 deletions Cargo.lock

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

54 changes: 34 additions & 20 deletions crates/fspy/src/unix/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<IpcStr>,
#[cfg(target_os = "macos")]
coreutils_path: Box<IpcStr>,

#[cfg(not(target_env = "musl"))]
preload_path: Box<IpcStr>,
Expand All @@ -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(),
})
}

Expand All @@ -79,24 +87,30 @@ impl SpyImpl {
let supervisor = supervise::<SyscallHandler>().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();
Expand Down
4 changes: 2 additions & 2 deletions crates/fspy/src/windows/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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();
Expand Down
38 changes: 23 additions & 15 deletions crates/fspy_client_unix/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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<Sender>,
}

Expand Down Expand Up @@ -51,21 +52,28 @@ impl Client {
reason = "the client intentionally reports an unavailable supervisor channel"
)]
pub fn from_env(envs: impl Iterator<Item = fspy_nostd::env::Entry>) -> 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 }
}
Expand Down
23 changes: 21 additions & 2 deletions crates/fspy_nostd_alloc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]

Expand All @@ -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;
Expand Down Expand Up @@ -87,6 +89,23 @@ impl Default for &'static ChunkPool<PageAllocator, CHUNK_SIZE, CHUNK_ALIGN, SLOT
}
}

/// [`page_bump`]'s settings: start without a chunk, so creating one
/// allocates nothing.
type PageBumpSettings = <BumpSettings as BumpAllocatorSettings>::WithGuaranteedAllocated<false>;

/// 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::<AllocatorApi2V02Compat<PageAllocator>, PageBumpSettings>::unallocated()
}

/// Creates a fresh bump backed by the process-wide chunk pool.
///
/// Creating the bump allocates nothing; the first allocation grabs a whole
Expand Down
10 changes: 6 additions & 4 deletions crates/fspy_preload_windows/src/windows/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Sender>,
}

Expand All @@ -33,7 +34,7 @@ impl<'a> Client<'a> {
}
};

Self { payload, ipc_sender }
Self { payload, payload_bytes, ipc_sender }
}

pub fn send(&self, access: PathAccess<'_>) {
Expand All @@ -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(),
)
}
}
Expand Down
Loading
Loading