Navigation Menu

Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

os/exec: Cannot execute command with space in the name on Windows, when there are parameters #17149

Open
calmh opened this issue Sep 18, 2016 · 35 comments
Labels
early-in-cycle A change that should be done early in the 3 month dev cycle. NeedsFix The path to resolution is known, but the work has not been done. OS-Windows
Milestone

Comments

@calmh
Copy link
Contributor

calmh commented Sep 18, 2016

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.

@calmh
Copy link
Contributor Author

calmh commented Sep 18, 2016

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. :(

@ianlancetaylor ianlancetaylor added this to the Go1.8 milestone Sep 19, 2016
@alexbrainman
Copy link
Member

It appears that having params on a command, where the command contains a space, breaks the parsing of it.

Go encodes child process parameters in a way that is understood by most programs. Go uses rules similar to what CommandLineToArgvW implements.

Unfortunately, your child process is cmd.exe (cmd.exe is called to execute the batch file you've requested). And cmd.exe parses its input parameters differently.

You can read this very long post http://daviddeley.com/autohotkey/parameters/parameters.htm#WIN about it all.

I haven't been able to work around this by experimenting with various ways of quoting the command, using backslashes or slashes, etc.

You should stop using exec.Command to build your child process command line, and build it yourself (as per rules described in the doco I mentioned). Then you can pass raw command line to your child process by setting exec.Cmd.SysProcAttr to syscall.SysProcAttr with CmdLine set appropriately.

Maybe we could fix this problem by changing Go encoding algorithm to use cmd.exe rules every time we execute batch file.

There is more of the same discussion on issue #15566.

Alex

@quentinmit quentinmit added the NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one. label Oct 10, 2016
@mattn
Copy link
Member

mattn commented Nov 1, 2016

I guess this should be fixed by small change.

diff --git a/src/syscall/exec_windows.go b/src/syscall/exec_windows.go
index cafce1e..a0e1f56 100644
--- a/src/syscall/exec_windows.go
+++ b/src/syscall/exec_windows.go
@@ -91,6 +91,9 @@ func makeCmdLine(args []string) string {
        }
        s += EscapeArg(v)
    }
+   if s != "" && s[0] == '"' && len(args) > 1 {
+       s = `"` + s + `"`
+   }
    return s
 }

If the first argument contain spaces, and it have arguments (no matter if the argument not contains spaces), whole of string should be quoted.

c:\dev\go-sandbox\space>go run main.go
Running: ".\\folder name\\test.bat" ""
Output: Success
Error: <nil>

Running: ".\\folder name\\test.bat" "one param"
Output: Success
Error: <nil>

@gopherbot
Copy link

CL https://golang.org/cl/32490 mentions this issue.

@rsc
Copy link
Contributor

rsc commented Nov 3, 2016

Postponing decisions about this to Go 1.9.

@rsc rsc modified the milestones: Go1.9Early, Go1.8 Nov 3, 2016
@bradfitz bradfitz modified the milestones: Go1.10Early, Go1.9Early May 3, 2017
@bradfitz bradfitz added NeedsDecision Feedback is required from experts, contributors, and/or the community before a change can be made. and removed NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one. labels May 3, 2017
@bradfitz bradfitz added early-in-cycle A change that should be done early in the 3 month dev cycle. and removed early-in-cycle A change that should be done early in the 3 month dev cycle. labels Jun 14, 2017
@bradfitz bradfitz modified the milestones: Go1.10Early, Go1.10 Jun 14, 2017
@basepack
Copy link

+1 This is still an issue here, also see: docker/machine#3152

@alexbrainman
Copy link
Member

@cannect docker/machine#3152 title mentions "powershell", but this issue is about running batch files. How do you know that if we make running batch file work, that it will fix docker/machine#3152 ?
Is there a simple way to reproduce docker/machine#3152 ?

Alex

@basepack
Copy link

As you can see here: docker/machine#3152 (comment) there is some confidence we think it is related with this issue.

@alexbrainman
Copy link
Member

@cannect do you know of a simple way to reproduce docker/machine#3152 ?
How did you personally discovered docker/machine#3152 ? Did you encountered that problem yourself?

