Skip to content

Commit c38d9f4

Browse files
authored
chore(fspy): cleanup unused code on Windows (#37)
### TL;DR Improved file access tracking for Windows process creation and fixed path access detection in tests. ### What changed? - Renamed `subprocess` test to `read_in_subprocess` and added a new `read_program` test to verify program file access detection - Enhanced `assert_contains` in test utilities to properly handle access mode comparison - Added Windows-specific tests for `CreateProcessA` and `CreateProcessW` functions - Removed unused `stack_once.rs` module and simplified process creation hooks - Fixed a re-entrance detection issue in process creation hooks by using thread-local state - Simplified `NativeStr` by removing the unused ANSI string support, as Windows paths are primarily handled as wide strings ### How to test? - Run the existing and new tests on Windows to verify process creation hooks work correctly - Verify that file access tracking properly detects program file access when creating processes ### Why make this change? The previous implementation had issues with detecting program file access during process creation on Windows. This change improves the reliability of file access tracking by simplifying the hook implementation and removing unnecessary complexity. The tests now properly verify that the system correctly tracks file access when creating processes, which is essential for accurate dependency tracking.
1 parent ede2325 commit c38d9f4

File tree

10 files changed

+141
-144
lines changed

10 files changed

+141
-144
lines changed

Cargo.lock

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

crates/fspy/tests/rust_std.rs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ async fn readdir() -> anyhow::Result<()> {
4747
}
4848

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

7373
Ok(())
7474
}
75+
76+
#[test(tokio::test)]
77+
async fn read_program() -> anyhow::Result<()> {
78+
let accesses = track_child!((), |(): ()| {
79+
let _ = std::process::Command::new("./not_exist.exe").spawn();
80+
})
81+
.await?;
82+
assert_contains(
83+
&accesses,
84+
current_dir().unwrap().join("not_exist.exe").as_path(),
85+
AccessMode::READ,
86+
);
87+
88+
Ok(())
89+
}

crates/fspy/tests/test_utils.rs

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,25 +11,34 @@ pub fn assert_contains(
1111
expected_path: &Path,
1212
expected_mode: AccessMode,
1313
) {
14-
let found = accesses.iter().any(|access| {
14+
let mut actual_mode: AccessMode = AccessMode::empty();
15+
for access in accesses.iter() {
1516
let Ok(stripped) =
1617
access.path.strip_path_prefix::<_, Result<PathBuf, StripPrefixError>, _>(
1718
expected_path,
1819
|strip_result| strip_result.map(Path::to_path_buf),
1920
)
2021
else {
21-
return false;
22+
continue;
2223
};
23-
stripped.as_os_str().is_empty() && access.mode == expected_mode
24-
});
25-
if !found {
26-
panic!(
27-
"Expected to find access to path {:?} with mode {:?}, but it was not found in: {:?}",
28-
expected_path,
29-
expected_mode,
30-
accesses.iter().collect::<Vec<_>>()
31-
);
24+
if stripped.as_os_str().is_empty() {
25+
actual_mode.insert(access.mode);
26+
}
3227
}
28+
29+
if actual_mode.contains(AccessMode::READ_DIR) {
30+
// READ_DIR already implies READ.
31+
actual_mode.remove(AccessMode::READ);
32+
}
33+
34+
assert_eq!(
35+
expected_mode,
36+
actual_mode,
37+
"Expected to find access to path {:?} with mode {:?}, but it was not found in: {:?}",
38+
expected_path,
39+
expected_mode,
40+
accesses.iter().collect::<Vec<_>>()
41+
);
3342
}
3443

3544
#[macro_export]

crates/fspy/tests/winapi.rs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
#![cfg(windows)]
2+
3+
mod test_utils;
4+
5+
use std::{ffi::OsStr, os::windows::ffi::OsStrExt, path::Path, ptr::null_mut};
6+
7+
use fspy::AccessMode;
8+
use test_log::test;
9+
use test_utils::assert_contains;
10+
use winapi::um::processthreadsapi::{
11+
CreateProcessA, CreateProcessW, PROCESS_INFORMATION, STARTUPINFOA, STARTUPINFOW,
12+
};
13+
14+
#[test(tokio::test)]
15+
async fn create_process_a() -> anyhow::Result<()> {
16+
let accesses = track_child!((), |(): ()| {
17+
let mut si: STARTUPINFOA = unsafe { std::mem::zeroed() };
18+
let mut pi: PROCESS_INFORMATION = unsafe { std::mem::zeroed() };
19+
unsafe {
20+
CreateProcessA(
21+
c"C:\\fspy_test_not_exist_program.exe".as_ptr().cast(),
22+
null_mut(),
23+
null_mut(),
24+
null_mut(),
25+
0,
26+
0,
27+
null_mut(),
28+
null_mut(),
29+
&mut si,
30+
&mut pi,
31+
)
32+
};
33+
})
34+
.await?;
35+
assert_contains(&accesses, Path::new("C:\\fspy_test_not_exist_program.exe"), AccessMode::READ);
36+
37+
Ok(())
38+
}
39+
40+
#[test(tokio::test)]
41+
async fn create_process_w() -> anyhow::Result<()> {
42+
let accesses = track_child!((), |(): ()| {
43+
let mut si: STARTUPINFOW = unsafe { std::mem::zeroed() };
44+
let mut pi: PROCESS_INFORMATION = unsafe { std::mem::zeroed() };
45+
let program =
46+
OsStr::new("C:\\fspy_test_not_exist_program.exe\0").encode_wide().collect::<Vec<u16>>();
47+
unsafe {
48+
CreateProcessW(
49+
program.as_ptr(),
50+
null_mut(),
51+
null_mut(),
52+
null_mut(),
53+
0,
54+
0,
55+
null_mut(),
56+
null_mut(),
57+
&mut si,
58+
&mut pi,
59+
)
60+
};
61+
})
62+
.await?;
63+
assert_contains(&accesses, Path::new("C:\\fspy_test_not_exist_program.exe"), AccessMode::READ);
64+
65+
Ok(())
66+
}
Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
#![cfg(windows)]
22
#![feature(sync_unsafe_cell)]
33

4-
mod stack_once;
54
pub mod windows;

crates/fspy_preload_windows/src/stack_once.rs

Lines changed: 0 additions & 40 deletions
This file was deleted.

crates/fspy_preload_windows/src/windows/client.rs

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -8,29 +8,11 @@ use fspy_shared::{
88
};
99
use winapi::{shared::minwindef::BOOL, um::winnt::HANDLE};
1010

11-
use crate::stack_once::{StackOnceGuard, stack_once_token};
12-
1311
pub struct Client<'a> {
1412
payload: Payload<'a>,
1513
ipc_sender: Option<Sender>,
1614
}
1715

