From 8055be4461b7e75b33e544d722443f96bf9d9a1e Mon Sep 17 00:00:00 2001 From: erjuan Date: Tue, 12 Nov 2024 20:15:47 +0800 Subject: [PATCH] [Backport]Fix windows bat file vulnerability Offering: Open Source Management Center CVE: CVE-2024-43402 Reference: https://github.com/rust-lang/rust/pull/129960/commits/b666f820546ad2fd15b591acc8dfd7e7f461147e On April 9th, 2024, the Rust Security Response WG disclosed CVE-2024-24576, where std::process::Command incorrectly escaped arguments when invoking batch files on Windows. We were notified that our fix for the vulnerability was incomplete, and it was possible to bypass the fix when the batch file name had trailing whitespace or periods (which are ignored and stripped by Windows). Signed-off-by: fengting --- RELEASES.md | 1 + library/std/src/ffi/os_str.rs | 24 +- library/std/src/sys/pal/windows/args.rs | 460 ------------------------ library/std/src/sys/windows/api.rs | 82 +++++ library/std/src/sys/windows/mod.rs | 2 + library/std/src/sys/windows/os_str.rs | 5 + library/std/src/sys/windows/path.rs | 5 + library/std/src/sys/windows/process.rs | 22 +- tests/ui/std/windows-bat-args.rs | 4 +- tests/ui/std/windows-bat-args1.bat | 2 +- tests/ui/std/windows-bat-args2.bat | 2 +- tests/ui/std/windows-bat-args3.bat | 2 +- 12 files changed, 141 insertions(+), 470 deletions(-) delete mode 100644 library/std/src/sys/pal/windows/args.rs create mode 100644 library/std/src/sys/windows/api.rs diff --git a/RELEASES.md b/RELEASES.md index a40b9797d4a..e4400142181 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -4,6 +4,7 @@ Version 1.77.2 (2024-04-09) - [CVE-2024-24576: fix escaping of Windows batch file arguments in `std::process::Command`](https://blog.rust-lang.org/2024/04/09/cve-2024-24576.html) +- Fix `Command`'s batch files argument escaping not working when file name has trailing whitespace or periods (CVE-2024-43402). Version 1.72.0 (2023-08-24) ========================== diff --git a/library/std/src/ffi/os_str.rs b/library/std/src/ffi/os_str.rs index e7bad9d542c..e5c1e6a4fbc 100644 --- a/library/std/src/ffi/os_str.rs +++ b/library/std/src/ffi/os_str.rs @@ -707,7 +707,7 @@ pub fn new + ?Sized>(s: &S) -> &OsStr { /// /// [conversions]: super#conversions #[inline] - #[unstable(feature = "os_str_bytes", issue = "111544")] + #[stable(feature = "os_str_bytes", since = "1.71.0")] pub unsafe fn from_os_str_bytes_unchecked(bytes: &[u8]) -> &Self { Self::from_inner(Slice::from_os_str_bytes_unchecked(bytes)) } @@ -820,6 +820,26 @@ pub fn to_os_string(&self) -> OsString { OsString { inner: self.inner.to_owned() } } + /// Converts an OS string slice to a byte slice. To convert the byte slice back into an OS + /// string slice, use the [`OsStr::from_encoded_bytes_unchecked`] function. + /// + /// The byte encoding is an unspecified, platform-specific, self-synchronizing superset of UTF-8. + /// By being a self-synchronizing superset of UTF-8, this encoding is also a superset of 7-bit + /// ASCII. + /// + /// Note: As the encoding is unspecified, any sub-slice of bytes that is not valid UTF-8 should + /// be treated as opaque and only comparable within the same rust version built for the same + /// target platform. For example, sending the slice over the network or storing it in a file + /// will likely result in incompatible byte slices. See [`OsString`] for more encoding details + /// and [`std::ffi`] for platform-specific, specified conversions. + /// + /// [`std::ffi`]: crate::ffi + #[inline] + #[stable(feature = "os_str_bytes", since = "1.71.0")] + pub fn as_encoded_bytes(&self) -> &[u8] { + self.inner.as_encoded_bytes() + } + /// Checks whether the `OsStr` is empty. /// /// # Examples @@ -897,7 +917,7 @@ pub fn into_os_string(self: Box) -> OsString { /// /// [`std::ffi`]: crate::ffi #[inline] - #[unstable(feature = "os_str_bytes", issue = "111544")] + #[stable(feature = "os_str_bytes", since = "1.71.0")] pub fn as_os_str_bytes(&self) -> &[u8] { self.inner.as_os_str_bytes() } diff --git a/library/std/src/sys/pal/windows/args.rs b/library/std/src/sys/pal/windows/args.rs deleted file mode 100644 index 48bcb89e669..00000000000 --- a/library/std/src/sys/pal/windows/args.rs +++ /dev/null @@ -1,460 +0,0 @@ -//! The Windows command line is just a string -//! -//! -//! This module implements the parsing necessary to turn that string into a list of arguments. - -#[cfg(test)] -mod tests; - -use super::os::current_exe; -use crate::ffi::{OsStr, OsString}; -use crate::fmt; -use crate::io; -use crate::num::NonZeroU16; -use crate::os::windows::prelude::*; -use crate::path::{Path, PathBuf}; -use crate::sys::path::get_long_path; -use crate::sys::process::ensure_no_nuls; -use crate::sys::{c, to_u16s}; -use crate::sys_common::wstr::WStrUnits; -use crate::sys_common::AsInner; -use crate::vec; - -use crate::iter; - -/// This is the const equivalent to `NonZeroU16::new(n).unwrap()` -/// -/// FIXME: This can be removed once `Option::unwrap` is stably const. -/// See the `const_option` feature (#67441). -const fn non_zero_u16(n: u16) -> NonZeroU16 { - match NonZeroU16::new(n) { - Some(n) => n, - None => panic!("called `unwrap` on a `None` value"), - } -} - -pub fn args() -> Args { - // SAFETY: `GetCommandLineW` returns a pointer to a null terminated UTF-16 - // string so it's safe for `WStrUnits` to use. - unsafe { - let lp_cmd_line = c::GetCommandLineW(); - let parsed_args_list = parse_lp_cmd_line(WStrUnits::new(lp_cmd_line), || { - current_exe().map(PathBuf::into_os_string).unwrap_or_else(|_| OsString::new()) - }); - - Args { parsed_args_list: parsed_args_list.into_iter() } - } -} - -/// Implements the Windows command-line argument parsing algorithm. -/// -/// Microsoft's documentation for the Windows CLI argument format can be found at -/// -/// -/// A more in-depth explanation is here: -/// -/// -/// Windows includes a function to do command line parsing in shell32.dll. -/// However, this is not used for two reasons: -/// -/// 1. Linking with that DLL causes the process to be registered as a GUI application. -/// GUI applications add a bunch of overhead, even if no windows are drawn. See -/// . -/// -/// 2. It does not follow the modern C/C++ argv rules outlined in the first two links above. -/// -/// This function was tested for equivalence to the C/C++ parsing rules using an -/// extensive test suite available at -/// . -fn parse_lp_cmd_line<'a, F: Fn() -> OsString>( - lp_cmd_line: Option>, - exe_name: F, -) -> Vec { - const BACKSLASH: NonZeroU16 = non_zero_u16(b'\\' as u16); - const QUOTE: NonZeroU16 = non_zero_u16(b'"' as u16); - const TAB: NonZeroU16 = non_zero_u16(b'\t' as u16); - const SPACE: NonZeroU16 = non_zero_u16(b' ' as u16); - - let mut ret_val = Vec::new(); - // If the cmd line pointer is null or it points to an empty string then - // return the name of the executable as argv[0]. - if lp_cmd_line.as_ref().and_then(|cmd| cmd.peek()).is_none() { - ret_val.push(exe_name()); - return ret_val; - } - let mut code_units = lp_cmd_line.unwrap(); - - // The executable name at the beginning is special. - let mut in_quotes = false; - let mut cur = Vec::new(); - for w in &mut code_units { - match w { - // A quote mark always toggles `in_quotes` no matter what because - // there are no escape characters when parsing the executable name. - QUOTE => in_quotes = !in_quotes, - // If not `in_quotes` then whitespace ends argv[0]. - SPACE | TAB if !in_quotes => break, - // In all other cases the code unit is taken literally. - _ => cur.push(w.get()), - } - } - // Skip whitespace. - code_units.advance_while(|w| w == SPACE || w == TAB); - ret_val.push(OsString::from_wide(&cur)); - - // Parse the arguments according to these rules: - // * All code units are taken literally except space, tab, quote and backslash. - // * When not `in_quotes`, space and tab separate arguments. Consecutive spaces and tabs are - // treated as a single separator. - // * A space or tab `in_quotes` is taken literally. - // * A quote toggles `in_quotes` mode unless it's escaped. An escaped quote is taken literally. - // * A quote can be escaped if preceded by an odd number of backslashes. - // * If any number of backslashes is immediately followed by a quote then the number of - // backslashes is halved (rounding down). - // * Backslashes not followed by a quote are all taken literally. - // * If `in_quotes` then a quote can also be escaped using another quote - // (i.e. two consecutive quotes become one literal quote). - let mut cur = Vec::new(); - let mut in_quotes = false; - while let Some(w) = code_units.next() { - match w { - // If not `in_quotes`, a space or tab ends the argument. - SPACE | TAB if !in_quotes => { - ret_val.push(OsString::from_wide(&cur[..])); - cur.truncate(0); - - // Skip whitespace. - code_units.advance_while(|w| w == SPACE || w == TAB); - } - // Backslashes can escape quotes or backslashes but only if consecutive backslashes are followed by a quote. - BACKSLASH => { - let backslash_count = code_units.advance_while(|w| w == BACKSLASH) + 1; - if code_units.peek() == Some(QUOTE) { - cur.extend(iter::repeat(BACKSLASH.get()).take(backslash_count / 2)); - // The quote is escaped if there are an odd number of backslashes. - if backslash_count % 2 == 1 { - code_units.next(); - cur.push(QUOTE.get()); - } - } else { - // If there is no quote on the end then there is no escaping. - cur.extend(iter::repeat(BACKSLASH.get()).take(backslash_count)); - } - } - // If `in_quotes` and not backslash escaped (see above) then a quote either - // unsets `in_quote` or is escaped by another quote. - QUOTE if in_quotes => match code_units.peek() { - // Two consecutive quotes when `in_quotes` produces one literal quote. - Some(QUOTE) => { - cur.push(QUOTE.get()); - code_units.next(); - } - // Otherwise set `in_quotes`. - Some(_) => in_quotes = false, - // The end of the command line. - // Push `cur` even if empty, which we do by breaking while `in_quotes` is still set. - None => break, - }, - // If not `in_quotes` and not BACKSLASH escaped (see above) then a quote sets `in_quote`. - QUOTE => in_quotes = true, - // Everything else is always taken literally. - _ => cur.push(w.get()), - } - } - // Push the final argument, if any. - if !cur.is_empty() || in_quotes { - ret_val.push(OsString::from_wide(&cur[..])); - } - ret_val -} - -pub struct Args { - parsed_args_list: vec::IntoIter, -} - -impl fmt::Debug for Args { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.parsed_args_list.as_slice().fmt(f) - } -} - -impl Iterator for Args { - type Item = OsString; - fn next(&mut self) -> Option { - self.parsed_args_list.next() - } - fn size_hint(&self) -> (usize, Option) { - self.parsed_args_list.size_hint() - } -} - -impl DoubleEndedIterator for Args { - fn next_back(&mut self) -> Option { - self.parsed_args_list.next_back() - } -} - -impl ExactSizeIterator for Args { - fn len(&self) -> usize { - self.parsed_args_list.len() - } -} - -#[derive(Debug)] -pub(crate) enum Arg { - /// Add quotes (if needed) - Regular(OsString), - /// Append raw string without quoting - Raw(OsString), -} - -enum Quote { - // Every arg is quoted - Always, - // Whitespace and empty args are quoted - Auto, - // Arg appended without any changes (#29494) - Never, -} - -pub(crate) fn append_arg(cmd: &mut Vec, arg: &Arg, force_quotes: bool) -> io::Result<()> { - let (arg, quote) = match arg { - Arg::Regular(arg) => (arg, if force_quotes { Quote::Always } else { Quote::Auto }), - Arg::Raw(arg) => (arg, Quote::Never), - }; - - // If an argument has 0 characters then we need to quote it to ensure - // that it actually gets passed through on the command line or otherwise - // it will be dropped entirely when parsed on the other end. - ensure_no_nuls(arg)?; - let arg_bytes = arg.as_encoded_bytes(); - let (quote, escape) = match quote { - Quote::Always => (true, true), - Quote::Auto => { - (arg_bytes.iter().any(|c| *c == b' ' || *c == b'\t') || arg_bytes.is_empty(), true) - } - Quote::Never => (false, false), - }; - if quote { - cmd.push('"' as u16); - } - - let mut backslashes: usize = 0; - for x in arg.encode_wide() { - if escape { - if x == '\\' as u16 { - backslashes += 1; - } else { - if x == '"' as u16 { - // Add n+1 backslashes to total 2n+1 before internal '"'. - cmd.extend((0..=backslashes).map(|_| '\\' as u16)); - } - backslashes = 0; - } - } - cmd.push(x); - } - - if quote { - // Add n backslashes to total 2n before ending '"'. - cmd.extend((0..backslashes).map(|_| '\\' as u16)); - cmd.push('"' as u16); - } - Ok(()) -} - -fn append_bat_arg(cmd: &mut Vec, arg: &OsStr, mut quote: bool) -> io::Result<()> { - ensure_no_nuls(arg)?; - // If an argument has 0 characters then we need to quote it to ensure - // that it actually gets passed through on the command line or otherwise - // it will be dropped entirely when parsed on the other end. - // - // We also need to quote the argument if it ends with `\` to guard against - // bat usage such as `"%~2"` (i.e. force quote arguments) otherwise a - // trailing slash will escape the closing quote. - if arg.is_empty() || arg.as_encoded_bytes().last() == Some(&b'\\') { - quote = true; - } - for cp in arg.as_inner().inner.code_points() { - if let Some(cp) = cp.to_char() { - // Rather than trying to find every ascii symbol that must be quoted, - // we assume that all ascii symbols must be quoted unless they're known to be good. - // We also quote Unicode control blocks for good measure. - // Note an unquoted `\` is fine so long as the argument isn't otherwise quoted. - static UNQUOTED: &str = r"#$*+-./:?@\_"; - let ascii_needs_quotes = - cp.is_ascii() && !(cp.is_ascii_alphanumeric() || UNQUOTED.contains(cp)); - if ascii_needs_quotes || cp.is_control() { - quote = true; - } - } - } - - if quote { - cmd.push('"' as u16); - } - // Loop through the string, escaping `\` only if followed by `"`. - // And escaping `"` by doubling them. - let mut backslashes: usize = 0; - for x in arg.encode_wide() { - if x == '\\' as u16 { - backslashes += 1; - } else { - if x == '"' as u16 { - // Add n backslashes to total 2n before internal `"`. - cmd.extend((0..backslashes).map(|_| '\\' as u16)); - // Appending an additional double-quote acts as an escape. - cmd.push(b'"' as u16) - } else if x == '%' as u16 || x == '\r' as u16 { - // yt-dlp hack: replaces `%` with `%%cd:~,%` to stop %VAR% being expanded as an environment variable. - // - // # Explanation - // - // cmd supports extracting a substring from a variable using the following syntax: - // %variable:~start_index,end_index% - // - // In the above command `cd` is used as the variable and the start_index and end_index are left blank. - // `cd` is a built-in variable that dynamically expands to the current directory so it's always available. - // Explicitly omitting both the start and end index creates a zero-length substring. - // - // Therefore it all resolves to nothing. However, by doing this no-op we distract cmd.exe - // from potentially expanding %variables% in the argument. - cmd.extend_from_slice(&[ - '%' as u16, '%' as u16, 'c' as u16, 'd' as u16, ':' as u16, '~' as u16, - ',' as u16, - ]); - } - backslashes = 0; - } - cmd.push(x); - } - if quote { - // Add n backslashes to total 2n before ending `"`. - cmd.extend((0..backslashes).map(|_| '\\' as u16)); - cmd.push('"' as u16); - } - Ok(()) -} - -pub(crate) fn make_bat_command_line( - script: &[u16], - args: &[Arg], - force_quotes: bool, -) -> io::Result> { - const INVALID_ARGUMENT_ERROR: io::Error = - io::const_io_error!(io::ErrorKind::InvalidInput, r#"batch file arguments are invalid"#); - // Set the start of the command line to `cmd.exe /c "` - // It is necessary to surround the command in an extra pair of quotes, - // hence the trailing quote here. It will be closed after all arguments - // have been added. - // Using /e:ON enables "command extensions" which is essential for the `%` hack to work. - let mut cmd: Vec = "cmd.exe /e:ON /v:OFF /d /c \"".encode_utf16().collect(); - - // Push the script name surrounded by its quote pair. - cmd.push(b'"' as u16); - // Windows file names cannot contain a `"` character or end with `\\`. - // If the script name does then return an error. - if script.contains(&(b'"' as u16)) || script.last() == Some(&(b'\\' as u16)) { - return Err(io::const_io_error!( - io::ErrorKind::InvalidInput, - "Windows file names may not contain `\"` or end with `\\`" - )); - } - cmd.extend_from_slice(script.strip_suffix(&[0]).unwrap_or(script)); - cmd.push(b'"' as u16); - - // Append the arguments. - // FIXME: This needs tests to ensure that the arguments are properly - // reconstructed by the batch script by default. - for arg in args { - cmd.push(' ' as u16); - match arg { - Arg::Regular(arg_os) => { - let arg_bytes = arg_os.as_encoded_bytes(); - // Disallow \r and \n as they may truncate the arguments. - const DISALLOWED: &[u8] = b"\r\n"; - if arg_bytes.iter().any(|c| DISALLOWED.contains(c)) { - return Err(INVALID_ARGUMENT_ERROR); - } - append_bat_arg(&mut cmd, arg_os, force_quotes)?; - } - _ => { - // Raw arguments are passed on as-is. - // It's the user's responsibility to properly handle arguments in this case. - append_arg(&mut cmd, arg, force_quotes)?; - } - }; - } - - // Close the quote we left opened earlier. - cmd.push(b'"' as u16); - - Ok(cmd) -} - -/// Takes a path and tries to return a non-verbatim path. -/// -/// This is necessary because cmd.exe does not support verbatim paths. -pub(crate) fn to_user_path(path: &Path) -> io::Result> { - from_wide_to_user_path(to_u16s(path)?) -} -pub(crate) fn from_wide_to_user_path(mut path: Vec) -> io::Result> { - use super::fill_utf16_buf; - use crate::ptr; - - // UTF-16 encoded code points, used in parsing and building UTF-16 paths. - // All of these are in the ASCII range so they can be cast directly to `u16`. - const SEP: u16 = b'\\' as _; - const QUERY: u16 = b'?' as _; - const COLON: u16 = b':' as _; - const U: u16 = b'U' as _; - const N: u16 = b'N' as _; - const C: u16 = b'C' as _; - - // Early return if the path is too long to remove the verbatim prefix. - const LEGACY_MAX_PATH: usize = 260; - if path.len() > LEGACY_MAX_PATH { - return Ok(path); - } - - match &path[..] { - // `\\?\C:\...` => `C:\...` - [SEP, SEP, QUERY, SEP, _, COLON, SEP, ..] => unsafe { - let lpfilename = path[4..].as_ptr(); - fill_utf16_buf( - |buffer, size| c::GetFullPathNameW(lpfilename, size, buffer, ptr::null_mut()), - |full_path: &[u16]| { - if full_path == &path[4..path.len() - 1] { - let mut path: Vec = full_path.into(); - path.push(0); - path - } else { - path - } - }, - ) - }, - // `\\?\UNC\...` => `\\...` - [SEP, SEP, QUERY, SEP, U, N, C, SEP, ..] => unsafe { - // Change the `C` in `UNC\` to `\` so we can get a slice that starts with `\\`. - path[6] = b'\\' as u16; - let lpfilename = path[6..].as_ptr(); - fill_utf16_buf( - |buffer, size| c::GetFullPathNameW(lpfilename, size, buffer, ptr::null_mut()), - |full_path: &[u16]| { - if full_path == &path[6..path.len() - 1] { - let mut path: Vec = full_path.into(); - path.push(0); - path - } else { - // Restore the 'C' in "UNC". - path[6] = b'C' as u16; - path - } - }, - ) - }, - // For everything else, leave the path unchanged. - _ => get_long_path(path, false), - } -} diff --git a/library/std/src/sys/windows/api.rs b/library/std/src/sys/windows/api.rs new file mode 100644 index 00000000000..00f14c42750 --- /dev/null +++ b/library/std/src/sys/windows/api.rs @@ -0,0 +1,82 @@ +/// Creates a UTF-16 string from a str without null termination. +pub macro utf16($str:expr) {{ + const UTF8: &str = $str; + const UTF16_LEN: usize = crate::sys::windows::api::utf16_len(UTF8); + const UTF16: [u16; UTF16_LEN] = crate::sys::windows::api::to_utf16(UTF8); + &UTF16 +}} + +/// Gets the UTF-16 length of a UTF-8 string, for use in the wide_str macro. +pub const fn utf16_len(s: &str) -> usize { + let s = s.as_bytes(); + let mut i = 0; + let mut len = 0; + while i < s.len() { + // the length of a UTF-8 encoded code-point is given by the number of + // leading ones, except in the case of ASCII. + let utf8_len = match s[i].leading_ones() { + 0 => 1, + n => n as usize, + }; + i += utf8_len; + // Note that UTF-16 surrogates (U+D800 to U+DFFF) are not encodable as UTF-8, + // so (unlike with WTF-8) we don't have to worry about how they'll get re-encoded. + len += if utf8_len < 4 { 1 } else { 2 }; + } + len +} + +/// Const convert UTF-8 to UTF-16, for use in the wide_str macro. +/// +/// Note that this is designed for use in const contexts so is not optimized. +pub const fn to_utf16(s: &str) -> [u16; UTF16_LEN] { + let mut output = [0_u16; UTF16_LEN]; + let mut pos = 0; + let s = s.as_bytes(); + let mut i = 0; + while i < s.len() { + match s[i].leading_ones() { + // Decode UTF-8 based on its length. + // See https://en.wikipedia.org/wiki/UTF-8 + 0 => { + // ASCII is the same in both encodings + output[pos] = s[i] as u16; + i += 1; + pos += 1; + } + 2 => { + // Bits: 110xxxxx 10xxxxxx + output[pos] = ((s[i] as u16 & 0b11111) << 6) | (s[i + 1] as u16 & 0b111111); + i += 2; + pos += 1; + } + 3 => { + // Bits: 1110xxxx 10xxxxxx 10xxxxxx + output[pos] = ((s[i] as u16 & 0b1111) << 12) + | ((s[i + 1] as u16 & 0b111111) << 6) + | (s[i + 2] as u16 & 0b111111); + i += 3; + pos += 1; + } + 4 => { + // Bits: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx + let mut c = ((s[i] as u32 & 0b111) << 18) + | ((s[i + 1] as u32 & 0b111111) << 12) + | ((s[i + 2] as u32 & 0b111111) << 6) + | (s[i + 3] as u32 & 0b111111); + // re-encode as UTF-16 (see https://en.wikipedia.org/wiki/UTF-16) + // - Subtract 0x10000 from the code point + // - For the high surrogate, shift right by 10 then add 0xD800 + // - For the low surrogate, take the low 10 bits then add 0xDC00 + c -= 0x10000; + output[pos] = ((c >> 10) + 0xD800) as u16; + output[pos + 1] = ((c & 0b1111111111) + 0xDC00) as u16; + i += 4; + pos += 2; + } + // valid UTF-8 cannot have any other values + _ => unreachable!(), + } + } + output +} diff --git a/library/std/src/sys/windows/mod.rs b/library/std/src/sys/windows/mod.rs index bcc172b0fae..c7aa869f68a 100644 --- a/library/std/src/sys/windows/mod.rs +++ b/library/std/src/sys/windows/mod.rs @@ -12,6 +12,8 @@ #[macro_use] pub mod compat; +pub mod api; + pub mod alloc; pub mod args; pub mod c; diff --git a/library/std/src/sys/windows/os_str.rs b/library/std/src/sys/windows/os_str.rs index 16c4f55c687..f1e07ffbcca 100644 --- a/library/std/src/sys/windows/os_str.rs +++ b/library/std/src/sys/windows/os_str.rs @@ -151,6 +151,11 @@ pub fn into_rc(&self) -> Rc { } impl Slice { + #[inline] + pub fn as_encoded_bytes(&self) -> &[u8] { + &self.inner.as_inner() + } + #[inline] pub fn as_os_str_bytes(&self) -> &[u8] { self.inner.as_bytes() diff --git a/library/std/src/sys/windows/path.rs b/library/std/src/sys/windows/path.rs index c9c2d10e6c4..ed4f0f75f37 100644 --- a/library/std/src/sys/windows/path.rs +++ b/library/std/src/sys/windows/path.rs @@ -3,6 +3,7 @@ use crate::io; use crate::path::{Path, PathBuf, Prefix}; use crate::ptr; +use crate::sys::api::utf16; #[cfg(test)] mod tests; @@ -20,6 +21,10 @@ pub fn is_verbatim_sep(b: u8) -> bool { b == b'\\' } +pub fn is_verbatim(path: &[u16]) -> bool { + path.starts_with(utf16!(r"\\?\")) || path.starts_with(utf16!(r"\??\")) +} + /// Returns true if `path` looks like a lone filename. pub(crate) fn is_file_name(path: &OsStr) -> bool { !path.as_os_str_bytes().iter().copied().any(is_sep_byte) diff --git a/library/std/src/sys/windows/process.rs b/library/std/src/sys/windows/process.rs index e3493cbb850..96175e11c9b 100644 --- a/library/std/src/sys/windows/process.rs +++ b/library/std/src/sys/windows/process.rs @@ -259,10 +259,24 @@ pub fn spawn( }; let program = resolve_exe(&self.program, || env::var_os("PATH"), child_paths)?; // Case insensitive "ends_with" of UTF-16 encoded ".bat" or ".cmd" - let is_batch_file = matches!( - program.len().checked_sub(5).and_then(|i| program.get(i..)), - Some([46, 98 | 66, 97 | 65, 116 | 84, 0] | [46, 99 | 67, 109 | 77, 100 | 68, 0]) - ); + let has_bat_extension = |program: &[u16]| { + matches!( + // Case insensitive "ends_with" of UTF-16 encoded ".bat" or ".cmd" + program.len().checked_sub(4).and_then(|i| program.get(i..)), + Some([46, 98 | 66, 97 | 65, 116 | 84] | [46, 99 | 67, 109 | 77, 100 | 68]) + ) + }; + let is_batch_file = if path::is_verbatim(&program) { + has_bat_extension(&program[..program.len() - 1]) + } else { + super::fill_utf16_buf( + |buffer, size| unsafe { + // resolve the path so we can test the final file name. + c::GetFullPathNameW(program.as_ptr(), size, buffer, ptr::null_mut()) + }, + |program| has_bat_extension(program), + )? + }; let (program, mut cmd_str) = if is_batch_file { ( command_prompt()?, diff --git a/tests/ui/std/windows-bat-args.rs b/tests/ui/std/windows-bat-args.rs index d2d5fe76c84..7d171595401 100644 --- a/tests/ui/std/windows-bat-args.rs +++ b/tests/ui/std/windows-bat-args.rs @@ -32,7 +32,9 @@ fn parent() { let bat2 = String::from(bat.to_str().unwrap()); bat.set_file_name("windows-bat-args3.bat"); let bat3 = String::from(bat.to_str().unwrap()); - let bat = [bat1.as_str(), bat2.as_str(), bat3.as_str()]; + bat.set_file_name("windows-bat-args1.bat .. "); + let bat4 = String::from(bat.to_str().unwrap()); + let bat = [bat1.as_str(), bat2.as_str(), bat3.as_str(), bat4.as_str()]; check_args(&bat, &["a", "b"]).unwrap(); check_args(&bat, &["c is for cat", "d is for dog"]).unwrap(); diff --git a/tests/ui/std/windows-bat-args1.bat b/tests/ui/std/windows-bat-args1.bat index edd36bd5530..15d32263f95 100644 --- a/tests/ui/std/windows-bat-args1.bat +++ b/tests/ui/std/windows-bat-args1.bat @@ -1 +1 @@ -@a.exe %* +@a.exe %* \ No newline at end of file diff --git a/tests/ui/std/windows-bat-args2.bat b/tests/ui/std/windows-bat-args2.bat index 8d5a7dd8a9e..b5326eee1d7 100644 --- a/tests/ui/std/windows-bat-args2.bat +++ b/tests/ui/std/windows-bat-args2.bat @@ -1 +1 @@ -@a.exe %1 %2 %3 %4 %5 %6 %7 %8 %9 +@a.exe %1 %2 %3 %4 %5 %6 %7 %8 %9 \ No newline at end of file diff --git a/tests/ui/std/windows-bat-args3.bat b/tests/ui/std/windows-bat-args3.bat index 7fe360a6d36..1f29aceb12f 100644 --- a/tests/ui/std/windows-bat-args3.bat +++ b/tests/ui/std/windows-bat-args3.bat @@ -1 +1 @@ -@a.exe "%~1" "%~2" "%~3" "%~4" "%~5" "%~6" "%~7" "%~8" "%~9" +@a.exe "%~1" "%~2" "%~3" "%~4" "%~5" "%~6" "%~7" "%~8" "%~9" \ No newline at end of file -- Gitee