go: os/exec: Cannot execute command with space in the name on Windows, when there are parameters
Go 1.7.1 on windows-amd64, Windows 10 latest.
Consider a test project:
src/github.com/calmh/wincmdtest/
main.go
folder name/
test.bat
main.go contents:
package main
import (
"fmt"
"os/exec"
"strings"
)
func main() {
execCmd("./folder name/test.bat")
execCmd("./folder name/test.bat", "one param")
}
func execCmd(path string, args ...string) {
fmt.Printf("Running: %q %q\n", path, strings.Join(args, " "))
cmd := exec.Command(path, args...)
bs, err := cmd.CombinedOutput()
fmt.Printf("Output: %s", bs)
fmt.Printf("Error: %v\n\n", err)
}
folder name/test.bat contents:
@echo off
echo Success
Expected output is two runs with “Success” in them.
Actual:
C:\Users\jb\Go\src\github.com\calmh\wincmdtest>go run main.go
Running: "./folder name/test.bat" ""
Output: Success
Error: <nil>
Running: "./folder name/test.bat" "one param"
Output: '.' is not recognized as an internal or external command,
operable program or batch file.
Error: exit status 1
It appears that having params on a command, where the command contains a space, breaks the parsing of it. I haven’t been able to work around this by experimenting with various ways of quoting the command, using backslashes or slashes, etc.
About this issue
- Original URL
- State: open
- Created 8 years ago
- Reactions: 2
- Comments: 35 (13 by maintainers)
Commits related to this issue
- syscall/exec: pass nil for CreateProcess() lpApplicationName on windows This addresses #17149 by calling CreateProcess() in the same way that that dotnet does for its equivalent syscall abstraction s... — committed to rafd123/go by rafd123 a year ago
- syscall/exec: pass nil for CreateProcess() lpApplicationName on windows This addresses #17149 by calling CreateProcess() and CreateProcessAsUser()in the same way that that dotnet does for its equival... — committed to rafd123/go by rafd123 a year ago
- syscall/exec: pass nil for CreateProcess() lpApplicationName on windows This addresses #17149 by calling CreateProcess() and CreateProcessAsUser()in the same way that that dotnet does for its equival... — committed to rafd123/go by rafd123 a year ago
- windows: convert TestCommandLineRecomposition to a fuzz test and fix discrepancies Notably, this fixes the escaping of the first argument when it contains quoted spaces, and fixes a panic in Decompos... — committed to golang/sys by bcmills 9 months ago
Digging into this a bit further, I noticed that the dotnet core equivalent of
exec.Commanddoes not suffer from this problem. Also note that dotnet is cross-platform these days and has a similar abstraction over OS-specific APIs for creating processes.Specifically, this works fine in dotnet:
Debugging how both golang and dotnet ultimately call Windows’
CreateProcessAPI, I noticed one meaningful difference: dotnet passesnullfor the optionallpApplicationNameparameter ofCreateProcesswhile golang does not.This is how dotnet calls
CreateProcess(note thenullforlpApplicationNameargument).This is how golang calls
CreateProcess(note the use ofargv0p…which in practice maps to thenameargument ofexec.Command…also note that in practiceargvpwill also contain thenameargument ofexec.Command).Empirically, I can also confirm that if I force golang to pass
nilforlpApplicationNamehere instead ofargv0p, executingexec.Command(`C:\Program Files\echo.bat`, "hello world")works without resorting to using theSysProcAttrescape hatch.All that said:
lpApplicationNameforCreateProcessis cited as a security best-practice as a fail-safe should the caller fail to add quotes around the path of the executable inlpCommandLine. FWIW, dotnet ensures that the executable is quoted for this very reason, and it turns out golang does, too, viamakeCmdLine’s use ofappendEscapeArg.CreateProcessas to why it works when NOT specifyinglpApplicationName, so at this point it’s all circumstantial. All I was able to find is someone else pointing out this suble behavioral difference way back in 2001.Why not fixed?
I started following this down through
syscall.StartProcess, but not really being a Windows programmer I ran into unacceptable levels of nope almost immediately so probably won’t try to fix this myself, sorry. 😦For anyone needing a more concrete example of how to do the manual path construction, it works something like this:
Note that you’re on the hook to have everything on the CmdLine escaped/quoted/etc properly, which can be a real chore.
I guess this should be fixed by small change.
If the first argument contain spaces, and it have arguments (no matter if the argument not contains spaces), whole of string should be quoted.
@bcmills @golang/windows I’d like to submit a PR that modifies
syscall/execto address this issue, and I’d like to get buyoff on the approach before doing so.TL:DR modify
syscall/execon Windows to passnilfor thelpApplicationNamearg toCreateProcess()andCreateProcessAsUser()to mimic what dotnet core does since dotnet doesn’t exhibit this issue due to this difference. For full background, see this comment: https://github.com/golang/go/issues/17149#issuecomment-1426878988Concretely, this is the code change I’d like to propose: https://github.com/rafd123/go/commit/d24b468bb4318ef6085569d7a2e9e3fd8f93a1b3#diff-ec673c10a75fe2d2faa9c788e03823294b263c68cc3de50f4a0865a53e28f3a3
My biggest concern with this change is that it assumes that
syscall/exec.StartProcess’sargv0is represented withinargv[0]. This assumption could be violated if, for example, folks don’t useos/exec.Commandand instead manually constructos/exec.Cmdwithout includingos/exec.Cmd.Pathinos/exec.Cmd.Args. I already ran into a violation of this assumption in one of the tests here: https://github.com/rafd123/go/commit/d24b468bb4318ef6085569d7a2e9e3fd8f93a1b3#diff-f0ee8d33c527f62f2e48195ea748d2d7295c26910144012ee6e108fbf08f21a6That said, there seems to be a precendent for this assumption in
syscall/exec_unix.gohere: https://github.com/golang/go/blob/e8c8b79f000515e086012df632f01fc0ec21076b/src/syscall/exec_unix.go#L169-L171What do you think? Is this a reasonable approach? If so, I’ll submit a PR.
@rafd123 I think you are right! So, the proper code becomes:
At this risk of sounding pedantic 😅, should the executable path be included in the
CmdLine, too? Not sure if it makes a difference in practice, but reasons I ask:exec.Commandwithout theSysProcAttrescape hatch, it automatically prepends the executable to the args (see https://github.com/golang/go/blob/master/src/os/exec/exec.go#L379). Those args ultimate are used to generate the cmdLine (see https://github.com/golang/go/blob/master/src/syscall/exec_windows.go#L302)I’m also wondering if the arguments should be escaped when we create the cmdline ourselves as a more general solution. We might get close by using
syscall.EscapeArgon each argument, but I thinkcmd.exehas its own set of crazy escaping rules.Wow. What a bug. XD
I agree with https://github.com/golang/go/issues/17149#issuecomment-257518448 that the answer here should be to rewrite the path for argv[0] from using slash to backslash. We should try to fix this early in the Go 1.12 cycle.