Skip to content

Commit eb1efd6

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 two paths; channel() returns only the Receiver, and Receiver::conf() derives the configuration from receiver-owned C strings (the lock path now stored beside the keeper path, and generated absolute like the shm path already was). - 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. - fspy_shared is allocator-agnostic: channel(), Receiver, and sender() are generic over the allocator, and decode_payload_from_env leaks its allocations into whichever allocator the caller passes, whose lifetime bounds the payload. The supervisor instantiates with the global allocator. - The unix preload attaches with one page-backed bump from fspy_nostd_alloc::page_bump(), held in ManuallyDrop from the start: the payload storage lands at its base, the sender's temporary path decode runs inside Bump::scoped so the same chunk is reused and reclaimed, and the never-dropped bump gives one mapping for the whole attach unless the payload outgrows the chunk, nothing from the global allocator, and no borrows into the mutable process environment. assume_process_lifetime documents the single unsafe step that names the leak. - 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 0fd7673 commit eb1efd6

18 files changed

Lines changed: 324 additions & 181 deletions

File tree

Cargo.lock

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

crates/fspy/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ license.workspace = true
66
publish = false
77

88
[dependencies]
9+
allocator-api2 = { workspace = true, features = ["alloc"] }
910
wincode = { workspace = true }
1011
bstr = { workspace = true, features = ["alloc", "std"] }
1112
bumpalo = { workspace = true }

crates/fspy/src/ipc.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use std::io;
22

3+
use allocator_api2::alloc::Global;
34
use fspy_shared::ipc::{
45
PathAccess,
56
channel::{Receiver, ReceiverLockGuard},
@@ -14,19 +15,19 @@ pub const SHM_CAPACITY: usize = 4 * 1024 * 1024 * 1024;
1415
#[ouroboros::self_referencing]
1516
pub struct OwnedReceiverLockGuard {
1617
/// Owns the shared memory
17-
receiver: Receiver,
18+
receiver: Receiver<Global>,
1819
/// Borrows the shared memory and owns the file lock
1920
#[borrows(receiver)]
2021
#[covariant]
2122
lock_guard: ReceiverLockGuard<'this>,
2223
}
2324

2425
impl OwnedReceiverLockGuard {
25-
pub fn lock(receiver: Receiver) -> io::Result<Self> {
26+
pub fn lock(receiver: Receiver<Global>) -> io::Result<Self> {
2627
Self::try_new(receiver, fspy_shared::ipc::channel::Receiver::lock)
2728
}
2829

29-
pub async fn lock_async(receiver: Receiver) -> io::Result<Self> {
30+
pub async fn lock_async(receiver: Receiver<Global>) -> io::Result<Self> {
3031
spawn_blocking(move || Self::lock(receiver)).await.expect("lock task panicked")
3132
}
3233

crates/fspy/src/unix/mod.rs

Lines changed: 35 additions & 21 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::{OwnedReceiverLockGuard, SHM_CAPACITY};
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,24 +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(SHM_CAPACITY).map_err(SpawnError::ChannelCreation)?;
90+
let ipc_receiver = channel(SHM_CAPACITY, allocator_api2::alloc::Global)
91+
.map_err(SpawnError::ChannelCreation)?;
8492

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

9299
#[cfg(not(target_env = "musl"))]
93-
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+
},
94107

95108
#[cfg(target_os = "linux")]
96109
seccomp_payload: supervisor.payload().clone(),
97110
};
98111

99-
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() };
100114

101115
let mut exec = command.get_exec();
102116
let mut exec_resolve_accesses = PathAccessArena::default();

crates/fspy/src/windows/mod.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,8 @@ impl SpyImpl {
8686

8787
command.creation_flags(CREATE_SUSPENDED);
8888

89-
let (channel_conf, receiver) =
90-
channel(SHM_CAPACITY).map_err(SpawnError::ChannelCreation)?;
89+
let receiver = channel(SHM_CAPACITY, allocator_api2::alloc::Global)
90+
.map_err(SpawnError::ChannelCreation)?;
9191

9292
let mut spawn_success = false;
9393
let spawn_success = &mut spawn_success;
@@ -107,7 +107,7 @@ impl SpyImpl {
107107
}
108108

109109
let payload = Payload {
110-
channel_conf: channel_conf.clone(),
110+
channel_conf: receiver.conf(),
111111
ansi_dll_path_with_nul: ansi_dll_path_with_nul.to_bytes(),
112112
};
113113
let payload_bytes = wincode::serialize(&payload).unwrap();

crates/fspy_client_unix/src/lib.rs

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ use raw_exec::RawExec;
2121
use wincode::Serialize as _;
2222

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

@@ -51,17 +51,28 @@ impl Client {
5151
reason = "the client intentionally reports an unavailable supervisor channel"
5252
)]
5353
pub fn from_env(envs: impl Iterator<Item = fspy_nostd::env::Entry>) -> Self {
54-
let encoded_payload = decode_payload_from_env(envs).unwrap();
55-
56-
let ipc_sender = match encoded_payload.payload.ipc_channel_conf.sender() {
57-
Ok(sender) => Some(sender),
58-
Err(err) => {
59-
// This can happen if the process starts after the root target
60-
// has exited and the receiver has closed the channel.
61-
eprintln!("fspy: failed to create ipc sender: {err}");
62-
None
54+
// One page-backed bump serves the whole attach: the payload's
55+
// process-lifetime storage first, then the scoped temporaries below —
56+
// one mapping unless the payload outgrows the first chunk. The
57+
// `ManuallyDrop` is never taken apart, so the bump is never dropped.
58+
let mut bump = core::mem::ManuallyDrop::new(fspy_nostd_alloc::page_bump());
59+
let encoded_payload = decode_payload_from_env(envs, &*bump).unwrap();
60+
// SAFETY: `bump` is `ManuallyDrop` and never dropped, so the storage
61+
// the payload borrows from is never freed, and the scoped temporaries
62+
// below reset only down to this point.
63+
let encoded_payload = unsafe { encoded_payload.assume_process_lifetime() };
64+
65+
let ipc_sender = bump.scoped(|scope| {
66+
match encoded_payload.payload.ipc_channel_conf.sender(&*scope) {
67+
Ok(sender) => Some(sender),
68+
Err(err) => {
69+
// This can happen if the process starts after the root target
70+
// has exited and the receiver has closed the channel.
71+
eprintln!("fspy: failed to create ipc sender: {err}");
72+
None
73+
}
6374
}
64-
};
75+
});
6576

6677
Self { encoded_payload, ipc_sender }
6778
}
@@ -143,7 +154,7 @@ impl Client {
143154
// SAFETY: mode contains a valid pointer (if ModeStr) or a plain value,
144155
// as provided by the caller.
145156
let mode = unsafe { mode.to_access_mode() };
146-
let arena = fspy_nostd_alloc::arena();
157+
let arena = fspy_nostd_alloc::pooled_bump();
147158
let Some(abs_path) = path.to_absolute_path(&arena)? else {
148159
return Ok(());
149160
};

crates/fspy_client_unix/src/raw_exec.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ impl RawExec {
5353
// execs), where malloc's lock may be held by a thread that no longer
5454
// exists. A per-call arena has exactly this lifetime, and hands back
5555
// the memory when the call ends.
56-
let arena = fspy_nostd_alloc::arena();
56+
let arena = fspy_nostd_alloc::pooled_bump();
5757
let mut ptr_vec = allocator_api2::vec::Vec::with_capacity_in(strs.len() + 1, &arena);
5858
for s in &mut strs {
5959
s.push(0);

0 commit comments

Comments
 (0)