diff --git a/Cargo.lock b/Cargo.lock index 4a53f9c9f5..31281c52a7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1608,7 +1608,6 @@ dependencies = [ "bincode", "bytemuck", "constcat", - "dashmap", "derive-where", "fspy_detours_sys", "fspy_shared", @@ -1661,6 +1660,7 @@ dependencies = [ "os_str_bytes", "phf", "shared_memory", + "thiserror 2.0.17", "tracing", "uuid", "winapi", diff --git a/crates/fspy/src/ipc.rs b/crates/fspy/src/ipc.rs new file mode 100644 index 0000000000..c2ab53321b --- /dev/null +++ b/crates/fspy/src/ipc.rs @@ -0,0 +1,42 @@ +use std::io; + +use bincode::borrow_decode_from_slice; +use fspy_shared::ipc::{ + BINCODE_CONFIG, PathAccess, + channel::{Receiver, ReceiverLockGuard}, +}; +use tokio::task::spawn_blocking; + +// Shared memory size for storing path accesses. +// 4 GiB is large enough to store path accesses in almost any realistic scenario. +// This doesn't allocate physical memory until it's actually used. +pub const SHM_CAPACITY: usize = 4 * 1024 * 1024 * 1024; + +#[ouroboros::self_referencing] +pub struct OwnedReceiverLockGuard { + /// Owns the shared memory + receiver: Receiver, + /// Borrows the shared memory and owns the file lock + #[borrows(receiver)] + #[covariant] + lock_guard: ReceiverLockGuard<'this>, +} + +impl OwnedReceiverLockGuard { + pub fn lock(receiver: Receiver) -> io::Result { + OwnedReceiverLockGuard::try_new(receiver, |receiver| receiver.lock()) + } + + pub async fn lock_async(receiver: Receiver) -> io::Result { + spawn_blocking(move || Self::lock(receiver)).await.expect("lock task panicked") + } + + pub fn iter_path_accesses(&self) -> impl Iterator> { + self.borrow_lock_guard().iter_frames().map(|frame| { + let (path_access, decoded_size) = + borrow_decode_from_slice::, _>(frame, BINCODE_CONFIG).unwrap(); + assert_eq!(decoded_size, frame.len()); + path_access + }) + } +} diff --git a/crates/fspy/src/lib.rs b/crates/fspy/src/lib.rs index d4674995eb..9cb22dc438 100644 --- a/crates/fspy/src/lib.rs +++ b/crates/fspy/src/lib.rs @@ -4,6 +4,8 @@ // Persist the injected DLL/shared library somewhere in the filesystem. mod fixture; +mod ipc; + #[cfg(unix)] #[path = "./unix/mod.rs"] mod os_impl; @@ -12,6 +14,7 @@ mod os_impl; #[path = "./windows/mod.rs"] mod os_impl; +#[cfg(unix)] mod arena; mod command; diff --git a/crates/fspy/src/unix/mod.rs b/crates/fspy/src/unix/mod.rs index 8bf5101403..be82e7c85e 100644 --- a/crates/fspy/src/unix/mod.rs +++ b/crates/fspy/src/unix/mod.rs @@ -1,7 +1,3 @@ -// use std::{os::unix::process::CommandExt}; - -// use tokio::process::Command; - #[cfg(target_os = "linux")] mod syscall_handler; @@ -10,13 +6,9 @@ mod macos_fixtures; use std::{io, path::Path}; -use bincode::borrow_decode_from_slice; #[cfg(target_os = "linux")] use fspy_seccomp_unotify::supervisor::supervise; -use fspy_shared::ipc::{ - BINCODE_CONFIG, NativeString, PathAccess, - channel::{Receiver, ReceiverLockGuard, channel}, -}; +use fspy_shared::ipc::{NativeString, PathAccess, channel::channel}; #[cfg(target_os = "macos")] use fspy_shared_unix::payload::Fixtures; use fspy_shared_unix::{ @@ -27,9 +19,12 @@ use fspy_shared_unix::{ use futures_util::FutureExt; #[cfg(target_os = "linux")] use syscall_handler::SyscallHandler; -use tokio::task::spawn_blocking; -use crate::{Command, TrackedChild, arena::PathAccessArena}; +use crate::{ + Command, TrackedChild, + arena::PathAccessArena, + ipc::{OwnedReceiverLockGuard, SHM_CAPACITY}, +}; #[derive(Debug, Clone)] pub struct SpyInner { @@ -71,16 +66,6 @@ impl SpyInner { } } -#[ouroboros::self_referencing] -struct OwnedReceiverLockGuard { - /// Owns the shared memory - receiver: Receiver, - /// Borrows the shared memory and owns the file lock - #[borrows(receiver)] - #[covariant] - lock_guard: ReceiverLockGuard<'this>, -} - pub struct PathAccessIterable { arenas: Vec, ipc_receiver_lock_guard: OwnedReceiverLockGuard, @@ -91,22 +76,11 @@ impl PathAccessIterable { let accesses_in_arena = self.arenas.iter().flat_map(|arena| arena.borrow_accesses().iter()).copied(); - let accesses_in_shm = - self.ipc_receiver_lock_guard.borrow_lock_guard().iter_frames().map(|frame| { - let (path_access, decoded_size) = - borrow_decode_from_slice::, _>(frame, BINCODE_CONFIG).unwrap(); - assert_eq!(decoded_size, frame.len()); - path_access - }); + let accesses_in_shm = self.ipc_receiver_lock_guard.iter_path_accesses(); accesses_in_shm.chain(accesses_in_arena) } } -// Shared memory size for storing path accesses. -// 4 GiB is large enough to store path accesses in almost any realistic scenario. -// This doesn't allocate physical memory until it's actually used. -const SHM_CAPACITY: usize = 4 * 1024 * 1024 * 1024; - pub(crate) async fn spawn_impl(mut command: Command) -> io::Result { #[cfg(target_os = "linux")] let supervisor = supervise::()?; @@ -170,10 +144,7 @@ pub(crate) async fn spawn_impl(mut command: Command) -> io::Result let accesses_future = async move { let arenas = arenas_future.await?; // `receiver.lock()` blocks. Run it inside `spawn_blocking` to avoid blocking the tokio runtime. - let ipc_receiver_lock_guard = spawn_blocking(move || { - OwnedReceiverLockGuard::try_new(ipc_receiver, |receiver| receiver.lock()) - }) - .await??; + let ipc_receiver_lock_guard = OwnedReceiverLockGuard::lock_async(ipc_receiver).await?; Ok(PathAccessIterable { arenas, ipc_receiver_lock_guard }) } .boxed(); diff --git a/crates/fspy/src/windows/mod.rs b/crates/fspy/src/windows/mod.rs index 7a5979623f..e2ac30040a 100644 --- a/crates/fspy/src/windows/mod.rs +++ b/crates/fspy/src/windows/mod.rs @@ -1,40 +1,31 @@ use std::{ - ffi::{CStr, c_char, c_void}, - fs::OpenOptions, + ffi::{CStr, c_char}, io, os::windows::{ffi::OsStrExt, io::AsRawHandle, process::ChildExt as _}, path::Path, - ptr::null_mut, sync::Arc, }; -use bincode::borrow_decode_from_slice; use const_format::formatcp; use fspy_detours_sys::{DetourCopyPayloadToProcess, DetourUpdateProcessWithDll}; use fspy_shared::{ - ipc::{BINCODE_CONFIG, PathAccess}, + ipc::{BINCODE_CONFIG, PathAccess, channel::channel}, windows::{PAYLOAD_ID, Payload}, }; use futures_util::FutureExt; -use tokio::{ - io::AsyncReadExt, - net::windows::named_pipe::{PipeMode, ServerOptions}, -}; -// use detours_sys2::{DetourAttach,}; use winapi::{ - shared::minwindef::{FALSE, TRUE}, - um::{ - handleapi::DuplicateHandle, - processthreadsapi::{GetCurrentProcess, ResumeThread}, - winbase::CREATE_SUSPENDED, - winnt::DUPLICATE_SAME_ACCESS, - }, + shared::minwindef::TRUE, + um::{processthreadsapi::ResumeThread, winbase::CREATE_SUSPENDED}, }; -// use windows_sys::Win32::System::Threading::{CREATE_SUSPENDED, ResumeThread}; use winsafe::co::{CP, WC}; use xxhash_rust::const_xxh3::xxh3_128; -use crate::{TrackedChild, arena::PathAccessArena, command::Command, fixture::Fixture}; +use crate::{ + TrackedChild, + command::Command, + fixture::Fixture, + ipc::{OwnedReceiverLockGuard, SHM_CAPACITY}, +}; const PRELOAD_CDYLIB_BINARY: &[u8] = include_bytes!(env!("CARGO_CDYLIB_FILE_FSPY_PRELOAD_WINDOWS")); const INTERPOSE_CDYLIB: Fixture = Fixture::new( @@ -43,38 +34,14 @@ const INTERPOSE_CDYLIB: Fixture = Fixture::new( formatcp!("{:x}", xxh3_128(PRELOAD_CDYLIB_BINARY)), ); -fn luid() -> io::Result { - let mut luid = unsafe { std::mem::zeroed::() }; - let ret = unsafe { winapi::um::securitybaseapi::AllocateLocallyUniqueId(&mut luid) }; - if ret == 0 { - return Err(io::Error::last_os_error()); - } - Ok((u64::from(luid.HighPart as u32)) << 32 | u64::from(luid.LowPart)) -} - pub struct PathAccessIterable { - arena: PathAccessArena, - // pipe_receiver: NamedPipeServer, + ipc_receiver_lock_guard: OwnedReceiverLockGuard, } -const MESSAGE_MAX_LEN: usize = 4096; - impl PathAccessIterable { pub fn iter(&self) -> impl Iterator> { - self.arena.borrow_accesses().iter().copied() + self.ipc_receiver_lock_guard.iter_path_accesses() } - // pub async fn next<'a>(&mut self, buf: &'a mut Vec) -> io::Result>> { - // buf.resize(MESSAGE_MAX_LEN, 0); - // let n = self.pipe_receiver.read(buf.as_mut_slice()).await?; - // if n == 0 { - // return Ok(None); - // } - // let msg = &buf[..n]; - // let (path_access, decoded_len) = - // borrow_decode_from_slice::<'_, PathAccess, _>(msg, BINCODE_CONFIG).unwrap(); - // assert_eq!(decoded_len, msg.len()); - // Ok(Some(path_access)) - // } } // pub struct TracedProcess { @@ -110,46 +77,13 @@ pub(crate) async fn spawn_impl(command: Command) -> io::Result { command.creation_flags(CREATE_SUSPENDED); - let pipe_name = format!(r"\\.\pipe\fspy_ipc_{:x}", luid()?); - - let mut pipe_receiver = ServerOptions::new() - .pipe_mode(PipeMode::Message) - .access_outbound(false) - .access_inbound(true) - .in_buffer_size(1024) - // .out_buffer_size(100 * 1024 * 1024) - .create(&pipe_name)?; - - let connect_fut = pipe_receiver.connect(); - - let pipe_sender = OpenOptions::new().write(true).open(&pipe_name).unwrap(); - - connect_fut.await?; - - // Temporary workaround before switching to shared memory IPC on Windows. - // The shared memory IPC requires `accesses_future` to be polled after the child process has exited. - // The test code has updated but the Windows implementation here has not yet adopted that model. - // As a result, accesses_future didn't get polled making the pipe_sender to block indefinitely. - // To unblock the pipe_sender, we spawn a separate task to keep reading pipe_receiver. - // This workaround can be removed once the shared memory IPC is used on Windows. - let accesses_future = tokio::task::spawn(async move { - let mut arena = PathAccessArena::default(); - - let mut buf = [0u8; MESSAGE_MAX_LEN]; - loop { - let n = pipe_receiver.read(&mut buf).await?; - if n == 0 { - break; - } - let msg = &buf[..n]; - let (path_access, decoded_len) = - borrow_decode_from_slice::<'_, PathAccess, _>(msg, BINCODE_CONFIG).unwrap(); - assert_eq!(decoded_len, msg.len()); - arena.add(path_access); - } - io::Result::Ok(PathAccessIterable { arena }) - }); - let accesses_future = async move { accesses_future.await.unwrap() }.boxed(); + let (channel_conf, receiver) = channel(SHM_CAPACITY)?; + + let accesses_future = async move { + let ipc_receiver_lock_guard = OwnedReceiverLockGuard::lock_async(receiver).await?; + io::Result::Ok(PathAccessIterable { ipc_receiver_lock_guard }) + } + .boxed(); // let path_access_stream = PathAccessIterable { pipe_receiver }; @@ -163,24 +97,8 @@ pub(crate) async fn spawn_impl(command: Command) -> io::Result { return Err(io::Error::last_os_error()); } - let mut handle_in_child: *mut c_void = null_mut(); - let ret = unsafe { - DuplicateHandle( - GetCurrentProcess(), - pipe_sender.as_raw_handle(), - process_handle, - &mut handle_in_child, - 0, - FALSE, - DUPLICATE_SAME_ACCESS, - ) - }; - if ret == 0 { - return Err(io::Error::last_os_error()); - } - let payload = Payload { - pipe_handle: handle_in_child.addr(), + channel_conf: channel_conf.clone(), asni_dll_path_with_nul: asni_dll_path_with_nul.to_bytes(), }; let payload_bytes = bincode::encode_to_vec(payload, BINCODE_CONFIG).unwrap(); @@ -207,6 +125,5 @@ pub(crate) async fn spawn_impl(command: Command) -> io::Result { Ok(std_child) })?; - drop(pipe_sender); Ok(TrackedChild { tokio_child: child, accesses_future }) } diff --git a/crates/fspy_preload_windows/Cargo.toml b/crates/fspy_preload_windows/Cargo.toml index 77b2819fce..9ca48169a6 100644 --- a/crates/fspy_preload_windows/Cargo.toml +++ b/crates/fspy_preload_windows/Cargo.toml @@ -26,6 +26,3 @@ winsafe = { workspace = true } [target.'cfg(target_os = "windows")'.dev-dependencies] tempfile = { workspace = true } - -[dependencies] -dashmap = { workspace = true } diff --git a/crates/fspy_preload_windows/src/windows/client.rs b/crates/fspy_preload_windows/src/windows/client.rs index abb7eb3850..cdaa4e4429 100644 --- a/crates/fspy_preload_windows/src/windows/client.rs +++ b/crates/fspy_preload_windows/src/windows/client.rs @@ -1,69 +1,33 @@ -use std::{ - cell::SyncUnsafeCell, - ffi::{CStr, c_void}, - mem::MaybeUninit, - ptr::null_mut, -}; +use std::{cell::SyncUnsafeCell, ffi::CStr, mem::MaybeUninit}; -use bincode::{borrow_decode_from_slice, encode_into_std_write, encode_to_vec}; -use dashmap::DashSet; +use bincode::{borrow_decode_from_slice, encode_to_vec}; use fspy_detours_sys::DetourCopyPayloadToProcess; use fspy_shared::{ - ipc::{BINCODE_CONFIG, PathAccess}, + ipc::{BINCODE_CONFIG, PathAccess, channel::Sender}, windows::{PAYLOAD_ID, Payload}, }; -use ntapi::ntobapi::DUPLICATE_SAME_ACCESS; -use smallvec::SmallVec; -use winapi::{ - shared::minwindef::{BOOL, DWORD, FALSE}, - um::{ - fileapi::WriteFile, handleapi::DuplicateHandle, processthreadsapi::GetCurrentProcess, - winnt::HANDLE, - }, -}; -use winsafe::GetLastError; +use winapi::{shared::minwindef::BOOL, um::winnt::HANDLE}; use crate::stack_once::{StackOnceGuard, stack_once_token}; -const MSG_INLINE_SIZE: usize = 256; - pub struct Client<'a> { payload: Payload<'a>, - messages: DashSet>, -} - -unsafe fn write_pipe_message(pipe: HANDLE, msg: &[u8]) { - let mut bytes_written: DWORD = 0; - let mut remaining_msg = msg; - while !remaining_msg.is_empty() { - let ret = unsafe { - WriteFile( - pipe, - msg.as_ptr().cast(), - msg.len().try_into().unwrap(), - &mut bytes_written, - null_mut(), - ) - }; - assert_ne!(ret, 0, "fspy WriteFile to pipe failed: {:?}", GetLastError()); - remaining_msg = &remaining_msg[bytes_written as usize..]; - } + ipc_sender: Option, } stack_once_token!(PATH_ACCESS_ONCE); pub struct PathAccessSender<'a> { - messages: &'a DashSet>, + ipc_sender: &'a Option, _once_guard: StackOnceGuard, } impl<'a> PathAccessSender<'a> { - pub unsafe fn send(&self, access: PathAccess<'_>) { - // TODO: send cwd as dir if the path is relative - let mut buf = SmallVec::::new(); - encode_into_std_write(access, &mut buf, BINCODE_CONFIG).unwrap(); - - self.messages.insert(buf); + pub fn send(&self, access: PathAccess<'_>) { + let Some(sender) = &self.ipc_sender else { + return; + }; + sender.write_encoded(&access, BINCODE_CONFIG).expect("failed to send path access"); } } @@ -73,50 +37,34 @@ impl<'a> Client<'a> { borrow_decode_from_slice::<'a, Payload, _>(payload_bytes, BINCODE_CONFIG).unwrap(); assert_eq!(decoded_len, payload_bytes.len()); - Self { payload, messages: DashSet::with_capacity(1024) } - } + let ipc_sender = match payload.channel_conf.sender() { + Ok(sender) => Some(sender), + Err(err) => { + // this can happen if the process is started after the root target process has exited. + // By that time the channel would have been closed in the receiver side. + // In this case we just leave a message and skip sending any path accesses. + eprintln!("fspy: failed to create ipc sender: {}", err); + None + } + }; - pub fn finish(&self) { - for msg in self.messages.iter() { - unsafe { write_pipe_message(self.payload.pipe_handle as _, &msg) }; - } + Self { payload, ipc_sender } } - pub unsafe fn send(&self, access: PathAccess<'_>) { - // TODO: send cwd as dir if the path is relative - let mut buf = SmallVec::::new(); - encode_into_std_write(access, &mut buf, BINCODE_CONFIG).unwrap(); - - self.messages.insert(buf); + pub fn send(&self, access: PathAccess<'_>) { + let Some(sender) = &self.ipc_sender else { + return; + }; + sender.write_encoded(&access, BINCODE_CONFIG).expect("failed to send path access"); } pub fn sender(&self) -> Option> { let guard = PATH_ACCESS_ONCE.try_enter()?; - Some(PathAccessSender { messages: &self.messages, _once_guard: guard }) + Some(PathAccessSender { ipc_sender: &self.ipc_sender, _once_guard: guard }) } pub unsafe fn prepare_child_process(&self, child_handle: HANDLE) -> BOOL { - let mut payload = self.payload; - - let mut handle_in_child: *mut c_void = null_mut(); - let ret = unsafe { - DuplicateHandle( - GetCurrentProcess(), - payload.pipe_handle as _, - child_handle, - &mut handle_in_child, - 0, - FALSE, - DUPLICATE_SAME_ACCESS, - ) - }; - if ret == 0 { - return 0; - } - - payload.pipe_handle = handle_in_child as usize; - - let payload_bytes = encode_to_vec(payload, BINCODE_CONFIG).unwrap(); + let payload_bytes = encode_to_vec(&self.payload, BINCODE_CONFIG).unwrap(); unsafe { DetourCopyPayloadToProcess( child_handle, diff --git a/crates/fspy_preload_windows/src/windows/mod.rs b/crates/fspy_preload_windows/src/windows/mod.rs index d806e9c517..97045d2579 100644 --- a/crates/fspy_preload_windows/src/windows/mod.rs +++ b/crates/fspy_preload_windows/src/windows/mod.rs @@ -23,7 +23,7 @@ use winapi::{ use winapi_utils::{ck, ck_long}; use winsafe::SetLastError; -use crate::windows::{client::global_client, detour::AttachContext}; +use crate::windows::detour::AttachContext; fn dll_main(_hinstance: HINSTANCE, reason: u32) -> winsafe::SysResult<()> { if unsafe { DetourIsHelperProcess() } == TRUE { @@ -56,7 +56,6 @@ fn dll_main(_hinstance: HINSTANCE, reason: u32) -> winsafe::SysResult<()> { ck_long(unsafe { DetourTransactionCommit() })?; } winnt::DLL_PROCESS_DETACH => { - unsafe { global_client() }.finish(); ck(unsafe { DetourTransactionBegin() })?; ck(unsafe { DetourUpdateThread(GetCurrentThread().cast()) })?; diff --git a/crates/fspy_shared/Cargo.toml b/crates/fspy_shared/Cargo.toml index 916d6b0af5..f9ed878503 100644 --- a/crates/fspy_shared/Cargo.toml +++ b/crates/fspy_shared/Cargo.toml @@ -11,6 +11,7 @@ bstr = { workspace = true } bytemuck = { workspace = true, features = ["must_cast"] } memmap2 = { workspace = true } shared_memory = { workspace = true } +thiserror = { workspace = true } tracing = { workspace = true } uuid = { workspace = true, features = ["v4"] } diff --git a/crates/fspy_shared/src/ipc/channel/mod.rs b/crates/fspy_shared/src/ipc/channel/mod.rs index 3b0b922ca0..9442e80494 100644 --- a/crates/fspy_shared/src/ipc/channel/mod.rs +++ b/crates/fspy_shared/src/ipc/channel/mod.rs @@ -26,11 +26,15 @@ 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())); - // Initialize the shared memory with a unique id. - let shm = ShmemConf::new() - .size(capacity) - .create() - .map_err(|err| io::Error::new(io::ErrorKind::Other, err))?; + #[allow(unused_mut)] + 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 = conf.create().map_err(|err| io::Error::new(io::ErrorKind::Other, err))?; let conf = ChannelConf { lock_file_path: lock_file_path.as_os_str().into(), @@ -49,11 +53,15 @@ impl ChannelConf { pub fn sender(&self) -> io::Result { let lock_file = File::open(self.lock_file_path.to_cow_os_str())?; lock_file.try_lock_shared()?; - let shm = ShmemConf::new() - .size(self.shm_size) - .os_id(&self.shm_id) - .open() - .map_err(|err| io::Error::new(io::ErrorKind::Other, err))?; + + #[allow(unused_mut)] + 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(|err| io::Error::new(io::ErrorKind::Other, err))?; let writer = unsafe { ShmWriter::new(shm) }; Ok(Sender { writer, lock_file, lock_file_path: self.lock_file_path.clone() }) } diff --git a/crates/fspy_shared/src/ipc/channel/shm_io.rs b/crates/fspy_shared/src/ipc/channel/shm_io.rs index e6f5ea5343..670946b15e 100644 --- a/crates/fspy_shared/src/ipc/channel/shm_io.rs +++ b/crates/fspy_shared/src/ipc/channel/shm_io.rs @@ -8,6 +8,9 @@ use std::{ sync::atomic::{AtomicI32, AtomicUsize, Ordering, fence}, }; +use bincode::{ + Encode, config::Config, enc::write::SizeWriter, encode_into_slice, encode_into_writer, +}; use bytemuck::must_cast; use shared_memory::Shmem; @@ -103,6 +106,16 @@ impl Drop for FrameMut<'_> { } } +#[derive(thiserror::Error, Debug)] +pub enum WriteEncodedError { + #[error("Failed to encode value into shared memory")] + EncodeError(#[from] bincode::error::EncodeError), + #[error("Tried to write a frame of zero size into shared memory")] + ZeroSizedFrame, + #[error("Not enough space in shared memory to write the encoded frame")] + InsufficientSpace, +} + impl ShmWriter { /// Create a new `ShmWriter` backed by a shared memory region. /// @@ -200,8 +213,30 @@ impl ShmWriter { }) } + /// Append an encoded value into the shared memory. + pub fn write_encoded( + &self, + value: &T, + config: C, + ) -> Result<(), WriteEncodedError> { + let mut size_writer = SizeWriter::default(); + encode_into_writer(value, &mut size_writer, config)?; + + let Some(frame_size) = NonZeroUsize::new(size_writer.bytes_written) else { + return Err(WriteEncodedError::ZeroSizedFrame); + }; + let Some(mut frame) = self.claim_frame(frame_size) else { + return Err(WriteEncodedError::InsufficientSpace); + }; + + let written_size = encode_into_slice(value, &mut frame, config)?; + assert_eq!(written_size, size_writer.bytes_written); + + Ok(()) + } + #[cfg(test)] - pub fn append_frame(&self, frame: &[u8]) -> bool { + pub fn try_write_frame(&self, frame: &[u8]) -> bool { let Some(frame_size) = NonZeroUsize::new(frame.len()) else { return false; }; @@ -334,10 +369,10 @@ mod tests { #[test] fn single_thread_basic() { let writer = unsafe { ShmWriter::new(MockedShm::alloc(1024)) }; - assert!(writer.append_frame(b"hello")); - assert!(writer.append_frame(b"world")); - assert!(writer.append_frame(b"this is a test")); - assert!(!writer.append_frame(&vec![0u8; 2048])); // too large + assert!(writer.try_write_frame(b"hello")); + assert!(writer.try_write_frame(b"world")); + assert!(writer.try_write_frame(b"this is a test")); + assert!(!writer.try_write_frame(&vec![0u8; 2048])); // too large let reader = ShmReader::new(writer.into_memory()); let mut frames = reader.iter_frames(); @@ -349,9 +384,9 @@ mod tests { #[test] fn single_thread_empty() { let writer = unsafe { ShmWriter::new(MockedShm::alloc(1024)) }; - assert!(writer.append_frame(b"hello")); - assert!(!writer.append_frame(b"")); - assert!(writer.append_frame(b"this is a test")); + assert!(writer.try_write_frame(b"hello")); + assert!(!writer.try_write_frame(b"")); + assert!(writer.try_write_frame(b"this is a test")); let reader = ShmReader::new(writer.into_memory()); let mut frames = reader.iter_frames(); @@ -363,14 +398,14 @@ mod tests { #[test] fn single_thread_crash_after_claim() { let mut writer = unsafe { ShmWriter::new(MockedShm::alloc(1024)) }; - assert!(writer.append_frame(b"foo")); + assert!(writer.try_write_frame(b"foo")); // Simulate crash during writing writer.set_fail_on_claim(true); - assert!(!writer.append_frame(b"hello")); + assert!(!writer.try_write_frame(b"hello")); writer.set_fail_on_claim(false); - assert!(writer.append_frame(b"bar")); + assert!(writer.try_write_frame(b"bar")); let reader = ShmReader::new(writer.into_memory()); let mut frames = reader.iter_frames(); @@ -382,14 +417,14 @@ mod tests { #[test] fn single_thread_crash_partial_write() { let writer = unsafe { ShmWriter::new(MockedShm::alloc(1024)) }; - assert!(writer.append_frame(b"foo")); + assert!(writer.try_write_frame(b"foo")); // Simulate crash during writing let mut frame = writer.claim_frame(5.try_into().unwrap()).unwrap(); frame[..3].copy_from_slice(b"wor"); std::mem::forget(frame); - assert!(writer.append_frame(b"bar")); + assert!(writer.try_write_frame(b"bar")); let reader = ShmReader::new(writer.into_memory()); let mut frames = reader.iter_frames(); @@ -407,11 +442,11 @@ mod tests { let mut writer = unsafe { ShmWriter::new(MockedShm::alloc(1024)) }; - assert!(writer.append_frame(b"foo")); + assert!(writer.try_write_frame(b"foo")); // First crash: AfterClaim (leaves frame header as 0) writer.set_fail_on_claim(true); - assert!(!writer.append_frame(b"world")); + assert!(!writer.try_write_frame(b"world")); writer.set_fail_on_claim(false); // Second crash: PartialWrite (leaves positive frame header) @@ -419,7 +454,7 @@ mod tests { frame[..3].copy_from_slice(b"wor"); std::mem::forget(frame); - assert!(writer.append_frame(b"bar")); + assert!(writer.try_write_frame(b"bar")); // ShmReader must skip BOTH invalid frames (0 header + partial header) // and find the valid frame beyond them - this tests the loop continuation @@ -438,7 +473,7 @@ mod tests { // in reverse order. This ensures the loop correctly handles different // sequences of invalid frame types (partial write -> after claim). - assert!(writer.append_frame(b"foo")); + assert!(writer.try_write_frame(b"foo")); // First crash: PartialWrite (leaves positive frame header) let mut frame = writer.claim_frame(5.try_into().unwrap()).unwrap(); @@ -447,10 +482,10 @@ mod tests { // Second crash: AfterClaim (leaves frame header as 0) writer.set_fail_on_claim(true); - assert!(!writer.append_frame(b"world")); + assert!(!writer.try_write_frame(b"world")); writer.set_fail_on_claim(false); - assert!(writer.append_frame(b"bar")); + assert!(writer.try_write_frame(b"bar")); let reader = ShmReader::new(writer.into_memory()); // ShmReader must skip BOTH invalid frames in this order and continue @@ -470,9 +505,9 @@ mod tests { s.spawn(|| { let writer = unsafe { ShmWriter::new(shm.clone()) }; for _ in 0..10 { - assert!(writer.append_frame(b"hello")); - assert!(writer.append_frame(b"foo")); - assert!(writer.append_frame(b"this is a test")); + assert!(writer.try_write_frame(b"hello")); + assert!(writer.try_write_frame(b"foo")); + assert!(writer.try_write_frame(b"this is a test")); } }); } @@ -494,9 +529,9 @@ mod tests { for _ in 0..4 { s.spawn(|| { for _ in 0..10 { - writer.append_frame(b"hello"); - writer.append_frame(b"foo"); - writer.append_frame(b"this is a test"); + writer.try_write_frame(b"hello"); + writer.try_write_frame(b"foo"); + writer.try_write_frame(b"this is a test"); } }); } @@ -521,10 +556,10 @@ mod tests { let large_frame = vec![0u8; (i32::MAX as usize) - 100]; // This should fail safely, not cause overflow - assert!(!writer.append_frame(&large_frame)); + assert!(!writer.try_write_frame(&large_frame)); // Small frame should still work - assert!(writer.append_frame(b"test")); + assert!(writer.try_write_frame(b"test")); let reader = ShmReader::new(writer.into_memory()); let mut frames = reader.iter_frames(); @@ -544,7 +579,8 @@ mod tests { for _ in 0..10 { s.spawn(|| { // Many threads trying to write large-ish frames - writer.append_frame(b"this_is_a_moderately_long_frame_that_might_cause_races"); + writer + .try_write_frame(b"this_is_a_moderately_long_frame_that_might_cause_races"); }); } }); @@ -617,7 +653,7 @@ mod tests { let shm = ShmemConf::new().os_id(shm_name).open().unwrap(); let writer = unsafe { ShmWriter::new(shm) }; for i in 0..FRAME_COUNT_EACH_CHILD { - assert!(writer.append_frame(format!("{child_index} {i}").as_bytes())); + assert!(writer.try_write_frame(format!("{child_index} {i}").as_bytes())); } std::process::exit(0); } diff --git a/crates/fspy_shared/src/windows/mod.rs b/crates/fspy_shared/src/windows/mod.rs index 02e5a41b4a..cb34aa8578 100644 --- a/crates/fspy_shared/src/windows/mod.rs +++ b/crates/fspy_shared/src/windows/mod.rs @@ -1,12 +1,14 @@ use bincode::{BorrowDecode, Encode}; use winapi::DEFINE_GUID; +use crate::ipc::channel::ChannelConf; + // Generated by guidgen.exe // {FC4845F1-3A8B-4F05-A3D3-A5E9E102AF33} DEFINE_GUID!(PAYLOAD_ID, 0xfc4845f1, 0x3a8b, 0x4f05, 0xa3, 0xd3, 0xa5, 0xe9, 0xe1, 0x2, 0xaf, 0x33); -#[derive(Encode, BorrowDecode, Debug, Clone, Copy)] +#[derive(Encode, BorrowDecode, Debug, Clone)] pub struct Payload<'a> { - pub pipe_handle: usize, + pub channel_conf: ChannelConf, pub asni_dll_path_with_nul: &'a [u8], }