11#[ cfg( target_os = "linux" ) ]
2- mod syscall_handler ;
2+ mod sigsys ;
33
44#[ cfg( target_os = "macos" ) ]
55mod macos_artifacts;
66
7+ #[ cfg( target_os = "linux" ) ]
8+ use std:: os:: fd:: AsRawFd as _;
79use std:: { io, path:: Path } ;
810
9- #[ cfg( target_os = "linux" ) ]
10- use fspy_seccomp_unotify:: supervisor:: supervise;
11- use fspy_shared:: ipc:: PathAccess ;
12- #[ cfg( not( target_env = "musl" ) ) ]
13- use fspy_shared:: ipc:: { NativeStr , channel:: channel} ;
11+ #[ cfg( target_os = "macos" ) ]
12+ use fspy_shared:: ipc:: NativeStr ;
13+ use fspy_shared:: ipc:: { PathAccess , channel:: channel} ;
1414#[ cfg( target_os = "macos" ) ]
1515use fspy_shared_unix:: payload:: Artifacts ;
1616use fspy_shared_unix:: {
@@ -19,31 +19,39 @@ use fspy_shared_unix::{
1919 spawn:: handle_exec,
2020} ;
2121use futures_util:: FutureExt ;
22- #[ cfg( target_os = "linux" ) ]
23- use syscall_handler:: SyscallHandler ;
2422use tokio:: task:: spawn_blocking;
2523use tokio_util:: sync:: CancellationToken ;
2624
27- #[ cfg( not( target_env = "musl" ) ) ]
28- use crate :: ipc:: { OwnedReceiverLockGuard , SHM_CAPACITY } ;
29- use crate :: { ChildTermination , Command , TrackedChild , arena:: PathAccessArena , error:: SpawnError } ;
25+ use crate :: {
26+ ChildTermination , Command , TrackedChild ,
27+ arena:: PathAccessArena ,
28+ error:: SpawnError ,
29+ ipc:: { OwnedReceiverLockGuard , SHM_CAPACITY } ,
30+ } ;
3031
3132#[ derive( Debug ) ]
3233pub struct SpyImpl {
3334 #[ cfg( target_os = "macos" ) ]
3435 artifacts : Artifacts ,
3536
36- #[ cfg( not ( target_env = "musl" ) ) ]
37+ #[ cfg( target_os = "macos" ) ]
3738 preload_path : Box < NativeStr > ,
3839}
3940
4041impl SpyImpl {
41- /// Initialize the fs access spy by writing the preload library on disk.
42- ///
43- /// On musl targets, we don't build a preload library —
44- /// only seccomp-based tracking is used.
45- pub fn init_in ( #[ cfg_attr( target_env = "musl" , allow( unused) ) ] dir : & Path ) -> io:: Result < Self > {
46- #[ cfg( not( target_env = "musl" ) ) ]
42+ /// Initializes platform artifacts. Linux injects its handler at exec and
43+ /// does not materialize a preload library.
44+ #[ cfg( target_os = "linux" ) ]
45+ #[ expect(
46+ clippy:: unnecessary_wraps,
47+ reason = "keeps initialization uniform with the fallible macOS backend"
48+ ) ]
49+ pub const fn init_in ( _dir : & Path ) -> io:: Result < Self > {
50+ Ok ( Self { } )
51+ }
52+
53+ #[ cfg( target_os = "macos" ) ]
54+ pub fn init_in ( dir : & Path ) -> io:: Result < Self > {
4755 let preload_path = {
4856 use materialized_artifact:: { Artifact , artifact} ;
4957
@@ -54,7 +62,7 @@ impl SpyImpl {
5462 } ;
5563
5664 Ok ( Self {
57- #[ cfg( not ( target_env = "musl" ) ) ]
65+ #[ cfg( target_os = "macos" ) ]
5866 preload_path,
5967 #[ cfg( target_os = "macos" ) ]
6068 artifacts : {
@@ -74,25 +82,21 @@ impl SpyImpl {
7482 mut command : Command ,
7583 cancellation_token : CancellationToken ,
7684 ) -> Result < TrackedChild , SpawnError > {
77- #[ cfg( target_os = "linux" ) ]
78- let supervisor = supervise :: < SyscallHandler > ( ) . map_err ( SpawnError :: Supervisor ) ?;
79-
80- #[ cfg( not( target_env = "musl" ) ) ]
85+ #[ cfg( target_os = "macos" ) ]
8186 let ( ipc_channel_conf, ipc_receiver) =
8287 channel ( SHM_CAPACITY ) . map_err ( SpawnError :: ChannelCreation ) ?;
88+ #[ cfg( target_os = "linux" ) ]
89+ let ( _, ipc_receiver) = channel ( SHM_CAPACITY ) . map_err ( SpawnError :: ChannelCreation ) ?;
8390
8491 let payload = Payload {
85- #[ cfg( not ( target_env = "musl" ) ) ]
92+ #[ cfg( target_os = "macos" ) ]
8693 ipc_channel_conf,
8794
8895 #[ cfg( target_os = "macos" ) ]
8996 artifacts : self . artifacts . clone ( ) ,
9097
91- #[ cfg( not ( target_env = "musl" ) ) ]
98+ #[ cfg( target_os = "macos" ) ]
9299 preload_path : self . preload_path . clone ( ) ,
93-
94- #[ cfg( target_os = "linux" ) ]
95- seccomp_payload : supervisor. payload ( ) . clone ( ) ,
96100 } ;
97101
98102 let encoded_payload = encode_payload ( payload) ;
@@ -108,28 +112,51 @@ impl SpyImpl {
108112 } ,
109113 )
110114 . map_err ( |err| SpawnError :: Injection ( err. into ( ) ) ) ?;
115+ #[ cfg( target_os = "linux" ) ]
116+ debug_assert ! ( pre_exec. is_none( ) ) ;
111117 command. set_exec ( exec) ;
112118 command. env ( "FSPY" , "1" ) ;
113119
114120 let mut tokio_command = command. into_tokio_command ( ) ;
115121
116- // SAFETY: the pre_exec closure only calls pre_exec.run() which is safe to call in a fork context
122+ #[ cfg( target_os = "linux" ) ]
123+ let shm_fd = ipc_receiver. shm_fd ( ) . as_raw_fd ( ) ;
124+ #[ cfg( target_os = "linux" ) ]
125+ let shm_len = ipc_receiver. shm_len ( ) ;
126+
127+ // SAFETY: both platform hooks are restricted to async-signal-safe raw
128+ // operations in the post-fork child.
117129 unsafe {
118130 tokio_command. pre_exec ( move || {
131+ #[ cfg( target_os = "linux" ) ]
132+ sigsys:: prepare ( shm_fd) ?;
133+ #[ cfg( target_os = "macos" ) ]
119134 if let Some ( pre_exec) = pre_exec. as_ref ( ) {
120135 pre_exec. run ( ) ?;
121136 }
122137 Ok ( ( ) )
123138 } ) ;
124139 }
125140
126- // tokio_command.spawn blocks while executing the `pre_exec` closure.
127- // Run it inside spawn_blocking to avoid blocking the tokio runtime, especially the supervisor loop,
128- // which needs to accept incoming connections while `pre_exec` is connecting to it.
129- let mut child = spawn_blocking ( move || tokio_command. spawn ( ) )
130- . await
131- . map_err ( |err| SpawnError :: OsSpawn ( err. into ( ) ) ) ?
132- . map_err ( SpawnError :: OsSpawn ) ?;
141+ // Spawn and the post-exec ptrace handshake are blocking operations.
142+ let mut child = spawn_blocking ( move || {
143+ let child = tokio_command. spawn ( ) . map_err ( SpawnError :: OsSpawn ) ?;
144+ #[ cfg( target_os = "linux" ) ]
145+ let child = {
146+ let mut child = child;
147+ let pid = child. id ( ) . ok_or_else ( || {
148+ SpawnError :: Injection ( io:: Error :: other ( "spawned child has no process id" ) )
149+ } ) ?;
150+ if let Err ( error) = sigsys:: inject ( pid, shm_fd, shm_len) {
151+ let _ = child. start_kill ( ) ;
152+ return Err ( SpawnError :: Injection ( error) ) ;
153+ }
154+ child
155+ } ;
156+ Ok ( child)
157+ } )
158+ . await
159+ . map_err ( |err| SpawnError :: OsSpawn ( err. into ( ) ) ) ??;
133160
134161 Ok ( TrackedChild {
135162 stdin : child. stdin . take ( ) ,
@@ -146,28 +173,13 @@ impl SpyImpl {
146173 }
147174 } ;
148175
149- let arenas = std:: iter:: once ( exec_resolve_accesses) ;
150- // Stop the supervisor and collect path accesses from it.
151- #[ cfg( target_os = "linux" ) ]
152- let arenas = arenas. chain (
153- supervisor
154- . stop ( )
155- . await ?
156- . into_iter ( )
157- . map ( syscall_handler:: SyscallHandler :: into_arena) ,
158- ) ;
159- let arenas = arenas. collect :: < Vec < _ > > ( ) ;
176+ let arenas = vec ! [ exec_resolve_accesses] ;
160177
161178 // Lock the ipc channel after the child has exited.
162179 // We are not interested in path accesses from descendants after the main child has exited.
163- #[ cfg( not( target_env = "musl" ) ) ]
164180 let ipc_receiver_lock_guard =
165181 OwnedReceiverLockGuard :: lock_async ( ipc_receiver) . await ?;
166- let path_accesses = PathAccessIterable {
167- arenas,
168- #[ cfg( not( target_env = "musl" ) ) ]
169- ipc_receiver_lock_guard,
170- } ;
182+ let path_accesses = PathAccessIterable { arenas, ipc_receiver_lock_guard } ;
171183
172184 io:: Result :: Ok ( ChildTermination { status, path_accesses } )
173185 } )
@@ -179,7 +191,6 @@ impl SpyImpl {
179191
180192pub struct PathAccessIterable {
181193 arenas : Vec < PathAccessArena > ,
182- #[ cfg( not( target_env = "musl" ) ) ]
183194 ipc_receiver_lock_guard : OwnedReceiverLockGuard ,
184195}
185196
@@ -188,14 +199,7 @@ impl PathAccessIterable {
188199 let accesses_in_arena =
189200 self . arenas . iter ( ) . flat_map ( |arena| arena. borrow_accesses ( ) . iter ( ) ) . copied ( ) ;
190201
191- #[ cfg( not( target_env = "musl" ) ) ]
192- {
193- let accesses_in_shm = self . ipc_receiver_lock_guard . iter_path_accesses ( ) ;
194- accesses_in_shm. chain ( accesses_in_arena)
195- }
196- #[ cfg( target_env = "musl" ) ]
197- {
198- accesses_in_arena
199- }
202+ let accesses_in_shm = self . ipc_receiver_lock_guard . iter_path_accesses ( ) ;
203+ accesses_in_shm. chain ( accesses_in_arena)
200204 }
201205}
0 commit comments