Skip to content

Commit 94b517a

Browse files
wan9chiclaude
andcommitted
refactor(fspy-shared): make the payload a borrowed view
The payload and its channel configuration are now views over storage their producer owns, the model the Windows preload already had with its static Detours page: - ChannelConf borrows its path in C-string form (the new IpcCStr, an IpcStr that keeps its NUL terminator): channel() returns only the Receiver, Receiver::conf() borrows the keeper's C string, and a unix sender attaches by borrowing the path straight from the conf — no allocation at all. Windows still re-aligns the wide path through a caller-provided allocator. IpcStr sheds the APIs whose last users this replaces (from_os_c_str, to_os_c_string_in, to_boxed). - The unix Payload and EncodedPayload borrow every path and the encoded string. The supervisor lends its session paths per spawn instead of cloning boxes. seccomp_payload stays owned until fspy_seccomp_unotify grows borrowed types. - decode_payload_from_env leaks its allocations into the allocator the caller lends, whose borrow bounds the payload. - The preload ctor owns the attach storage — one page-backed bump from fspy_nostd_alloc::page_bump(), held in ManuallyDrop and never dropped — and lends it to from_env, which is safe code end to end: decode leaks the payload into the bump and the sender borrows its path from the decoded bytes. The ctor's Client::assume_process_lifetime is the attach's single unsafe step, reasoning against the bump the ctor itself owns. One mapping serves the whole attach unless the payload outgrows the chunk, nothing comes from the global allocator, and nothing borrows the mutable process environment. - The Windows preload deserializes its payload zero-copy from the static page and forwards those original bytes to children instead of re-serializing per spawn. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent d362c5f commit 94b517a

15 files changed

Lines changed: 358 additions & 160 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/fspy/src/unix/mod.rs

Lines changed: 35 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,9 @@ use fspy_seccomp_unotify::supervisor::supervise;
1111
use fspy_shared::ipc::PathAccess;
1212
#[cfg(not(target_env = "musl"))]
1313
use fspy_shared::ipc::{IpcStr, channel::channel};
14-
#[cfg(target_os = "macos")]
15-
use fspy_shared_unix::payload::Artifacts;
1614
use fspy_shared_unix::{
1715
exec::ExecResolveConfig,
18-
payload::{Payload, encode_payload},
16+
payload::{EncodedPayload, Payload, encode_payload},
1917
spawn::handle_exec,
2018
};
2119
use futures_util::FutureExt;
@@ -29,9 +27,15 @@ use crate::ipc::ChannelAccesses;
2927
use crate::{ChildTermination, Command, TrackedChild, arena::PathAccessArena, error::SpawnError};
3028

3129
#[derive(Debug)]
30+
#[cfg_attr(
31+
target_os = "macos",
32+
expect(clippy::struct_field_names, reason = "each field names a distinct injected path")
33+
)]
3234
pub struct SpyImpl {
3335
#[cfg(target_os = "macos")]
34-
artifacts: Artifacts,
36+
bash_path: Box<IpcStr>,
37+
#[cfg(target_os = "macos")]
38+
coreutils_path: Box<IpcStr>,
3539

3640
#[cfg(not(target_env = "musl"))]
3741
preload_path: Box<IpcStr>,
@@ -58,15 +62,19 @@ impl SpyImpl {
5862
#[cfg(not(target_env = "musl"))]
5963
preload_path,
6064
#[cfg(target_os = "macos")]
61-
artifacts: {
62-
let coreutils_path =
63-
macos_artifacts::COREUTILS_BINARY.materialize().executable().at(dir)?;
64-
let bash_path = macos_artifacts::OILS_BINARY.materialize().executable().at(dir)?;
65-
Artifacts {
66-
bash_path: bash_path.as_path().into(),
67-
coreutils_path: coreutils_path.as_path().into(),
68-
}
69-
},
65+
bash_path: macos_artifacts::OILS_BINARY
66+
.materialize()
67+
.executable()
68+
.at(dir)?
69+
.as_path()
70+
.into(),
71+
#[cfg(target_os = "macos")]
72+
coreutils_path: macos_artifacts::COREUTILS_BINARY
73+
.materialize()
74+
.executable()
75+
.at(dir)?
76+
.as_path()
77+
.into(),
7078
})
7179
}
7280