Alex

@basepack
Copy link

basepack commented Oct 16, 2017

Hi @alexbrainman ,

I had exact the same issues as described in the first post of docker/machine#3152.
I do not have the knowledge to reproduce that issue in Go. But I can tell you my steps:

  • WIndows 10 Pro
  • Docker for Windows
  • Started two docker machines in Windows 10 bash, listed as active in docker-machine ls
  • Looked those docker machines up with: docker-machine ls in Powershell, machines not active. (but they were active of course but not listed as such in Powershell)

Unfortunately I have no experience whatsoever with Go.

@alexbrainman
Copy link
Member

@cannect thank you very much for these steps. But I have never used Docker for Windows. How do I install it and use it?

Docker for Windows

How do I install it? What are the steps?

Looked those docker machines up with: docker-machine ls in Powershell, machines not active. (but they were active of course but not listed as such in Powershell)

What are the commands I should run to reproduce this? What did you do? What did you expect to see? What did you see instead?

Thank you

Alex

@bradfitz bradfitz modified the milestones: Go1.10, Go1.11 Nov 29, 2017
@siradam7th
Copy link

It took me a while to figure out what was wrong, I had to use Process Monitor in order to see what Go was passing as arguments to the process, any news on a fix?

@alexbrainman
Copy link
Member

any news on a fix?

I am not working on this issue, if that is what you are asking.

Alex

@ItsMattL
Copy link

For anyone needing a more concrete example of how to do the manual path construction, it works something like this:

cmd := exec.Command(os.Getenv("SystemRoot")+`\System32\cmd.exe`) 
args :=  []string{`/c`, `""C:\Program Files\echo.bat"`, `"hello friend""`}
cmd.SysProcAttr = &syscall.SysProcAttr{CmdLine: " " + strings.Join(args, " ")}
out, _ := cmd.CombinedOutput()
fmt.Printf("Output: %s", out)

Note that you're on the hook to have everything on the CmdLine escaped/quoted/etc properly, which can be a real chore.

@bertysentry
Copy link

Here's an updated version of @ItsMattL's workaround:

// command to execute, may contain quotes, backslashes and spaces
var commandLine = `"C:\Program Files\echo.bat" "hello friend"`

var comSpec = os.Getenv("COMSPEC")
if comSpec == "" {
	comSpec = os.Getenv("SystemRoot") + "\\System32\\cmd.exe"
}
childProcess = exec.Command(comSpec)
childProcess.SysProcAttr = &syscall.SysProcAttr{CmdLine: "/C \"" + commandLine + "\""}

// Then execute and read the output
out, _ := childProcess.CombinedOutput()
fmt.Printf("Output: %s", out)

@vulcan-lin
Copy link

vulcan-lin commented Aug 24, 2022

For Linux system:
input = strings.ReplaceAll(input, " ", "\ ")
args := []string{
"ffmpeg",
fmt.Sprintf("-i %s", input),
fmt.Sprintf("-vf %s", "scale=-1:360"),
fmt.Sprintf("-c:v %s", "libx264"),
fmt.Sprintf("-crf %s", "18"),
fmt.Sprintf("-c:a %s", "copy"),
"b.mp4",
"-y",
}
myCmd := strings.Join(args, " ")
cmd := exec.Command("bash","-c", myCmd)

@rafd123
Copy link

rafd123 commented Feb 3, 2023

Here's an updated version of @ItsMattL's workaround:

// command to execute, may contain quotes, backslashes and spaces
var commandLine = `"C:\Program Files\echo.bat" "hello friend"`

var comSpec = os.Getenv("COMSPEC")
if comSpec == "" {
	comSpec = os.Getenv("SystemRoot") + "\\System32\\cmd.exe"
}
childProcess = exec.Command(comSpec)
childProcess.SysProcAttr = &syscall.SysProcAttr{CmdLine: "/C \"" + commandLine + "\""}

// Then execute and read the output
out, _ := childProcess.CombinedOutput()
fmt.Printf("Output: %s", out)

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:

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.EscapeArg on each argument, but I think cmd.exe has its own set of crazy escaping rules.

