Skip to content

Commit a928209

Browse files
branchseerclaude
andcommitted
fix(fspy): hook NtQueryDirectoryFileEx for READ_DIR tracking on Windows
Add hook for NtQueryDirectoryFileEx (newer Windows API) to properly track READ_DIR accesses. This fixes the oxlint_reads_directory test which uses the newer API not covered by NtQueryDirectoryFile. - Add ntdll.dll lookup support for dynamic symbols in DetourAny::attach - Define NtQueryDirectoryFileEx function type (not in ntapi crate) - Use Detour::dynamic for runtime symbol resolution - Update readdir test to use tempdir with cross-platform explanation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent ce00ecc commit a928209

3 files changed

Lines changed: 77 additions & 6 deletions

File tree

crates/fspy/tests/rust_std.rs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,25 @@ async fn open_write() -> anyhow::Result<()> {
3737

3838
#[test(tokio::test)]
3939
async fn readdir() -> anyhow::Result<()> {
40-
let accesses = track_child!((), |(): ()| {
40+
let tmpdir = tempfile::tempdir()?;
41+
let tmpdir_path = std::fs::canonicalize(tmpdir.path())?;
42+
// Reading a non-existent directory results in different tracked accesses on different platforms:
43+
// - Windows: READ, because the NT APIs open the directory as handle just like files (NtCreateFile/NtOpenFile),
44+
// and if that fails, not read dir call (NtQueryDirectoryFile/NtQueryDirectoryFileEx) is made.
45+
// - macOS/Linux:
46+
// - opendir results in a read_dir access. This call is directly made without trying to open the directory as a fd first.
47+
// - open + fopendir results in READ access, because open would fail with ENOENT, and fopendir is not called.
48+
//
49+
// This difference is acceptable because both will result in a "not found" fingerprint in vite-task.
50+
// To keep the test consistent across platforms, we create the directory first.
51+
std::fs::create_dir(tmpdir.path().join("hello_dir"))?;
52+
53+
let accesses = track_child!(tmpdir_path.to_str().unwrap().to_owned(), |tmpdir_path: String| {
54+
std::env::set_current_dir(tmpdir_path).unwrap();
4155
let _ = std::fs::read_dir("hello_dir");
4256
})
4357
.await?;
44-
assert_contains(&accesses, current_dir()?.join("hello_dir").as_path(), AccessMode::READ_DIR);
58+
assert_contains(&accesses, tmpdir_path.join("hello_dir").as_path(), AccessMode::READ_DIR);
4559

4660
Ok(())
4761
}

crates/fspy_preload_windows/src/windows/detour.rs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ impl<T: Copy> Detour<T> {
2121
Detour { symbol_name, target: UnsafeCell::new(unsafe { transmute_copy(&target) }), new }
2222
}
2323

24-
#[expect(dead_code)]
2524
pub const unsafe fn dynamic(symbol_name: &'static CStr, new: T) -> Self {
2625
Detour { symbol_name, target: UnsafeCell::new(null_mut()), new }
2726
}
@@ -52,15 +51,18 @@ pub struct DetourAny {
5251
pub struct AttachContext {
5352
kernelbase: HMODULE,
5453
kernel32: HMODULE,
54+
ntdll: HMODULE,
5555
}
5656

5757
impl AttachContext {
5858
pub fn new() -> Self {
5959
let kernelbase = unsafe { LoadLibraryA(c"kernelbase".as_ptr()) };
6060
let kernel32 = unsafe { LoadLibraryA(c"kernel32".as_ptr()) };
61+
let ntdll = unsafe { LoadLibraryA(c"ntdll".as_ptr()) };
6162
assert_ne!(kernelbase, null_mut());
6263
assert_ne!(kernel32, null_mut());
63-
Self { kernelbase, kernel32 }
64+
assert_ne!(ntdll, null_mut());
65+
Self { kernelbase, kernel32, ntdll }
6466
}
6567
}
6668

@@ -74,9 +76,14 @@ impl DetourAny {
7476
unsafe { *self.target = symbol_in_kernelbase.cast() };
7577
} else {
7678
if unsafe { *self.target }.is_null() {
77-
// dynamic symbol
79+
// dynamic symbol - look up from kernel32 or ntdll
7880
let symbol_in_kernel32 = unsafe { GetProcAddress(ctx.kernel32, symbol_name) };
79-
unsafe { *self.target = symbol_in_kernel32.cast() };
81+
if !symbol_in_kernel32.is_null() {
82+
unsafe { *self.target = symbol_in_kernel32.cast() };
83+
} else {
84+
let symbol_in_ntdll = unsafe { GetProcAddress(ctx.ntdll, symbol_name) };
85+
unsafe { *self.target = symbol_in_ntdll.cast() };
86+
}
8087
}
8188
}
8289
if unsafe { *self.target }.is_null() {

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

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,55 @@ static DETOUR_NT_QUERY_DIRECTORY_FILE: Detour<
286286
})
287287
};
288288

289+
// NtQueryDirectoryFileEx is not in ntapi crate, so we define it here.
290+
// https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ntifs/nf-ntifs-ntquerydirectoryfileex
291+
type NtQueryDirectoryFileExFn = unsafe extern "system" fn(
292+
file_handle: HANDLE,
293+
event: HANDLE,
294+
apc_routine: PIO_APC_ROUTINE,
295+
apc_context: PVOID,
296+
io_status_block: PIO_STATUS_BLOCK,
297+
file_information: PVOID,
298+
length: ULONG,
299+
file_information_class: FILE_INFORMATION_CLASS,
300+
query_flags: ULONG,
301+
file_name: PUNICODE_STRING,
302+
) -> NTSTATUS;
303+
304+
static DETOUR_NT_QUERY_DIRECTORY_FILE_EX: Detour<NtQueryDirectoryFileExFn> = unsafe {
305+
Detour::dynamic(c"NtQueryDirectoryFileEx", {
306+
unsafe extern "system" fn new_fn(
307+
file_handle: HANDLE,
308+
event: HANDLE,
309+
apc_routine: PIO_APC_ROUTINE,
310+
apc_context: PVOID,
311+
io_status_block: PIO_STATUS_BLOCK,
312+
file_information: PVOID,
313+
length: ULONG,
314+
file_information_class: FILE_INFORMATION_CLASS,
315+
query_flags: ULONG,
316+
file_name: PUNICODE_STRING,
317+
) -> NTSTATUS {
318+
unsafe { handle_open(AccessMode::READ_DIR, file_handle) };
319+
unsafe {
320+
(DETOUR_NT_QUERY_DIRECTORY_FILE_EX.real())(
321+
file_handle,
322+
event,
323+
apc_routine,
324+
apc_context,
325+
io_status_block,
326+
file_information,
327+
length,
328+
file_information_class,
329+
query_flags,
330+
file_name,
331+
)
332+
}
333+
}
334+
new_fn
335+
})
336+
};
337+
289338
pub const DETOURS: &[DetourAny] = &[
290339
DETOUR_NT_CREATE_FILE.as_any(),
291340
DETOUR_NT_OPEN_FILE.as_any(),
@@ -294,4 +343,5 @@ pub const DETOURS: &[DetourAny] = &[
294343
DETOUR_NT_OPEN_SYMBOLIC_LINK_OBJECT.as_any(),
295344
DETOUR_NT_QUERY_INFORMATION_BY_NAME.as_any(),
296345
DETOUR_NT_QUERY_DIRECTORY_FILE.as_any(),
346+
DETOUR_NT_QUERY_DIRECTORY_FILE_EX.as_any(),
297347
];

0 commit comments

Comments
 (0)