From d73b15f472742bbf614dc40afcacaac1c3fa3d00 Mon Sep 17 00:00:00 2001 From: Dong Shin Date: Sat, 3 Oct 2026 17:08:24 +0900 Subject: [PATCH] cap-primitives: Keep a NUL after the name `CreateFileAtW` passes to `NtCreateFile`. `CreateFileAtW` passes `NtCreateFile` a `UNICODE_STRING` built from an unterminated slice. That is valid, but Win32 paths (`RtlInitUnicodeString`, `RtlDosPathNameToNtPathName_U`) always leave a NUL right after the name, and some third-party endpoint-security products hook `NtCreateFile` and copy `ObjectName->Buffer` up to a NUL instead of `Length` bytes. With cap-std's directory-relative opens such a hook reads past the allocation, and the process crashes when the next page is inaccessible. We hit this in production on Windows machines with such a product installed, and carrying this change as a patch fixed it. Make `CreateFileAtW` take a NUL-terminated slice, with `Length` excluding the terminator and `MaximumLength` including it, as `RtlInitUnicodeString` does. `open_at`, its only caller, now appends the NUL once for both the `CreateFileW` and `CreateFileAtW` branches. The name the kernel sees is unchanged. --- .../src/windows/fs/create_file_at_w.rs | 24 ++++++++++++------- .../src/windows/fs/open_unchecked.rs | 13 ++++------ 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/cap-primitives/src/windows/fs/create_file_at_w.rs b/cap-primitives/src/windows/fs/create_file_at_w.rs index 392bc886..b0da0db8 100644 --- a/cap-primitives/src/windows/fs/create_file_at_w.rs +++ b/cap-primitives/src/windows/fs/create_file_at_w.rs @@ -70,9 +70,12 @@ const FILE_ATTRIBUTE_VALID_FLAGS: FILE_FLAGS_AND_ATTRIBUTES = FILE_ATTRIBUTE_EA /// Like Windows' `CreateFileW`, but takes a `dir` argument to use as the /// root directory. /// -/// Also, the `lpfilename` is a Rust slice instead of a C-style NUL-terminated -/// array, because that's what our callers have and it's closer to what -/// `NtCreatePath` takes. +/// Also, the `lpfilename` is a Rust slice ending in a NUL terminator. The +/// terminator is not part of the name passed to `NtCreateFile`, but it is +/// kept in memory right after it, as `RtlInitUnicodeString` does: filter +/// drivers and user-mode hooks that read `ObjectName->Buffer` up to a NUL +/// instead of `Length` bytes would otherwise read past the end of the +/// allocation. #[allow(non_snake_case)] pub unsafe fn CreateFileAtW( dir: HANDLE, @@ -118,9 +121,14 @@ pub unsafe fn CreateFileAtW( } }; - // Convert `lpfilename` to a `UNICODE_STRING`. + // Convert `lpfilename` to a `UNICODE_STRING`, keeping the NUL terminator + // out of `Length` but inside `MaximumLength`. + let Some((&0, name)) = lpfilename.split_last() else { + SetLastError(ERROR_INVALID_PARAMETER); + return HandleOrInvalid::from_raw_handle(INVALID_HANDLE_VALUE as _); + }; let byte_length = lpfilename.len() * mem::size_of::(); - let length: u16 = match byte_length.try_into() { + let maximum_length: u16 = match byte_length.try_into() { Ok(length) => length, Err(_) => { SetLastError(ERROR_INVALID_NAME); @@ -128,9 +136,9 @@ pub unsafe fn CreateFileAtW( } }; let mut unicode_string = UNICODE_STRING { - Buffer: lpfilename.as_ptr() as *mut u16, - Length: length, - MaximumLength: length, + Buffer: name.as_ptr() as *mut u16, + Length: maximum_length - mem::size_of::() as u16, + MaximumLength: maximum_length, }; let mut handle = INVALID_HANDLE_VALUE; diff --git a/cap-primitives/src/windows/fs/open_unchecked.rs b/cap-primitives/src/windows/fs/open_unchecked.rs index 36243b0b..c7a8d30b 100644 --- a/cap-primitives/src/windows/fs/open_unchecked.rs +++ b/cap-primitives/src/windows/fs/open_unchecked.rs @@ -88,16 +88,16 @@ fn open_at(start: &fs::File, path: &Path, opts: &OpenOptions) -> io::Result>(); + // Both `CreateFileW` and our own `CreateFileAtW` take a NUL-terminated + // filename. + let wide = OsStr::encode_wide(rebuilt.as_os_str()) + .chain(Some(0)) + .collect::>(); // If we ended up re-rooting, use Windows' `CreateFileW` instead of our // own `CreateFileAtW` so that it does the requisite magic for absolute // paths. if dir == 0 as HANDLE { - // We're calling the windows-sys `CreateFileW` which expects a - // NUL-terminated filename, so add a NUL terminator. - wide.push(0); - let handle = unsafe { CreateFileW( wide.as_ptr(), @@ -115,9 +115,6 @@ fn open_at(start: &fs::File, path: &Path, opts: &OpenOptions) -> io::Result