@bertysentry
Copy link

@rafd123 I think you are right! So, the proper code becomes:

// command to execute, may contain quotes, backslashes and spaces
var commandLine = `"C:\Program Files\echo.bat" "hello friend"`

var comSpec = os.Getenv("COMSPEC")
if comSpec == "" {
	comSpec = os.Getenv("SystemRoot") + "\\System32\\cmd.exe"
}
childProcess = exec.Command(comSpec)
childProcess.SysProcAttr = &syscall.SysProcAttr{CmdLine: comSpec + " /C \"" + commandLine + "\""}

// Then execute and read the output
out, _ := childProcess.CombinedOutput()
fmt.Printf("Output: %s", out)

@rafd123
Copy link

rafd123 commented Feb 3, 2023

What do you think about something like this as a general solution?

Caveats:

  • syscall.EscapeArg probably needs to be replaced with something that is aware of cmd.exe's escaping rules in order to be airtight; see this.
  • I can imagine adding handling for the problems inherent with running msiexec, too.

exec.go

package exec

import (
	"os/exec"
)

// MakeCmd returns the Cmd struct to execute the named program with
// the given arguments.
//
// It handles the case where the program is a Windows batch file
// and the implication it has on argument quoting.
func MakeCmd(name string, args ...string) (*exec.Cmd, error) {
	return makeCmd(name, args...)
}

exec_windows.go

package exec

import (
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"syscall"
)

func makeCmd(name string, args ...string) (*exec.Cmd, error) {
	if len(args) == 0 {
		return exec.Command(name), nil
	}

	name, err := exec.LookPath(name)
	if err != nil {
		return nil, err
	}

	if !isBatchFile(name) {
		return exec.Command(name, args...), nil
	}

	argsEscaped := make([]string, len(args)+1)
	argsEscaped[0] = syscall.EscapeArg(name)
	for i, a := range args {
		argsEscaped[i+1] = syscall.EscapeArg(a)
	}

	shell := os.Getenv("COMSPEC")
	if shell == "" {
		shell = "cmd.exe" // Will be expanded by exec.LookPath in exec.Command
	}

	cmd := exec.Command(shell)
	cmd.Args = nil
	cmd.SysProcAttr = &syscall.SysProcAttr{
		CmdLine: fmt.Sprintf(`%s /c "%s"`, syscall.EscapeArg(cmd.Path), strings.Join(argsEscaped, " ")),
	}

	return cmd, nil
}

func isBatchFile(path string) bool {
	ext := filepath.Ext(path)
	return strings.EqualFold(ext, ".bat") || strings.EqualFold(ext, ".cmd")
}

exec_other.go

//go:build !windows

package exec

func makeCmd(name string, args ...string) (*exec.Cmd, error) {
	return exec.Command(name, args...), nil
}

@bertysentry
Copy link

@rafd123 Looks like a good improvement. However I don't see why we would need to handle batch files differently. It's important to pass them through CMD.EXE too, so they need the same escaping.

And yeah, you would need to follow the excellent article that you linked to, and escape meta characters with ^. Otherwise, there is a serious security issue where the one could inject commands by using "&malicious.exe in any of the arguments.

We need a Windows-specific escaping mechanism.

@rafd123
Copy link

rafd123 commented Feb 11, 2023

Digging into this a bit further, I noticed that the dotnet core equivalent of exec.Command does 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:

using System.Diagnostics;

Process.Start(@"C:\Program Files\echo.bat", new[] {"hello world"});

Debugging how both golang and dotnet ultimately call Windows' CreateProcess API, I noticed one meaningful difference: dotnet passes null for the optional lpApplicationName parameter of CreateProcess while golang does not.

This is how dotnet calls CreateProcess (note the null for lpApplicationName argument).

This is how golang calls CreateProcess (note the use of argv0p...which in practice maps to the name argument of exec.Command...also note that in practice argvp will also contain the name argument of exec.Command).

Empirically, I can also confirm that if I force golang to pass nil for lpApplicationName here instead of argv0p, executing exec.Command(`C:\Program Files\echo.bat`, "hello world") works without resorting to using the SysProcAttr escape hatch.

