I want to call WinApi NtQueryInformationFile function to retrieve the name of the DLL (as String).
To do that I need to pass a pointer to this struct (with string allocation continuing after the struct ends).
Can I assume that allocating a Vec<u16> will always return a pointer with an alignment of at least 4? Even though, strictly, the pointer to u16 should only be aligned to 2. https://doc.rust-lang.org/reference/type-layout.html
This is the code I have in mind:
pub unsafe fn get_dll_name(handle: windows::Win32::Foundation::HANDLE) -> String {
use windows::Wdk::Storage::FileSystem::{
FileNameInformation, NtQueryInformationFile, FILE_NAME_INFORMATION,
};
use windows::Win32::System::IO::IO_STATUS_BLOCK;
let mut io_status_block = IO_STATUS_BLOCK::default();
// #[repr(C)]
// pub struct FILE_NAME_INFORMATION {
// pub FileNameLength: u32,
// pub FileName: [u16; 1],
// }
// `FILE_NAME_INFORMATION` has an alignment of 4
const _: () = assert!(std::mem::align_of::<FILE_NAME_INFORMATION>() == 4);
const _: () = assert!(std::mem::size_of::<FILE_NAME_INFORMATION>() == 8);
let mut file_name_information = vec![0_u16; std::mem::size_of::<u32>() / 2 + MAX_PATH as usize];
// Can this assert ever fail?
assert_eq!(file_name_information.as_ptr().align_offset(4), 0);
let ntstatus = unsafe {
NtQueryInformationFile(
handle,
&mut io_status_block as *mut _,
file_name_information.as_mut_ptr() as *mut std::ffi::c_void,
(file_name_information.len() * 2) as u32, // in bytes
FileNameInformation,
)
};
if ntstatus.is_err() {
panic!()
}
let fni = file_name_information.as_ptr() as *const FILE_NAME_INFORMATION;
// # SAFETY: IIUC, this is only safe if `file_name_information` has an alignment of at least 4.
let byte_len = unsafe { (*fni).FileNameLength } as usize;
assert_eq!(byte_len % 2, 0);
let len = byte_len / 2;
// # SAFETY: `u16` has an alignment of 2.
let ptr = unsafe { &(*fni).FileName } as *const u16;
// # SAFETY: Assuming that upper two `unsafe` calls are safe, this follows all conditions for `from_raw_parts`
let slice = std::slice::from_raw_parts(ptr, len);
String::from_utf16_lossy(slice)
}
Vec does not guarantee such over-alignment, even though you probably will get that with the default system allocator. A custom #[global_allocator] might not though.
If you're going to use a constant MAX_PATH, I would just create a struct with that full array length and allocate it in a Box. For dynamic lengths, I'd suggest a manual Layout.
Though I still don't know how to allocate a struct in a Box without using the stack. From what I understand Box::new_zeroed() is still not stable and I have to manually use std::alloc::alloc (which requires Layout anyway).
As of Rust 1.82, you can use Box::new_uninit, but you should probably still zero it, because the query might not write the whole length. After that you can assume_init.
Yes, I need to zero it, since after calling NtQueryInformationFile only part of the statically sized buffer will be initialized.
But, then again, how can I zero MaybeUninit memory? In the PR implementing Box::new_zeroed, they used std::ptr::write_bytes, which I think I can use as well?
Either that, or drop down to std::alloc::alloc_zeroed and use Box::from_raw after. It might be more efficient for the allocator to return zeroed memory directly, and that's what Box::new_zeroed uses now.
unless you are creating a public library, I would suggest to just use a ffi compatible struct with a fixed max length that suits the application specific use case. e.g.:
// tweak this to suit your use case.
const MAX_FILE_NAME_LENGTH: usize = 1024;
#[repr(C)]
struct FileNameInformation {
file_name_length: std::ffi::c_ulong,
file_name: [u16; MAX_FILE_NAME_LENGTH],
}
impl FileNameInformation {
fn to_string(&self) -> Result<String, FromUtf16Error> {
String::from_utf16(&self.file_name[0..self.file_name_length as usize])
}
}
PS:
if you want to implement the ffi data structure "properly", it is possible with a rust DST to emulate C's "flexible array member", but you'll have to use the allocator api and raw pointers (probably requiring unsafe), or you can check out the fambox crate.
According to the documentation, the maximum segment length is obtained through a call to GetVolumeInformation. The documentation does state "this value is commonly 255 characters". But that's obviously not a guarentee. A safer choice is 32,767 plus some.
With an API like NtQueryFileInformation, it's better to ask the API itself how much space needs to be allocated rather than guessing or allocating the full 64kb each time. Starting with a small buffer, you can check the FileNameLength field for the needed size. Caveat: some third party drivers may not set the FileNameLength field in this case, so then you will need to manually decide on a size (either by growing by degrees or making a large allocation).
This only applies if using a system allocator (i.e. HeapAlloc). Rust's allocator makes no such guarantees. You have to specify the required alignment by passing Layout information.