From ec0d6eb1ff3495c95995256b88f348e898711225 Mon Sep 17 00:00:00 2001 From: Ludea Date: Fri, 10 Apr 2026 08:07:43 +0200 Subject: [PATCH 01/11] android support --- Cargo.lock | 16 ++- crates/fspy/Cargo.toml | 4 +- crates/fspy/src/artifact.rs | 2 +- crates/fspy/src/lib.rs | 4 +- crates/fspy/src/unix/mod.rs | 16 ++- crates/fspy_preload_unix/src/lib.rs | 13 +- crates/fspy_seccomp_unotify/Cargo.toml | 2 +- crates/fspy_seccomp_unotify/src/lib.rs | 2 +- crates/fspy_seccomp_unotify/src/target.rs | 10 ++ crates/fspy_shared/Cargo.toml | 8 +- .../src/ipc/channel/backend/memfd.rs | 29 ++++ .../src/ipc/channel/backend/mod.rs | 10 ++ .../src/ipc/channel/backend/shmem.rs | 20 +++ crates/fspy_shared/src/ipc/channel/mod.rs | 131 +++++++++++------- crates/fspy_shared/src/ipc/channel/shm_io.rs | 10 ++ crates/fspy_shared_unix/Cargo.toml | 2 +- crates/fspy_shared_unix/src/lib.rs | 5 +- crates/fspy_shared_unix/src/payload.rs | 4 +- .../fspy_shared_unix/src/spawn/linux/mod.rs | 13 +- crates/fspy_shared_unix/src/spawn/mod.rs | 2 +- 20 files changed, 215 insertions(+), 88 deletions(-) create mode 100644 crates/fspy_shared/src/ipc/channel/backend/memfd.rs create mode 100644 crates/fspy_shared/src/ipc/channel/backend/mod.rs create mode 100644 crates/fspy_shared/src/ipc/channel/backend/shmem.rs diff --git a/Cargo.lock b/Cargo.lock index c0f34b73b..ab7151695 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1167,7 +1167,6 @@ dependencies = [ "derive_more", "flate2", "fspy_detours_sys", - "fspy_preload_unix", "fspy_preload_windows", "fspy_seccomp_unotify", "fspy_shared", @@ -1273,6 +1272,8 @@ dependencies = [ "bstr", "bytemuck", "ctor", + "memfd", + "memmap2", "os_str_bytes", "rustc-hash", "shared_memory", @@ -1861,11 +1862,20 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[package]] +name = "memfd" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad38eb12aea514a0466ea40a80fd8cc83637065948eb4a426e4aa46261175227" +dependencies = [ + "rustix", +] + [[package]] name = "memmap2" -version = "0.9.9" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "744133e4a0e0a658e1374cf3bf8e415c4052a15a111acd372764c55b4177d490" +checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" dependencies = [ "libc", ] diff --git a/crates/fspy/Cargo.toml b/crates/fspy/Cargo.toml index 7269f0f99..bc9509255 100644 --- a/crates/fspy/Cargo.toml +++ b/crates/fspy/Cargo.toml @@ -29,8 +29,8 @@ fspy_seccomp_unotify = { workspace = true, features = ["supervisor"] } nix = { workspace = true, features = ["uio"] } tokio = { workspace = true, features = ["bytes"] } -[target.'cfg(all(unix, not(target_env = "musl")))'.dependencies] -fspy_preload_unix = { workspace = true } +#[target.'cfg(all(unix, any(not(target_env = "musl"), not(target_os = "android"))))'.dependencies] +#fspy_preload_unix = { workspace = true } [target.'cfg(unix)'.dependencies] fspy_shared_unix = { workspace = true } diff --git a/crates/fspy/src/artifact.rs b/crates/fspy/src/artifact.rs index 0c3fcba0e..4b03362ac 100644 --- a/crates/fspy/src/artifact.rs +++ b/crates/fspy/src/artifact.rs @@ -28,7 +28,7 @@ macro_rules! artifact { pub use artifact; impl Artifact { - #[cfg(not(target_os = "linux"))] + #[cfg(all(not(target_os = "android"), not(target_os = "linux")))] pub const fn new(name: &'static str, content: &'static [u8], hash: &'static str) -> Self { Self { name, content, hash } } diff --git a/crates/fspy/src/lib.rs b/crates/fspy/src/lib.rs index 70756003e..4946cffef 100644 --- a/crates/fspy/src/lib.rs +++ b/crates/fspy/src/lib.rs @@ -1,9 +1,9 @@ #![cfg_attr(target_os = "windows", feature(windows_process_extensions_main_thread_handle))] -#![feature(once_cell_try)] +#![cfg_attr(not(target_os = "android"), feature(once_cell_try))] // Persist the injected DLL/shared library somewhere in the filesystem. // Not needed on musl (seccomp-only tracking). -#[cfg(not(target_env = "musl"))] +#[cfg(all(not(target_os = "android"), not(target_env = "musl")))] mod artifact; pub mod error; diff --git a/crates/fspy/src/unix/mod.rs b/crates/fspy/src/unix/mod.rs index 7f9460057..e7bb7782f 100644 --- a/crates/fspy/src/unix/mod.rs +++ b/crates/fspy/src/unix/mod.rs @@ -8,7 +8,7 @@ use std::{io, path::Path}; #[cfg(target_os = "linux")] use fspy_seccomp_unotify::supervisor::supervise; -#[cfg(not(target_env = "musl"))] +#[cfg(all(not(target_os = "android"), not(target_env = "musl")))] use fspy_shared::ipc::NativeStr; use fspy_shared::ipc::{PathAccess, channel::channel}; #[cfg(target_os = "macos")] @@ -36,11 +36,11 @@ pub struct SpyImpl { #[cfg(target_os = "macos")] artifacts: Artifacts, - #[cfg(not(target_env = "musl"))] + #[cfg(all(not(target_os = "android"), not(target_env = "musl")))] preload_path: Box, } -#[cfg(not(target_env = "musl"))] +#[cfg(all(not(target_os = "android"), not(target_env = "musl")))] const PRELOAD_CDYLIB_BINARY: &[u8] = include_bytes!(env!("CARGO_CDYLIB_FILE_FSPY_PRELOAD_UNIX")); impl SpyImpl { @@ -48,8 +48,10 @@ impl SpyImpl { /// /// On musl targets, we don't build a preload library — /// only seccomp-based tracking is used. - pub fn init_in(#[cfg_attr(target_env = "musl", allow(unused))] dir: &Path) -> io::Result { - #[cfg(not(target_env = "musl"))] + pub fn init_in( + #[cfg_attr(any(target_os = "android", target_env = "musl"), allow(unused))] dir: &Path, + ) -> io::Result { + #[cfg(all(not(target_os = "android"), not(target_env = "musl")))] let preload_path = { use const_format::formatcp; use xxhash_rust::const_xxh3::xxh3_128; @@ -67,7 +69,7 @@ impl SpyImpl { }; Ok(Self { - #[cfg(not(target_env = "musl"))] + #[cfg(all(not(target_os = "android"), not(target_env = "musl")))] preload_path, #[cfg(target_os = "macos")] artifacts: { @@ -98,7 +100,7 @@ impl SpyImpl { #[cfg(target_os = "macos")] artifacts: self.artifacts.clone(), - #[cfg(not(target_env = "musl"))] + #[cfg(all(not(target_os = "android"), not(target_env = "musl")))] preload_path: self.preload_path.clone(), #[cfg(target_os = "linux")] diff --git a/crates/fspy_preload_unix/src/lib.rs b/crates/fspy_preload_unix/src/lib.rs index 9728cd98f..5a8aab8ed 100644 --- a/crates/fspy_preload_unix/src/lib.rs +++ b/crates/fspy_preload_unix/src/lib.rs @@ -1,12 +1,15 @@ // On musl targets, fspy_preload_unix is not needed since we can track accesses via seccomp-only. // Compile as an empty crate to avoid build failures from missing libc symbols. -#![cfg_attr(not(target_env = "musl"), feature(c_variadic))] +#![cfg_attr(not(target_os = "android"), not(target_env = "musl"), feature(c_variadic))] -#[cfg(all(unix, not(target_env = "musl")))] +#[cfg(all(target_os = "linux", not(target_env = "musl")))] mod client; -#[cfg(all(unix, not(target_env = "musl")))] + +#[cfg(all(target_os = "linux", not(target_env = "musl")))] mod interceptions; -#[cfg(all(unix, not(target_env = "musl")))] + +#[cfg(all(target_os = "linux", not(target_env = "musl")))] mod libc; -#[cfg(all(unix, not(target_env = "musl")))] + +#[cfg(all(target_os = "linux", not(target_env = "musl")))] mod macros; diff --git a/crates/fspy_seccomp_unotify/Cargo.toml b/crates/fspy_seccomp_unotify/Cargo.toml index 9a4a2a08c..c0733c682 100644 --- a/crates/fspy_seccomp_unotify/Cargo.toml +++ b/crates/fspy_seccomp_unotify/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2024" publish = false -[target.'cfg(target_os = "linux")'.dependencies] +[target.'cfg(any(target_os = "android", target_os = "linux"))'.dependencies] bincode = { workspace = true } libc = { workspace = true } nix = { workspace = true, features = ["process", "fs", "poll", "socket", "uio"] } diff --git a/crates/fspy_seccomp_unotify/src/lib.rs b/crates/fspy_seccomp_unotify/src/lib.rs index a70b0a816..b530f3ba5 100644 --- a/crates/fspy_seccomp_unotify/src/lib.rs +++ b/crates/fspy_seccomp_unotify/src/lib.rs @@ -1,4 +1,4 @@ -#![cfg(target_os = "linux")] +#![cfg(any(target_os = "android", target_os = "linux"))] #[cfg(any(feature = "supervisor", feature = "target"))] mod bindings; diff --git a/crates/fspy_seccomp_unotify/src/target.rs b/crates/fspy_seccomp_unotify/src/target.rs index 5406f7961..3561dcdac 100644 --- a/crates/fspy_seccomp_unotify/src/target.rs +++ b/crates/fspy_seccomp_unotify/src/target.rs @@ -7,6 +7,7 @@ use std::{ }; use libc::sock_filter; +#[cfg(not(target_os = "android"))] use nix::sys::prctl::set_no_new_privs; use passfd::FdPassingExt; @@ -19,7 +20,16 @@ use crate::{bindings::install_unotify_filter, payload::SeccompPayload}; /// Returns an error if setting no-new-privs fails, the filter cannot be installed, /// or the IPC socket communication fails. pub fn install_target(payload: &SeccompPayload) -> nix::Result<()> { + #[cfg(not(target_os = "android"))] set_no_new_privs()?; + #[cfg(target_os = "android")] + use libc::{PR_SET_NO_NEW_PRIVS, prctl}; + + let ret = unsafe { prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) }; + if ret != 0 { + return Err(nix::Error::last()); + } + let sock_filters = payload.filter.0.iter().copied().map(sock_filter::from).collect::>(); let notify_fd = install_unotify_filter(&sock_filters)?; diff --git a/crates/fspy_shared/Cargo.toml b/crates/fspy_shared/Cargo.toml index 756e45128..af4ea41d1 100644 --- a/crates/fspy_shared/Cargo.toml +++ b/crates/fspy_shared/Cargo.toml @@ -10,11 +10,17 @@ bincode = { workspace = true } bitflags = { workspace = true } bstr = { workspace = true } bytemuck = { workspace = true, features = ["must_cast", "derive"] } -shared_memory = { workspace = true, features = ["logging"] } thiserror = { workspace = true } tracing = { workspace = true } uuid = { workspace = true, features = ["v4"] } +[target.'cfg(not(target_os = "android"))'.dependencies] +shared_memory = { workspace = true, features = ["logging"] } + +[target.'cfg(target_os = "android")'.dependencies] +memmap2 = "0.9.10" +memfd = "0.6" + [target.'cfg(target_os = "windows")'.dependencies] bytemuck = { workspace = true } os_str_bytes = { workspace = true } diff --git a/crates/fspy_shared/src/ipc/channel/backend/memfd.rs b/crates/fspy_shared/src/ipc/channel/backend/memfd.rs new file mode 100644 index 000000000..e29ec9746 --- /dev/null +++ b/crates/fspy_shared/src/ipc/channel/backend/memfd.rs @@ -0,0 +1,29 @@ +use std::{fs::File, io, os::unix::io::AsRawFd, sync::Arc}; + +use memfd::MemfdOptions; +use memmap2::{MmapMut, MmapOptions}; + +pub struct Shm { + pub mmap: MmapMut, + pub fd_path: Arc, +} + +pub fn create(size: usize) -> io::Result { + let memfd = MemfdOptions::default().create("ipc").map_err(io::Error::other)?; + memfd.as_file().set_len(size as u64)?; + + let fd = memfd.as_file().as_raw_fd(); + let fd_path = format!("/proc/self/fd/{fd}"); + + let mmap = unsafe { MmapOptions::new().len(size).map_mut(memfd.as_file())? }; + + Ok(Shm { mmap, fd_path: fd_path.into() }) +} + +pub fn open(shm_id: &str, size: usize) -> io::Result { + let file = File::open(shm_id)?; + + let mmap = unsafe { memmap2::MmapOptions::new().len(size).map_mut(&file)? }; + + Ok(mmap) +} diff --git a/crates/fspy_shared/src/ipc/channel/backend/mod.rs b/crates/fspy_shared/src/ipc/channel/backend/mod.rs new file mode 100644 index 000000000..40fadd6aa --- /dev/null +++ b/crates/fspy_shared/src/ipc/channel/backend/mod.rs @@ -0,0 +1,10 @@ +#[cfg(target_os = "android")] +mod memfd; + +#[cfg(not(target_os = "android"))] +mod shmem; + +#[cfg(target_os = "android")] +pub use memfd::*; +#[cfg(not(target_os = "android"))] +pub use shmem::*; diff --git a/crates/fspy_shared/src/ipc/channel/backend/shmem.rs b/crates/fspy_shared/src/ipc/channel/backend/shmem.rs new file mode 100644 index 000000000..f79907e8f --- /dev/null +++ b/crates/fspy_shared/src/ipc/channel/backend/shmem.rs @@ -0,0 +1,20 @@ +use std::io; + +use shared_memory::{Shmem, ShmemConf}; + +pub struct Shm { + pub shm: Shmem, + pub os_id: String, +} + +pub fn create(size: usize) -> io::Result { + let shm = ShmemConf::new().size(size).create().map_err(io::Error::other)?; + + let os_id = shm.get_os_id().to_string(); + + Ok(Shm { shm, os_id }) +} + +pub fn open(os_id: &str, size: usize) -> io::Result { + ShmemConf::new().size(size).os_id(os_id).open().map_err(io::Error::other) +} diff --git a/crates/fspy_shared/src/ipc/channel/mod.rs b/crates/fspy_shared/src/ipc/channel/mod.rs index 3e67cea80..fab516f6e 100644 --- a/crates/fspy_shared/src/ipc/channel/mod.rs +++ b/crates/fspy_shared/src/ipc/channel/mod.rs @@ -1,19 +1,17 @@ //! Fast mpsc IPC channel implementation based on shared memory. -mod shm_io; - use std::{env::temp_dir, fs::File, io, ops::Deref, path::PathBuf, sync::Arc}; use bincode::{Decode, Encode}; -use shared_memory::{Shmem, ShmemConf}; -pub use shm_io::FrameMut; use shm_io::{ShmReader, ShmWriter}; use tracing::debug; use uuid::Uuid; use super::NativeStr; -/// Serializable configuration to create channel senders. +mod backend; +mod shm_io; + #[derive(Encode, Decode, Clone, Debug)] pub struct ChannelConf { lock_file_path: Box, @@ -21,31 +19,24 @@ pub struct ChannelConf { shm_size: usize, } -/// 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-vite crate: cannot use vite_str/vite_path types" -)] pub fn channel(capacity: usize) -> io::Result<(ChannelConf, Receiver)> { - // Initialize the lock file with a unique name. let lock_file_path = temp_dir().join(format!("fspy_ipc_{}.lock", Uuid::new_v4())); - #[cfg_attr( - not(windows), - expect(unused_mut, reason = "mut required on Windows, unused on Unix") - )] - let mut conf = ShmemConf::new().size(capacity); - // On Windows, allow opening raw shared memory (without backing file) for DLL injection scenarios - #[cfg(target_os = "windows")] - { - conf = conf.allow_raw(true); - } + let shm = backend::create(capacity)?; + + #[cfg(target_os = "android")] + let conf = ChannelConf { + lock_file_path: lock_file_path.as_os_str().into(), - let shm = conf.create().map_err(io::Error::other)?; + shm_id: shm.fd_path.clone(), + shm_size: capacity, + }; + #[cfg(not(target_os = "android"))] let conf = ChannelConf { lock_file_path: lock_file_path.as_os_str().into(), - shm_id: shm.get_os_id().into(), + + os_id: shm.os_id.clone().into(), shm_size: capacity, }; @@ -65,26 +56,38 @@ impl ChannelConf { let lock_file = File::open(self.lock_file_path.to_cow_os_str())?; lock_file.try_lock_shared()?; - #[cfg_attr( - not(windows), - expect(unused_mut, reason = "mut required on Windows, unused on Unix") - )] - let mut conf = ShmemConf::new().size(self.shm_size).os_id(&self.shm_id); - // On Windows, allow opening raw shared memory (without backing file) for DLL injection scenarios - #[cfg(target_os = "windows")] - { - conf = conf.allow_raw(true); - } - let shm = conf.open().map_err(io::Error::other)?; - // SAFETY: `shm` is a freshly opened shared memory region with valid pointer and size. - // Exclusive write access is ensured by the shared file lock held by this sender. - let writer = unsafe { ShmWriter::new(shm) }; + #[cfg(target_os = "android")] + let writer = { + let mmap = backend::open(&self.shm_id, self.shm_size)?; + + WriterHandle::MemFd(unsafe { ShmWriter::new(mmap) }) + }; + + #[cfg(not(target_os = "android"))] + let writer = { + #[cfg_attr( + not(windows), + expect(unused_mut, reason = "mut required on Windows, unused on Unix") + )] + let mut conf = ShmemConf::new().size(self.shm_size).os_id(&self.shm_id); + + #[cfg(target_os = "windows")] + { + conf = conf.allow_raw(true); + } + + let shm = conf.open().map_err(io::Error::other)?; + + // SAFETY: shm valide + lock garantit exclusivité + unsafe { ShmWriter::new(shm) } + }; + Ok(Sender { writer, lock_file, lock_file_path: self.lock_file_path.clone() }) } } pub struct Sender { - writer: ShmWriter, + writer: WriterHandle, lock_file_path: Box, lock_file: File, } @@ -98,7 +101,7 @@ impl Drop for Sender { } impl Deref for Sender { - type Target = ShmWriter; + type Target = WriterHandle; fn deref(&self) -> &Self::Target { &self.writer @@ -115,22 +118,27 @@ unsafe impl Send for Sender {} /// SAFETY: `Sender` holds a shared file lock that ensures there's no reader, so `shm` can be safely written to. unsafe impl Sync for Sender {} -/// The unique receiver side of an IPC channel. -/// Owns the lock file and removes it on drop. +enum ShmHandle { + #[cfg(target_os = "android")] + MemFd(memmap2::MmapMut), + #[cfg(not(target_os = "android"))] + Shmem(shared_memory::Shmem), +} + +pub enum WriterHandle { + #[cfg(target_os = "android")] + MemFd(ShmWriter), + #[cfg(not(target_os = "android"))] + Shmem(ShmWriter), +} + pub struct Receiver { lock_file_path: PathBuf, lock_file: File, - shm: Shmem, + shm: ShmHandle, } -#[expect( - clippy::non_send_fields_in_send_ty, - reason = "Receiver doesn't read or write `shm`. It only pass it to `ReceiverLockGuard` under the lock" -)] -/// SAFETY: `Receiver` doesn't read or write `shm`. It only passes it to `ReceiverLockGuard` under the lock. unsafe impl Send for Receiver {} - -/// SAFETY: `Receiver` doesn't read or write `shm`. It only passes it to `ReceiverLockGuard` under the lock. unsafe impl Sync for Receiver {} impl Drop for Receiver { @@ -142,8 +150,15 @@ impl Drop for Receiver { } impl Receiver { - fn new(lock_file_path: PathBuf, shm: Shmem) -> io::Result { + fn new(lock_file_path: PathBuf, shm: backend::Shm) -> io::Result { let lock_file = File::create(&lock_file_path)?; + + #[cfg(target_os = "android")] + let shm = ShmHandle::MemFd(shm.mmap); + + #[cfg(not(target_os = "android"))] + let shm = ShmHandle::Shmem(shm.shm); + Ok(Self { lock_file_path, lock_file, shm }) } @@ -156,9 +171,19 @@ impl Receiver { )] pub fn lock(&self) -> io::Result> { self.lock_file.lock()?; - // SAFETY: The exclusive file lock is held, so no writers can access the shared memory. - // The lock ensures all prior writes are visible to this thread. - let reader = ShmReader::new(unsafe { self.shm.as_slice() }); + + let slice: &[u8] = match &self.shm { + #[cfg(target_os = "android")] + ShmHandle::MemFd(mmap) => unsafe { + std::slice::from_raw_parts(mmap.as_ptr(), mmap.len()) + }, + + #[cfg(not(target_os = "android"))] + ShmHandle::Shmem(shm) => unsafe { shm.as_slice() }, + }; + + let reader = ShmReader::new(slice); + Ok(ReceiverLockGuard { reader, lock_file: &self.lock_file }) } } diff --git a/crates/fspy_shared/src/ipc/channel/shm_io.rs b/crates/fspy_shared/src/ipc/channel/shm_io.rs index 39a490d19..95aa67045 100644 --- a/crates/fspy_shared/src/ipc/channel/shm_io.rs +++ b/crates/fspy_shared/src/ipc/channel/shm_io.rs @@ -12,6 +12,9 @@ use bincode::{ Encode, config::Config, enc::write::SizeWriter, encode_into_slice, encode_into_writer, }; use bytemuck::must_cast; +#[cfg(target_os = "android")] +use memmap2::MmapMut; +#[cfg(not(target_os = "android"))] use shared_memory::Shmem; // `ShmWriter` writes headers using atomic operations to prevent partial writes due to crashes, @@ -30,11 +33,18 @@ pub trait AsRawSlice { fn as_raw_slice(&self) -> *mut [u8]; } +#[cfg(not(target_os = "android"))] impl AsRawSlice for Shmem { fn as_raw_slice(&self) -> *mut [u8] { slice_from_raw_parts_mut(self.as_ptr(), self.len()) } } +#[cfg(target_os = "android")] +impl AsRawSlice for MmapMut { + fn as_raw_slice(&self) -> *mut [u8] { + slice_from_raw_parts_mut(self.as_ptr() as *mut u8, self.len()) + } +} /// A concurrent shared memory writer. /// diff --git a/crates/fspy_shared_unix/Cargo.toml b/crates/fspy_shared_unix/Cargo.toml index 4afedf816..c5266c390 100644 --- a/crates/fspy_shared_unix/Cargo.toml +++ b/crates/fspy_shared_unix/Cargo.toml @@ -15,7 +15,7 @@ stackalloc = { workspace = true } [dev-dependencies] -[target.'cfg(target_os = "linux")'.dependencies] +[target.'cfg(any(target_os = "android",target_os = "linux"))'.dependencies] elf = { workspace = true } fspy_seccomp_unotify = { workspace = true, features = ["target"] } memmap2 = { workspace = true } diff --git a/crates/fspy_shared_unix/src/lib.rs b/crates/fspy_shared_unix/src/lib.rs index 5437a3efc..d39490f9e 100644 --- a/crates/fspy_shared_unix/src/lib.rs +++ b/crates/fspy_shared_unix/src/lib.rs @@ -5,8 +5,9 @@ pub(crate) mod open_exec; pub mod payload; pub mod spawn; -#[cfg(target_os = "linux")] +#[cfg(any(target_os = "android", target_os = "linux"))] mod elf; -#[cfg(target_os = "linux")] // exposed for verifying static executables in fspy tests +#[cfg(any(target_os = "android", target_os = "linux"))] +// exposed for verifying static executables in fspy tests pub use elf::is_dynamically_linked_to_libc; diff --git a/crates/fspy_shared_unix/src/payload.rs b/crates/fspy_shared_unix/src/payload.rs index e23f7ce04..252f8c01d 100644 --- a/crates/fspy_shared_unix/src/payload.rs +++ b/crates/fspy_shared_unix/src/payload.rs @@ -3,7 +3,7 @@ use std::os::unix::ffi::OsStringExt; use base64::{Engine as _, prelude::BASE64_STANDARD_NO_PAD}; use bincode::{Decode, Encode, config::standard}; use bstr::BString; -#[cfg(not(target_env = "musl"))] +#[cfg(all(not(target_os = "android"), not(target_env = "musl")))] use fspy_shared::ipc::NativeStr; use fspy_shared::ipc::channel::ChannelConf; @@ -11,7 +11,7 @@ use fspy_shared::ipc::channel::ChannelConf; pub struct Payload { pub ipc_channel_conf: ChannelConf, - #[cfg(not(target_env = "musl"))] + #[cfg(all(not(target_os = "android"), not(target_env = "musl")))] pub preload_path: Box, #[cfg(target_os = "macos")] diff --git a/crates/fspy_shared_unix/src/spawn/linux/mod.rs b/crates/fspy_shared_unix/src/spawn/linux/mod.rs index 0632999d0..9ff6be499 100644 --- a/crates/fspy_shared_unix/src/spawn/linux/mod.rs +++ b/crates/fspy_shared_unix/src/spawn/linux/mod.rs @@ -1,11 +1,11 @@ -#[cfg(not(target_env = "musl"))] +#[cfg(all(not(target_os = "android"), not(target_env = "musl")))] use std::{ffi::OsStr, os::unix::ffi::OsStrExt as _, path::Path}; use fspy_seccomp_unotify::{payload::SeccompPayload, target::install_target}; -#[cfg(not(target_env = "musl"))] +#[cfg(all(not(target_os = "android"), not(target_env = "musl")))] use memmap2::Mmap; -#[cfg(not(target_env = "musl"))] +#[cfg(all(not(target_os = "android"), not(target_env = "musl")))] use crate::{elf, exec::ensure_env, open_exec::open_executable}; use crate::{ exec::Exec, @@ -28,11 +28,11 @@ impl PreExec { pub fn handle_exec( command: &mut Exec, - encoded_payload: &EncodedPayload, + _encoded_payload: &EncodedPayload, ) -> nix::Result> { // On musl targets, LD_PRELOAD is not available (cdylib not supported). // Always use seccomp-based tracking instead. - #[cfg(not(target_env = "musl"))] + #[cfg(all(not(target_os = "android"), not(target_env = "musl")))] { let executable_fd = open_executable(Path::new(OsStr::from_bytes(&command.program)))?; // SAFETY: The file descriptor is valid and we only read from the mapping. @@ -51,5 +51,6 @@ pub fn handle_exec( } command.envs.retain(|(name, _)| name != LD_PRELOAD && name != PAYLOAD_ENV_NAME); - Ok(Some(PreExec(encoded_payload.payload.seccomp_payload.clone()))) + Ok(None) + // Ok(Some(PreExec(encoded_payload.payload.seccomp_payload.clone()))) } diff --git a/crates/fspy_shared_unix/src/spawn/mod.rs b/crates/fspy_shared_unix/src/spawn/mod.rs index e48125772..7273030b3 100644 --- a/crates/fspy_shared_unix/src/spawn/mod.rs +++ b/crates/fspy_shared_unix/src/spawn/mod.rs @@ -1,4 +1,4 @@ -#[cfg(target_os = "linux")] +#[cfg(any(target_os = "android", target_os = "linux"))] #[path = "./linux/mod.rs"] mod os_specific; From f13c2f6f3cd2b37f2527fa658e55f536c973b2f0 Mon Sep 17 00:00:00 2001 From: Ludea Date: Fri, 10 Apr 2026 09:13:27 +0200 Subject: [PATCH 02/11] missing type from shared_memory crate --- crates/fspy_shared/src/ipc/channel/mod.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/fspy_shared/src/ipc/channel/mod.rs b/crates/fspy_shared/src/ipc/channel/mod.rs index fab516f6e..306a371f6 100644 --- a/crates/fspy_shared/src/ipc/channel/mod.rs +++ b/crates/fspy_shared/src/ipc/channel/mod.rs @@ -3,6 +3,8 @@ use std::{env::temp_dir, fs::File, io, ops::Deref, path::PathBuf, sync::Arc}; use bincode::{Decode, Encode}; +#[cfg(not(target_os = "android"))] +use shared_memory::{Shmem, ShmemConf}; use shm_io::{ShmReader, ShmWriter}; use tracing::debug; use uuid::Uuid; From 9ca884e89bb5dff55ad2d859f33536e4c8f8b1a1 Mon Sep 17 00:00:00 2001 From: Ludea Date: Fri, 10 Apr 2026 09:30:00 +0200 Subject: [PATCH 03/11] move use statement outside install_target fn definition --- crates/fspy_seccomp_unotify/src/target.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/crates/fspy_seccomp_unotify/src/target.rs b/crates/fspy_seccomp_unotify/src/target.rs index 3561dcdac..8e424451e 100644 --- a/crates/fspy_seccomp_unotify/src/target.rs +++ b/crates/fspy_seccomp_unotify/src/target.rs @@ -7,6 +7,8 @@ use std::{ }; use libc::sock_filter; +#[cfg(target_os = "android")] +use libc::{PR_SET_NO_NEW_PRIVS, prctl}; #[cfg(not(target_os = "android"))] use nix::sys::prctl::set_no_new_privs; use passfd::FdPassingExt; @@ -22,12 +24,13 @@ use crate::{bindings::install_unotify_filter, payload::SeccompPayload}; pub fn install_target(payload: &SeccompPayload) -> nix::Result<()> { #[cfg(not(target_os = "android"))] set_no_new_privs()?; - #[cfg(target_os = "android")] - use libc::{PR_SET_NO_NEW_PRIVS, prctl}; - let ret = unsafe { prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) }; - if ret != 0 { - return Err(nix::Error::last()); + #[cfg(target_os = "android")] + { + let ret = unsafe { prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) }; + if ret != 0 { + return Err(nix::Error::last()); + } } let sock_filters = From a178b2a2d7f8daf351d1417787383c9c048c21c6 Mon Sep 17 00:00:00 2001 From: Ludea Date: Fri, 10 Apr 2026 09:34:15 +0200 Subject: [PATCH 04/11] fix variable name --- crates/fspy_shared/src/ipc/channel/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/fspy_shared/src/ipc/channel/mod.rs b/crates/fspy_shared/src/ipc/channel/mod.rs index 306a371f6..30d25bde4 100644 --- a/crates/fspy_shared/src/ipc/channel/mod.rs +++ b/crates/fspy_shared/src/ipc/channel/mod.rs @@ -38,7 +38,7 @@ pub fn channel(capacity: usize) -> io::Result<(ChannelConf, Receiver)> { let conf = ChannelConf { lock_file_path: lock_file_path.as_os_str().into(), - os_id: shm.os_id.clone().into(), + shm_id: shm.os_id.clone().into(), shm_size: capacity, }; From 926549b12b4eb6d28828d3df0580162d01049f31 Mon Sep 17 00:00:00 2001 From: Ludea Date: Fri, 10 Apr 2026 12:44:42 +0200 Subject: [PATCH 05/11] re enable fspy_preload_unix deps --- crates/fspy/Cargo.toml | 4 ++-- crates/fspy_shared/src/ipc/channel/mod.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/fspy/Cargo.toml b/crates/fspy/Cargo.toml index bc9509255..bb4a5ce92 100644 --- a/crates/fspy/Cargo.toml +++ b/crates/fspy/Cargo.toml @@ -29,8 +29,8 @@ fspy_seccomp_unotify = { workspace = true, features = ["supervisor"] } nix = { workspace = true, features = ["uio"] } tokio = { workspace = true, features = ["bytes"] } -#[target.'cfg(all(unix, any(not(target_env = "musl"), not(target_os = "android"))))'.dependencies] -#fspy_preload_unix = { workspace = true } +[target.'cfg(all(unix, all(not(target_env = "musl"), not(target_os = "android"))))'.dependencies] +fspy_preload_unix = { workspace = true } [target.'cfg(unix)'.dependencies] fspy_shared_unix = { workspace = true } diff --git a/crates/fspy_shared/src/ipc/channel/mod.rs b/crates/fspy_shared/src/ipc/channel/mod.rs index 30d25bde4..d84a123af 100644 --- a/crates/fspy_shared/src/ipc/channel/mod.rs +++ b/crates/fspy_shared/src/ipc/channel/mod.rs @@ -4,7 +4,7 @@ use std::{env::temp_dir, fs::File, io, ops::Deref, path::PathBuf, sync::Arc}; use bincode::{Decode, Encode}; #[cfg(not(target_os = "android"))] -use shared_memory::{Shmem, ShmemConf}; +use shared_memory::ShmemConf; use shm_io::{ShmReader, ShmWriter}; use tracing::debug; use uuid::Uuid; From 25d238db0737268f18ba929050e20ffc00daff37 Mon Sep 17 00:00:00 2001 From: Ludea Date: Fri, 10 Apr 2026 12:53:21 +0200 Subject: [PATCH 06/11] remove unused import --- crates/fspy_shared/src/ipc/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/fspy_shared/src/ipc/mod.rs b/crates/fspy_shared/src/ipc/mod.rs index 755627d3b..f137a8440 100644 --- a/crates/fspy_shared/src/ipc/mod.rs +++ b/crates/fspy_shared/src/ipc/mod.rs @@ -1,3 +1,4 @@ +#[cfg(not(target_os = "android"))] pub mod channel; mod native_path; pub(crate) mod native_str; From aa4fed1a854498f3ec001f85d3084428a459ae5d Mon Sep 17 00:00:00 2001 From: Ludea Date: Fri, 10 Apr 2026 13:14:57 +0200 Subject: [PATCH 07/11] right type --- crates/fspy_shared/src/ipc/channel/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/fspy_shared/src/ipc/channel/mod.rs b/crates/fspy_shared/src/ipc/channel/mod.rs index d84a123af..3bfdeeb24 100644 --- a/crates/fspy_shared/src/ipc/channel/mod.rs +++ b/crates/fspy_shared/src/ipc/channel/mod.rs @@ -81,7 +81,7 @@ impl ChannelConf { let shm = conf.open().map_err(io::Error::other)?; // SAFETY: shm valide + lock garantit exclusivité - unsafe { ShmWriter::new(shm) } + WriterHandle::Shmem(unsafe { ShmWriter::new(shm) }) }; Ok(Sender { writer, lock_file, lock_file_path: self.lock_file_path.clone() }) From 3e8be9f4be92d4a96e0f212b107a7c103d723524 Mon Sep 17 00:00:00 2001 From: Ludea Date: Sat, 11 Apr 2026 09:15:58 +0200 Subject: [PATCH 08/11] Remove unused open fn Signed-off-by: Ludea --- crates/fspy_shared/src/ipc/channel/backend/shmem.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/crates/fspy_shared/src/ipc/channel/backend/shmem.rs b/crates/fspy_shared/src/ipc/channel/backend/shmem.rs index f79907e8f..c7b6afff9 100644 --- a/crates/fspy_shared/src/ipc/channel/backend/shmem.rs +++ b/crates/fspy_shared/src/ipc/channel/backend/shmem.rs @@ -14,7 +14,3 @@ pub fn create(size: usize) -> io::Result { Ok(Shm { shm, os_id }) } - -pub fn open(os_id: &str, size: usize) -> io::Result { - ShmemConf::new().size(size).os_id(os_id).open().map_err(io::Error::other) -} From e8435ebc4e2a8ca176c9ae3eb3ac4a54e4505089 Mon Sep 17 00:00:00 2001 From: Ludea Date: Mon, 13 Apr 2026 09:37:51 +0200 Subject: [PATCH 09/11] exclude ipc module for android host --- crates/fspy/src/lib.rs | 2 +- crates/fspy/src/unix/mod.rs | 18 +++++++++--------- crates/fspy_shared_unix/src/payload.rs | 4 ++-- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/crates/fspy/src/lib.rs b/crates/fspy/src/lib.rs index 40b567683..acdf63e48 100644 --- a/crates/fspy/src/lib.rs +++ b/crates/fspy/src/lib.rs @@ -8,7 +8,7 @@ mod artifact; pub mod error; -#[cfg(not(target_env = "musl"))] +#[cfg(all(not(target_os = "android"), not(target_env = "musl")))] mod ipc; #[cfg(unix)] diff --git a/crates/fspy/src/unix/mod.rs b/crates/fspy/src/unix/mod.rs index a17aa6afe..d42e7c80f 100644 --- a/crates/fspy/src/unix/mod.rs +++ b/crates/fspy/src/unix/mod.rs @@ -9,7 +9,7 @@ use std::{io, path::Path}; #[cfg(target_os = "linux")] use fspy_seccomp_unotify::supervisor::supervise; use fspy_shared::ipc::PathAccess; -#[cfg(not(target_env = "musl"))] +#[cfg(all(not(target_os = "android"), not(target_env = "musl")))] use fspy_shared::ipc::{NativeStr, channel::channel}; #[cfg(target_os = "macos")] use fspy_shared_unix::payload::Artifacts; @@ -24,7 +24,7 @@ use syscall_handler::SyscallHandler; use tokio::task::spawn_blocking; use tokio_util::sync::CancellationToken; -#[cfg(not(target_env = "musl"))] +#[cfg(all(not(target_os = "android"), not(target_env = "musl")))] use crate::ipc::{OwnedReceiverLockGuard, SHM_CAPACITY}; use crate::{ChildTermination, Command, TrackedChild, arena::PathAccessArena, error::SpawnError}; @@ -88,12 +88,12 @@ impl SpyImpl { #[cfg(target_os = "linux")] let supervisor = supervise::().map_err(SpawnError::Supervisor)?; - #[cfg(not(target_env = "musl"))] + #[cfg(all(not(target_os = "android"), not(target_env = "musl")))] let (ipc_channel_conf, ipc_receiver) = channel(SHM_CAPACITY).map_err(SpawnError::ChannelCreation)?; let payload = Payload { - #[cfg(not(target_env = "musl"))] + #[cfg(all(not(target_os = "android"), not(target_env = "musl")))] ipc_channel_conf, #[cfg(target_os = "macos")] @@ -171,12 +171,12 @@ impl SpyImpl { // Lock the ipc channel after the child has exited. // We are not interested in path accesses from descendants after the main child has exited. - #[cfg(not(target_env = "musl"))] + #[cfg(all(not(target_os = "android"), not(target_env = "musl")))] let ipc_receiver_lock_guard = OwnedReceiverLockGuard::lock_async(ipc_receiver).await?; let path_accesses = PathAccessIterable { arenas, - #[cfg(not(target_env = "musl"))] + #[cfg(all(not(target_os = "android"), not(target_env = "musl")))] ipc_receiver_lock_guard, }; @@ -190,7 +190,7 @@ impl SpyImpl { pub struct PathAccessIterable { arenas: Vec, - #[cfg(not(target_env = "musl"))] + #[cfg(all(not(target_os = "android"), not(target_env = "musl")))] ipc_receiver_lock_guard: OwnedReceiverLockGuard, } @@ -199,12 +199,12 @@ impl PathAccessIterable { let accesses_in_arena = self.arenas.iter().flat_map(|arena| arena.borrow_accesses().iter()).copied(); - #[cfg(not(target_env = "musl"))] + #[cfg(all(not(target_os = "android"), not(target_env = "musl")))] { let accesses_in_shm = self.ipc_receiver_lock_guard.iter_path_accesses(); accesses_in_shm.chain(accesses_in_arena) } - #[cfg(target_env = "musl")] + #[cfg(any(target_os = "android", target_env = "musl"))] { accesses_in_arena } diff --git a/crates/fspy_shared_unix/src/payload.rs b/crates/fspy_shared_unix/src/payload.rs index a35a2de66..599d01bb7 100644 --- a/crates/fspy_shared_unix/src/payload.rs +++ b/crates/fspy_shared_unix/src/payload.rs @@ -5,12 +5,12 @@ use bincode::{Decode, Encode, config::standard}; use bstr::BString; #[cfg(all(not(target_os = "android"), not(target_env = "musl")))] use fspy_shared::ipc::NativeStr; -#[cfg(not(target_env = "musl"))] +#[cfg(all(not(target_os = "android"), not(target_env = "musl")))] use fspy_shared::ipc::channel::ChannelConf; #[derive(Debug, Encode, Decode)] pub struct Payload { - #[cfg(not(target_env = "musl"))] + #[cfg(all(not(target_os = "android"), not(target_env = "musl")))] pub ipc_channel_conf: ChannelConf, #[cfg(all(not(target_os = "android"), not(target_env = "musl")))] From 45ca273984f62117a5a884bdc9f4e6582069835e Mon Sep 17 00:00:00 2001 From: Ludea Date: Mon, 13 Apr 2026 15:24:18 +0200 Subject: [PATCH 10/11] review fzedback --- .../src/ipc/channel/backend/memfd.rs | 29 -- .../src/ipc/channel/backend/mod.rs | 10 - .../src/ipc/channel/backend/shmem.rs | 16 - crates/fspy_shared/src/ipc/channel/mod.rs | 315 ++---------------- crates/fspy_shared/src/ipc/mod.rs | 2 +- 5 files changed, 37 insertions(+), 335 deletions(-) delete mode 100644 crates/fspy_shared/src/ipc/channel/backend/memfd.rs delete mode 100644 crates/fspy_shared/src/ipc/channel/backend/mod.rs delete mode 100644 crates/fspy_shared/src/ipc/channel/backend/shmem.rs diff --git a/crates/fspy_shared/src/ipc/channel/backend/memfd.rs b/crates/fspy_shared/src/ipc/channel/backend/memfd.rs deleted file mode 100644 index e29ec9746..000000000 --- a/crates/fspy_shared/src/ipc/channel/backend/memfd.rs +++ /dev/null @@ -1,29 +0,0 @@ -use std::{fs::File, io, os::unix::io::AsRawFd, sync::Arc}; - -use memfd::MemfdOptions; -use memmap2::{MmapMut, MmapOptions}; - -pub struct Shm { - pub mmap: MmapMut, - pub fd_path: Arc, -} - -pub fn create(size: usize) -> io::Result { - let memfd = MemfdOptions::default().create("ipc").map_err(io::Error::other)?; - memfd.as_file().set_len(size as u64)?; - - let fd = memfd.as_file().as_raw_fd(); - let fd_path = format!("/proc/self/fd/{fd}"); - - let mmap = unsafe { MmapOptions::new().len(size).map_mut(memfd.as_file())? }; - - Ok(Shm { mmap, fd_path: fd_path.into() }) -} - -pub fn open(shm_id: &str, size: usize) -> io::Result { - let file = File::open(shm_id)?; - - let mmap = unsafe { memmap2::MmapOptions::new().len(size).map_mut(&file)? }; - - Ok(mmap) -} diff --git a/crates/fspy_shared/src/ipc/channel/backend/mod.rs b/crates/fspy_shared/src/ipc/channel/backend/mod.rs deleted file mode 100644 index 40fadd6aa..000000000 --- a/crates/fspy_shared/src/ipc/channel/backend/mod.rs +++ /dev/null @@ -1,10 +0,0 @@ -#[cfg(target_os = "android")] -mod memfd; - -#[cfg(not(target_os = "android"))] -mod shmem; - -#[cfg(target_os = "android")] -pub use memfd::*; -#[cfg(not(target_os = "android"))] -pub use shmem::*; diff --git a/crates/fspy_shared/src/ipc/channel/backend/shmem.rs b/crates/fspy_shared/src/ipc/channel/backend/shmem.rs deleted file mode 100644 index c7b6afff9..000000000 --- a/crates/fspy_shared/src/ipc/channel/backend/shmem.rs +++ /dev/null @@ -1,16 +0,0 @@ -use std::io; - -use shared_memory::{Shmem, ShmemConf}; - -pub struct Shm { - pub shm: Shmem, - pub os_id: String, -} - -pub fn create(size: usize) -> io::Result { - let shm = ShmemConf::new().size(size).create().map_err(io::Error::other)?; - - let os_id = shm.get_os_id().to_string(); - - Ok(Shm { shm, os_id }) -} diff --git a/crates/fspy_shared/src/ipc/channel/mod.rs b/crates/fspy_shared/src/ipc/channel/mod.rs index 3bfdeeb24..c3309edc3 100644 --- a/crates/fspy_shared/src/ipc/channel/mod.rs +++ b/crates/fspy_shared/src/ipc/channel/mod.rs @@ -1,296 +1,53 @@ -//! Fast mpsc IPC channel implementation based on shared memory. +#[cfg(not(target_env = "musl"))] +pub mod channel; +mod native_path; +pub(crate) mod native_str; -use std::{env::temp_dir, fs::File, io, ops::Deref, path::PathBuf, sync::Arc}; +use std::fmt::Debug; -use bincode::{Decode, Encode}; -#[cfg(not(target_os = "android"))] -use shared_memory::ShmemConf; -use shm_io::{ShmReader, ShmWriter}; -use tracing::debug; -use uuid::Uuid; +use bincode::{BorrowDecode, Encode, config::Configuration}; +use bitflags::bitflags; +pub use native_path::NativePath; +pub use native_str::NativeStr; -use super::NativeStr; +pub const BINCODE_CONFIG: Configuration = bincode::config::standard(); -mod backend; -mod shm_io; +#[derive(Encode, BorrowDecode, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] +pub struct AccessMode(u8); -#[derive(Encode, Decode, Clone, Debug)] -pub struct ChannelConf { - lock_file_path: Box, - shm_id: Arc, - shm_size: usize, -} - -pub fn channel(capacity: usize) -> io::Result<(ChannelConf, Receiver)> { - let lock_file_path = temp_dir().join(format!("fspy_ipc_{}.lock", Uuid::new_v4())); - - let shm = backend::create(capacity)?; - - #[cfg(target_os = "android")] - let conf = ChannelConf { - lock_file_path: lock_file_path.as_os_str().into(), - - shm_id: shm.fd_path.clone(), - shm_size: capacity, - }; - - #[cfg(not(target_os = "android"))] - let conf = ChannelConf { - lock_file_path: lock_file_path.as_os_str().into(), - - shm_id: shm.os_id.clone().into(), - shm_size: capacity, - }; - - let receiver = Receiver::new(lock_file_path, shm)?; - Ok((conf, receiver)) -} - -impl ChannelConf { - /// Creates a sender. - /// - /// This doesn't block on the file lock. Instead it returns immediately with error if the receiver is locked or dropped. - #[expect( - clippy::missing_errors_doc, - reason = "error conditions are self-evident from return type" - )] - pub fn sender(&self) -> io::Result { - let lock_file = File::open(self.lock_file_path.to_cow_os_str())?; - lock_file.try_lock_shared()?; - - #[cfg(target_os = "android")] - let writer = { - let mmap = backend::open(&self.shm_id, self.shm_size)?; - - WriterHandle::MemFd(unsafe { ShmWriter::new(mmap) }) - }; - - #[cfg(not(target_os = "android"))] - let writer = { - #[cfg_attr( - not(windows), - expect(unused_mut, reason = "mut required on Windows, unused on Unix") - )] - let mut conf = ShmemConf::new().size(self.shm_size).os_id(&self.shm_id); - - #[cfg(target_os = "windows")] - { - conf = conf.allow_raw(true); - } - - let shm = conf.open().map_err(io::Error::other)?; - - // SAFETY: shm valide + lock garantit exclusivité - WriterHandle::Shmem(unsafe { ShmWriter::new(shm) }) - }; - - Ok(Sender { writer, lock_file, lock_file_path: self.lock_file_path.clone() }) +bitflags! { + impl AccessMode: u8 { + const READ = 1; + const WRITE = 1 << 1; + const READ_DIR = 1 << 2; } } -pub struct Sender { - writer: WriterHandle, - lock_file_path: Box, - lock_file: File, -} - -impl Drop for Sender { - fn drop(&mut self) { - if let Err(err) = self.lock_file.unlock() { - debug!("Failed to unlock the shared IPC lock {:?}: {}", self.lock_file_path, err); - } - } -} - -impl Deref for Sender { - type Target = WriterHandle; - - fn deref(&self) -> &Self::Target { - &self.writer - } -} - -#[expect( - clippy::non_send_fields_in_send_ty, - reason = "`Sender` holds a shared file lock that ensures there's no reader, so `shm` can be safely written to" -)] -/// SAFETY: `Sender` holds a shared file lock that ensures there's no reader, so `shm` can be safely written to. -unsafe impl Send for Sender {} - -/// SAFETY: `Sender` holds a shared file lock that ensures there's no reader, so `shm` can be safely written to. -unsafe impl Sync for Sender {} - -enum ShmHandle { - #[cfg(target_os = "android")] - MemFd(memmap2::MmapMut), - #[cfg(not(target_os = "android"))] - Shmem(shared_memory::Shmem), -} - -pub enum WriterHandle { - #[cfg(target_os = "android")] - MemFd(ShmWriter), - #[cfg(not(target_os = "android"))] - Shmem(ShmWriter), -} - -pub struct Receiver { - lock_file_path: PathBuf, - lock_file: File, - shm: ShmHandle, -} - -unsafe impl Send for Receiver {} -unsafe impl Sync for Receiver {} - -impl Drop for Receiver { - fn drop(&mut self) { - if let Err(err) = std::fs::remove_file(&self.lock_file_path) { - debug!("Failed to remove IPC lock file {:?}: {}", self.lock_file_path, err); - } - } -} - -impl Receiver { - fn new(lock_file_path: PathBuf, shm: backend::Shm) -> io::Result { - let lock_file = File::create(&lock_file_path)?; - - #[cfg(target_os = "android")] - let shm = ShmHandle::MemFd(shm.mmap); - - #[cfg(not(target_os = "android"))] - let shm = ShmHandle::Shmem(shm.shm); - - Ok(Self { lock_file_path, lock_file, shm }) - } - - /// Lock the shared memory for unique read access. - /// Blocks until all the senders have dropped (or processes owning them have all exited) so the shared memory can be safely read. - /// During the lifetime of returned `ReceiverReadGuard`, no new senders can be created (`ChannelConf::sender` would fail). - #[expect( - clippy::missing_errors_doc, - reason = "error conditions are self-evident from return type" - )] - pub fn lock(&self) -> io::Result> { - self.lock_file.lock()?; - - let slice: &[u8] = match &self.shm { - #[cfg(target_os = "android")] - ShmHandle::MemFd(mmap) => unsafe { - std::slice::from_raw_parts(mmap.as_ptr(), mmap.len()) - }, - - #[cfg(not(target_os = "android"))] - ShmHandle::Shmem(shm) => unsafe { shm.as_slice() }, - }; - - let reader = ShmReader::new(slice); - - Ok(ReceiverLockGuard { reader, lock_file: &self.lock_file }) - } -} - -pub struct ReceiverLockGuard<'a> { - reader: ShmReader<&'a [u8]>, - lock_file: &'a File, -} - -impl Drop for ReceiverLockGuard<'_> { - fn drop(&mut self) { - if let Err(err) = self.lock_file.unlock() { - debug!("Failed to unlock IPC lock file: {}", err); +impl Debug for AccessMode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + struct InternalAccessMode(AccessMode); + impl Debug for InternalAccessMode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + bitflags::parser::to_writer(&self.0, f) + } } + f.debug_tuple("AccessMode").field(&InternalAccessMode(*self)).finish() } } -impl<'a> Deref for ReceiverLockGuard<'a> { - type Target = ShmReader<&'a [u8]>; - fn deref(&self) -> &Self::Target { - &self.reader - } +#[derive(Encode, BorrowDecode, Debug, Clone, Copy, PartialEq, Eq)] +pub struct PathAccess<'a> { + pub mode: AccessMode, + pub path: &'a NativePath, + // TODO: add follow_symlinks (O_NOFOLLOW) } -#[cfg(test)] -mod tests { - use std::{num::NonZeroUsize, str::from_utf8}; - - use bstr::B; - use subprocess_test::command_for_fn; - - use super::*; - - #[test] - fn smoke() { - let (conf, receiver) = channel(100).unwrap(); - let cmd = command_for_fn!(conf, |conf: ChannelConf| { - let sender = conf.sender().unwrap(); - let frame_size = NonZeroUsize::new(2).unwrap(); - let mut frame = sender.claim_frame(frame_size).unwrap(); - frame.copy_from_slice(&[4, 2]); - }); - assert!(std::process::Command::from(cmd).status().unwrap().success()); - - let lock = receiver.lock().unwrap(); - let mut frames = lock.iter_frames(); - - let received_frame = frames.next().unwrap(); - assert_eq!(received_frame, &[4, 2]); - - assert!(frames.next().is_none()); +impl<'a> PathAccess<'a> { + pub fn read(path: impl Into<&'a NativePath>) -> Self { + Self { mode: AccessMode::READ, path: path.into() } } - #[test] - #[expect(clippy::print_stdout, reason = "test diagnostics")] - fn forbid_new_senders_after_locked() { - let (conf, receiver) = channel(42).unwrap(); - let _lock = receiver.lock().unwrap(); - - let cmd = command_for_fn!(conf, |conf: ChannelConf| { - print!("{}", conf.sender().is_ok()); - }); - let output = std::process::Command::from(cmd).output().unwrap(); - assert_eq!(B(&output.stdout), B("false")); - } - - #[test] - #[expect(clippy::print_stdout, reason = "test diagnostics")] - fn forbid_new_senders_after_receiver_dropped() { - let (conf, receiver) = channel(42).unwrap(); - drop(receiver); - - let cmd = command_for_fn!(conf, |conf: ChannelConf| { - print!("{}", conf.sender().is_ok()); - }); - let output = std::process::Command::from(cmd).output().unwrap(); - assert_eq!(B(&output.stdout), B("false")); + pub fn read_dir(path: impl Into<&'a NativePath>) -> Self { + Self { mode: AccessMode::READ_DIR, path: path.into() } } - - #[test] - fn concurrent_senders() { - let (conf, receiver) = channel(8192).unwrap(); - for i in 0u16..200 { - let cmd = command_for_fn!((conf.clone(), i), |(conf, i): (ChannelConf, u16)| { - let sender = conf.sender().unwrap(); - let data_to_send = i.to_string(); - sender - .claim_frame(NonZeroUsize::new(data_to_send.len()).unwrap()) - .unwrap() - .copy_from_slice(data_to_send.as_bytes()); - }); - let output = std::process::Command::from(cmd).output().unwrap(); - assert!( - output.status.success(), - "Failed to send in iteration {}: {:?}", - i, - B(&output.stderr) - ); - } - let lock = receiver.lock().unwrap(); - let mut received_values: Vec = lock - .iter_frames() - .map(|frame| from_utf8(frame).unwrap().parse::().unwrap()) - .collect(); - received_values.sort_unstable(); - assert_eq!(received_values, (0u16..200).collect::>()); - } -} +} \ No newline at end of file diff --git a/crates/fspy_shared/src/ipc/mod.rs b/crates/fspy_shared/src/ipc/mod.rs index 17df1c2d3..ddaea6a8a 100644 --- a/crates/fspy_shared/src/ipc/mod.rs +++ b/crates/fspy_shared/src/ipc/mod.rs @@ -1,4 +1,4 @@ -#[cfg(not(target_env = "musl"))] +#[cfg(all(not(target_os = "android"), not(target_env = "musl")))] pub mod channel; mod native_path; pub(crate) mod native_str; From 503f8cddd0964e0b88274b057bf3085c7adaf37a Mon Sep 17 00:00:00 2001 From: Ludea Date: Tue, 14 Apr 2026 06:39:41 +0200 Subject: [PATCH 11/11] revert all changes into shm_io --- crates/fspy_shared/src/ipc/channel/shm_io.rs | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/crates/fspy_shared/src/ipc/channel/shm_io.rs b/crates/fspy_shared/src/ipc/channel/shm_io.rs index 95aa67045..09e363ffd 100644 --- a/crates/fspy_shared/src/ipc/channel/shm_io.rs +++ b/crates/fspy_shared/src/ipc/channel/shm_io.rs @@ -12,9 +12,6 @@ use bincode::{ Encode, config::Config, enc::write::SizeWriter, encode_into_slice, encode_into_writer, }; use bytemuck::must_cast; -#[cfg(target_os = "android")] -use memmap2::MmapMut; -#[cfg(not(target_os = "android"))] use shared_memory::Shmem; // `ShmWriter` writes headers using atomic operations to prevent partial writes due to crashes, @@ -33,18 +30,11 @@ pub trait AsRawSlice { fn as_raw_slice(&self) -> *mut [u8]; } -#[cfg(not(target_os = "android"))] impl AsRawSlice for Shmem { fn as_raw_slice(&self) -> *mut [u8] { slice_from_raw_parts_mut(self.as_ptr(), self.len()) } } -#[cfg(target_os = "android")] -impl AsRawSlice for MmapMut { - fn as_raw_slice(&self) -> *mut [u8] { - slice_from_raw_parts_mut(self.as_ptr() as *mut u8, self.len()) - } -} /// A concurrent shared memory writer. /// @@ -724,4 +714,4 @@ mod tests { } } } -} +} \ No newline at end of file