All that said:

@bertysentry
Copy link

@rafd123 Following what is done in .NET Core, and adapting it to the expected behavior in Go is probably the best we can do. Thank you for the excellent research!

In your suggested makeCmd(), you would just need to add proper escaping for CMD.EXE. We probably need to use ^ instead of " to surround the arguments. WDYT?

@bcmills
Copy link
Contributor

bcmills commented Feb 22, 2023

(CC @golang/windows)

@iyzyi
Copy link

iyzyi commented Mar 10, 2023

It still exists in golang 1.20, e.g:
cmd.exe /c copy/b "input 1.ts"+"input 2.ts" ouput.ts
I guess that golang can automatically add double quotes to paths that contain Spaces, but many commands have their own coding rules, such as copy/b. The "+" in copy/b means concatenating the input files, but golang cannot parse it and cannot add double quotes to paths of input files that contain Spaces correctly.

rafd123 added a commit to rafd123/go that referenced this issue Apr 27, 2023
This addresses golang#17149 by calling CreateProcess() in the same way that
that dotnet does for its equivalent syscall abstraction since dotnet
doesn't suffer from the same problem due to this difference.

This change assumes that argv0 always represents the name/path of the
executable and is represented as the argv[0] element per:
- How exec.Command() builds exec.Cmd.Args
- The following syscall/exec_unix code path: https://github.com/golang/go/blob/e8c8b79f000515e086012df632f01fc0ec21076b/src/syscall/exec_unix.go#L169-L171
rafd123 added a commit to rafd123/go that referenced this issue Apr 27, 2023
This addresses golang#17149 by calling CreateProcess() and
CreateProcessAsUser()in the same way that that dotnet does for its
equivalent syscall abstraction since dotnet doesn't suffer from the same
problem due to this difference.

See the dotnet equivalent here: https://github.com/dotnet/runtime/blob/2d411c4dfc1d71b2387ac64089014ec811ad7af0/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Windows.cs#L578
...and here: https://github.com/dotnet/runtime/blob/2d411c4dfc1d71b2387ac64089014ec811ad7af0/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Windows.cs#L554

This change assumes that argv0 always represents the name/path of the
executable and is represented as the argv[0] element per:
- How exec.Command() builds exec.Cmd.Args
- The following syscall/exec_unix code path: https://github.com/golang/go/blob/e8c8b79f000515e086012df632f01fc0ec21076b/src/syscall/exec_unix.go#L169-L171
@rafd123
Copy link

rafd123 commented Apr 27, 2023

@bcmills @golang/windows I'd like to submit a PR that modifies syscall/exec to address this issue, and I'd like to get buyoff on the approach before doing so.

TL:DR modify syscall/exec on Windows to pass nil for the lpApplicationName arg to CreateProcess() and CreateProcessAsUser() to mimic what dotnet core does since dotnet doesn't exhibit this issue due to this difference. For full background, see this comment: #17149 (comment)

Concretely, this is the code change I'd like to propose: rafd123@d24b468#diff-ec673c10a75fe2d2faa9c788e03823294b263c68cc3de50f4a0865a53e28f3a3

My biggest concern with this change is that it assumes that syscall/exec.StartProcess's argv0 is represented within argv[0]. This assumption could be violated if, for example, folks don't use os/exec.Command and instead manually construct os/exec.Cmd without including os/exec.Cmd.Path in os/exec.Cmd.Args. I already ran into a violation of this assumption in one of the tests here: rafd123@d24b468#diff-f0ee8d33c527f62f2e48195ea748d2d7295c26910144012ee6e108fbf08f21a6

That said, there seems to be a precendent for this assumption in syscall/exec_unix.go here:

go/src/syscall/exec_unix.go

Lines 169 to 171 in e8c8b79

if (runtime.GOOS == "freebsd" || runtime.GOOS == "dragonfly") && len(argv[0]) > len(argv0) {
argvp[0] = argv0p
}

What do you think? Is this a reasonable approach? If so, I'll submit a PR.

