Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit c97807a

Browse files
committedMar 23, 2025
Avoid verbatim paths in Command::current_dir
If possible, we should try not to use verbatim paths in Command::current_dir. It might work but it might also break code (including some Windows APIs) that assume the current directory isn't verbatim.
1 parent 850ddbf commit c97807a

File tree

4 files changed

+84
-6
lines changed

4 files changed

+84
-6
lines changed
 

‎library/std/src/sys/path/windows.rs

+42
Original file line numberDiff line numberDiff line change
@@ -350,3 +350,45 @@ pub(crate) fn absolute(path: &Path) -> io::Result<PathBuf> {
350350
pub(crate) fn is_absolute(path: &Path) -> bool {
351351
path.has_root() && path.prefix().is_some()
352352
}
353+
354+
/// Test that the path is absolute, fully qualified and unchanged when processed by the Windows API.
355+
///
356+
/// For example:
357+
///
358+
/// - `C:\path\to\file` will return true.
359+
/// - `C:\path\to\nul` returns false because the Windows API will convert it to \\.\NUL
360+
/// - `C:\path\to\..\file` returns false because it will be resolved to `C:\path\file`.
361+
///
362+
/// This is a useful property because it means the path can be converted from and to and verbatim
363+
/// path just by changing the prefix.
364+
pub(crate) fn is_absolute_exact(path: &[u16]) -> bool {
365+
// This is implemented by checking that passing the path through
366+
// GetFullPathNameW does not change the path in any way.
367+
368+
// Windows paths are limited to i16::MAX length
369+
// though the API here accepts a u32 for the length.
370+
if path.is_empty() || path.len() > u32::MAX as usize || path.last() != Some(&0) {
371+
return false;
372+
}
373+
// The path returned by `GetFullPathNameW` must be the same length as the
374+
// given path, otherwise they're not equal.
375+
let buffer_len = path.len();
376+
let mut new_path = Vec::with_capacity(buffer_len);
377+
let result = unsafe {
378+
c::GetFullPathNameW(
379+
path.as_ptr(),
380+
new_path.capacity() as u32,
381+
new_path.as_mut_ptr(),
382+
crate::ptr::null_mut(),
383+
)
384+
};
385+
// Note: if non-zero, the returned result is the length of the buffer without the null termination
386+
if result == 0 || result as usize != buffer_len - 1 {
387+
false
388+
} else {
389+
unsafe {
390+
new_path.set_len((result as usize) + 1);
391+
}
392+
path == &new_path
393+
}
394+
}

‎library/std/src/sys/path/windows/tests.rs

+12
Original file line numberDiff line numberDiff line change
@@ -135,3 +135,15 @@ fn broken_unc_path() {
135135
assert_eq!(components.next(), Some(Component::Normal("foo".as_ref())));
136136
assert_eq!(components.next(), Some(Component::Normal("bar".as_ref())));
137137
}
138+
139+
#[test]
140+
fn test_is_absolute_exact() {
141+
use crate::sys::pal::api::wide_str;
142+
// These paths can be made verbatim by only changing their prefix.
143+
assert!(is_absolute_exact(wide_str!(r"C:\path\to\file")));
144+
assert!(is_absolute_exact(wide_str!(r"\\server\share\path\to\file")));
145+
// These paths change more substantially
146+
assert!(!is_absolute_exact(wide_str!(r"C:\path\to\..\file")));
147+
assert!(!is_absolute_exact(wide_str!(r"\\server\share\path\to\..\file")));
148+
assert!(!is_absolute_exact(wide_str!(r"C:\path\to\NUL"))); // Converts to \\.\NUL
149+
}

‎library/std/src/sys/process/windows.rs

+28-4
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ use crate::sys::args::{self, Arg};
1919
use crate::sys::c::{self, EXIT_FAILURE, EXIT_SUCCESS};
2020
use crate::sys::fs::{File, OpenOptions};
2121
use crate::sys::handle::Handle;
22-
use crate::sys::pal::api::{self, WinError};
22+
use crate::sys::pal::api::{self, WinError, utf16};
2323
use crate::sys::pal::{ensure_no_nuls, fill_utf16_buf};
2424
use crate::sys::pipe::{self, AnonPipe};
2525
use crate::sys::{cvt, path, stdio};
@@ -880,9 +880,33 @@ fn make_envp(maybe_env: Option<BTreeMap<EnvKey, OsString>>) -> io::Result<(*mut
880880
fn make_dirp(d: Option<&OsString>) -> io::Result<(*const u16, Vec<u16>)> {
881881
match d {
882882
Some(dir) => {
883-
let mut dir_str: Vec<u16> = ensure_no_nuls(dir)?.encode_wide().collect();
884-
dir_str.push(0);
885-
Ok((dir_str.as_ptr(), dir_str))
883+
let mut dir_str: Vec<u16> = ensure_no_nuls(dir)?.encode_wide().chain([0]).collect();
884+
// Try to remove the `\\?\` prefix, if any.
885+
// This is necessary because the current directory does not support verbatim paths.
886+
// However. this can only be done if it doesn't change how the path will be resolved.
887+
let ptr = if dir_str.starts_with(utf16!(r"\\?\UNC")) {
888+
// Turn the `C` in `UNC` into a `\` so we can then use `\\rest\of\path`.
889+
let start = r"\\?\UN".len();
890+
dir_str[start] = b'\\' as u16;
891+
if path::is_absolute_exact(&dir_str[start..]) {
892+
unsafe { dir_str.as_ptr().add(start) }
893+
} else {
894+
// Revert the above change.
895+
dir_str[start] = b'C' as u16;
896+
dir_str.as_ptr()
897+
}
898+
} else if dir_str.starts_with(utf16!(r"\\?\")) {
899+
// Strip the leading `\\?\`
900+
let start = r"\\?\".len();
901+
if path::is_absolute_exact(&dir_str[start..]) {
902+
unsafe { dir_str.as_ptr().add(start) }
903+
} else {
904+
dir_str.as_ptr()
905+
}
906+
} else {
907+
dir_str.as_ptr()
908+
};
909+
Ok((ptr, dir_str))
886910
}
887911
None => Ok((ptr::null(), Vec::new())),
888912
}

‎tests/ui/process/win-command-curdir-no-verbatim.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
//@ only-windows
55
//@ needs-subprocess
66

7+
use std::env;
78
use std::process::Command;
8-
use std::{env, fs};
99

1010
fn main() {
1111
if env::args().skip(1).any(|s| s == "--child") {
@@ -20,7 +20,7 @@ fn parent() {
2020
let dir = env::current_dir().unwrap();
2121
let status = Command::new(&exe)
2222
.arg("--child")
23-
.current_dir(fs::canonicalize(dir).unwrap())
23+
.current_dir(dir.canonicalize().unwrap())
2424
.spawn()
2525
.unwrap()
2626
.wait()

0 commit comments

Comments
 (0)
Failed to load comments.