Skip to content

Commit faa5a60

Browse files
authored
host_env: bound symlink probe; add Win32 times (#8733)
nt::symlink's directory probe now uses two WCHAR[MAX_PATH] buffers and treats an overflowing join as not a directory. winapi::version_ex_triple exposes GetVersionExW. nt::set_file_times opens with FILE_WRITE_ATTRIBUTES and writes wrapping FILETIMEs. Assisted-by: Claude
1 parent 300d8bd commit faa5a60

2 files changed

Lines changed: 105 additions & 17 deletions

File tree

‎crates/host_env/src/nt.rs‎

Lines changed: 89 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ use windows_sys::{
3131
ERROR_FILENAME_EXCED_RANGE, ERROR_INSUFFICIENT_BUFFER, ERROR_INVALID_FUNCTION,
3232
ERROR_INVALID_HANDLE, ERROR_INVALID_NAME, ERROR_INVALID_PARAMETER, ERROR_MORE_DATA,
3333
ERROR_NOT_READY, ERROR_NOT_SUPPORTED, ERROR_PATH_NOT_FOUND, ERROR_SHARING_VIOLATION,
34-
GENERIC_READ, GENERIC_WRITE, GetHandleInformation, GetLastError, HANDLE,
34+
FILETIME, GENERIC_READ, GENERIC_WRITE, GetHandleInformation, GetLastError, HANDLE,
3535
HANDLE_FLAG_INHERIT, INVALID_HANDLE_VALUE, MAX_PATH, SetHandleInformation,
3636
},
3737
Globalization::{CP_UTF8, MultiByteToWideChar, WideCharToMultiByte},
@@ -48,7 +48,7 @@ use windows_sys::{
4848
GetVolumePathNameW, GetVolumePathNamesForVolumeNameW, INVALID_FILE_ATTRIBUTES,
4949
OPEN_EXISTING, RemoveDirectoryW, SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE,
5050
SYMBOLIC_LINK_FLAG_DIRECTORY, SetFileAttributesW, SetFileInformationByHandle,
51-
WIN32_FILE_ATTRIBUTE_DATA, WIN32_FIND_DATAW,
51+
SetFileTime, WIN32_FILE_ATTRIBUTE_DATA, WIN32_FIND_DATAW,
5252
},
5353
System::{
5454
Console,
@@ -288,33 +288,58 @@ pub fn supports_virtual_terminal() -> bool {
288288
}
289289

290290
pub fn symlink(
291-
src: &Path,
292-
dst: &Path,
291+
_src: &Path,
292+
_dst: &Path,
293293
src_wide: &widestring::WideCStr,
294294
dst_wide: &widestring::WideCStr,
295295
target_is_directory: bool,
296296
) -> io::Result<()> {
297297
static HAS_UNPRIVILEGED_FLAG: AtomicBool = AtomicBool::new(true);
298298

299-
fn check_dir(src: &Path, dst: &Path) -> bool {
299+
fn check_dir(src: &widestring::WideCStr, dst: &widestring::WideCStr) -> bool {
300300
use windows_sys::Win32::Storage::FileSystem::GetFileExInfoStandard;
301301

302-
let Some(dst_parent) = dst.parent() else {
302+
// `_check_dirW`: two WCHAR[MAX_PATH] buffers; a join that does not
303+
// fit is "not a directory".
304+
let src = src.as_slice();
305+
let dst = dst.as_slice();
306+
let max_path = MAX_PATH_USIZE;
307+
if dst.len() >= max_path {
303308
return false;
304-
};
305-
let resolved = if src.is_absolute() {
306-
src.to_path_buf()
309+
}
310+
let parent_len = dst
311+
.iter()
312+
.rposition(|&unit| unit == b'\\' as u16 || unit == b'/' as u16)
313+
.unwrap_or(0);
314+
let parent = &dst[..parent_len];
315+
let absolute = src
316+
.first()
317+
.is_some_and(|&unit| unit == b'\\' as u16 || unit == b'/' as u16)
318+
|| (src.first().is_some_and(|&unit| unit != 0) && src.get(1) == Some(&(b':' as u16)));
319+
320+
let mut resolved = Vec::with_capacity(max_path);
321+
if absolute {
322+
if src.len() >= max_path {
323+
return false;
324+
}
325+
resolved.extend_from_slice(src);
307326
} else {
308-
dst_parent.join(src)
309-
};
310-
let wide = match widestring::WideCString::from_os_str(&resolved) {
311-
Ok(wide) => wide,
312-
Err(_) => return false,
313-
};
327+
let separator_len = usize::from(!parent.is_empty());
328+
if parent.len() + separator_len + src.len() >= max_path {
329+
return false;
330+
}
331+
resolved.extend_from_slice(parent);
332+
if !parent.is_empty() {
333+
resolved.push(b'\\' as u16);
334+
}
335+
resolved.extend_from_slice(src);
336+
}
337+
resolved.push(0);
338+
314339
let mut info: WIN32_FILE_ATTRIBUTE_DATA = unsafe { core::mem::zeroed() };
315340
let ok = unsafe {
316341
GetFileAttributesExW(
317-
wide.as_ptr(),
342+
resolved.as_ptr(),
318343
GetFileExInfoStandard,
319344
(&mut info as *mut WIN32_FILE_ATTRIBUTE_DATA).cast(),
320345
)
@@ -326,7 +351,7 @@ pub fn symlink(
326351
if HAS_UNPRIVILEGED_FLAG.load(Ordering::Relaxed) {
327352
flags |= SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE;
328353
}
329-
if target_is_directory || check_dir(src, dst) {
354+
if target_is_directory || check_dir(src_wide, dst_wide) {
330355
flags |= SYMBOLIC_LINK_FLAG_DIRECTORY;
331356
}
332357

@@ -349,6 +374,53 @@ pub fn symlink(
349374
}
350375
}
351376

377+
/// `time_t_to_FILE_TIME` plus `SetFileTime`. A FILETIME counts 100ns ticks
378+
/// from 1601-01-01, so the epoch shift makes a second before 1970 an ordinary
379+
/// positive tick count. The arithmetic wraps: a second this filesystem cannot
380+
/// hold writes the bits the multiplication leaves.
381+
pub fn set_file_times(
382+
path: &widestring::WideCStr,
383+
atime_sec: i64,
384+
atime_nsec: i64,
385+
mtime_sec: i64,
386+
mtime_nsec: i64,
387+
) -> io::Result<()> {
388+
const EPOCH_DIFF: i64 = 11_644_473_600;
389+
let to_filetime = |sec: i64, nsec: i64| -> FILETIME {
390+
let ticks = sec
391+
.wrapping_add(EPOCH_DIFF)
392+
.wrapping_mul(10_000_000)
393+
.wrapping_add(nsec / 100) as u64;
394+
FILETIME {
395+
dwLowDateTime: ticks as u32,
396+
dwHighDateTime: (ticks >> 32) as u32,
397+
}
398+
};
399+
let atime = to_filetime(atime_sec, atime_nsec);
400+
let mtime = to_filetime(mtime_sec, mtime_nsec);
401+
let handle = unsafe {
402+
CreateFileW(
403+
path.as_ptr(),
404+
FILE_WRITE_ATTRIBUTES,
405+
0,
406+
core::ptr::null(),
407+
OPEN_EXISTING,
408+
FILE_FLAG_BACKUP_SEMANTICS,
409+
core::ptr::null_mut(),
410+
)
411+
};
412+
if handle == INVALID_HANDLE_VALUE {
413+
return Err(io::Error::last_os_error());
414+
}
415+
let wrote = unsafe { SetFileTime(handle, core::ptr::null(), &atime, &mtime) };
416+
let error = (wrote == 0).then(io::Error::last_os_error);
417+
unsafe { CloseHandle(handle) };
418+
match error {
419+
Some(error) => Err(error),
420+
None => Ok(()),
421+
}
422+
}
423+
352424
#[allow(clippy::not_unsafe_ptr_arg_deref)]
353425
pub fn win32_hchmod(handle: HANDLE, mode: u32, write_bit: u32) -> io::Result<()> {
354426
let mut info: FILE_BASIC_INFO = unsafe { core::mem::zeroed() };

‎crates/host_env/src/winapi.rs‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -790,6 +790,22 @@ pub fn get_version() -> u32 {
790790
unsafe { windows_sys::Win32::System::SystemInformation::GetVersion() }
791791
}
792792

793+
/// `GetVersionExW` major/minor/build. Not `get_version()`: that DWORD is
794+
/// `GetVersion()`, and `get_windows_version()` overwrites the triple with
795+
/// kernel32's file version.
796+
#[must_use]
797+
pub fn version_ex_triple() -> Option<(u32, u32, u32)> {
798+
use windows_sys::Win32::System::SystemInformation::{GetVersionExW, OSVERSIONINFOW};
799+
800+
let mut info: OSVERSIONINFOW = unsafe { core::mem::zeroed() };
801+
info.dwOSVersionInfoSize = core::mem::size_of::<OSVERSIONINFOW>() as u32;
802+
(unsafe { GetVersionExW(&mut info) } != 0).then_some((
803+
info.dwMajorVersion,
804+
info.dwMinorVersion,
805+
info.dwBuildNumber,
806+
))
807+
}
808+
793809
pub fn create_job_object_w(name: Option<&widestring::WideCStr>) -> io::Result<HANDLE> {
794810
let name_ptr = name.map_or(core::ptr::null(), |n| n.as_ptr());
795811
unsafe { windows_sys::Win32::System::JobObjects::CreateJobObjectW(core::ptr::null(), name_ptr) }

0 commit comments

Comments
 (0)