@@ -79,25 +87,30 @@ impl SpyImpl {
7987
let supervisor = supervise::<SyscallHandler>().map_err(SpawnError::Supervisor)?;
8088

8189
#[cfg(not(target_env = "musl"))]
82-
let (ipc_channel_conf, ipc_receiver) =
83-
channel(crate::ipc::shm_capacity(), allocator_api2::alloc::Global)
84-
.map_err(SpawnError::ChannelCreation)?;
90+
let ipc_receiver = channel(crate::ipc::shm_capacity(), allocator_api2::alloc::Global)
91+
.map_err(SpawnError::ChannelCreation)?;
8592

8693
let payload = Payload {
8794
#[cfg(not(target_env = "musl"))]
88-
ipc_channel_conf,
89-
90-
#[cfg(target_os = "macos")]
91-
artifacts: self.artifacts.clone(),
95+
ipc_channel_conf: ipc_receiver.conf(),
96+
#[cfg(target_env = "musl")]
97+
ipc_channel_conf: core::marker::PhantomData,
9298

9399
#[cfg(not(target_env = "musl"))]
94-
preload_path: self.preload_path.clone(),
100+
preload_path: &self.preload_path,
101+
102+
#[cfg(target_os = "macos")]
103+
artifacts: fspy_shared_unix::payload::Artifacts {
104+
bash_path: &self.bash_path,
105+
coreutils_path: &self.coreutils_path,
106+
},
95107

96108
#[cfg(target_os = "linux")]
97109
seccomp_payload: supervisor.payload().clone(),
98110
};
99111

100-
let encoded_payload = encode_payload(payload);
112+
let encoded_string = encode_payload(&payload);
113+
let encoded_payload = EncodedPayload { payload, encoded_string: encoded_string.as_ref() };
101114

102115
let mut exec = command.get_exec();
103116
let mut exec_resolve_accesses = PathAccessArena::default();

crates/fspy/src/windows/mod.rs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,8 @@ impl SpyImpl {
8383

8484
command.creation_flags(CREATE_SUSPENDED);
8585

86-
let (channel_conf, receiver) =
87-
channel(crate::ipc::shm_capacity(), allocator_api2::alloc::Global)
88-
.map_err(SpawnError::ChannelCreation)?;
86+
let receiver = channel(crate::ipc::shm_capacity(), allocator_api2::alloc::Global)
87+
.map_err(SpawnError::ChannelCreation)?;
8988

9089
let mut spawn_success = false;
9190
let spawn_success = &mut spawn_success;
@@ -105,7 +104,7 @@ impl SpyImpl {
105104
}
106105

107106
let payload = Payload {
108-
channel_conf: channel_conf.clone(),
107+
channel_conf: receiver.conf(),
109108
ansi_dll_path_with_nul: ansi_dll_path_with_nul.to_bytes(),
110109
};
111110
let payload_bytes = wincode::serialize(&payload).unwrap();

crates/fspy_client_unix/src/lib.rs

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,28 +20,33 @@ use fspy_shared_unix::{
2020
};
2121
use raw_exec::RawExec;
2222

23-
pub struct Client {
24-
encoded_payload: EncodedPayload,
23+
pub struct Client<'a> {
24+
encoded_payload: EncodedPayload<'a>,
2525
ipc_sender: Option<Sender>,
2626
}
2727

2828
// SAFETY: construction owns every field, later methods borrow them immutably,
2929
// and the sender synchronizes its shared-memory access.
3030
#[cfg(target_os = "macos")]
31-
unsafe impl Sync for Client {}
31+
unsafe impl Sync for Client<'_> {}
3232
// SAFETY: ownership of every field can move with the client, and the sender
3333
// synchronizes its shared-memory access.
3434
#[cfg(target_os = "macos")]
35-
unsafe impl Send for Client {}
35+
unsafe impl Send for Client<'_> {}
3636

37-
impl Debug for Client {
37+
impl Debug for Client<'_> {
3838
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3939
f.debug_struct("Client").finish()
4040
}
4141
}
4242

43-
impl Client {
44-
/// Constructs a client from the encoded payload in the process environment.
43+
impl<'a> Client<'a> {
44+
/// Constructs a client from the encoded payload in the process
45+
/// environment, leaking the payload's storage into `allocator`.
46+
///
47+
/// That storage is all the attach needs: the channel's path travels in
48+
/// C-string form inside the payload, so attaching the sender borrows
49+
/// it and allocates nothing further.
4550
///
4651
/// # Panics
4752
///
@@ -50,19 +55,33 @@ impl Client {
5055
/// [`ChannelConf::sender`](fspy_shared::ipc::channel::ChannelConf::sender)).
5156
pub fn from_env(
5257
envs: impl Iterator<Item = fspy_nostd::env::Entry>,
53-
allocator: impl Allocator,
58+
allocator: &'a impl Allocator,
5459
) -> Self {
55-
let encoded_payload = decode_payload_from_env(envs).unwrap();
60+
let encoded_payload = decode_payload_from_env(envs, allocator).unwrap();
5661

5762
// `None` when the channel is already over, which happens when this
5863
// process starts after the root target exited. Nothing is said
5964
// about it: a preload library writing to the traced process's
6065
// stderr corrupts whatever that process is printing.
61-
let ipc_sender = encoded_payload.payload.ipc_channel_conf.sender(allocator);
66+
let ipc_sender = encoded_payload.payload.ipc_channel_conf.sender();
6267

6368
Self { encoded_payload, ipc_sender }
6469
}
6570

71+
/// Extends the client's borrows to the process lifetime.
72+
///
73+
/// # Safety
74+
///
75+
/// The storage the client borrows from must never be freed for the
76+
/// rest of the process — for example, allocations from a bump
77+
/// allocator that is leaked instead of dropped.
78+
#[must_use]
79+
pub unsafe fn assume_process_lifetime(self) -> Client<'static> {
80+
// SAFETY: the types differ only in their lifetime parameter, and the
81+
// caller guarantees the backing storage is never freed.
82+
unsafe { core::mem::transmute(self) }
83+
}
84+
6685
fn send(&self, mode: fspy_shared::ipc::AccessMode, path: &Path) {
6786
let Some(ipc_sender) = &self.ipc_sender else {
6887
return;

0 commit comments

Comments
 (0)