// Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 use std::{ ffi::{OsString, c_uchar, c_ulong, c_ushort, c_void}, io, os::windows::ffi::OsStringExt, path::{Path, PathBuf}, }; use windows::{ Wdk::System::SystemInformation::{NtQuerySystemInformation, SYSTEM_INFORMATION_CLASS}, Win32::{ Foundation::{HANDLE, MAX_PATH, NTSTATUS, STATUS_INFO_LENGTH_MISMATCH}, Storage::FileSystem::{ FILE_NAME_NORMALIZED, FILE_TYPE_DISK, GetFileType, GetFinalPathNameByHandleW, }, System::{ Memory::{ GetProcessHeap, HEAP_FLAGS, HEAP_ZERO_MEMORY, HeapAlloc, HeapFree, HeapReAlloc, }, Threading::GetCurrentProcessId, }, }, }; /// Private information class used to retrieve open file handles const SYSTEM_HANDLE_INFORMATION_CLASS: SYSTEM_INFORMATION_CLASS = SYSTEM_INFORMATION_CLASS(0x10); /// Initial buffer size holding the handle info /// The number is based on what I observe on a pretty standard Windows 11 const SYSTEM_HANDLE_INFORMATION_INITIAL_SIZE: usize = 2_500_000; /// Check if there are no open handles to the given files. /// /// Uses undocumented NT API to obtain open handles on the system. /// See: https://www.ired.team/miscellaneous-reversing-forensics/windows-kernel-internals/get-all-open-handles-and-kernel-object-address-from-userland pub async fn check_files_closed(file_paths: &[&Path]) -> io::Result { let current_pid = unsafe { GetCurrentProcessId() }; // Allocate info struct on heap with some initial value let mut reserved_memory = SYSTEM_HANDLE_INFORMATION_INITIAL_SIZE; let mut handle_table_info = HeapGuard::::new(reserved_memory)?; // Request system handle information let mut status: NTSTATUS = NTSTATUS::default(); let mut return_len = reserved_memory as u32; for _ in 0..2 { status = unsafe { NtQuerySystemInformation( SYSTEM_HANDLE_INFORMATION_CLASS, handle_table_info.as_mut_ptr() as _, return_len, &mut return_len, ) }; // Buffer is too small, resize memory and retry again. if status == STATUS_INFO_LENGTH_MISMATCH { tracing::trace!("Buffer is too small ({reserved_memory}), resizing to {return_len}"); reserved_memory = return_len as usize; handle_table_info.reallocate(reserved_memory)?; } else { break; } } status.ok()?; // Convert returned data into slice let num_handles = unsafe { (*handle_table_info.inner).number_of_handles }; let proc_entries = unsafe { std::slice::from_raw_parts( (*handle_table_info.as_mut_ptr()).handles.as_ptr(), num_handles as usize, ) }; // Iterate over open file handle entries for entry in proc_entries { if entry.unique_process_id == current_pid { let file_handle = HANDLE(entry.handle_value as _); // Filter everything except disk files if unsafe { GetFileType(file_handle) } == FILE_TYPE_DISK { // Obtain canonical path for file handle let mut file_handle_path = vec![0u16; MAX_PATH as usize]; let num_chars_without_nul = unsafe { GetFinalPathNameByHandleW( file_handle, &mut file_handle_path, FILE_NAME_NORMALIZED, ) as usize }; if num_chars_without_nul > 0 { let path_str = OsString::from_wide(&file_handle_path[0..num_chars_without_nul]); let file_handle_pathbuf = PathBuf::from(path_str); if file_paths.contains(&file_handle_pathbuf.as_path()) { return Ok(false); } } } } } Ok(true) } #[repr(C)] #[derive(Copy, Clone)] struct SystemHandleInformation { pub number_of_handles: c_ulong, pub handles: [SystemHandleTableEntryInfo; 1], } #[repr(C)] #[derive(Copy, Clone)] struct SystemHandleTableEntryInfo { pub unique_process_id: c_ulong, pub object_type_index: c_uchar, pub handle_attributes: c_uchar, pub handle_value: c_ushort, pub object: *mut c_void, pub granted_access: c_ulong, } /// Managed heap memory struct HeapGuard { inner: *mut T, process_heap: HANDLE, } impl HeapGuard { /// Allocate new memory using `HealAlloc` fn new(length: usize) -> io::Result { let process_heap = unsafe { GetProcessHeap()? }; let inner: *mut T = unsafe { HeapAlloc(process_heap, HEAP_ZERO_MEMORY, length) as _ }; if inner.is_null() { Err(io::Error::other("Failed to allocate memory")) } else { Ok(Self { inner, process_heap, }) } } /// Reallocate existing chunk of memory /// /// On success: the internal memory pointer is replaced. /// On failure: the internal memory pointer remains the same and still valid. fn reallocate(&mut self, new_length: usize) -> io::Result<()> { let new_ptr: *mut T = unsafe { HeapReAlloc( self.process_heap, HEAP_ZERO_MEMORY, Some(self.inner as _), new_length, ) as _ }; if new_ptr.is_null() { Err(io::Error::other("Failed to reallocate memory")) } else { self.inner = new_ptr; Ok(()) } } fn as_mut_ptr(&self) -> *mut T { self.inner } } impl Drop for HeapGuard { fn drop(&mut self) { #[allow(clippy::expect_used)] unsafe { HeapFree( self.process_heap, HEAP_FLAGS(0), Some(self.inner as *mut c_void), ) } .expect("HeapFree failure"); } }