Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 16 additions & 1 deletion crates/fspy/tests/rust_std.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ async fn readdir() -> anyhow::Result<()> {
}

#[test(tokio::test)]
async fn subprocess() -> anyhow::Result<()> {
async fn read_in_subprocess() -> anyhow::Result<()> {
let accesses = track_child!((), |(): ()| {
let mut command = if cfg!(windows) {
let mut command = std::process::Command::new("cmd");
Expand All @@ -72,3 +72,18 @@ async fn subprocess() -> anyhow::Result<()> {

Ok(())
}

#[test(tokio::test)]
async fn read_program() -> anyhow::Result<()> {
let accesses = track_child!((), |(): ()| {
let _ = std::process::Command::new("./not_exist.exe").spawn();
})
.await?;
assert_contains(
&accesses,
current_dir().unwrap().join("not_exist.exe").as_path(),
AccessMode::READ,
);

Ok(())
}
31 changes: 20 additions & 11 deletions crates/fspy/tests/test_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,25 +11,34 @@ pub fn assert_contains(
expected_path: &Path,
expected_mode: AccessMode,
) {
let found = accesses.iter().any(|access| {
let mut actual_mode: AccessMode = AccessMode::empty();
for access in accesses.iter() {
let Ok(stripped) =
access.path.strip_path_prefix::<_, Result<PathBuf, StripPrefixError>, _>(
expected_path,
|strip_result| strip_result.map(Path::to_path_buf),
)
else {
return false;
continue;
};
stripped.as_os_str().is_empty() && access.mode == expected_mode
});
if !found {
panic!(
"Expected to find access to path {:?} with mode {:?}, but it was not found in: {:?}",
expected_path,
expected_mode,
accesses.iter().collect::<Vec<_>>()
);
if stripped.as_os_str().is_empty() {
actual_mode.insert(access.mode);
}
}

if actual_mode.contains(AccessMode::READ_DIR) {
// READ_DIR already implies READ.
actual_mode.remove(AccessMode::READ);
}

assert_eq!(
expected_mode,
actual_mode,
"Expected to find access to path {:?} with mode {:?}, but it was not found in: {:?}",
expected_path,
expected_mode,
accesses.iter().collect::<Vec<_>>()
);
}

#[macro_export]
Expand Down
66 changes: 66 additions & 0 deletions crates/fspy/tests/winapi.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
#![cfg(windows)]

mod test_utils;

use std::{ffi::OsStr, os::windows::ffi::OsStrExt, path::Path, ptr::null_mut};

use fspy::AccessMode;
use test_log::test;
use test_utils::assert_contains;
use winapi::um::processthreadsapi::{
CreateProcessA, CreateProcessW, PROCESS_INFORMATION, STARTUPINFOA, STARTUPINFOW,
};

#[test(tokio::test)]
async fn create_process_a() -> anyhow::Result<()> {
let accesses = track_child!((), |(): ()| {
let mut si: STARTUPINFOA = unsafe { std::mem::zeroed() };
let mut pi: PROCESS_INFORMATION = unsafe { std::mem::zeroed() };
unsafe {
CreateProcessA(
c"C:\\fspy_test_not_exist_program.exe".as_ptr().cast(),
null_mut(),
null_mut(),
null_mut(),
0,
0,
null_mut(),
null_mut(),
&mut si,
&mut pi,
)
};
})
.await?;
assert_contains(&accesses, Path::new("C:\\fspy_test_not_exist_program.exe"), AccessMode::READ);

Ok(())
}

#[test(tokio::test)]
async fn create_process_w() -> anyhow::Result<()> {
let accesses = track_child!((), |(): ()| {
let mut si: STARTUPINFOW = unsafe { std::mem::zeroed() };
let mut pi: PROCESS_INFORMATION = unsafe { std::mem::zeroed() };
let program =
OsStr::new("C:\\fspy_test_not_exist_program.exe\0").encode_wide().collect::<Vec<u16>>();
unsafe {
CreateProcessW(
program.as_ptr(),
null_mut(),
null_mut(),
null_mut(),
0,
0,
null_mut(),
null_mut(),
&mut si,
&mut pi,
)
};
})
.await?;
assert_contains(&accesses, Path::new("C:\\fspy_test_not_exist_program.exe"), AccessMode::READ);

Ok(())
}
1 change: 0 additions & 1 deletion crates/fspy_preload_windows/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
#![cfg(windows)]
#![feature(sync_unsafe_cell)]

mod stack_once;
pub mod windows;
40 changes: 0 additions & 40 deletions crates/fspy_preload_windows/src/stack_once.rs

This file was deleted.

23 changes: 0 additions & 23 deletions crates/fspy_preload_windows/src/windows/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,29 +8,11 @@ use fspy_shared::{
};
use winapi::{shared::minwindef::BOOL, um::winnt::HANDLE};

use crate::stack_once::{StackOnceGuard, stack_once_token};

pub struct Client<'a> {
payload: Payload<'a>,
ipc_sender: Option<Sender>,
}

stack_once_token!(PATH_ACCESS_ONCE);

pub struct PathAccessSender<'a> {
ipc_sender: &'a Option<Sender>,
_once_guard: StackOnceGuard,
}

impl<'a> PathAccessSender<'a> {
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");
}
}

impl<'a> Client<'a> {
pub fn from_payload_bytes(payload_bytes: &'a [u8]) -> Self {
let (payload, decoded_len) =
Expand Down Expand Up @@ -58,11 +40,6 @@ impl<'a> Client<'a> {
sender.write_encoded(&access, BINCODE_CONFIG).expect("failed to send path access");
}

pub fn sender(&self) -> Option<PathAccessSender<'_>> {
let guard = PATH_ACCESS_ONCE.try_enter()?;
Some(PathAccessSender { ipc_sender: &self.ipc_sender, _once_guard: guard })
}

pub unsafe fn prepare_child_process(&self, child_handle: HANDLE) -> BOOL {
let payload_bytes = encode_to_vec(&self.payload, BINCODE_CONFIG).unwrap();
unsafe {
Expand Down
58 changes: 29 additions & 29 deletions crates/fspy_preload_windows/src/windows/detours/create_process.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
use std::ffi::CStr;

use fspy_detours_sys::{DetourCreateProcessWithDllExA, DetourCreateProcessWithDllExW};
use fspy_shared::ipc::{AccessMode, NativeStr, PathAccess};
use widestring::U16CStr;
use winapi::{
shared::{
minwindef::{BOOL, DWORD, LPVOID},
Expand All @@ -24,6 +20,29 @@ use crate::windows::{
detour::{Detour, DetourAny},
};

thread_local! {
static IS_HOOKING_CREATE_PROCESS: std::cell::Cell<bool> = std::cell::Cell::new(false);
}

struct HookGuard;
impl HookGuard {
pub fn new() -> Option<Self> {
let already_hooking = IS_HOOKING_CREATE_PROCESS.with(|c| {
let v = c.get();
c.set(true);
v
});
if already_hooking { None } else { Some(Self) }
}
}
impl Drop for HookGuard {
fn drop(&mut self) {
IS_HOOKING_CREATE_PROCESS.with(|c| {
c.set(false);
});
}
}

static DETOUR_CREATE_PROCESS_W: Detour<
unsafe extern "system" fn(
LPCWSTR,
Expand Down Expand Up @@ -51,8 +70,7 @@ static DETOUR_CREATE_PROCESS_W: Detour<
lp_startup_info: LPSTARTUPINFOW,
lp_process_information: LPPROCESS_INFORMATION,
) -> BOOL {
let client = unsafe { global_client() };
let Some(sender) = client.sender() else {
let Some(_hook_guard) = HookGuard::new() else {
// Detect re-entrance and avoid double hooking
return unsafe {
(DETOUR_CREATE_PROCESS_W.real())(
Expand All @@ -61,7 +79,7 @@ static DETOUR_CREATE_PROCESS_W: Detour<
lp_process_attributes,
lp_thread_attributes,
b_inherit_handles,
dw_creation_flags | CREATE_SUSPENDED,
dw_creation_flags,
lp_environment,
lp_current_directory,
lp_startup_info,
Expand All @@ -70,16 +88,7 @@ static DETOUR_CREATE_PROCESS_W: Detour<
};
};

if !lp_application_name.is_null() {
unsafe {
sender.send(PathAccess {
mode: AccessMode::READ,
path: NativeStr::from_wide(
U16CStr::from_ptr_str(lp_application_name).as_slice(),
),
});
}
}
let client = unsafe { global_client() };

unsafe extern "system" fn create_process_with_payload_w(
lp_application_name: LPCWSTR,
Expand Down Expand Up @@ -175,8 +184,7 @@ static DETOUR_CREATE_PROCESS_A: Detour<
lp_startup_info: LPSTARTUPINFOA,
lp_process_information: LPPROCESS_INFORMATION,
) -> BOOL {
let client = unsafe { global_client() };
let Some(sender) = client.sender() else {
let Some(_hook_guard) = HookGuard::new() else {
// Detect re-entrance and avoid double hooking
return unsafe {
(DETOUR_CREATE_PROCESS_A.real())(
Expand All @@ -185,23 +193,15 @@ static DETOUR_CREATE_PROCESS_A: Detour<
lp_process_attributes,
lp_thread_attributes,
b_inherit_handles,
dw_creation_flags | CREATE_SUSPENDED,
dw_creation_flags,
lp_environment,
lp_current_directory,
lp_startup_info,
lp_process_information,
)
};
};

if !lp_application_name.is_null() {
unsafe {
sender.send(PathAccess {
mode: AccessMode::READ,
path: NativeStr::from_ansi(CStr::from_ptr(lp_application_name).to_bytes()),
});
}
}
let client = unsafe { global_client() };

unsafe extern "system" fn create_process_with_payload_a(
lp_application_name: LPCSTR,
Expand Down
1 change: 0 additions & 1 deletion crates/fspy_shared/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ uuid = { workspace = true, features = ["v4"] }
bytemuck = { workspace = true }
os_str_bytes = { workspace = true }
winapi = { workspace = true, features = ["std"] }
winsafe = { workspace = true }

[dev-dependencies]
assert2 = { workspace = true }
Expand Down
Loading