Currently the windows implementation of Command will automatically attempt to escape arguments before concatenating them into a single command line string to be passed to CreateProcess. However, the escaping method used doesn't work for many windows command-line tools, including cmd
, msiexec
and others.
As an example, it is impossible to specify arguments like this:
ARG="some parameter with spaces"
because Command will try to quote the entire thing, whereas msiexec expects only the value to be quoted.
What's required is a way to pass arguments through unchanged: I suggest an arg_raw
or arg_unescaped
method be added to Command. This function can either be a windows-specific extension, or a no-op on other platforms. The advantage of the latter is that it avoids the need to cfg
out code that relies on it at compile time.
Perhaps the implementation can be changed to not be that terrible? I know absolutely nothing about the win32 API, but I do know that Python's subprocess
module uses a function called CreateProcess
and AFAIK doesn't have this problem.
Edit: Nevermind, a look at the documentation reveals that this function takes a single string as command to be executed.
At the very least it should strive to perform the exact, minimal inverse of CommandLineToArgvW (a very nontrivial task unfortunately, see: http://blogs.msdn.com/b/twistylittlepassagesallalike/archive/2011/04/23/everyone-quotes-arguments-the-wrong-way.aspx which describes the problems well enough, but the final solution given is actually still wrong!)
This would be enough to fix the problems for me with msiexec and possibly cmd. However, there's no requirement that processes on windows use CommandLineToArgvW
at all, and many in fact do not, so regardless a way of passing through arguments unmolested is required.
For reference, the string ARG="some parameter with spaces"
does not need escaping: CommandLineToArgvW will return it as a single argument, which at the very least shows that rusts "inverse" function is currently incorrect.
edit: Actually, no it seems it does still need escaping, it may just be that msiexec does not use CommandLineToArgvW.
This could probably be solved by adding a CommandExt
method that specifies the entire command line as a single string overriding how arguments would otherwise be specified. Any code that needs behavior other than the default can then use that method to have full control over the command line.
This popped up on Stack Overflow:
let comm = r#""C:\Program Files\Google\Chrome\Application\chrome.exe" https://stackoverflow.com/"#;
let mut cmd = Command::new("cmd");
cmd.arg("/c");
cmd.arg(comm);
As I understand it, it is expected to execute
"cmd" "/c" ""C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe" https://stackoverflow.com/"
But instead executes
"cmd" "/c" "\"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe\" https://stackoverflow.com/"
Note that the inner "
have been escaped.
Met this issue too. Do we have any workaround now?
The workaround is calling CreateProcessW
yourself. The solution is providing a Windows specific CommandExt
that allows specifying a custom command string.
@retep998 I changed the argument in the command line to avoid the quotes.
I'm now running into this issue while trying to fix env
for windows in the coretools repo.
The summary at this point seems to be, at least for Windows, with its some-what crazy command-line quoting, that there are simply some command lines which are valid and used which cannot be generated by the current std::Command
(eg, powershell -command "Get-WmiObject -class Win32_Product"
; see https://users.rust-lang.org/t/std-process-is-escaping-a-raw-string-literal-when-i-dont-want-it-to/19441 and https://stackoverflow.com/questions/44757893/cmd-c-doesnt-work-in-rust-when-command-includes-spaces).
IMO, there needs to be some way of using std::Command
to generate completely arbitrary command lines. Specifically, argument escaping needs to be more controllable by the user. Pushing users to drop back to calling CreateProcessW
, although possibly workable, doesn't seem to be the right path to encourage portability in rust
.
An addition to the API of Any thoughts? And what should be done to push this issue forward? cc: @rivy std::Command
(eg, arg_raw()
) such as suggested by @kornelski on the rust-internals discussion board (see
Same problem here, we can't use std::process::Command
to run something like:
app.exe --folder="C:\Program Files\"
Also ran into this, trying to open Explorer with a selected file, like this:
let result = std::process::Command::new("explorer.exe").arg(format!("/select,\"{:?}\"", path)).spawn();
This does not work, due to the quotes getting escaped when they shouldn't be. Explorer.exe expects to be called like this:
explorer.exe /select,"C:\my\qouted\path"
which becomes
explorer.exe /select,""C:\my\qouted\path""
which it doesn't accept.
Would a pull request adding arg_raw() be accepted at this point? This really needs a resolution, it's not acceptable to have to drop down to CreateProcess.
At the very least it should strive to perform the exact, minimal inverse of CommandLineToArgvW (a very nontrivial task unfortunately, see: http://blogs.msdn.com/b/twistylittlepassagesallalike/archive/2011/04/23/everyone-quotes-arguments-the-wrong-way.aspx which describes the problems well enough,
The new link is at https://docs.microsoft.com/en-gb/archive/blogs/twistylittlepassagesallalike/everyone-quotes-command-line-arguments-the-wrong-way.
but the final solution given is actually still wrong!)
How exactly is it wrong? This appears to be a pretty good inverse of the function as implemented in .NET CoreFX. There are two caveats though:
&
and |
because they are not tested for quoting. Even if they quote it, a quote would easily break it because cmd does not see \"
as a way to un-mark an end-quotation part. The workaround is to use the undocumented, but ubiquitously accepted ""
escape_setargv
is linked in, but exceptions exist in certain versions of the MS runtime. Just do your best, pretend this is wine __getmainargs
, and quote.(Yeah, the "better method" in this article is pretty verbose if you consider the workaround from 1. I generally just do a JS one-liner of return `"${arg.replace(/(\\*)($|")/g, '$1$1$2').replace(/"/g, '""')}"`;
for it, so that it works in both.)
Anyway, this blog code is what process::sys(windows)::Command does. It seems fine.
There is another reason for a raw command-line thing that many people might be more familiar with: Cygwin. Cygwin and MSYS2 ship an incompatible command-line parser in their dcrt0.cc, and I don't think I need to explain how common these things are these days. If you want to interface with, say, Git for Windows correctly, you gotta escape the strings differently.
I'd like something like .arg_raw()
that just injects an argument without any escaping.
Should it have a more scary name? .arg_arbitrary_code_execution_vulnerability_if_you_dont_properly_escape_this_yourself()
?
Is it acceptable for this method to surround the unescaped argument with spaces? Is there any command that cares if there is a space between exe and the args? e.g. foo.exe/arg
is different from foo.exe /arg
?
Should there be an escaping helper function to help build raw arg that includes both quoted and unquoted parts? Or maybe raw args should not add spaces to be able to prefix other quoted args?.raw_no_spaces("foo,").arg("bar") == foo,"bar"
arg_arbitrary_code_execution_vulnerability_if_you_dont_properly_escape_this_yourself
This should not be named as such for our CreateProcess-based invocation, since it does not really make it less safe than the default escape. We are not escaping for cmd, which actually has metacharacters for pipes and semicolons.
Is there any command that cares if there is a space between exe and the args?
No. There is a DOS-inherited cmd-ism here, but it is exclusively for internal commands. See https://stackoverflow.com/questions/4094699/how-does-the-windows-command-interpreter-cmd-exe-parse-scripts.
The CreateProcess documentation only mentions using spaces to tokenize when the file argument is null.
helper
Maybe, with some big warnings about non-generality preferably. The argv[0] will almost always need quoting.
Doing this does raise the question of how many kinds of helpers are supposed to be in stdlib though.
Specifying an arbitrary command line should be completely safe. The only way you could cause unsound behavior is if the spawned process has a bug in its command line parser, which isn't Rust's problem.
Just add a windows specific method to let users specify the full command line themselves, and that's all libstd
needs to do to enable spawning those quirky processes with Command
.
Most helpful comment
I'm now running into this issue while trying to fix
env
for windows in the coretools repo.The summary at this point seems to be, at least for Windows, with its some-what crazy command-line quoting, that there are simply some command lines which are valid and used which cannot be generated by the current
std::Command
(eg,powershell -command "Get-WmiObject -class Win32_Product"
; see https://users.rust-lang.org/t/std-process-is-escaping-a-raw-string-literal-when-i-dont-want-it-to/19441 and https://stackoverflow.com/questions/44757893/cmd-c-doesnt-work-in-rust-when-command-includes-spaces).IMO, there needs to be some way of using
std::Command
to generate completely arbitrary command lines. Specifically, argument escaping needs to be more controllable by the user. Pushing users to drop back to callingCreateProcessW
, although possibly workable, doesn't seem to be the right path to encourage portability inrust
.An addition to the API of
std::Command
(eg,arg_raw()
) such as suggested by @kornelski on the rust-internals discussion board (seeAny thoughts?
And what should be done to push this issue forward?
cc: @rivy