18-
stack_once_token!(PATH_ACCESS_ONCE);
19-
20-
pub struct PathAccessSender<'a> {
21-
ipc_sender: &'a Option<Sender>,
22-
_once_guard: StackOnceGuard,
23-
}
24-
25-
impl<'a> PathAccessSender<'a> {
26-
pub fn send(&self, access: PathAccess<'_>) {
27-
let Some(sender) = &self.ipc_sender else {
28-
return;
29-
};
30-
sender.write_encoded(&access, BINCODE_CONFIG).expect("failed to send path access");
31-
}
32-
}
33-
3416
impl<'a> Client<'a> {
3517
pub fn from_payload_bytes(payload_bytes: &'a [u8]) -> Self {
3618
let (payload, decoded_len) =
@@ -58,11 +40,6 @@ impl<'a> Client<'a> {
5840
sender.write_encoded(&access, BINCODE_CONFIG).expect("failed to send path access");
5941
}
6042

61-
pub fn sender(&self) -> Option<PathAccessSender<'_>> {
62-
let guard = PATH_ACCESS_ONCE.try_enter()?;
63-
Some(PathAccessSender { ipc_sender: &self.ipc_sender, _once_guard: guard })
64-
}
65-
6643
pub unsafe fn prepare_child_process(&self, child_handle: HANDLE) -> BOOL {
6744
let payload_bytes = encode_to_vec(&self.payload, BINCODE_CONFIG).unwrap();
6845
unsafe {

crates/fspy_preload_windows/src/windows/detours/create_process.rs

Lines changed: 29 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
1-
use std::ffi::CStr;
2-
31
use fspy_detours_sys::{DetourCreateProcessWithDllExA, DetourCreateProcessWithDllExW};
4-
use fspy_shared::ipc::{AccessMode, NativeStr, PathAccess};
5-
use widestring::U16CStr;
62
use winapi::{
73
shared::{
84
minwindef::{BOOL, DWORD, LPVOID},
@@ -24,6 +20,29 @@ use crate::windows::{
2420
detour::{Detour, DetourAny},
2521
};
2622

23+
thread_local! {
24+
static IS_HOOKING_CREATE_PROCESS: std::cell::Cell<bool> = std::cell::Cell::new(false);
25+
}
26+
27+
struct HookGuard;
28+
impl HookGuard {
29+
pub fn new() -> Option<Self> {
30+
let already_hooking = IS_HOOKING_CREATE_PROCESS.with(|c| {
31+
let v = c.get();
32+
c.set(true);
33+
v
34+
});
35+
if already_hooking { None } else { Some(Self) }
36+
}
37+
}
38+
impl Drop for HookGuard {
39+
fn drop(&mut self) {
40+
IS_HOOKING_CREATE_PROCESS.with(|c| {
41+
c.set(false);
42+
});
43+
}
44+
}
45+
2746
static DETOUR_CREATE_PROCESS_W: Detour<
2847
unsafe extern "system" fn(
2948
LPCWSTR,
@@ -51,8 +70,7 @@ static DETOUR_CREATE_PROCESS_W: Detour<
5170
lp_startup_info: LPSTARTUPINFOW,
5271
lp_process_information: LPPROCESS_INFORMATION,
5372
) -> BOOL {
54-
let client = unsafe { global_client() };
55-
let Some(sender) = client.sender() else {
73+
let Some(_hook_guard) = HookGuard::new() else {
5674
// Detect re-entrance and avoid double hooking
5775
return unsafe {
5876
(DETOUR_CREATE_PROCESS_W.real())(
@@ -61,7 +79,7 @@ static DETOUR_CREATE_PROCESS_W: Detour<
6179
lp_process_attributes,
6280
lp_thread_attributes,
6381
b_inherit_handles,
64-
dw_creation_flags | CREATE_SUSPENDED,
82+
dw_creation_flags,
6583
lp_environment,
6684
lp_current_directory,
6785
lp_startup_info,
@@ -70,16 +88,7 @@ static DETOUR_CREATE_PROCESS_W: Detour<
7088
};
7189
};
7290

73-
if !lp_application_name.is_null() {
74-
unsafe {
75-
sender.send(PathAccess {
76-
mode: AccessMode::READ,
77-
path: NativeStr::from_wide(
78-
U16CStr::from_ptr_str(lp_application_name).as_slice(),
79-
),
80-
});
81-
}
82-
}
91+
let client = unsafe { global_client() };
8392

8493
unsafe extern "system" fn create_process_with_payload_w(
8594
lp_application_name: LPCWSTR,
@@ -175,8 +184,7 @@ static DETOUR_CREATE_PROCESS_A: Detour<
175184
lp_startup_info: LPSTARTUPINFOA,
176185
lp_process_information: LPPROCESS_INFORMATION,
177186
) -> BOOL {
178-
let client = unsafe { global_client() };
179-
let Some(sender) = client.sender() else {
187+
let Some(_hook_guard) = HookGuard::new() else {
180188
// Detect re-entrance and avoid double hooking
181189
return unsafe {
182190
(DETOUR_CREATE_PROCESS_A.real())(
@@ -185,23 +193,15 @@ static DETOUR_CREATE_PROCESS_A: Detour<
185193
lp_process_attributes,
186194
lp_thread_attributes,
187195
b_inherit_handles,
188-
dw_creation_flags | CREATE_SUSPENDED,
196+
dw_creation_flags,
189197
lp_environment,
190198
lp_current_directory,
191199
lp_startup_info,
192200
lp_process_information,
193201
)
194202
};
195203
};
196-
197-
if !lp_application_name.is_null() {
198-
unsafe {
199-
sender.send(PathAccess {
200-
mode: AccessMode::READ,
201-
path: NativeStr::from_ansi(CStr::from_ptr(lp_application_name).to_bytes()),
202-
});
203-
}
204-
}
204+
let client = unsafe { global_client() };
205205

206206
unsafe extern "system" fn create_process_with_payload_a(
207207
lp_application_name: LPCSTR,

crates/fspy_shared/Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ uuid = { workspace = true, features = ["v4"] }
1919
bytemuck = { workspace = true }
2020
os_str_bytes = { workspace = true }
2121
winapi = { workspace = true, features = ["std"] }
22-
winsafe = { workspace = true }
2322

2423
[dev-dependencies]
2524
assert2 = { workspace = true }

0 commit comments

Comments
 (0)