Skip to content

Commit 25ef9e3

Browse files
committed
Auto merge of #123681 - pietroalbini:pa-cve-2024-24576-stable, r=pietroalbini
[stable] Prepare Rust 1.77.2 See https://blog.rust-lang.org/2024/04/09/cve-2024-24576.html
2 parents 7cf61eb + e7b09f5 commit 25ef9e3

File tree

11 files changed

+331
-16
lines changed

11 files changed

+331
-16
lines changed

RELEASES.md

+7
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
Version 1.77.2 (2024-04-09)
2+
===========================
3+
4+
<a id="1.77.2"></a>
5+
6+
- [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)
7+
18
Version 1.77.1 (2024-03-28)
29
===========================
310

library/std/src/os/windows/process.rs

+54-2
Original file line numberDiff line numberDiff line change
@@ -196,8 +196,60 @@ pub trait CommandExt: Sealed {
196196

197197
/// Append literal text to the command line without any quoting or escaping.
198198
///
199-
/// This is useful for passing arguments to `cmd.exe /c`, which doesn't follow
200-
/// `CommandLineToArgvW` escaping rules.
199+
/// This is useful for passing arguments to applications which doesn't follow
200+
/// the standard C run-time escaping rules, such as `cmd.exe /c`.
201+
///
202+
/// # Bat files
203+
///
204+
/// Note the `cmd /c` command line has slightly different escaping rules then bat files
205+
/// themselves. If possible, it may be better to write complex arguments to a temporary
206+
/// .bat file, with appropriate escaping, and simply run that using:
207+
///
208+
/// ```no_run
209+
/// # use std::process::Command;
210+
/// # let temp_bat_file = "";
211+
/// # #[allow(unused)]
212+
/// let output = Command::new("cmd").args(["/c", &format!("\"{temp_bat_file}\"")]).output();
213+
/// ```
214+
///
215+
/// # Example
216+
///
217+
/// Run a bat script using both trusted and untrusted arguments.
218+
///
219+
/// ```no_run
220+
/// #[cfg(windows)]
221+
/// // `my_script_path` is a path to known bat file.
222+
/// // `user_name` is an untrusted name given by the user.
223+
/// fn run_script(
224+
/// my_script_path: &str,
225+
/// user_name: &str,
226+
/// ) -> Result<std::process::Output, std::io::Error> {
227+
/// use std::io::{Error, ErrorKind};
228+
/// use std::os::windows::process::CommandExt;
229+
/// use std::process::Command;
230+
///
231+
/// // Create the command line, making sure to quote the script path.
232+
/// // This assumes the fixed arguments have been tested to work with the script we're using.
233+
/// let mut cmd_args = format!(r#""{my_script_path}" "--features=[a,b,c]""#);
234+
///
235+
/// // Make sure the user name is safe. In particular we need to be
236+
/// // cautious of ascii symbols that cmd may interpret specially.
237+
/// // Here we only allow alphanumeric characters.
238+
/// if !user_name.chars().all(|c| c.is_alphanumeric()) {
239+
/// return Err(Error::new(ErrorKind::InvalidInput, "invalid user name"));
240+
/// }
241+
/// // now we've checked the user name, let's add that too.
242+
/// cmd_args.push(' ');
243+
/// cmd_args.push_str(&format!("--user {user_name}"));
244+
///
245+
/// // call cmd.exe and return the output
246+
/// Command::new("cmd.exe")
247+
/// .arg("/c")
248+
/// // surround the entire command in an extra pair of quotes, as required by cmd.exe.
249+
/// .raw_arg(&format!("\"{cmd_args}\""))
250+
/// .output()
251+
/// }
252+
/// ````
201253
#[stable(feature = "windows_process_extensions_raw_arg", since = "1.62.0")]
202254
fn raw_arg<S: AsRef<OsStr>>(&mut self, text_to_append_as_is: S) -> &mut process::Command;
203255

library/std/src/process.rs

+79
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,47 @@
8888
//! assert_eq!(b"test", output.stdout.as_slice());
8989
//! ```
9090
//!
91+
//! # Windows argument splitting
92+
//!
93+
//! On Unix systems arguments are passed to a new process as an array of strings
94+
//! but on Windows arguments are passed as a single commandline string and it's
95+
//! up to the child process to parse it into an array. Therefore the parent and
96+
//! child processes must agree on how the commandline string is encoded.
97+
//!
98+
//! Most programs use the standard C run-time `argv`, which in practice results
99+
//! in consistent argument handling. However some programs have their own way of
100+
//! parsing the commandline string. In these cases using [`arg`] or [`args`] may
101+
//! result in the child process seeing a different array of arguments then the
102+
//! parent process intended.
103+
//!
104+
//! Two ways of mitigating this are:
105+
//!
106+
//! * Validate untrusted input so that only a safe subset is allowed.
107+
//! * Use [`raw_arg`] to build a custom commandline. This bypasses the escaping
108+
//! rules used by [`arg`] so should be used with due caution.
109+
//!
110+
//! `cmd.exe` and `.bat` use non-standard argument parsing and are especially
111+
//! vulnerable to malicious input as they may be used to run arbitrary shell
112+
//! commands. Untrusted arguments should be restricted as much as possible.
113+
//! For examples on handling this see [`raw_arg`].
114+
//!
115+
//! ### Bat file special handling
116+
//!
117+
//! On Windows, `Command` uses the Windows API function [`CreateProcessW`] to
118+
//! spawn new processes. An undocumented feature of this function is that,
119+
//! when given a `.bat` file as the application to run, it will automatically
120+
//! convert that into running `cmd.exe /c` with the bat file as the next argument.
121+
//!
122+
//! For historical reasons Rust currently preserves this behaviour when using
123+
//! [`Command::new`], and escapes the arguments according to `cmd.exe` rules.
124+
//! Due to the complexity of `cmd.exe` argument handling, it might not be
125+
//! possible to safely escape some special chars, and using them will result
126+
//! in an error being returned at process spawn. The set of unescapeable
127+
//! special chars might change between releases.
128+
//!
129+
//! Also note that running `.bat` scripts in this way may be removed in the
130+
//! future and so should not be relied upon.
131+
//!
91132
//! [`spawn`]: Command::spawn
92133
//! [`output`]: Command::output
93134
//!
@@ -97,6 +138,12 @@
97138
//!
98139
//! [`Write`]: io::Write
99140
//! [`Read`]: io::Read
141+
//!
142+
//! [`arg`]: Command::arg
143+
//! [`args`]: Command::args
144+
//! [`raw_arg`]: crate::os::windows::process::CommandExt::raw_arg
145+
//!
146+
//! [`CreateProcessW`]: https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessw
100147
101148
#![stable(feature = "process", since = "1.0.0")]
102149
#![deny(unsafe_op_in_unsafe_fn)]
@@ -611,6 +658,22 @@ impl Command {
611658
/// escaped characters, word splitting, glob patterns, variable substitution, etc.
612659
/// have no effect.
613660
///
661+
/// <div class="warning">
662+
///
663+
/// On Windows use caution with untrusted inputs. Most applications use the
664+
/// standard convention for decoding arguments passed to them. These are safe to use with `arg`.
665+
/// However some applications, such as `cmd.exe` and `.bat` files, use a non-standard way of decoding arguments
666+
/// and are therefore vulnerable to malicious input.
667+
/// In the case of `cmd.exe` this is especially important because a malicious argument can potentially run arbitrary shell commands.
668+
///
669+
/// See [Windows argument splitting][windows-args] for more details
670+
/// or [`raw_arg`] for manually implementing non-standard argument encoding.
671+
///
672+
/// [`raw_arg`]: crate::os::windows::process::CommandExt::raw_arg
673+
/// [windows-args]: crate::process#windows-argument-splitting
674+
///
675+
/// </div>
676+
///
614677
/// # Examples
615678
///
616679
/// Basic usage:
@@ -641,6 +704,22 @@ impl Command {
641704
/// escaped characters, word splitting, glob patterns, variable substitution, etc.
642705
/// have no effect.
643706
///
707+
/// <div class="warning">
708+
///
709+
/// On Windows use caution with untrusted inputs. Most applications use the
710+
/// standard convention for decoding arguments passed to them. These are safe to use with `args`.
711+
/// However some applications, such as `cmd.exe` and `.bat` files, use a non-standard way of decoding arguments
712+
/// and are therefore vulnerable to malicious input.
713+
/// In the case of `cmd.exe` this is especially important because a malicious argument can potentially run arbitrary shell commands.
714+
///
715+
/// See [Windows argument splitting][windows-args] for more details
716+
/// or [`raw_arg`] for manually implementing non-standard argument encoding.
717+
///
718+
/// [`raw_arg`]: crate::os::windows::process::CommandExt::raw_arg
719+
/// [windows-args]: crate::process#windows-argument-splitting
720+
///
721+
/// </div>
722+
///
644723
/// # Examples
645724
///
646725
/// Basic usage:

library/std/src/sys/pal/windows/args.rs

+93-12
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
mod tests;
88

99
use super::os::current_exe;
10-
use crate::ffi::OsString;
10+
use crate::ffi::{OsStr, OsString};
1111
use crate::fmt;
1212
use crate::io;
1313
use crate::num::NonZeroU16;
@@ -17,6 +17,7 @@ use crate::sys::path::get_long_path;
1717
use crate::sys::process::ensure_no_nuls;
1818
use crate::sys::{c, to_u16s};
1919
use crate::sys_common::wstr::WStrUnits;
20+
use crate::sys_common::AsInner;
2021
use crate::vec;
2122

2223
use crate::iter;
@@ -262,16 +263,92 @@ pub(crate) fn append_arg(cmd: &mut Vec<u16>, arg: &Arg, force_quotes: bool) -> i
262263
Ok(())
263264
}
264265

266+
fn append_bat_arg(cmd: &mut Vec<u16>, arg: &OsStr, mut quote: bool) -> io::Result<()> {
267+
ensure_no_nuls(arg)?;
268+
// If an argument has 0 characters then we need to quote it to ensure
269+
// that it actually gets passed through on the command line or otherwise
270+
// it will be dropped entirely when parsed on the other end.
271+
//
272+
// We also need to quote the argument if it ends with `\` to guard against
273+
// bat usage such as `"%~2"` (i.e. force quote arguments) otherwise a
274+
// trailing slash will escape the closing quote.
275+
if arg.is_empty() || arg.as_encoded_bytes().last() == Some(&b'\\') {
276+
quote = true;
277+
}
278+
for cp in arg.as_inner().inner.code_points() {
279+
if let Some(cp) = cp.to_char() {
280+
// Rather than trying to find every ascii symbol that must be quoted,
281+
// we assume that all ascii symbols must be quoted unless they're known to be good.
282+
// We also quote Unicode control blocks for good measure.
283+
// Note an unquoted `\` is fine so long as the argument isn't otherwise quoted.
284+
static UNQUOTED: &str = r"#$*+-./:?@\_";
285+
let ascii_needs_quotes =
286+
cp.is_ascii() && !(cp.is_ascii_alphanumeric() || UNQUOTED.contains(cp));
287+
if ascii_needs_quotes || cp.is_control() {
288+
quote = true;
289+
}
290+
}
291+
}
292+
293+
if quote {
294+
cmd.push('"' as u16);
295+
}
296+
// Loop through the string, escaping `\` only if followed by `"`.
297+
// And escaping `"` by doubling them.
298+
let mut backslashes: usize = 0;
299+
for x in arg.encode_wide() {
300+
if x == '\\' as u16 {
301+
backslashes += 1;
302+
} else {
303+
if x == '"' as u16 {
304+
// Add n backslashes to total 2n before internal `"`.
305+
cmd.extend((0..backslashes).map(|_| '\\' as u16));
306+
// Appending an additional double-quote acts as an escape.
307+
cmd.push(b'"' as u16)
308+
} else if x == '%' as u16 || x == '\r' as u16 {
309+
// yt-dlp hack: replaces `%` with `%%cd:~,%` to stop %VAR% being expanded as an environment variable.
310+
//
311+
// # Explanation
312+
//
313+
// cmd supports extracting a substring from a variable using the following syntax:
314+
// %variable:~start_index,end_index%
315+
//
316+
// In the above command `cd` is used as the variable and the start_index and end_index are left blank.
317+
// `cd` is a built-in variable that dynamically expands to the current directory so it's always available.
318+
// Explicitly omitting both the start and end index creates a zero-length substring.
319+
//
320+
// Therefore it all resolves to nothing. However, by doing this no-op we distract cmd.exe
321+
// from potentially expanding %variables% in the argument.
322+
cmd.extend_from_slice(&[
323+
'%' as u16, '%' as u16, 'c' as u16, 'd' as u16, ':' as u16, '~' as u16,
324+
',' as u16,
325+
]);
326+
}
327+
backslashes = 0;
328+
}
329+
cmd.push(x);
330+
}
331+
if quote {
332+
// Add n backslashes to total 2n before ending `"`.
333+
cmd.extend((0..backslashes).map(|_| '\\' as u16));
334+
cmd.push('"' as u16);
335+
}
336+
Ok(())
337+
}
338+
265339
pub(crate) fn make_bat_command_line(
266340
script: &[u16],
267341
args: &[Arg],
268342
force_quotes: bool,
269343
) -> io::Result<Vec<u16>> {
344+
const INVALID_ARGUMENT_ERROR: io::Error =
345+
io::const_io_error!(io::ErrorKind::InvalidInput, r#"batch file arguments are invalid"#);
270346
// Set the start of the command line to `cmd.exe /c "`
271347
// It is necessary to surround the command in an extra pair of quotes,
272348
// hence the trailing quote here. It will be closed after all arguments
273349
// have been added.
274-
let mut cmd: Vec<u16> = "cmd.exe /d /c \"".encode_utf16().collect();
350+
// Using /e:ON enables "command extensions" which is essential for the `%` hack to work.
351+
let mut cmd: Vec<u16> = "cmd.exe /e:ON /v:OFF /d /c \"".encode_utf16().collect();
275352

276353
// Push the script name surrounded by its quote pair.
277354
cmd.push(b'"' as u16);
@@ -291,18 +368,22 @@ pub(crate) fn make_bat_command_line(
291368
// reconstructed by the batch script by default.
292369
for arg in args {
293370
cmd.push(' ' as u16);
294-
// Make sure to always quote special command prompt characters, including:
295-
// * Characters `cmd /?` says require quotes.
296-
// * `%` for environment variables, as in `%TMP%`.
297-
// * `|<>` pipe/redirect characters.
298-
const SPECIAL: &[u8] = b"\t &()[]{}^=;!'+,`~%|<>";
299-
let force_quotes = match arg {
300-
Arg::Regular(arg) if !force_quotes => {
301-
arg.as_encoded_bytes().iter().any(|c| SPECIAL.contains(c))
371+
match arg {
372+
Arg::Regular(arg_os) => {
373+
let arg_bytes = arg_os.as_encoded_bytes();
374+
// Disallow \r and \n as they may truncate the arguments.
375+
const DISALLOWED: &[u8] = b"\r\n";
376+
if arg_bytes.iter().any(|c| DISALLOWED.contains(c)) {
377+
return Err(INVALID_ARGUMENT_ERROR);
378+
}
379+
append_bat_arg(&mut cmd, arg_os, force_quotes)?;
380+
}
381+
_ => {
382+
// Raw arguments are passed on as-is.
383+
// It's the user's responsibility to properly handle arguments in this case.
384+
append_arg(&mut cmd, arg, force_quotes)?;
302385
}
303-
_ => force_quotes,
304386
};
305-
append_arg(&mut cmd, arg, force_quotes)?;
306387
}
307388

308389
// Close the quote we left opened earlier.

src/ci/scripts/install-ninja.sh

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ source "$(cd "$(dirname "$0")" && pwd)/../shared.sh"
88

99
if isWindows; then
1010
mkdir ninja
11-
curl -o ninja.zip "${MIRRORS_BASE}/2017-03-15-ninja-win.zip"
11+
curl -o ninja.zip "${MIRRORS_BASE}/2024-03-28-v1.11.1-ninja-win.zip"
1212
7z x -oninja ninja.zip
1313
rm ninja.zip
1414
ciCommandSetEnv "RUST_CONFIGURE_ARGS" "${RUST_CONFIGURE_ARGS} --enable-ninja"

src/tools/tidy/src/ui_tests.rs

+3
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ const EXTENSION_EXCEPTION_PATHS: &[&str] = &[
4343
"tests/ui/shell-argfiles/shell-argfiles-badquotes.args", // passing args via a file
4444
"tests/ui/shell-argfiles/shell-argfiles-via-argfile-shell.args", // passing args via a file
4545
"tests/ui/shell-argfiles/shell-argfiles-via-argfile.args", // passing args via a file
46+
"tests/ui/std/windows-bat-args1.bat", // tests escaping arguments through batch files
47+
"tests/ui/std/windows-bat-args2.bat", // tests escaping arguments through batch files
48+
"tests/ui/std/windows-bat-args3.bat", // tests escaping arguments through batch files
4649
];
4750

4851
fn check_entries(tests_path: &Path, bad: &mut bool) {

src/version

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1.77.1
1+
1.77.2

0 commit comments

Comments
 (0)