rafd123 added a commit to rafd123/go that referenced this issue Apr 28, 2023
This addresses golang#17149 by calling CreateProcess() and
CreateProcessAsUser()in the same way that that dotnet does for its
equivalent syscall abstraction since dotnet doesn't suffer from the same
problem due to this difference.

See the dotnet equivalent here: https://github.com/dotnet/runtime/blob/2d411c4dfc1d71b2387ac64089014ec811ad7af0/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Windows.cs#L578
...and here: https://github.com/dotnet/runtime/blob/2d411c4dfc1d71b2387ac64089014ec811ad7af0/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Windows.cs#L554

This change assumes that argv0 always represents the name/path of the
executable and is represented as the argv[0] element per:
- How exec.Command() builds exec.Cmd.Args
- The following syscall/exec_unix code path: https://github.com/golang/go/blob/e8c8b79f000515e086012df632f01fc0ec21076b/src/syscall/exec_unix.go#L169-L171
@bcmills
Copy link
Contributor

bcmills commented Jun 27, 2023

@rafd123, on Unix it is possible for an exec.Cmd to have an Args[0] different from its Path. I would expect the same to be possible in general on Windows too, which would seem to imply calling CreateProcessW with an ApplicationName that matches Path and a CommandLine that matches Args.

But maybe we could leave ApplicationName nil when Path is already equal to Args[0], as would be the case if the user already called exec.LookPath to resolve the path passed to exec.Command?

Or, since this quoting seems to be mostly specific to cmd.exe and commands that implicitly reinvoke %COMSPEC% (specifically, commands with the extensions .bat and .cmd), maybe we should change the quoting algorithm to match that used by cmd.exe when the resolved Path has a .bat or .cmd extension? (Are there cases where that would regress existing programs, or do we expect that those are basically all using the SysProcAttr.CmdLine workaround already?)

@gopherbot
Copy link

Change https://go.dev/cl/530275 mentions this issue: windows: convert TestCommandLineRecomposition to a fuzz test and fix discrepancies

gopherbot pushed a commit to golang/sys that referenced this issue Sep 26, 2023
…discrepancies

Notably, this fixes the escaping of the first argument when it
contains quoted spaces, and fixes a panic in DecomposeCommandLine
when it contains more than 8192 arguments.

Fixes golang/go#58817.
For golang/go#17149.
For golang/go#63236.

Change-Id: Ib72913b8182998adc1420d73ee0f9dc017dfbf32
Reviewed-on: https://go-review.googlesource.com/c/sys/+/530275
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Quim Muntal <quimmuntal@gmail.com>
Reviewed-by: Than McIntosh <thanm@google.com>
Auto-Submit: Bryan Mills <bcmills@google.com>
@rbergmanaf
Copy link

I've been struggling with this bug and have recreated the proposed fixes in this conversation in my app. The latest proposal works well for the use case in the OP, but I think it should be noted that when using cmd.exe /c cmd metachars also need to be handled (not just spaces and quotes). These need to be further escaped with ^, though for reasons I can't yet explain that needs to itself be escaped as ^^^ in the final CmdLine string used in SysProcAttrs.

@bcmills
Copy link
Contributor

bcmills commented Oct 31, 2023

Based on the fuzz test added in https://go.dev/cl/530275, I believe that one part of the fix for this will be to change syscall.makeCmdLine to use the same approach as was taken for windows.ComposeCommandLine in that change.

Another part of the fix for this issue will probably be to add a fuzz test to os/exec, similar to windows.FuzzComposeCommandLine but using os/exec to start a Go program that prints its arguments (compare cmd/go/internal/work.FuzzSplitPkgConfigOutput). That will ensure that os.commandLineToArgv remains consistent with syscall.makeCmdLine.

Finally, we should either change os.commandLineToArgv to use CommandLineToArgvW directly, or add a fuzz test to ensure that its implementation is consistent with some other Windows function or program (perhaps CommandLineToArgvW, cmd.exe, or some other well-defined reference program).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
early-in-cycle A change that should be done early in the 3 month dev cycle. NeedsFix The path to resolution is known, but the work has not been done. OS-Windows
Projects
None yet
Development

No branches or pull requests