From ccd992355df7192993c666236047820244914598 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Tue, 16 Apr 2024 21:19:13 +0200 Subject: Adding upstream version 1.21.8. Signed-off-by: Daniel Baumann --- src/os/exec/bench_test.go | 23 + src/os/exec/dot_test.go | 192 +++ src/os/exec/env_test.go | 67 + src/os/exec/example_test.go | 169 +++ src/os/exec/exec.go | 1303 ++++++++++++++++++ src/os/exec/exec_linux_test.go | 45 + src/os/exec/exec_other_test.go | 14 + src/os/exec/exec_plan9.go | 19 + src/os/exec/exec_posix_test.go | 276 ++++ src/os/exec/exec_test.go | 1784 +++++++++++++++++++++++++ src/os/exec/exec_unix.go | 24 + src/os/exec/exec_unix_test.go | 17 + src/os/exec/exec_windows.go | 23 + src/os/exec/exec_windows_test.go | 109 ++ src/os/exec/internal/fdtest/exists_plan9.go | 20 + src/os/exec/internal/fdtest/exists_test.go | 21 + src/os/exec/internal/fdtest/exists_unix.go | 19 + src/os/exec/internal/fdtest/exists_windows.go | 12 + src/os/exec/internal_test.go | 61 + src/os/exec/lp_linux_test.go | 88 ++ src/os/exec/lp_plan9.go | 66 + src/os/exec/lp_test.go | 33 + src/os/exec/lp_unix.go | 82 ++ src/os/exec/lp_unix_test.go | 50 + src/os/exec/lp_wasm.go | 23 + src/os/exec/lp_windows.go | 145 ++ src/os/exec/lp_windows_test.go | 612 +++++++++ src/os/exec/read3.go | 91 ++ 28 files changed, 5388 insertions(+) create mode 100644 src/os/exec/bench_test.go create mode 100644 src/os/exec/dot_test.go create mode 100644 src/os/exec/env_test.go create mode 100644 src/os/exec/example_test.go create mode 100644 src/os/exec/exec.go create mode 100644 src/os/exec/exec_linux_test.go create mode 100644 src/os/exec/exec_other_test.go create mode 100644 src/os/exec/exec_plan9.go create mode 100644 src/os/exec/exec_posix_test.go create mode 100644 src/os/exec/exec_test.go create mode 100644 src/os/exec/exec_unix.go create mode 100644 src/os/exec/exec_unix_test.go create mode 100644 src/os/exec/exec_windows.go create mode 100644 src/os/exec/exec_windows_test.go create mode 100644 src/os/exec/internal/fdtest/exists_plan9.go create mode 100644 src/os/exec/internal/fdtest/exists_test.go create mode 100644 src/os/exec/internal/fdtest/exists_unix.go create mode 100644 src/os/exec/internal/fdtest/exists_windows.go create mode 100644 src/os/exec/internal_test.go create mode 100644 src/os/exec/lp_linux_test.go create mode 100644 src/os/exec/lp_plan9.go create mode 100644 src/os/exec/lp_test.go create mode 100644 src/os/exec/lp_unix.go create mode 100644 src/os/exec/lp_unix_test.go create mode 100644 src/os/exec/lp_wasm.go create mode 100644 src/os/exec/lp_windows.go create mode 100644 src/os/exec/lp_windows_test.go create mode 100644 src/os/exec/read3.go (limited to 'src/os/exec') diff --git a/src/os/exec/bench_test.go b/src/os/exec/bench_test.go new file mode 100644 index 0000000..9a94001 --- /dev/null +++ b/src/os/exec/bench_test.go @@ -0,0 +1,23 @@ +// Copyright 2019 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package exec + +import ( + "testing" +) + +func BenchmarkExecHostname(b *testing.B) { + b.ReportAllocs() + path, err := LookPath("hostname") + if err != nil { + b.Fatalf("could not find hostname: %v", err) + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + if err := Command(path).Run(); err != nil { + b.Fatalf("hostname: %v", err) + } + } +} diff --git a/src/os/exec/dot_test.go b/src/os/exec/dot_test.go new file mode 100644 index 0000000..66c92f7 --- /dev/null +++ b/src/os/exec/dot_test.go @@ -0,0 +1,192 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package exec_test + +import ( + "errors" + "internal/testenv" + "os" + . "os/exec" + "path/filepath" + "runtime" + "strings" + "testing" +) + +var pathVar string = func() string { + if runtime.GOOS == "plan9" { + return "path" + } + return "PATH" +}() + +func TestLookPath(t *testing.T) { + testenv.MustHaveExec(t) + // Not parallel: uses os.Chdir and t.Setenv. + + tmpDir := filepath.Join(t.TempDir(), "testdir") + if err := os.Mkdir(tmpDir, 0777); err != nil { + t.Fatal(err) + } + + executable := "execabs-test" + if runtime.GOOS == "windows" { + executable += ".exe" + } + if err := os.WriteFile(filepath.Join(tmpDir, executable), []byte{1, 2, 3}, 0777); err != nil { + t.Fatal(err) + } + cwd, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + defer func() { + if err := os.Chdir(cwd); err != nil { + panic(err) + } + }() + if err = os.Chdir(tmpDir); err != nil { + t.Fatal(err) + } + t.Setenv("PWD", tmpDir) + t.Logf(". is %#q", tmpDir) + + origPath := os.Getenv(pathVar) + + // Add "." to PATH so that exec.LookPath looks in the current directory on all systems. + // And try to trick it with "../testdir" too. + for _, errdot := range []string{"1", "0"} { + t.Run("GODEBUG=execerrdot="+errdot, func(t *testing.T) { + t.Setenv("GODEBUG", "execerrdot="+errdot+",execwait=2") + for _, dir := range []string{".", "../testdir"} { + t.Run(pathVar+"="+dir, func(t *testing.T) { + t.Setenv(pathVar, dir+string(filepath.ListSeparator)+origPath) + good := dir + "/execabs-test" + if found, err := LookPath(good); err != nil || !strings.HasPrefix(found, good) { + t.Fatalf(`LookPath(%#q) = %#q, %v, want "%s...", nil`, good, found, err, good) + } + if runtime.GOOS == "windows" { + good = dir + `\execabs-test` + if found, err := LookPath(good); err != nil || !strings.HasPrefix(found, good) { + t.Fatalf(`LookPath(%#q) = %#q, %v, want "%s...", nil`, good, found, err, good) + } + } + + _, err := LookPath("execabs-test") + if errdot == "1" { + if err == nil { + t.Fatalf("LookPath didn't fail when finding a non-relative path") + } else if !errors.Is(err, ErrDot) { + t.Fatalf("LookPath returned unexpected error: want Is ErrDot, got %q", err) + } + } else { + if err != nil { + t.Fatalf("LookPath failed unexpectedly: %v", err) + } + } + + cmd := Command("execabs-test") + if errdot == "1" { + if cmd.Err == nil { + t.Fatalf("Command didn't fail when finding a non-relative path") + } else if !errors.Is(cmd.Err, ErrDot) { + t.Fatalf("Command returned unexpected error: want Is ErrDot, got %q", cmd.Err) + } + cmd.Err = nil + } else { + if cmd.Err != nil { + t.Fatalf("Command failed unexpectedly: %v", err) + } + } + + // Clearing cmd.Err should let the execution proceed, + // and it should fail because it's not a valid binary. + if err := cmd.Run(); err == nil { + t.Fatalf("Run did not fail: expected exec error") + } else if errors.Is(err, ErrDot) { + t.Fatalf("Run returned unexpected error ErrDot: want error like ENOEXEC: %q", err) + } + }) + } + }) + } + + // Test the behavior when the first entry in PATH is an absolute name for the + // current directory. + // + // On Windows, "." may or may not be implicitly included before the explicit + // %PATH%, depending on the process environment; + // see https://go.dev/issue/4394. + // + // If the relative entry from "." resolves to the same executable as what + // would be resolved from an absolute entry in %PATH% alone, LookPath should + // return the absolute version of the path instead of ErrDot. + // (See https://go.dev/issue/53536.) + // + // If PATH does not implicitly include "." (such as on Unix platforms, or on + // Windows configured with NoDefaultCurrentDirectoryInExePath), then this + // lookup should succeed regardless of the behavior for ".", so it may be + // useful to run as a control case even on those platforms. + t.Run(pathVar+"=$PWD", func(t *testing.T) { + t.Setenv(pathVar, tmpDir+string(filepath.ListSeparator)+origPath) + good := filepath.Join(tmpDir, "execabs-test") + if found, err := LookPath(good); err != nil || !strings.HasPrefix(found, good) { + t.Fatalf(`LookPath(%#q) = %#q, %v, want \"%s...\", nil`, good, found, err, good) + } + + if found, err := LookPath("execabs-test"); err != nil || !strings.HasPrefix(found, good) { + t.Fatalf(`LookPath(%#q) = %#q, %v, want \"%s...\", nil`, "execabs-test", found, err, good) + } + + cmd := Command("execabs-test") + if cmd.Err != nil { + t.Fatalf("Command(%#q).Err = %v; want nil", "execabs-test", cmd.Err) + } + }) + + t.Run(pathVar+"=$OTHER", func(t *testing.T) { + // Control case: if the lookup returns ErrDot when PATH is empty, then we + // know that PATH implicitly includes ".". If it does not, then we don't + // expect to see ErrDot at all in this test (because the path will be + // unambiguously absolute). + wantErrDot := false + t.Setenv(pathVar, "") + if found, err := LookPath("execabs-test"); errors.Is(err, ErrDot) { + wantErrDot = true + } else if err == nil { + t.Fatalf(`with PATH='', LookPath(%#q) = %#q; want non-nil error`, "execabs-test", found) + } + + // Set PATH to include an explicit directory that contains a completely + // independent executable that happens to have the same name as an + // executable in ".". If "." is included implicitly, looking up the + // (unqualified) executable name will return ErrDot; otherwise, the + // executable in "." should have no effect and the lookup should + // unambiguously resolve to the directory in PATH. + + dir := t.TempDir() + executable := "execabs-test" + if runtime.GOOS == "windows" { + executable += ".exe" + } + if err := os.WriteFile(filepath.Join(dir, executable), []byte{1, 2, 3}, 0777); err != nil { + t.Fatal(err) + } + t.Setenv(pathVar, dir+string(filepath.ListSeparator)+origPath) + + found, err := LookPath("execabs-test") + if wantErrDot { + wantFound := filepath.Join(".", executable) + if found != wantFound || !errors.Is(err, ErrDot) { + t.Fatalf(`LookPath(%#q) = %#q, %v, want %#q, Is ErrDot`, "execabs-test", found, err, wantFound) + } + } else { + wantFound := filepath.Join(dir, executable) + if found != wantFound || err != nil { + t.Fatalf(`LookPath(%#q) = %#q, %v, want %#q, nil`, "execabs-test", found, err, wantFound) + } + } + }) +} diff --git a/src/os/exec/env_test.go b/src/os/exec/env_test.go new file mode 100644 index 0000000..ea06af3 --- /dev/null +++ b/src/os/exec/env_test.go @@ -0,0 +1,67 @@ +// Copyright 2017 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package exec + +import ( + "reflect" + "testing" +) + +func TestDedupEnv(t *testing.T) { + t.Parallel() + + tests := []struct { + noCase bool + nulOK bool + in []string + want []string + wantErr bool + }{ + { + noCase: true, + in: []string{"k1=v1", "k2=v2", "K1=v3"}, + want: []string{"k2=v2", "K1=v3"}, + }, + { + noCase: false, + in: []string{"k1=v1", "K1=V2", "k1=v3"}, + want: []string{"K1=V2", "k1=v3"}, + }, + { + in: []string{"=a", "=b", "foo", "bar"}, + want: []string{"=b", "foo", "bar"}, + }, + { + // #49886: preserve weird Windows keys with leading "=" signs. + noCase: true, + in: []string{`=C:=C:\golang`, `=D:=D:\tmp`, `=D:=D:\`}, + want: []string{`=C:=C:\golang`, `=D:=D:\`}, + }, + { + // #52436: preserve invalid key-value entries (for now). + // (Maybe filter them out or error out on them at some point.) + in: []string{"dodgy", "entries"}, + want: []string{"dodgy", "entries"}, + }, + { + // Filter out entries containing NULs. + in: []string{"A=a\x00b", "B=b", "C\x00C=c"}, + want: []string{"B=b"}, + wantErr: true, + }, + { + // Plan 9 needs to preserve environment variables with NUL (#56544). + nulOK: true, + in: []string{"path=one\x00two"}, + want: []string{"path=one\x00two"}, + }, + } + for _, tt := range tests { + got, err := dedupEnvCase(tt.noCase, tt.nulOK, tt.in) + if !reflect.DeepEqual(got, tt.want) || (err != nil) != tt.wantErr { + t.Errorf("Dedup(%v, %q) = %q, %v; want %q, error:%v", tt.noCase, tt.in, got, err, tt.want, tt.wantErr) + } + } +} diff --git a/src/os/exec/example_test.go b/src/os/exec/example_test.go new file mode 100644 index 0000000..150f5cf --- /dev/null +++ b/src/os/exec/example_test.go @@ -0,0 +1,169 @@ +// Copyright 2012 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package exec_test + +import ( + "context" + "encoding/json" + "fmt" + "io" + "log" + "os" + "os/exec" + "strings" + "time" +) + +func ExampleLookPath() { + path, err := exec.LookPath("fortune") + if err != nil { + log.Fatal("installing fortune is in your future") + } + fmt.Printf("fortune is available at %s\n", path) +} + +func ExampleCommand() { + cmd := exec.Command("tr", "a-z", "A-Z") + cmd.Stdin = strings.NewReader("some input") + var out strings.Builder + cmd.Stdout = &out + err := cmd.Run() + if err != nil { + log.Fatal(err) + } + fmt.Printf("in all caps: %q\n", out.String()) +} + +func ExampleCommand_environment() { + cmd := exec.Command("prog") + cmd.Env = append(os.Environ(), + "FOO=duplicate_value", // ignored + "FOO=actual_value", // this value is used + ) + if err := cmd.Run(); err != nil { + log.Fatal(err) + } +} + +func ExampleCmd_Output() { + out, err := exec.Command("date").Output() + if err != nil { + log.Fatal(err) + } + fmt.Printf("The date is %s\n", out) +} + +func ExampleCmd_Run() { + cmd := exec.Command("sleep", "1") + log.Printf("Running command and waiting for it to finish...") + err := cmd.Run() + log.Printf("Command finished with error: %v", err) +} + +func ExampleCmd_Start() { + cmd := exec.Command("sleep", "5") + err := cmd.Start() + if err != nil { + log.Fatal(err) + } + log.Printf("Waiting for command to finish...") + err = cmd.Wait() + log.Printf("Command finished with error: %v", err) +} + +func ExampleCmd_StdoutPipe() { + cmd := exec.Command("echo", "-n", `{"Name": "Bob", "Age": 32}`) + stdout, err := cmd.StdoutPipe() + if err != nil { + log.Fatal(err) + } + if err := cmd.Start(); err != nil { + log.Fatal(err) + } + var person struct { + Name string + Age int + } + if err := json.NewDecoder(stdout).Decode(&person); err != nil { + log.Fatal(err) + } + if err := cmd.Wait(); err != nil { + log.Fatal(err) + } + fmt.Printf("%s is %d years old\n", person.Name, person.Age) +} + +func ExampleCmd_StdinPipe() { + cmd := exec.Command("cat") + stdin, err := cmd.StdinPipe() + if err != nil { + log.Fatal(err) + } + + go func() { + defer stdin.Close() + io.WriteString(stdin, "values written to stdin are passed to cmd's standard input") + }() + + out, err := cmd.CombinedOutput() + if err != nil { + log.Fatal(err) + } + + fmt.Printf("%s\n", out) +} + +func ExampleCmd_StderrPipe() { + cmd := exec.Command("sh", "-c", "echo stdout; echo 1>&2 stderr") + stderr, err := cmd.StderrPipe() + if err != nil { + log.Fatal(err) + } + + if err := cmd.Start(); err != nil { + log.Fatal(err) + } + + slurp, _ := io.ReadAll(stderr) + fmt.Printf("%s\n", slurp) + + if err := cmd.Wait(); err != nil { + log.Fatal(err) + } +} + +func ExampleCmd_CombinedOutput() { + cmd := exec.Command("sh", "-c", "echo stdout; echo 1>&2 stderr") + stdoutStderr, err := cmd.CombinedOutput() + if err != nil { + log.Fatal(err) + } + fmt.Printf("%s\n", stdoutStderr) +} + +func ExampleCmd_Environ() { + cmd := exec.Command("pwd") + + // Set Dir before calling cmd.Environ so that it will include an + // updated PWD variable (on platforms where that is used). + cmd.Dir = ".." + cmd.Env = append(cmd.Environ(), "POSIXLY_CORRECT=1") + + out, err := cmd.Output() + if err != nil { + log.Fatal(err) + } + fmt.Printf("%s\n", out) +} + +func ExampleCommandContext() { + ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) + defer cancel() + + if err := exec.CommandContext(ctx, "sleep", "5").Run(); err != nil { + // This will fail after 100 milliseconds. The 5 second sleep + // will be interrupted. + } +} diff --git a/src/os/exec/exec.go b/src/os/exec/exec.go new file mode 100644 index 0000000..138be29 --- /dev/null +++ b/src/os/exec/exec.go @@ -0,0 +1,1303 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package exec runs external commands. It wraps os.StartProcess to make it +// easier to remap stdin and stdout, connect I/O with pipes, and do other +// adjustments. +// +// Unlike the "system" library call from C and other languages, the +// os/exec package intentionally does not invoke the system shell and +// does not expand any glob patterns or handle other expansions, +// pipelines, or redirections typically done by shells. The package +// behaves more like C's "exec" family of functions. To expand glob +// patterns, either call the shell directly, taking care to escape any +// dangerous input, or use the path/filepath package's Glob function. +// To expand environment variables, use package os's ExpandEnv. +// +// Note that the examples in this package assume a Unix system. +// They may not run on Windows, and they do not run in the Go Playground +// used by golang.org and godoc.org. +// +// # Executables in the current directory +// +// The functions Command and LookPath look for a program +// in the directories listed in the current path, following the +// conventions of the host operating system. +// Operating systems have for decades included the current +// directory in this search, sometimes implicitly and sometimes +// configured explicitly that way by default. +// Modern practice is that including the current directory +// is usually unexpected and often leads to security problems. +// +// To avoid those security problems, as of Go 1.19, this package will not resolve a program +// using an implicit or explicit path entry relative to the current directory. +// That is, if you run exec.LookPath("go"), it will not successfully return +// ./go on Unix nor .\go.exe on Windows, no matter how the path is configured. +// Instead, if the usual path algorithms would result in that answer, +// these functions return an error err satisfying errors.Is(err, ErrDot). +// +// For example, consider these two program snippets: +// +// path, err := exec.LookPath("prog") +// if err != nil { +// log.Fatal(err) +// } +// use(path) +// +// and +// +// cmd := exec.Command("prog") +// if err := cmd.Run(); err != nil { +// log.Fatal(err) +// } +// +// These will not find and run ./prog or .\prog.exe, +// no matter how the current path is configured. +// +// Code that always wants to run a program from the current directory +// can be rewritten to say "./prog" instead of "prog". +// +// Code that insists on including results from relative path entries +// can instead override the error using an errors.Is check: +// +// path, err := exec.LookPath("prog") +// if errors.Is(err, exec.ErrDot) { +// err = nil +// } +// if err != nil { +// log.Fatal(err) +// } +// use(path) +// +// and +// +// cmd := exec.Command("prog") +// if errors.Is(cmd.Err, exec.ErrDot) { +// cmd.Err = nil +// } +// if err := cmd.Run(); err != nil { +// log.Fatal(err) +// } +// +// Setting the environment variable GODEBUG=execerrdot=0 +// disables generation of ErrDot entirely, temporarily restoring the pre-Go 1.19 +// behavior for programs that are unable to apply more targeted fixes. +// A future version of Go may remove support for this variable. +// +// Before adding such overrides, make sure you understand the +// security implications of doing so. +// See https://go.dev/blog/path-security for more information. +package exec + +import ( + "bytes" + "context" + "errors" + "internal/godebug" + "internal/syscall/execenv" + "io" + "os" + "path/filepath" + "runtime" + "strconv" + "strings" + "syscall" + "time" +) + +// Error is returned by LookPath when it fails to classify a file as an +// executable. +type Error struct { + // Name is the file name for which the error occurred. + Name string + // Err is the underlying error. + Err error +} + +func (e *Error) Error() string { + return "exec: " + strconv.Quote(e.Name) + ": " + e.Err.Error() +} + +func (e *Error) Unwrap() error { return e.Err } + +// ErrWaitDelay is returned by (*Cmd).Wait if the process exits with a +// successful status code but its output pipes are not closed before the +// command's WaitDelay expires. +var ErrWaitDelay = errors.New("exec: WaitDelay expired before I/O complete") + +// wrappedError wraps an error without relying on fmt.Errorf. +type wrappedError struct { + prefix string + err error +} + +func (w wrappedError) Error() string { + return w.prefix + ": " + w.err.Error() +} + +func (w wrappedError) Unwrap() error { + return w.err +} + +// Cmd represents an external command being prepared or run. +// +// A Cmd cannot be reused after calling its Run, Output or CombinedOutput +// methods. +type Cmd struct { + // Path is the path of the command to run. + // + // This is the only field that must be set to a non-zero + // value. If Path is relative, it is evaluated relative + // to Dir. + Path string + + // Args holds command line arguments, including the command as Args[0]. + // If the Args field is empty or nil, Run uses {Path}. + // + // In typical use, both Path and Args are set by calling Command. + Args []string + + // Env specifies the environment of the process. + // Each entry is of the form "key=value". + // If Env is nil, the new process uses the current process's + // environment. + // If Env contains duplicate environment keys, only the last + // value in the slice for each duplicate key is used. + // As a special case on Windows, SYSTEMROOT is always added if + // missing and not explicitly set to the empty string. + Env []string + + // Dir specifies the working directory of the command. + // If Dir is the empty string, Run runs the command in the + // calling process's current directory. + Dir string + + // Stdin specifies the process's standard input. + // + // If Stdin is nil, the process reads from the null device (os.DevNull). + // + // If Stdin is an *os.File, the process's standard input is connected + // directly to that file. + // + // Otherwise, during the execution of the command a separate + // goroutine reads from Stdin and delivers that data to the command + // over a pipe. In this case, Wait does not complete until the goroutine + // stops copying, either because it has reached the end of Stdin + // (EOF or a read error), or because writing to the pipe returned an error, + // or because a nonzero WaitDelay was set and expired. + Stdin io.Reader + + // Stdout and Stderr specify the process's standard output and error. + // + // If either is nil, Run connects the corresponding file descriptor + // to the null device (os.DevNull). + // + // If either is an *os.File, the corresponding output from the process + // is connected directly to that file. + // + // Otherwise, during the execution of the command a separate goroutine + // reads from the process over a pipe and delivers that data to the + // corresponding Writer. In this case, Wait does not complete until the + // goroutine reaches EOF or encounters an error or a nonzero WaitDelay + // expires. + // + // If Stdout and Stderr are the same writer, and have a type that can + // be compared with ==, at most one goroutine at a time will call Write. + Stdout io.Writer + Stderr io.Writer + + // ExtraFiles specifies additional open files to be inherited by the + // new process. It does not include standard input, standard output, or + // standard error. If non-nil, entry i becomes file descriptor 3+i. + // + // ExtraFiles is not supported on Windows. + ExtraFiles []*os.File + + // SysProcAttr holds optional, operating system-specific attributes. + // Run passes it to os.StartProcess as the os.ProcAttr's Sys field. + SysProcAttr *syscall.SysProcAttr + + // Process is the underlying process, once started. + Process *os.Process + + // ProcessState contains information about an exited process. + // If the process was started successfully, Wait or Run will + // populate its ProcessState when the command completes. + ProcessState *os.ProcessState + + // ctx is the context passed to CommandContext, if any. + ctx context.Context + + Err error // LookPath error, if any. + + // If Cancel is non-nil, the command must have been created with + // CommandContext and Cancel will be called when the command's + // Context is done. By default, CommandContext sets Cancel to + // call the Kill method on the command's Process. + // + // Typically a custom Cancel will send a signal to the command's + // Process, but it may instead take other actions to initiate cancellation, + // such as closing a stdin or stdout pipe or sending a shutdown request on a + // network socket. + // + // If the command exits with a success status after Cancel is + // called, and Cancel does not return an error equivalent to + // os.ErrProcessDone, then Wait and similar methods will return a non-nil + // error: either an error wrapping the one returned by Cancel, + // or the error from the Context. + // (If the command exits with a non-success status, or Cancel + // returns an error that wraps os.ErrProcessDone, Wait and similar methods + // continue to return the command's usual exit status.) + // + // If Cancel is set to nil, nothing will happen immediately when the command's + // Context is done, but a nonzero WaitDelay will still take effect. That may + // be useful, for example, to work around deadlocks in commands that do not + // support shutdown signals but are expected to always finish quickly. + // + // Cancel will not be called if Start returns a non-nil error. + Cancel func() error + + // If WaitDelay is non-zero, it bounds the time spent waiting on two sources + // of unexpected delay in Wait: a child process that fails to exit after the + // associated Context is canceled, and a child process that exits but leaves + // its I/O pipes unclosed. + // + // The WaitDelay timer starts when either the associated Context is done or a + // call to Wait observes that the child process has exited, whichever occurs + // first. When the delay has elapsed, the command shuts down the child process + // and/or its I/O pipes. + // + // If the child process has failed to exit — perhaps because it ignored or + // failed to receive a shutdown signal from a Cancel function, or because no + // Cancel function was set — then it will be terminated using os.Process.Kill. + // + // Then, if the I/O pipes communicating with the child process are still open, + // those pipes are closed in order to unblock any goroutines currently blocked + // on Read or Write calls. + // + // If pipes are closed due to WaitDelay, no Cancel call has occurred, + // and the command has otherwise exited with a successful status, Wait and + // similar methods will return ErrWaitDelay instead of nil. + // + // If WaitDelay is zero (the default), I/O pipes will be read until EOF, + // which might not occur until orphaned subprocesses of the command have + // also closed their descriptors for the pipes. + WaitDelay time.Duration + + // childIOFiles holds closers for any of the child process's + // stdin, stdout, and/or stderr files that were opened by the Cmd itself + // (not supplied by the caller). These should be closed as soon as they + // are inherited by the child process. + childIOFiles []io.Closer + + // parentIOPipes holds closers for the parent's end of any pipes + // connected to the child's stdin, stdout, and/or stderr streams + // that were opened by the Cmd itself (not supplied by the caller). + // These should be closed after Wait sees the command and copying + // goroutines exit, or after WaitDelay has expired. + parentIOPipes []io.Closer + + // goroutine holds a set of closures to execute to copy data + // to and/or from the command's I/O pipes. + goroutine []func() error + + // If goroutineErr is non-nil, it receives the first error from a copying + // goroutine once all such goroutines have completed. + // goroutineErr is set to nil once its error has been received. + goroutineErr <-chan error + + // If ctxResult is non-nil, it receives the result of watchCtx exactly once. + ctxResult <-chan ctxResult + + // The stack saved when the Command was created, if GODEBUG contains + // execwait=2. Used for debugging leaks. + createdByStack []byte + + // For a security release long ago, we created x/sys/execabs, + // which manipulated the unexported lookPathErr error field + // in this struct. For Go 1.19 we exported the field as Err error, + // above, but we have to keep lookPathErr around for use by + // old programs building against new toolchains. + // The String and Start methods look for an error in lookPathErr + // in preference to Err, to preserve the errors that execabs sets. + // + // In general we don't guarantee misuse of reflect like this, + // but the misuse of reflect was by us, the best of various bad + // options to fix the security problem, and people depend on + // those old copies of execabs continuing to work. + // The result is that we have to leave this variable around for the + // rest of time, a compatibility scar. + // + // See https://go.dev/blog/path-security + // and https://go.dev/issue/43724 for more context. + lookPathErr error +} + +// A ctxResult reports the result of watching the Context associated with a +// running command (and sending corresponding signals if needed). +type ctxResult struct { + err error + + // If timer is non-nil, it expires after WaitDelay has elapsed after + // the Context is done. + // + // (If timer is nil, that means that the Context was not done before the + // command completed, or no WaitDelay was set, or the WaitDelay already + // expired and its effect was already applied.) + timer *time.Timer +} + +var execwait = godebug.New("#execwait") +var execerrdot = godebug.New("execerrdot") + +// Command returns the Cmd struct to execute the named program with +// the given arguments. +// +// It sets only the Path and Args in the returned structure. +// +// If name contains no path separators, Command uses LookPath to +// resolve name to a complete path if possible. Otherwise it uses name +// directly as Path. +// +// The returned Cmd's Args field is constructed from the command name +// followed by the elements of arg, so arg should not include the +// command name itself. For example, Command("echo", "hello"). +// Args[0] is always name, not the possibly resolved Path. +// +// On Windows, processes receive the whole command line as a single string +// and do their own parsing. Command combines and quotes Args into a command +// line string with an algorithm compatible with applications using +// CommandLineToArgvW (which is the most common way). Notable exceptions are +// msiexec.exe and cmd.exe (and thus, all batch files), which have a different +// unquoting algorithm. In these or other similar cases, you can do the +// quoting yourself and provide the full command line in SysProcAttr.CmdLine, +// leaving Args empty. +func Command(name string, arg ...string) *Cmd { + cmd := &Cmd{ + Path: name, + Args: append([]string{name}, arg...), + } + + if v := execwait.Value(); v != "" { + if v == "2" { + // Obtain the caller stack. (This is equivalent to runtime/debug.Stack, + // copied to avoid importing the whole package.) + stack := make([]byte, 1024) + for { + n := runtime.Stack(stack, false) + if n < len(stack) { + stack = stack[:n] + break + } + stack = make([]byte, 2*len(stack)) + } + + if i := bytes.Index(stack, []byte("\nos/exec.Command(")); i >= 0 { + stack = stack[i+1:] + } + cmd.createdByStack = stack + } + + runtime.SetFinalizer(cmd, func(c *Cmd) { + if c.Process != nil && c.ProcessState == nil { + debugHint := "" + if c.createdByStack == nil { + debugHint = " (set GODEBUG=execwait=2 to capture stacks for debugging)" + } else { + os.Stderr.WriteString("GODEBUG=execwait=2 detected a leaked exec.Cmd created by:\n") + os.Stderr.Write(c.createdByStack) + os.Stderr.WriteString("\n") + debugHint = "" + } + panic("exec: Cmd started a Process but leaked without a call to Wait" + debugHint) + } + }) + } + + if filepath.Base(name) == name { + lp, err := LookPath(name) + if lp != "" { + // Update cmd.Path even if err is non-nil. + // If err is ErrDot (especially on Windows), lp may include a resolved + // extension (like .exe or .bat) that should be preserved. + cmd.Path = lp + } + if err != nil { + cmd.Err = err + } + } + return cmd +} + +// CommandContext is like Command but includes a context. +// +// The provided context is used to interrupt the process +// (by calling cmd.Cancel or os.Process.Kill) +// if the context becomes done before the command completes on its own. +// +// CommandContext sets the command's Cancel function to invoke the Kill method +// on its Process, and leaves its WaitDelay unset. The caller may change the +// cancellation behavior by modifying those fields before starting the command. +func CommandContext(ctx context.Context, name string, arg ...string) *Cmd { + if ctx == nil { + panic("nil Context") + } + cmd := Command(name, arg...) + cmd.ctx = ctx + cmd.Cancel = func() error { + return cmd.Process.Kill() + } + return cmd +} + +// String returns a human-readable description of c. +// It is intended only for debugging. +// In particular, it is not suitable for use as input to a shell. +// The output of String may vary across Go releases. +func (c *Cmd) String() string { + if c.Err != nil || c.lookPathErr != nil { + // failed to resolve path; report the original requested path (plus args) + return strings.Join(c.Args, " ") + } + // report the exact executable path (plus args) + b := new(strings.Builder) + b.WriteString(c.Path) + for _, a := range c.Args[1:] { + b.WriteByte(' ') + b.WriteString(a) + } + return b.String() +} + +// interfaceEqual protects against panics from doing equality tests on +// two interfaces with non-comparable underlying types. +func interfaceEqual(a, b any) bool { + defer func() { + recover() + }() + return a == b +} + +func (c *Cmd) argv() []string { + if len(c.Args) > 0 { + return c.Args + } + return []string{c.Path} +} + +func (c *Cmd) childStdin() (*os.File, error) { + if c.Stdin == nil { + f, err := os.Open(os.DevNull) + if err != nil { + return nil, err + } + c.childIOFiles = append(c.childIOFiles, f) + return f, nil + } + + if f, ok := c.Stdin.(*os.File); ok { + return f, nil + } + + pr, pw, err := os.Pipe() + if err != nil { + return nil, err + } + + c.childIOFiles = append(c.childIOFiles, pr) + c.parentIOPipes = append(c.parentIOPipes, pw) + c.goroutine = append(c.goroutine, func() error { + _, err := io.Copy(pw, c.Stdin) + if skipStdinCopyError(err) { + err = nil + } + if err1 := pw.Close(); err == nil { + err = err1 + } + return err + }) + return pr, nil +} + +func (c *Cmd) childStdout() (*os.File, error) { + return c.writerDescriptor(c.Stdout) +} + +func (c *Cmd) childStderr(childStdout *os.File) (*os.File, error) { + if c.Stderr != nil && interfaceEqual(c.Stderr, c.Stdout) { + return childStdout, nil + } + return c.writerDescriptor(c.Stderr) +} + +// writerDescriptor returns an os.File to which the child process +// can write to send data to w. +// +// If w is nil, writerDescriptor returns a File that writes to os.DevNull. +func (c *Cmd) writerDescriptor(w io.Writer) (*os.File, error) { + if w == nil { + f, err := os.OpenFile(os.DevNull, os.O_WRONLY, 0) + if err != nil { + return nil, err + } + c.childIOFiles = append(c.childIOFiles, f) + return f, nil + } + + if f, ok := w.(*os.File); ok { + return f, nil + } + + pr, pw, err := os.Pipe() + if err != nil { + return nil, err + } + + c.childIOFiles = append(c.childIOFiles, pw) + c.parentIOPipes = append(c.parentIOPipes, pr) + c.goroutine = append(c.goroutine, func() error { + _, err := io.Copy(w, pr) + pr.Close() // in case io.Copy stopped due to write error + return err + }) + return pw, nil +} + +func closeDescriptors(closers []io.Closer) { + for _, fd := range closers { + fd.Close() + } +} + +// Run starts the specified command and waits for it to complete. +// +// The returned error is nil if the command runs, has no problems +// copying stdin, stdout, and stderr, and exits with a zero exit +// status. +// +// If the command starts but does not complete successfully, the error is of +// type *ExitError. Other error types may be returned for other situations. +// +// If the calling goroutine has locked the operating system thread +// with runtime.LockOSThread and modified any inheritable OS-level +// thread state (for example, Linux or Plan 9 name spaces), the new +// process will inherit the caller's thread state. +func (c *Cmd) Run() error { + if err := c.Start(); err != nil { + return err + } + return c.Wait() +} + +// lookExtensions finds windows executable by its dir and path. +// It uses LookPath to try appropriate extensions. +// lookExtensions does not search PATH, instead it converts `prog` into `.\prog`. +func lookExtensions(path, dir string) (string, error) { + if filepath.Base(path) == path { + path = "." + string(filepath.Separator) + path + } + if dir == "" { + return LookPath(path) + } + if filepath.VolumeName(path) != "" { + return LookPath(path) + } + if len(path) > 1 && os.IsPathSeparator(path[0]) { + return LookPath(path) + } + dirandpath := filepath.Join(dir, path) + // We assume that LookPath will only add file extension. + lp, err := LookPath(dirandpath) + if err != nil { + return "", err + } + ext := strings.TrimPrefix(lp, dirandpath) + return path + ext, nil +} + +// Start starts the specified command but does not wait for it to complete. +// +// If Start returns successfully, the c.Process field will be set. +// +// After a successful call to Start the Wait method must be called in +// order to release associated system resources. +func (c *Cmd) Start() error { + // Check for doubled Start calls before we defer failure cleanup. If the prior + // call to Start succeeded, we don't want to spuriously close its pipes. + if c.Process != nil { + return errors.New("exec: already started") + } + + started := false + defer func() { + closeDescriptors(c.childIOFiles) + c.childIOFiles = nil + + if !started { + closeDescriptors(c.parentIOPipes) + c.parentIOPipes = nil + } + }() + + if c.Path == "" && c.Err == nil && c.lookPathErr == nil { + c.Err = errors.New("exec: no command") + } + if c.Err != nil || c.lookPathErr != nil { + if c.lookPathErr != nil { + return c.lookPathErr + } + return c.Err + } + if runtime.GOOS == "windows" { + lp, err := lookExtensions(c.Path, c.Dir) + if err != nil { + return err + } + c.Path = lp + } + if c.Cancel != nil && c.ctx == nil { + return errors.New("exec: command with a non-nil Cancel was not created with CommandContext") + } + if c.ctx != nil { + select { + case <-c.ctx.Done(): + return c.ctx.Err() + default: + } + } + + childFiles := make([]*os.File, 0, 3+len(c.ExtraFiles)) + stdin, err := c.childStdin() + if err != nil { + return err + } + childFiles = append(childFiles, stdin) + stdout, err := c.childStdout() + if err != nil { + return err + } + childFiles = append(childFiles, stdout) + stderr, err := c.childStderr(stdout) + if err != nil { + return err + } + childFiles = append(childFiles, stderr) + childFiles = append(childFiles, c.ExtraFiles...) + + env, err := c.environ() + if err != nil { + return err + } + + c.Process, err = os.StartProcess(c.Path, c.argv(), &os.ProcAttr{ + Dir: c.Dir, + Files: childFiles, + Env: env, + Sys: c.SysProcAttr, + }) + if err != nil { + return err + } + started = true + + // Don't allocate the goroutineErr channel unless there are goroutines to start. + if len(c.goroutine) > 0 { + goroutineErr := make(chan error, 1) + c.goroutineErr = goroutineErr + + type goroutineStatus struct { + running int + firstErr error + } + statusc := make(chan goroutineStatus, 1) + statusc <- goroutineStatus{running: len(c.goroutine)} + for _, fn := range c.goroutine { + go func(fn func() error) { + err := fn() + + status := <-statusc + if status.firstErr == nil { + status.firstErr = err + } + status.running-- + if status.running == 0 { + goroutineErr <- status.firstErr + } else { + statusc <- status + } + }(fn) + } + c.goroutine = nil // Allow the goroutines' closures to be GC'd when they complete. + } + + // If we have anything to do when the command's Context expires, + // start a goroutine to watch for cancellation. + // + // (Even if the command was created by CommandContext, a helper library may + // have explicitly set its Cancel field back to nil, indicating that it should + // be allowed to continue running after cancellation after all.) + if (c.Cancel != nil || c.WaitDelay != 0) && c.ctx != nil && c.ctx.Done() != nil { + resultc := make(chan ctxResult) + c.ctxResult = resultc + go c.watchCtx(resultc) + } + + return nil +} + +// watchCtx watches c.ctx until it is able to send a result to resultc. +// +// If c.ctx is done before a result can be sent, watchCtx calls c.Cancel, +// and/or kills cmd.Process it after c.WaitDelay has elapsed. +// +// watchCtx manipulates c.goroutineErr, so its result must be received before +// c.awaitGoroutines is called. +func (c *Cmd) watchCtx(resultc chan<- ctxResult) { + select { + case resultc <- ctxResult{}: + return + case <-c.ctx.Done(): + } + + var err error + if c.Cancel != nil { + if interruptErr := c.Cancel(); interruptErr == nil { + // We appear to have successfully interrupted the command, so any + // program behavior from this point may be due to ctx even if the + // command exits with code 0. + err = c.ctx.Err() + } else if errors.Is(interruptErr, os.ErrProcessDone) { + // The process already finished: we just didn't notice it yet. + // (Perhaps c.Wait hadn't been called, or perhaps it happened to race with + // c.ctx being cancelled.) Don't inject a needless error. + } else { + err = wrappedError{ + prefix: "exec: canceling Cmd", + err: interruptErr, + } + } + } + if c.WaitDelay == 0 { + resultc <- ctxResult{err: err} + return + } + + timer := time.NewTimer(c.WaitDelay) + select { + case resultc <- ctxResult{err: err, timer: timer}: + // c.Process.Wait returned and we've handed the timer off to c.Wait. + // It will take care of goroutine shutdown from here. + return + case <-timer.C: + } + + killed := false + if killErr := c.Process.Kill(); killErr == nil { + // We appear to have killed the process. c.Process.Wait should return a + // non-nil error to c.Wait unless the Kill signal races with a successful + // exit, and if that does happen we shouldn't report a spurious error, + // so don't set err to anything here. + killed = true + } else if !errors.Is(killErr, os.ErrProcessDone) { + err = wrappedError{ + prefix: "exec: killing Cmd", + err: killErr, + } + } + + if c.goroutineErr != nil { + select { + case goroutineErr := <-c.goroutineErr: + // Forward goroutineErr only if we don't have reason to believe it was + // caused by a call to Cancel or Kill above. + if err == nil && !killed { + err = goroutineErr + } + default: + // Close the child process's I/O pipes, in case it abandoned some + // subprocess that inherited them and is still holding them open + // (see https://go.dev/issue/23019). + // + // We close the goroutine pipes only after we have sent any signals we're + // going to send to the process (via Signal or Kill above): if we send + // SIGKILL to the process, we would prefer for it to die of SIGKILL, not + // SIGPIPE. (However, this may still cause any orphaned subprocesses to + // terminate with SIGPIPE.) + closeDescriptors(c.parentIOPipes) + // Wait for the copying goroutines to finish, but report ErrWaitDelay for + // the error: any other error here could result from closing the pipes. + _ = <-c.goroutineErr + if err == nil { + err = ErrWaitDelay + } + } + + // Since we have already received the only result from c.goroutineErr, + // set it to nil to prevent awaitGoroutines from blocking on it. + c.goroutineErr = nil + } + + resultc <- ctxResult{err: err} +} + +// An ExitError reports an unsuccessful exit by a command. +type ExitError struct { + *os.ProcessState + + // Stderr holds a subset of the standard error output from the + // Cmd.Output method if standard error was not otherwise being + // collected. + // + // If the error output is long, Stderr may contain only a prefix + // and suffix of the output, with the middle replaced with + // text about the number of omitted bytes. + // + // Stderr is provided for debugging, for inclusion in error messages. + // Users with other needs should redirect Cmd.Stderr as needed. + Stderr []byte +} + +func (e *ExitError) Error() string { + return e.ProcessState.String() +} + +// Wait waits for the command to exit and waits for any copying to +// stdin or copying from stdout or stderr to complete. +// +// The command must have been started by Start. +// +// The returned error is nil if the command runs, has no problems +// copying stdin, stdout, and stderr, and exits with a zero exit +// status. +// +// If the command fails to run or doesn't complete successfully, the +// error is of type *ExitError. Other error types may be +// returned for I/O problems. +// +// If any of c.Stdin, c.Stdout or c.Stderr are not an *os.File, Wait also waits +// for the respective I/O loop copying to or from the process to complete. +// +// Wait releases any resources associated with the Cmd. +func (c *Cmd) Wait() error { + if c.Process == nil { + return errors.New("exec: not started") + } + if c.ProcessState != nil { + return errors.New("exec: Wait was already called") + } + + state, err := c.Process.Wait() + if err == nil && !state.Success() { + err = &ExitError{ProcessState: state} + } + c.ProcessState = state + + var timer *time.Timer + if c.ctxResult != nil { + watch := <-c.ctxResult + timer = watch.timer + // If c.Process.Wait returned an error, prefer that. + // Otherwise, report any error from the watchCtx goroutine, + // such as a Context cancellation or a WaitDelay overrun. + if err == nil && watch.err != nil { + err = watch.err + } + } + + if goroutineErr := c.awaitGoroutines(timer); err == nil { + // Report an error from the copying goroutines only if the program otherwise + // exited normally on its own. Otherwise, the copying error may be due to the + // abnormal termination. + err = goroutineErr + } + closeDescriptors(c.parentIOPipes) + c.parentIOPipes = nil + + return err +} + +// awaitGoroutines waits for the results of the goroutines copying data to or +// from the command's I/O pipes. +// +// If c.WaitDelay elapses before the goroutines complete, awaitGoroutines +// forcibly closes their pipes and returns ErrWaitDelay. +// +// If timer is non-nil, it must send to timer.C at the end of c.WaitDelay. +func (c *Cmd) awaitGoroutines(timer *time.Timer) error { + defer func() { + if timer != nil { + timer.Stop() + } + c.goroutineErr = nil + }() + + if c.goroutineErr == nil { + return nil // No running goroutines to await. + } + + if timer == nil { + if c.WaitDelay == 0 { + return <-c.goroutineErr + } + + select { + case err := <-c.goroutineErr: + // Avoid the overhead of starting a timer. + return err + default: + } + + // No existing timer was started: either there is no Context associated with + // the command, or c.Process.Wait completed before the Context was done. + timer = time.NewTimer(c.WaitDelay) + } + + select { + case <-timer.C: + closeDescriptors(c.parentIOPipes) + // Wait for the copying goroutines to finish, but ignore any error + // (since it was probably caused by closing the pipes). + _ = <-c.goroutineErr + return ErrWaitDelay + + case err := <-c.goroutineErr: + return err + } +} + +// Output runs the command and returns its standard output. +// Any returned error will usually be of type *ExitError. +// If c.Stderr was nil, Output populates ExitError.Stderr. +func (c *Cmd) Output() ([]byte, error) { + if c.Stdout != nil { + return nil, errors.New("exec: Stdout already set") + } + var stdout bytes.Buffer + c.Stdout = &stdout + + captureErr := c.Stderr == nil + if captureErr { + c.Stderr = &prefixSuffixSaver{N: 32 << 10} + } + + err := c.Run() + if err != nil && captureErr { + if ee, ok := err.(*ExitError); ok { + ee.Stderr = c.Stderr.(*prefixSuffixSaver).Bytes() + } + } + return stdout.Bytes(), err +} + +// CombinedOutput runs the command and returns its combined standard +// output and standard error. +func (c *Cmd) CombinedOutput() ([]byte, error) { + if c.Stdout != nil { + return nil, errors.New("exec: Stdout already set") + } + if c.Stderr != nil { + return nil, errors.New("exec: Stderr already set") + } + var b bytes.Buffer + c.Stdout = &b + c.Stderr = &b + err := c.Run() + return b.Bytes(), err +} + +// StdinPipe returns a pipe that will be connected to the command's +// standard input when the command starts. +// The pipe will be closed automatically after Wait sees the command exit. +// A caller need only call Close to force the pipe to close sooner. +// For example, if the command being run will not exit until standard input +// is closed, the caller must close the pipe. +func (c *Cmd) StdinPipe() (io.WriteCloser, error) { + if c.Stdin != nil { + return nil, errors.New("exec: Stdin already set") + } + if c.Process != nil { + return nil, errors.New("exec: StdinPipe after process started") + } + pr, pw, err := os.Pipe() + if err != nil { + return nil, err + } + c.Stdin = pr + c.childIOFiles = append(c.childIOFiles, pr) + c.parentIOPipes = append(c.parentIOPipes, pw) + return pw, nil +} + +// StdoutPipe returns a pipe that will be connected to the command's +// standard output when the command starts. +// +// Wait will close the pipe after seeing the command exit, so most callers +// need not close the pipe themselves. It is thus incorrect to call Wait +// before all reads from the pipe have completed. +// For the same reason, it is incorrect to call Run when using StdoutPipe. +// See the example for idiomatic usage. +func (c *Cmd) StdoutPipe() (io.ReadCloser, error) { + if c.Stdout != nil { + return nil, errors.New("exec: Stdout already set") + } + if c.Process != nil { + return nil, errors.New("exec: StdoutPipe after process started") + } + pr, pw, err := os.Pipe() + if err != nil { + return nil, err + } + c.Stdout = pw + c.childIOFiles = append(c.childIOFiles, pw) + c.parentIOPipes = append(c.parentIOPipes, pr) + return pr, nil +} + +// StderrPipe returns a pipe that will be connected to the command's +// standard error when the command starts. +// +// Wait will close the pipe after seeing the command exit, so most callers +// need not close the pipe themselves. It is thus incorrect to call Wait +// before all reads from the pipe have completed. +// For the same reason, it is incorrect to use Run when using StderrPipe. +// See the StdoutPipe example for idiomatic usage. +func (c *Cmd) StderrPipe() (io.ReadCloser, error) { + if c.Stderr != nil { + return nil, errors.New("exec: Stderr already set") + } + if c.Process != nil { + return nil, errors.New("exec: StderrPipe after process started") + } + pr, pw, err := os.Pipe() + if err != nil { + return nil, err + } + c.Stderr = pw + c.childIOFiles = append(c.childIOFiles, pw) + c.parentIOPipes = append(c.parentIOPipes, pr) + return pr, nil +} + +// prefixSuffixSaver is an io.Writer which retains the first N bytes +// and the last N bytes written to it. The Bytes() methods reconstructs +// it with a pretty error message. +type prefixSuffixSaver struct { + N int // max size of prefix or suffix + prefix []byte + suffix []byte // ring buffer once len(suffix) == N + suffixOff int // offset to write into suffix + skipped int64 + + // TODO(bradfitz): we could keep one large []byte and use part of it for + // the prefix, reserve space for the '... Omitting N bytes ...' message, + // then the ring buffer suffix, and just rearrange the ring buffer + // suffix when Bytes() is called, but it doesn't seem worth it for + // now just for error messages. It's only ~64KB anyway. +} + +func (w *prefixSuffixSaver) Write(p []byte) (n int, err error) { + lenp := len(p) + p = w.fill(&w.prefix, p) + + // Only keep the last w.N bytes of suffix data. + if overage := len(p) - w.N; overage > 0 { + p = p[overage:] + w.skipped += int64(overage) + } + p = w.fill(&w.suffix, p) + + // w.suffix is full now if p is non-empty. Overwrite it in a circle. + for len(p) > 0 { // 0, 1, or 2 iterations. + n := copy(w.suffix[w.suffixOff:], p) + p = p[n:] + w.skipped += int64(n) + w.suffixOff += n + if w.suffixOff == w.N { + w.suffixOff = 0 + } + } + return lenp, nil +} + +// fill appends up to len(p) bytes of p to *dst, such that *dst does not +// grow larger than w.N. It returns the un-appended suffix of p. +func (w *prefixSuffixSaver) fill(dst *[]byte, p []byte) (pRemain []byte) { + if remain := w.N - len(*dst); remain > 0 { + add := minInt(len(p), remain) + *dst = append(*dst, p[:add]...) + p = p[add:] + } + return p +} + +func (w *prefixSuffixSaver) Bytes() []byte { + if w.suffix == nil { + return w.prefix + } + if w.skipped == 0 { + return append(w.prefix, w.suffix...) + } + var buf bytes.Buffer + buf.Grow(len(w.prefix) + len(w.suffix) + 50) + buf.Write(w.prefix) + buf.WriteString("\n... omitting ") + buf.WriteString(strconv.FormatInt(w.skipped, 10)) + buf.WriteString(" bytes ...\n") + buf.Write(w.suffix[w.suffixOff:]) + buf.Write(w.suffix[:w.suffixOff]) + return buf.Bytes() +} + +func minInt(a, b int) int { + if a < b { + return a + } + return b +} + +// environ returns a best-effort copy of the environment in which the command +// would be run as it is currently configured. If an error occurs in computing +// the environment, it is returned alongside the best-effort copy. +func (c *Cmd) environ() ([]string, error) { + var err error + + env := c.Env + if env == nil { + env, err = execenv.Default(c.SysProcAttr) + if err != nil { + env = os.Environ() + // Note that the non-nil err is preserved despite env being overridden. + } + + if c.Dir != "" { + switch runtime.GOOS { + case "windows", "plan9": + // Windows and Plan 9 do not use the PWD variable, so we don't need to + // keep it accurate. + default: + // On POSIX platforms, PWD represents “an absolute pathname of the + // current working directory.” Since we are changing the working + // directory for the command, we should also update PWD to reflect that. + // + // Unfortunately, we didn't always do that, so (as proposed in + // https://go.dev/issue/50599) to avoid unintended collateral damage we + // only implicitly update PWD when Env is nil. That way, we're much + // less likely to override an intentional change to the variable. + if pwd, absErr := filepath.Abs(c.Dir); absErr == nil { + env = append(env, "PWD="+pwd) + } else if err == nil { + err = absErr + } + } + } + } + + env, dedupErr := dedupEnv(env) + if err == nil { + err = dedupErr + } + return addCriticalEnv(env), err +} + +// Environ returns a copy of the environment in which the command would be run +// as it is currently configured. +func (c *Cmd) Environ() []string { + // Intentionally ignore errors: environ returns a best-effort environment no matter what. + env, _ := c.environ() + return env +} + +// dedupEnv returns a copy of env with any duplicates removed, in favor of +// later values. +// Items not of the normal environment "key=value" form are preserved unchanged. +// Except on Plan 9, items containing NUL characters are removed, and +// an error is returned along with the remaining values. +func dedupEnv(env []string) ([]string, error) { + return dedupEnvCase(runtime.GOOS == "windows", runtime.GOOS == "plan9", env) +} + +// dedupEnvCase is dedupEnv with a case option for testing. +// If caseInsensitive is true, the case of keys is ignored. +// If nulOK is false, items containing NUL characters are allowed. +func dedupEnvCase(caseInsensitive, nulOK bool, env []string) ([]string, error) { + // Construct the output in reverse order, to preserve the + // last occurrence of each key. + var err error + out := make([]string, 0, len(env)) + saw := make(map[string]bool, len(env)) + for n := len(env); n > 0; n-- { + kv := env[n-1] + + // Reject NUL in environment variables to prevent security issues (#56284); + // except on Plan 9, which uses NUL as os.PathListSeparator (#56544). + if !nulOK && strings.IndexByte(kv, 0) != -1 { + err = errors.New("exec: environment variable contains NUL") + continue + } + + i := strings.Index(kv, "=") + if i == 0 { + // We observe in practice keys with a single leading "=" on Windows. + // TODO(#49886): Should we consume only the first leading "=" as part + // of the key, or parse through arbitrarily many of them until a non-"="? + i = strings.Index(kv[1:], "=") + 1 + } + if i < 0 { + if kv != "" { + // The entry is not of the form "key=value" (as it is required to be). + // Leave it as-is for now. + // TODO(#52436): should we strip or reject these bogus entries? + out = append(out, kv) + } + continue + } + k := kv[:i] + if caseInsensitive { + k = strings.ToLower(k) + } + if saw[k] { + continue + } + + saw[k] = true + out = append(out, kv) + } + + // Now reverse the slice to restore the original order. + for i := 0; i < len(out)/2; i++ { + j := len(out) - i - 1 + out[i], out[j] = out[j], out[i] + } + + return out, err +} + +// addCriticalEnv adds any critical environment variables that are required +// (or at least almost always required) on the operating system. +// Currently this is only used for Windows. +func addCriticalEnv(env []string) []string { + if runtime.GOOS != "windows" { + return env + } + for _, kv := range env { + k, _, ok := strings.Cut(kv, "=") + if !ok { + continue + } + if strings.EqualFold(k, "SYSTEMROOT") { + // We already have it. + return env + } + } + return append(env, "SYSTEMROOT="+os.Getenv("SYSTEMROOT")) +} + +// ErrDot indicates that a path lookup resolved to an executable +// in the current directory due to ‘.’ being in the path, either +// implicitly or explicitly. See the package documentation for details. +// +// Note that functions in this package do not return ErrDot directly. +// Code should use errors.Is(err, ErrDot), not err == ErrDot, +// to test whether a returned error err is due to this condition. +var ErrDot = errors.New("cannot run executable found relative to current directory") diff --git a/src/os/exec/exec_linux_test.go b/src/os/exec/exec_linux_test.go new file mode 100644 index 0000000..b9f6b7b --- /dev/null +++ b/src/os/exec/exec_linux_test.go @@ -0,0 +1,45 @@ +// Copyright 2020 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build linux && cgo + +// On systems that use glibc, calling malloc can create a new arena, +// and creating a new arena can read /sys/devices/system/cpu/online. +// If we are using cgo, we will call malloc when creating a new thread. +// That can break TestExtraFiles if we create a new thread that creates +// a new arena and opens the /sys file while we are checking for open +// file descriptors. Work around the problem by creating threads up front. +// See issue 25628. + +package exec_test + +import ( + "os" + "sync" + "syscall" + "time" +) + +func init() { + if os.Getenv("GO_EXEC_TEST_PID") == "" { + return + } + + // Start some threads. 10 is arbitrary but intended to be enough + // to ensure that the code won't have to create any threads itself. + // In particular this should be more than the number of threads + // the garbage collector might create. + const threads = 10 + + var wg sync.WaitGroup + wg.Add(threads) + ts := syscall.NsecToTimespec((100 * time.Microsecond).Nanoseconds()) + for i := 0; i < threads; i++ { + go func() { + defer wg.Done() + syscall.Nanosleep(&ts, nil) + }() + } + wg.Wait() +} diff --git a/src/os/exec/exec_other_test.go b/src/os/exec/exec_other_test.go new file mode 100644 index 0000000..64c819c --- /dev/null +++ b/src/os/exec/exec_other_test.go @@ -0,0 +1,14 @@ +// Copyright 2021 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build !unix && !windows + +package exec_test + +import "os" + +var ( + quitSignal os.Signal = nil + pipeSignal os.Signal = nil +) diff --git a/src/os/exec/exec_plan9.go b/src/os/exec/exec_plan9.go new file mode 100644 index 0000000..8920bec --- /dev/null +++ b/src/os/exec/exec_plan9.go @@ -0,0 +1,19 @@ +// Copyright 2019 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package exec + +import "io/fs" + +// skipStdinCopyError optionally specifies a function which reports +// whether the provided stdin copy error should be ignored. +func skipStdinCopyError(err error) bool { + // Ignore hungup errors copying to stdin if the program + // completed successfully otherwise. + // See Issue 35753. + pe, ok := err.(*fs.PathError) + return ok && + pe.Op == "write" && pe.Path == "|1" && + pe.Err.Error() == "i/o on hungup channel" +} diff --git a/src/os/exec/exec_posix_test.go b/src/os/exec/exec_posix_test.go new file mode 100644 index 0000000..5d828b3 --- /dev/null +++ b/src/os/exec/exec_posix_test.go @@ -0,0 +1,276 @@ +// Copyright 2017 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build unix + +package exec_test + +import ( + "fmt" + "internal/testenv" + "io" + "os" + "os/user" + "path/filepath" + "reflect" + "runtime" + "strconv" + "strings" + "syscall" + "testing" + "time" +) + +func init() { + registerHelperCommand("pwd", cmdPwd) +} + +func cmdPwd(...string) { + pwd, err := os.Getwd() + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + fmt.Println(pwd) +} + +func TestCredentialNoSetGroups(t *testing.T) { + if runtime.GOOS == "android" { + maySkipHelperCommand("echo") + t.Skip("unsupported on Android") + } + t.Parallel() + + u, err := user.Current() + if err != nil { + t.Fatalf("error getting current user: %v", err) + } + + uid, err := strconv.Atoi(u.Uid) + if err != nil { + t.Fatalf("error converting Uid=%s to integer: %v", u.Uid, err) + } + + gid, err := strconv.Atoi(u.Gid) + if err != nil { + t.Fatalf("error converting Gid=%s to integer: %v", u.Gid, err) + } + + // If NoSetGroups is true, setgroups isn't called and cmd.Run should succeed + cmd := helperCommand(t, "echo", "foo") + cmd.SysProcAttr = &syscall.SysProcAttr{ + Credential: &syscall.Credential{ + Uid: uint32(uid), + Gid: uint32(gid), + NoSetGroups: true, + }, + } + + if err = cmd.Run(); err != nil { + t.Errorf("Failed to run command: %v", err) + } +} + +// For issue #19314: make sure that SIGSTOP does not cause the process +// to appear done. +func TestWaitid(t *testing.T) { + t.Parallel() + + cmd := helperCommand(t, "pipetest") + stdin, err := cmd.StdinPipe() + if err != nil { + t.Fatal(err) + } + stdout, err := cmd.StdoutPipe() + if err != nil { + t.Fatal(err) + } + if err := cmd.Start(); err != nil { + t.Fatal(err) + } + + // Wait for the child process to come up and register any signal handlers. + const msg = "O:ping\n" + if _, err := io.WriteString(stdin, msg); err != nil { + t.Fatal(err) + } + buf := make([]byte, len(msg)) + if _, err := io.ReadFull(stdout, buf); err != nil { + t.Fatal(err) + } + // Now leave the pipes open so that the process will hang until we close stdin. + + if err := cmd.Process.Signal(syscall.SIGSTOP); err != nil { + cmd.Process.Kill() + t.Fatal(err) + } + + ch := make(chan error) + go func() { + ch <- cmd.Wait() + }() + + // Give a little time for Wait to block on waiting for the process. + // (This is just to give some time to trigger the bug; it should not be + // necessary for the test to pass.) + if testing.Short() { + time.Sleep(1 * time.Millisecond) + } else { + time.Sleep(10 * time.Millisecond) + } + + // This call to Signal should succeed because the process still exists. + // (Prior to the fix for #19314, this would fail with os.ErrProcessDone + // or an equivalent error.) + if err := cmd.Process.Signal(syscall.SIGCONT); err != nil { + t.Error(err) + syscall.Kill(cmd.Process.Pid, syscall.SIGCONT) + } + + // The SIGCONT should allow the process to wake up, notice that stdin + // is closed, and exit successfully. + stdin.Close() + err = <-ch + if err != nil { + t.Fatal(err) + } +} + +// https://go.dev/issue/50599: if Env is not set explicitly, setting Dir should +// implicitly update PWD to the correct path, and Environ should list the +// updated value. +func TestImplicitPWD(t *testing.T) { + t.Parallel() + + cwd, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + + cases := []struct { + name string + dir string + want string + }{ + {"empty", "", cwd}, + {"dot", ".", cwd}, + {"dotdot", "..", filepath.Dir(cwd)}, + {"PWD", cwd, cwd}, + {"PWDdotdot", cwd + string(filepath.Separator) + "..", filepath.Dir(cwd)}, + } + + for _, tc := range cases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + cmd := helperCommand(t, "pwd") + if cmd.Env != nil { + t.Fatalf("test requires helperCommand not to set Env field") + } + cmd.Dir = tc.dir + + var pwds []string + for _, kv := range cmd.Environ() { + if strings.HasPrefix(kv, "PWD=") { + pwds = append(pwds, strings.TrimPrefix(kv, "PWD=")) + } + } + + wantPWDs := []string{tc.want} + if tc.dir == "" { + if _, ok := os.LookupEnv("PWD"); !ok { + wantPWDs = nil + } + } + if !reflect.DeepEqual(pwds, wantPWDs) { + t.Errorf("PWD entries in cmd.Environ():\n\t%s\nwant:\n\t%s", strings.Join(pwds, "\n\t"), strings.Join(wantPWDs, "\n\t")) + } + + cmd.Stderr = new(strings.Builder) + out, err := cmd.Output() + if err != nil { + t.Fatalf("%v:\n%s", err, cmd.Stderr) + } + got := strings.Trim(string(out), "\r\n") + t.Logf("in\n\t%s\n`pwd` reported\n\t%s", tc.dir, got) + if got != tc.want { + t.Errorf("want\n\t%s", tc.want) + } + }) + } +} + +// However, if cmd.Env is set explicitly, setting Dir should not override it. +// (This checks that the implementation for https://go.dev/issue/50599 doesn't +// break existing users who may have explicitly mismatched the PWD variable.) +func TestExplicitPWD(t *testing.T) { + t.Parallel() + + maySkipHelperCommand("pwd") + testenv.MustHaveSymlink(t) + + cwd, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + + link := filepath.Join(t.TempDir(), "link") + if err := os.Symlink(cwd, link); err != nil { + t.Fatal(err) + } + + // Now link is another equally-valid name for cwd. If we set Dir to one and + // PWD to the other, the subprocess should report the PWD version. + cases := []struct { + name string + dir string + pwd string + }{ + {name: "original PWD", pwd: cwd}, + {name: "link PWD", pwd: link}, + {name: "in link with original PWD", dir: link, pwd: cwd}, + {name: "in dir with link PWD", dir: cwd, pwd: link}, + // Ideally we would also like to test what happens if we set PWD to + // something totally bogus (or the empty string), but then we would have no + // idea what output the subprocess should actually produce: cwd itself may + // contain symlinks preserved from the PWD value in the test's environment. + } + for _, tc := range cases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + cmd := helperCommand(t, "pwd") + // This is intentionally opposite to the usual order of setting cmd.Dir + // and then calling cmd.Environ. Here, we *want* PWD not to match cmd.Dir, + // so we don't care whether cmd.Dir is reflected in cmd.Environ. + cmd.Env = append(cmd.Environ(), "PWD="+tc.pwd) + cmd.Dir = tc.dir + + var pwds []string + for _, kv := range cmd.Environ() { + if strings.HasPrefix(kv, "PWD=") { + pwds = append(pwds, strings.TrimPrefix(kv, "PWD=")) + } + } + + wantPWDs := []string{tc.pwd} + if !reflect.DeepEqual(pwds, wantPWDs) { + t.Errorf("PWD entries in cmd.Environ():\n\t%s\nwant:\n\t%s", strings.Join(pwds, "\n\t"), strings.Join(wantPWDs, "\n\t")) + } + + cmd.Stderr = new(strings.Builder) + out, err := cmd.Output() + if err != nil { + t.Fatalf("%v:\n%s", err, cmd.Stderr) + } + got := strings.Trim(string(out), "\r\n") + t.Logf("in\n\t%s\nwith PWD=%s\nsubprocess os.Getwd() reported\n\t%s", tc.dir, tc.pwd, got) + if got != tc.pwd { + t.Errorf("want\n\t%s", tc.pwd) + } + }) + } +} diff --git a/src/os/exec/exec_test.go b/src/os/exec/exec_test.go new file mode 100644 index 0000000..473f92b --- /dev/null +++ b/src/os/exec/exec_test.go @@ -0,0 +1,1784 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Use an external test to avoid os/exec -> net/http -> crypto/x509 -> os/exec +// circular dependency on non-cgo darwin. + +package exec_test + +import ( + "bufio" + "bytes" + "context" + "errors" + "flag" + "fmt" + "internal/poll" + "internal/testenv" + "io" + "log" + "net" + "net/http" + "net/http/httptest" + "os" + "os/exec" + "os/exec/internal/fdtest" + "os/signal" + "path/filepath" + "runtime" + "runtime/debug" + "strconv" + "strings" + "sync" + "sync/atomic" + "testing" + "time" +) + +// haveUnexpectedFDs is set at init time to report whether any file descriptors +// were open at program start. +var haveUnexpectedFDs bool + +func init() { + godebug := os.Getenv("GODEBUG") + if godebug != "" { + godebug += "," + } + godebug += "execwait=2" + os.Setenv("GODEBUG", godebug) + + if os.Getenv("GO_EXEC_TEST_PID") != "" { + return + } + if runtime.GOOS == "windows" { + return + } + for fd := uintptr(3); fd <= 100; fd++ { + if poll.IsPollDescriptor(fd) { + continue + } + + if fdtest.Exists(fd) { + haveUnexpectedFDs = true + return + } + } +} + +// TestMain allows the test binary to impersonate many other binaries, +// some of which may manipulate os.Stdin, os.Stdout, and/or os.Stderr +// (and thus cannot run as an ordinary Test function, since the testing +// package monkey-patches those variables before running tests). +func TestMain(m *testing.M) { + flag.Parse() + + pid := os.Getpid() + if os.Getenv("GO_EXEC_TEST_PID") == "" { + os.Setenv("GO_EXEC_TEST_PID", strconv.Itoa(pid)) + + code := m.Run() + if code == 0 && flag.Lookup("test.run").Value.String() == "" && flag.Lookup("test.list").Value.String() == "" { + for cmd := range helperCommands { + if _, ok := helperCommandUsed.Load(cmd); !ok { + fmt.Fprintf(os.Stderr, "helper command unused: %q\n", cmd) + code = 1 + } + } + } + + if !testing.Short() { + // Run a couple of GC cycles to increase the odds of detecting + // process leaks using the finalizers installed by GODEBUG=execwait=2. + runtime.GC() + runtime.GC() + } + + os.Exit(code) + } + + args := flag.Args() + if len(args) == 0 { + fmt.Fprintf(os.Stderr, "No command\n") + os.Exit(2) + } + + cmd, args := args[0], args[1:] + f, ok := helperCommands[cmd] + if !ok { + fmt.Fprintf(os.Stderr, "Unknown command %q\n", cmd) + os.Exit(2) + } + f(args...) + os.Exit(0) +} + +// registerHelperCommand registers a command that the test process can impersonate. +// A command should be registered in the same source file in which it is used. +// If all tests are run and pass, all registered commands must be used. +// (This prevents stale commands from accreting if tests are removed or +// refactored over time.) +func registerHelperCommand(name string, f func(...string)) { + if helperCommands[name] != nil { + panic("duplicate command registered: " + name) + } + helperCommands[name] = f +} + +// maySkipHelperCommand records that the test that uses the named helper command +// was invoked, but may call Skip on the test before actually calling +// helperCommand. +func maySkipHelperCommand(name string) { + helperCommandUsed.Store(name, true) +} + +// helperCommand returns an exec.Cmd that will run the named helper command. +func helperCommand(t *testing.T, name string, args ...string) *exec.Cmd { + t.Helper() + return helperCommandContext(t, nil, name, args...) +} + +// helperCommandContext is like helperCommand, but also accepts a Context under +// which to run the command. +func helperCommandContext(t *testing.T, ctx context.Context, name string, args ...string) (cmd *exec.Cmd) { + helperCommandUsed.LoadOrStore(name, true) + + t.Helper() + testenv.MustHaveExec(t) + + cs := append([]string{name}, args...) + if ctx != nil { + cmd = exec.CommandContext(ctx, exePath(t), cs...) + } else { + cmd = exec.Command(exePath(t), cs...) + } + return cmd +} + +// exePath returns the path to the running executable. +func exePath(t testing.TB) string { + exeOnce.Do(func() { + // Use os.Executable instead of os.Args[0] in case the caller modifies + // cmd.Dir: if the test binary is invoked like "./exec.test", it should + // not fail spuriously. + exeOnce.path, exeOnce.err = os.Executable() + }) + + if exeOnce.err != nil { + if t == nil { + panic(exeOnce.err) + } + t.Fatal(exeOnce.err) + } + + return exeOnce.path +} + +var exeOnce struct { + path string + err error + sync.Once +} + +var helperCommandUsed sync.Map + +var helperCommands = map[string]func(...string){ + "echo": cmdEcho, + "echoenv": cmdEchoEnv, + "cat": cmdCat, + "pipetest": cmdPipeTest, + "stdinClose": cmdStdinClose, + "exit": cmdExit, + "describefiles": cmdDescribeFiles, + "stderrfail": cmdStderrFail, + "yes": cmdYes, + "hang": cmdHang, +} + +func cmdEcho(args ...string) { + iargs := []any{} + for _, s := range args { + iargs = append(iargs, s) + } + fmt.Println(iargs...) +} + +func cmdEchoEnv(args ...string) { + for _, s := range args { + fmt.Println(os.Getenv(s)) + } +} + +func cmdCat(args ...string) { + if len(args) == 0 { + io.Copy(os.Stdout, os.Stdin) + return + } + exit := 0 + for _, fn := range args { + f, err := os.Open(fn) + if err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + exit = 2 + } else { + defer f.Close() + io.Copy(os.Stdout, f) + } + } + os.Exit(exit) +} + +func cmdPipeTest(...string) { + bufr := bufio.NewReader(os.Stdin) + for { + line, _, err := bufr.ReadLine() + if err == io.EOF { + break + } else if err != nil { + os.Exit(1) + } + if bytes.HasPrefix(line, []byte("O:")) { + os.Stdout.Write(line) + os.Stdout.Write([]byte{'\n'}) + } else if bytes.HasPrefix(line, []byte("E:")) { + os.Stderr.Write(line) + os.Stderr.Write([]byte{'\n'}) + } else { + os.Exit(1) + } + } +} + +func cmdStdinClose(...string) { + b, err := io.ReadAll(os.Stdin) + if err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) + } + if s := string(b); s != stdinCloseTestString { + fmt.Fprintf(os.Stderr, "Error: Read %q, want %q", s, stdinCloseTestString) + os.Exit(1) + } +} + +func cmdExit(args ...string) { + n, _ := strconv.Atoi(args[0]) + os.Exit(n) +} + +func cmdDescribeFiles(args ...string) { + f := os.NewFile(3, fmt.Sprintf("fd3")) + ln, err := net.FileListener(f) + if err == nil { + fmt.Printf("fd3: listener %s\n", ln.Addr()) + ln.Close() + } +} + +func cmdStderrFail(...string) { + fmt.Fprintf(os.Stderr, "some stderr text\n") + os.Exit(1) +} + +func cmdYes(args ...string) { + if len(args) == 0 { + args = []string{"y"} + } + s := strings.Join(args, " ") + "\n" + for { + _, err := os.Stdout.WriteString(s) + if err != nil { + os.Exit(1) + } + } +} + +func TestEcho(t *testing.T) { + t.Parallel() + + bs, err := helperCommand(t, "echo", "foo bar", "baz").Output() + if err != nil { + t.Errorf("echo: %v", err) + } + if g, e := string(bs), "foo bar baz\n"; g != e { + t.Errorf("echo: want %q, got %q", e, g) + } +} + +func TestCommandRelativeName(t *testing.T) { + t.Parallel() + + cmd := helperCommand(t, "echo", "foo") + + // Run our own binary as a relative path + // (e.g. "_test/exec.test") our parent directory. + base := filepath.Base(os.Args[0]) // "exec.test" + dir := filepath.Dir(os.Args[0]) // "/tmp/go-buildNNNN/os/exec/_test" + if dir == "." { + t.Skip("skipping; running test at root somehow") + } + parentDir := filepath.Dir(dir) // "/tmp/go-buildNNNN/os/exec" + dirBase := filepath.Base(dir) // "_test" + if dirBase == "." { + t.Skipf("skipping; unexpected shallow dir of %q", dir) + } + + cmd.Path = filepath.Join(dirBase, base) + cmd.Dir = parentDir + + out, err := cmd.Output() + if err != nil { + t.Errorf("echo: %v", err) + } + if g, e := string(out), "foo\n"; g != e { + t.Errorf("echo: want %q, got %q", e, g) + } +} + +func TestCatStdin(t *testing.T) { + t.Parallel() + + // Cat, testing stdin and stdout. + input := "Input string\nLine 2" + p := helperCommand(t, "cat") + p.Stdin = strings.NewReader(input) + bs, err := p.Output() + if err != nil { + t.Errorf("cat: %v", err) + } + s := string(bs) + if s != input { + t.Errorf("cat: want %q, got %q", input, s) + } +} + +func TestEchoFileRace(t *testing.T) { + t.Parallel() + + cmd := helperCommand(t, "echo") + stdin, err := cmd.StdinPipe() + if err != nil { + t.Fatalf("StdinPipe: %v", err) + } + if err := cmd.Start(); err != nil { + t.Fatalf("Start: %v", err) + } + wrote := make(chan bool) + go func() { + defer close(wrote) + fmt.Fprint(stdin, "echo\n") + }() + if err := cmd.Wait(); err != nil { + t.Fatalf("Wait: %v", err) + } + <-wrote +} + +func TestCatGoodAndBadFile(t *testing.T) { + t.Parallel() + + // Testing combined output and error values. + bs, err := helperCommand(t, "cat", "/bogus/file.foo", "exec_test.go").CombinedOutput() + if _, ok := err.(*exec.ExitError); !ok { + t.Errorf("expected *exec.ExitError from cat combined; got %T: %v", err, err) + } + errLine, body, ok := strings.Cut(string(bs), "\n") + if !ok { + t.Fatalf("expected two lines from cat; got %q", bs) + } + if !strings.HasPrefix(errLine, "Error: open /bogus/file.foo") { + t.Errorf("expected stderr to complain about file; got %q", errLine) + } + if !strings.Contains(body, "func TestCatGoodAndBadFile(t *testing.T)") { + t.Errorf("expected test code; got %q (len %d)", body, len(body)) + } +} + +func TestNoExistExecutable(t *testing.T) { + t.Parallel() + + // Can't run a non-existent executable + err := exec.Command("/no-exist-executable").Run() + if err == nil { + t.Error("expected error from /no-exist-executable") + } +} + +func TestExitStatus(t *testing.T) { + t.Parallel() + + // Test that exit values are returned correctly + cmd := helperCommand(t, "exit", "42") + err := cmd.Run() + want := "exit status 42" + switch runtime.GOOS { + case "plan9": + want = fmt.Sprintf("exit status: '%s %d: 42'", filepath.Base(cmd.Path), cmd.ProcessState.Pid()) + } + if werr, ok := err.(*exec.ExitError); ok { + if s := werr.Error(); s != want { + t.Errorf("from exit 42 got exit %q, want %q", s, want) + } + } else { + t.Fatalf("expected *exec.ExitError from exit 42; got %T: %v", err, err) + } +} + +func TestExitCode(t *testing.T) { + t.Parallel() + + // Test that exit code are returned correctly + cmd := helperCommand(t, "exit", "42") + cmd.Run() + want := 42 + if runtime.GOOS == "plan9" { + want = 1 + } + got := cmd.ProcessState.ExitCode() + if want != got { + t.Errorf("ExitCode got %d, want %d", got, want) + } + + cmd = helperCommand(t, "/no-exist-executable") + cmd.Run() + want = 2 + if runtime.GOOS == "plan9" { + want = 1 + } + got = cmd.ProcessState.ExitCode() + if want != got { + t.Errorf("ExitCode got %d, want %d", got, want) + } + + cmd = helperCommand(t, "exit", "255") + cmd.Run() + want = 255 + if runtime.GOOS == "plan9" { + want = 1 + } + got = cmd.ProcessState.ExitCode() + if want != got { + t.Errorf("ExitCode got %d, want %d", got, want) + } + + cmd = helperCommand(t, "cat") + cmd.Run() + want = 0 + got = cmd.ProcessState.ExitCode() + if want != got { + t.Errorf("ExitCode got %d, want %d", got, want) + } + + // Test when command does not call Run(). + cmd = helperCommand(t, "cat") + want = -1 + got = cmd.ProcessState.ExitCode() + if want != got { + t.Errorf("ExitCode got %d, want %d", got, want) + } +} + +func TestPipes(t *testing.T) { + t.Parallel() + + check := func(what string, err error) { + if err != nil { + t.Fatalf("%s: %v", what, err) + } + } + // Cat, testing stdin and stdout. + c := helperCommand(t, "pipetest") + stdin, err := c.StdinPipe() + check("StdinPipe", err) + stdout, err := c.StdoutPipe() + check("StdoutPipe", err) + stderr, err := c.StderrPipe() + check("StderrPipe", err) + + outbr := bufio.NewReader(stdout) + errbr := bufio.NewReader(stderr) + line := func(what string, br *bufio.Reader) string { + line, _, err := br.ReadLine() + if err != nil { + t.Fatalf("%s: %v", what, err) + } + return string(line) + } + + err = c.Start() + check("Start", err) + + _, err = stdin.Write([]byte("O:I am output\n")) + check("first stdin Write", err) + if g, e := line("first output line", outbr), "O:I am output"; g != e { + t.Errorf("got %q, want %q", g, e) + } + + _, err = stdin.Write([]byte("E:I am error\n")) + check("second stdin Write", err) + if g, e := line("first error line", errbr), "E:I am error"; g != e { + t.Errorf("got %q, want %q", g, e) + } + + _, err = stdin.Write([]byte("O:I am output2\n")) + check("third stdin Write 3", err) + if g, e := line("second output line", outbr), "O:I am output2"; g != e { + t.Errorf("got %q, want %q", g, e) + } + + stdin.Close() + err = c.Wait() + check("Wait", err) +} + +const stdinCloseTestString = "Some test string." + +// Issue 6270. +func TestStdinClose(t *testing.T) { + t.Parallel() + + check := func(what string, err error) { + if err != nil { + t.Fatalf("%s: %v", what, err) + } + } + cmd := helperCommand(t, "stdinClose") + stdin, err := cmd.StdinPipe() + check("StdinPipe", err) + // Check that we can access methods of the underlying os.File.` + if _, ok := stdin.(interface { + Fd() uintptr + }); !ok { + t.Error("can't access methods of underlying *os.File") + } + check("Start", cmd.Start()) + + var wg sync.WaitGroup + wg.Add(1) + defer wg.Wait() + go func() { + defer wg.Done() + + _, err := io.Copy(stdin, strings.NewReader(stdinCloseTestString)) + check("Copy", err) + + // Before the fix, this next line would race with cmd.Wait. + if err := stdin.Close(); err != nil && !errors.Is(err, os.ErrClosed) { + t.Errorf("Close: %v", err) + } + }() + + check("Wait", cmd.Wait()) +} + +// Issue 17647. +// It used to be the case that TestStdinClose, above, would fail when +// run under the race detector. This test is a variant of TestStdinClose +// that also used to fail when run under the race detector. +// This test is run by cmd/dist under the race detector to verify that +// the race detector no longer reports any problems. +func TestStdinCloseRace(t *testing.T) { + t.Parallel() + + cmd := helperCommand(t, "stdinClose") + stdin, err := cmd.StdinPipe() + if err != nil { + t.Fatalf("StdinPipe: %v", err) + } + if err := cmd.Start(); err != nil { + t.Fatalf("Start: %v", err) + + } + + var wg sync.WaitGroup + wg.Add(2) + defer wg.Wait() + + go func() { + defer wg.Done() + // We don't check the error return of Kill. It is + // possible that the process has already exited, in + // which case Kill will return an error "process + // already finished". The purpose of this test is to + // see whether the race detector reports an error; it + // doesn't matter whether this Kill succeeds or not. + cmd.Process.Kill() + }() + + go func() { + defer wg.Done() + // Send the wrong string, so that the child fails even + // if the other goroutine doesn't manage to kill it first. + // This test is to check that the race detector does not + // falsely report an error, so it doesn't matter how the + // child process fails. + io.Copy(stdin, strings.NewReader("unexpected string")) + if err := stdin.Close(); err != nil && !errors.Is(err, os.ErrClosed) { + t.Errorf("stdin.Close: %v", err) + } + }() + + if err := cmd.Wait(); err == nil { + t.Fatalf("Wait: succeeded unexpectedly") + } +} + +// Issue 5071 +func TestPipeLookPathLeak(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("we don't currently suppore counting open handles on windows") + } + // Not parallel: checks for leaked file descriptors + + openFDs := func() []uintptr { + var fds []uintptr + for i := uintptr(0); i < 100; i++ { + if fdtest.Exists(i) { + fds = append(fds, i) + } + } + return fds + } + + old := map[uintptr]bool{} + for _, fd := range openFDs() { + old[fd] = true + } + + for i := 0; i < 6; i++ { + cmd := exec.Command("something-that-does-not-exist-executable") + cmd.StdoutPipe() + cmd.StderrPipe() + cmd.StdinPipe() + if err := cmd.Run(); err == nil { + t.Fatal("unexpected success") + } + } + + // Since this test is not running in parallel, we don't expect any new file + // descriptors to be opened while it runs. However, if there are additional + // FDs present at the start of the test (for example, opened by libc), those + // may be closed due to a timeout of some sort. Allow those to go away, but + // check that no new FDs are added. + for _, fd := range openFDs() { + if !old[fd] { + t.Errorf("leaked file descriptor %v", fd) + } + } +} + +func TestExtraFiles(t *testing.T) { + if testing.Short() { + t.Skipf("skipping test in short mode that would build a helper binary") + } + + if haveUnexpectedFDs { + // The point of this test is to make sure that any + // descriptors we open are marked close-on-exec. + // If haveUnexpectedFDs is true then there were other + // descriptors open when we started the test, + // so those descriptors are clearly not close-on-exec, + // and they will confuse the test. We could modify + // the test to expect those descriptors to remain open, + // but since we don't know where they came from or what + // they are doing, that seems fragile. For example, + // perhaps they are from the startup code on this + // system for some reason. Also, this test is not + // system-specific; as long as most systems do not skip + // the test, we will still be testing what we care about. + t.Skip("skipping test because test was run with FDs open") + } + + testenv.MustHaveExec(t) + testenv.MustHaveGoBuild(t) + + // This test runs with cgo disabled. External linking needs cgo, so + // it doesn't work if external linking is required. + testenv.MustInternalLink(t, false) + + if runtime.GOOS == "windows" { + t.Skipf("skipping test on %q", runtime.GOOS) + } + + // Force network usage, to verify the epoll (or whatever) fd + // doesn't leak to the child, + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + defer ln.Close() + + // Make sure duplicated fds don't leak to the child. + f, err := ln.(*net.TCPListener).File() + if err != nil { + t.Fatal(err) + } + defer f.Close() + ln2, err := net.FileListener(f) + if err != nil { + t.Fatal(err) + } + defer ln2.Close() + + // Force TLS root certs to be loaded (which might involve + // cgo), to make sure none of that potential C code leaks fds. + ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})) + // quiet expected TLS handshake error "remote error: bad certificate" + ts.Config.ErrorLog = log.New(io.Discard, "", 0) + ts.StartTLS() + defer ts.Close() + _, err = http.Get(ts.URL) + if err == nil { + t.Errorf("success trying to fetch %s; want an error", ts.URL) + } + + tf, err := os.CreateTemp("", "") + if err != nil { + t.Fatalf("TempFile: %v", err) + } + defer os.Remove(tf.Name()) + defer tf.Close() + + const text = "Hello, fd 3!" + _, err = tf.Write([]byte(text)) + if err != nil { + t.Fatalf("Write: %v", err) + } + _, err = tf.Seek(0, io.SeekStart) + if err != nil { + t.Fatalf("Seek: %v", err) + } + + tempdir := t.TempDir() + exe := filepath.Join(tempdir, "read3.exe") + + c := testenv.Command(t, testenv.GoToolPath(t), "build", "-o", exe, "read3.go") + // Build the test without cgo, so that C library functions don't + // open descriptors unexpectedly. See issue 25628. + c.Env = append(os.Environ(), "CGO_ENABLED=0") + if output, err := c.CombinedOutput(); err != nil { + t.Logf("go build -o %s read3.go\n%s", exe, output) + t.Fatalf("go build failed: %v", err) + } + + // Use a deadline to try to get some output even if the program hangs. + ctx := context.Background() + if deadline, ok := t.Deadline(); ok { + // Leave a 20% grace period to flush output, which may be large on the + // linux/386 builders because we're running the subprocess under strace. + deadline = deadline.Add(-time.Until(deadline) / 5) + + var cancel context.CancelFunc + ctx, cancel = context.WithDeadline(ctx, deadline) + defer cancel() + } + + c = exec.CommandContext(ctx, exe) + var stdout, stderr strings.Builder + c.Stdout = &stdout + c.Stderr = &stderr + c.ExtraFiles = []*os.File{tf} + if runtime.GOOS == "illumos" { + // Some facilities in illumos are implemented via access + // to /proc by libc; such accesses can briefly occupy a + // low-numbered fd. If this occurs concurrently with the + // test that checks for leaked descriptors, the check can + // become confused and report a spurious leaked descriptor. + // (See issue #42431 for more detailed analysis.) + // + // Attempt to constrain the use of additional threads in the + // child process to make this test less flaky: + c.Env = append(os.Environ(), "GOMAXPROCS=1") + } + err = c.Run() + if err != nil { + t.Fatalf("Run: %v\n--- stdout:\n%s--- stderr:\n%s", err, stdout.String(), stderr.String()) + } + if stdout.String() != text { + t.Errorf("got stdout %q, stderr %q; want %q on stdout", stdout.String(), stderr.String(), text) + } +} + +func TestExtraFilesRace(t *testing.T) { + if runtime.GOOS == "windows" { + maySkipHelperCommand("describefiles") + t.Skip("no operating system support; skipping") + } + t.Parallel() + + listen := func() net.Listener { + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + return ln + } + listenerFile := func(ln net.Listener) *os.File { + f, err := ln.(*net.TCPListener).File() + if err != nil { + t.Fatal(err) + } + return f + } + runCommand := func(c *exec.Cmd, out chan<- string) { + bout, err := c.CombinedOutput() + if err != nil { + out <- "ERROR:" + err.Error() + } else { + out <- string(bout) + } + } + + for i := 0; i < 10; i++ { + if testing.Short() && i >= 3 { + break + } + la := listen() + ca := helperCommand(t, "describefiles") + ca.ExtraFiles = []*os.File{listenerFile(la)} + lb := listen() + cb := helperCommand(t, "describefiles") + cb.ExtraFiles = []*os.File{listenerFile(lb)} + ares := make(chan string) + bres := make(chan string) + go runCommand(ca, ares) + go runCommand(cb, bres) + if got, want := <-ares, fmt.Sprintf("fd3: listener %s\n", la.Addr()); got != want { + t.Errorf("iteration %d, process A got:\n%s\nwant:\n%s\n", i, got, want) + } + if got, want := <-bres, fmt.Sprintf("fd3: listener %s\n", lb.Addr()); got != want { + t.Errorf("iteration %d, process B got:\n%s\nwant:\n%s\n", i, got, want) + } + la.Close() + lb.Close() + for _, f := range ca.ExtraFiles { + f.Close() + } + for _, f := range cb.ExtraFiles { + f.Close() + } + } +} + +type delayedInfiniteReader struct{} + +func (delayedInfiniteReader) Read(b []byte) (int, error) { + time.Sleep(100 * time.Millisecond) + for i := range b { + b[i] = 'x' + } + return len(b), nil +} + +// Issue 9173: ignore stdin pipe writes if the program completes successfully. +func TestIgnorePipeErrorOnSuccess(t *testing.T) { + t.Parallel() + + testWith := func(r io.Reader) func(*testing.T) { + return func(t *testing.T) { + t.Parallel() + + cmd := helperCommand(t, "echo", "foo") + var out strings.Builder + cmd.Stdin = r + cmd.Stdout = &out + if err := cmd.Run(); err != nil { + t.Fatal(err) + } + if got, want := out.String(), "foo\n"; got != want { + t.Errorf("output = %q; want %q", got, want) + } + } + } + t.Run("10MB", testWith(strings.NewReader(strings.Repeat("x", 10<<20)))) + t.Run("Infinite", testWith(delayedInfiniteReader{})) +} + +type badWriter struct{} + +func (w *badWriter) Write(data []byte) (int, error) { + return 0, io.ErrUnexpectedEOF +} + +func TestClosePipeOnCopyError(t *testing.T) { + t.Parallel() + + cmd := helperCommand(t, "yes") + cmd.Stdout = new(badWriter) + err := cmd.Run() + if err == nil { + t.Errorf("yes unexpectedly completed successfully") + } +} + +func TestOutputStderrCapture(t *testing.T) { + t.Parallel() + + cmd := helperCommand(t, "stderrfail") + _, err := cmd.Output() + ee, ok := err.(*exec.ExitError) + if !ok { + t.Fatalf("Output error type = %T; want ExitError", err) + } + got := string(ee.Stderr) + want := "some stderr text\n" + if got != want { + t.Errorf("ExitError.Stderr = %q; want %q", got, want) + } +} + +func TestContext(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + c := helperCommandContext(t, ctx, "pipetest") + stdin, err := c.StdinPipe() + if err != nil { + t.Fatal(err) + } + stdout, err := c.StdoutPipe() + if err != nil { + t.Fatal(err) + } + if err := c.Start(); err != nil { + t.Fatal(err) + } + + if _, err := stdin.Write([]byte("O:hi\n")); err != nil { + t.Fatal(err) + } + buf := make([]byte, 5) + n, err := io.ReadFull(stdout, buf) + if n != len(buf) || err != nil || string(buf) != "O:hi\n" { + t.Fatalf("ReadFull = %d, %v, %q", n, err, buf[:n]) + } + go cancel() + + if err := c.Wait(); err == nil { + t.Fatal("expected Wait failure") + } +} + +func TestContextCancel(t *testing.T) { + if runtime.GOOS == "netbsd" && runtime.GOARCH == "arm64" { + maySkipHelperCommand("cat") + testenv.SkipFlaky(t, 42061) + } + + // To reduce noise in the final goroutine dump, + // let other parallel tests complete if possible. + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + c := helperCommandContext(t, ctx, "cat") + + stdin, err := c.StdinPipe() + if err != nil { + t.Fatal(err) + } + defer stdin.Close() + + if err := c.Start(); err != nil { + t.Fatal(err) + } + + // At this point the process is alive. Ensure it by sending data to stdin. + if _, err := io.WriteString(stdin, "echo"); err != nil { + t.Fatal(err) + } + + cancel() + + // Calling cancel should have killed the process, so writes + // should now fail. Give the process a little while to die. + start := time.Now() + delay := 1 * time.Millisecond + for { + if _, err := io.WriteString(stdin, "echo"); err != nil { + break + } + + if time.Since(start) > time.Minute { + // Panic instead of calling t.Fatal so that we get a goroutine dump. + // We want to know exactly what the os/exec goroutines got stuck on. + debug.SetTraceback("system") + panic("canceling context did not stop program") + } + + // Back off exponentially (up to 1-second sleeps) to give the OS time to + // terminate the process. + delay *= 2 + if delay > 1*time.Second { + delay = 1 * time.Second + } + time.Sleep(delay) + } + + if err := c.Wait(); err == nil { + t.Error("program unexpectedly exited successfully") + } else { + t.Logf("exit status: %v", err) + } +} + +// test that environment variables are de-duped. +func TestDedupEnvEcho(t *testing.T) { + t.Parallel() + + cmd := helperCommand(t, "echoenv", "FOO") + cmd.Env = append(cmd.Environ(), "FOO=bad", "FOO=good") + out, err := cmd.CombinedOutput() + if err != nil { + t.Fatal(err) + } + if got, want := strings.TrimSpace(string(out)), "good"; got != want { + t.Errorf("output = %q; want %q", got, want) + } +} + +func TestEnvNULCharacter(t *testing.T) { + if runtime.GOOS == "plan9" { + t.Skip("plan9 explicitly allows NUL in the environment") + } + cmd := helperCommand(t, "echoenv", "FOO", "BAR") + cmd.Env = append(cmd.Environ(), "FOO=foo\x00BAR=bar") + out, err := cmd.CombinedOutput() + if err == nil { + t.Errorf("output = %q; want error", string(out)) + } +} + +func TestString(t *testing.T) { + t.Parallel() + + echoPath, err := exec.LookPath("echo") + if err != nil { + t.Skip(err) + } + tests := [...]struct { + path string + args []string + want string + }{ + {"echo", nil, echoPath}, + {"echo", []string{"a"}, echoPath + " a"}, + {"echo", []string{"a", "b"}, echoPath + " a b"}, + } + for _, test := range tests { + cmd := exec.Command(test.path, test.args...) + if got := cmd.String(); got != test.want { + t.Errorf("String(%q, %q) = %q, want %q", test.path, test.args, got, test.want) + } + } +} + +func TestStringPathNotResolved(t *testing.T) { + t.Parallel() + + _, err := exec.LookPath("makemeasandwich") + if err == nil { + t.Skip("wow, thanks") + } + + cmd := exec.Command("makemeasandwich", "-lettuce") + want := "makemeasandwich -lettuce" + if got := cmd.String(); got != want { + t.Errorf("String(%q, %q) = %q, want %q", "makemeasandwich", "-lettuce", got, want) + } +} + +func TestNoPath(t *testing.T) { + err := new(exec.Cmd).Start() + want := "exec: no command" + if err == nil || err.Error() != want { + t.Errorf("new(Cmd).Start() = %v, want %q", err, want) + } +} + +// TestDoubleStartLeavesPipesOpen checks for a regression in which calling +// Start twice, which returns an error on the second call, would spuriously +// close the pipes established in the first call. +func TestDoubleStartLeavesPipesOpen(t *testing.T) { + t.Parallel() + + cmd := helperCommand(t, "pipetest") + in, err := cmd.StdinPipe() + if err != nil { + t.Fatal(err) + } + out, err := cmd.StdoutPipe() + if err != nil { + t.Fatal(err) + } + + if err := cmd.Start(); err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + if err := cmd.Wait(); err != nil { + t.Error(err) + } + }) + + if err := cmd.Start(); err == nil || !strings.HasSuffix(err.Error(), "already started") { + t.Fatalf("second call to Start returned a nil; want an 'already started' error") + } + + outc := make(chan []byte, 1) + go func() { + b, err := io.ReadAll(out) + if err != nil { + t.Error(err) + } + outc <- b + }() + + const msg = "O:Hello, pipe!\n" + + _, err = io.WriteString(in, msg) + if err != nil { + t.Fatal(err) + } + in.Close() + + b := <-outc + if !bytes.Equal(b, []byte(msg)) { + t.Fatalf("read %q from stdout pipe; want %q", b, msg) + } +} + +func cmdHang(args ...string) { + sleep, err := time.ParseDuration(args[0]) + if err != nil { + panic(err) + } + + fs := flag.NewFlagSet("hang", flag.ExitOnError) + exitOnInterrupt := fs.Bool("interrupt", false, "if true, commands should exit 0 on os.Interrupt") + subsleep := fs.Duration("subsleep", 0, "amount of time for the 'hang' helper to leave an orphaned subprocess sleeping with stderr open") + probe := fs.Duration("probe", 0, "if nonzero, the 'hang' helper should write to stderr at this interval, and exit nonzero if a write fails") + read := fs.Bool("read", false, "if true, the 'hang' helper should read stdin to completion before sleeping") + fs.Parse(args[1:]) + + pid := os.Getpid() + + if *subsleep != 0 { + cmd := exec.Command(exePath(nil), "hang", subsleep.String(), "-read=true", "-probe="+probe.String()) + cmd.Stdin = os.Stdin + cmd.Stderr = os.Stderr + out, err := cmd.StdoutPipe() + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + cmd.Start() + + buf := new(strings.Builder) + if _, err := io.Copy(buf, out); err != nil { + fmt.Fprintln(os.Stderr, err) + cmd.Process.Kill() + cmd.Wait() + os.Exit(1) + } + fmt.Fprintf(os.Stderr, "%d: started %d: %v\n", pid, cmd.Process.Pid, cmd) + go cmd.Wait() // Release resources if cmd happens not to outlive this process. + } + + if *exitOnInterrupt { + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt) + go func() { + sig := <-c + fmt.Fprintf(os.Stderr, "%d: received %v\n", pid, sig) + os.Exit(0) + }() + } else { + signal.Ignore(os.Interrupt) + } + + // Signal that the process is set up by closing stdout. + os.Stdout.Close() + + if *read { + if pipeSignal != nil { + signal.Ignore(pipeSignal) + } + r := bufio.NewReader(os.Stdin) + for { + line, err := r.ReadBytes('\n') + if len(line) > 0 { + // Ignore write errors: we want to keep reading even if stderr is closed. + fmt.Fprintf(os.Stderr, "%d: read %s", pid, line) + } + if err != nil { + fmt.Fprintf(os.Stderr, "%d: finished read: %v", pid, err) + break + } + } + } + + if *probe != 0 { + ticker := time.NewTicker(*probe) + go func() { + for range ticker.C { + if _, err := fmt.Fprintf(os.Stderr, "%d: ok\n", pid); err != nil { + os.Exit(1) + } + } + }() + } + + if sleep != 0 { + time.Sleep(sleep) + fmt.Fprintf(os.Stderr, "%d: slept %v\n", pid, sleep) + } +} + +// A tickReader reads an unbounded sequence of timestamps at no more than a +// fixed interval. +type tickReader struct { + interval time.Duration + lastTick time.Time + s string +} + +func newTickReader(interval time.Duration) *tickReader { + return &tickReader{interval: interval} +} + +func (r *tickReader) Read(p []byte) (n int, err error) { + if len(r.s) == 0 { + if d := r.interval - time.Since(r.lastTick); d > 0 { + time.Sleep(d) + } + r.lastTick = time.Now() + r.s = r.lastTick.Format(time.RFC3339Nano + "\n") + } + + n = copy(p, r.s) + r.s = r.s[n:] + return n, nil +} + +func startHang(t *testing.T, ctx context.Context, hangTime time.Duration, interrupt os.Signal, waitDelay time.Duration, flags ...string) *exec.Cmd { + t.Helper() + + args := append([]string{hangTime.String()}, flags...) + cmd := helperCommandContext(t, ctx, "hang", args...) + cmd.Stdin = newTickReader(1 * time.Millisecond) + cmd.Stderr = new(strings.Builder) + if interrupt == nil { + cmd.Cancel = nil + } else { + cmd.Cancel = func() error { + return cmd.Process.Signal(interrupt) + } + } + cmd.WaitDelay = waitDelay + out, err := cmd.StdoutPipe() + if err != nil { + t.Fatal(err) + } + + t.Log(cmd) + if err := cmd.Start(); err != nil { + t.Fatal(err) + } + + // Wait for cmd to close stdout to signal that its handlers are installed. + buf := new(strings.Builder) + if _, err := io.Copy(buf, out); err != nil { + t.Error(err) + cmd.Process.Kill() + cmd.Wait() + t.FailNow() + } + if buf.Len() > 0 { + t.Logf("stdout %v:\n%s", cmd.Args, buf) + } + + return cmd +} + +func TestWaitInterrupt(t *testing.T) { + t.Parallel() + + // tooLong is an arbitrary duration that is expected to be much longer than + // the test runs, but short enough that leaked processes will eventually exit + // on their own. + const tooLong = 10 * time.Minute + + // Control case: with no cancellation and no WaitDelay, we should wait for the + // process to exit. + t.Run("Wait", func(t *testing.T) { + t.Parallel() + cmd := startHang(t, context.Background(), 1*time.Millisecond, os.Kill, 0) + err := cmd.Wait() + t.Logf("stderr:\n%s", cmd.Stderr) + t.Logf("[%d] %v", cmd.Process.Pid, err) + + if err != nil { + t.Errorf("Wait: %v; want ", err) + } + if ps := cmd.ProcessState; !ps.Exited() { + t.Errorf("cmd did not exit: %v", ps) + } else if code := ps.ExitCode(); code != 0 { + t.Errorf("cmd.ProcessState.ExitCode() = %v; want 0", code) + } + }) + + // With a very long WaitDelay and no Cancel function, we should wait for the + // process to exit even if the command's Context is cancelled. + t.Run("WaitDelay", func(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skipf("skipping: os.Interrupt is not implemented on Windows") + } + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + cmd := startHang(t, ctx, tooLong, nil, tooLong, "-interrupt=true") + cancel() + + time.Sleep(1 * time.Millisecond) + // At this point cmd should still be running (because we passed nil to + // startHang for the cancel signal). Sending it an explicit Interrupt signal + // should succeed. + if err := cmd.Process.Signal(os.Interrupt); err != nil { + t.Error(err) + } + + err := cmd.Wait() + t.Logf("stderr:\n%s", cmd.Stderr) + t.Logf("[%d] %v", cmd.Process.Pid, err) + + // This program exits with status 0, + // but pretty much always does so during the wait delay. + // Since the Cmd itself didn't do anything to stop the process when the + // context expired, a successful exit is valid (even if late) and does + // not merit a non-nil error. + if err != nil { + t.Errorf("Wait: %v; want nil", err) + } + if ps := cmd.ProcessState; !ps.Exited() { + t.Errorf("cmd did not exit: %v", ps) + } else if code := ps.ExitCode(); code != 0 { + t.Errorf("cmd.ProcessState.ExitCode() = %v; want 0", code) + } + }) + + // If the context is cancelled and the Cancel function sends os.Kill, + // the process should be terminated immediately, and its output + // pipes should be closed (causing Wait to return) after WaitDelay + // even if a child process is still writing to them. + t.Run("SIGKILL-hang", func(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + cmd := startHang(t, ctx, tooLong, os.Kill, 10*time.Millisecond, "-subsleep=10m", "-probe=1ms") + cancel() + err := cmd.Wait() + t.Logf("stderr:\n%s", cmd.Stderr) + t.Logf("[%d] %v", cmd.Process.Pid, err) + + // This test should kill the child process after 10ms, + // leaving a grandchild process writing probes in a loop. + // The child process should be reported as failed, + // and the grandchild will exit (or die by SIGPIPE) once the + // stderr pipe is closed. + if ee := new(*exec.ExitError); !errors.As(err, ee) { + t.Errorf("Wait error = %v; want %T", err, *ee) + } + }) + + // If the process exits with status 0 but leaves a child behind writing + // to its output pipes, Wait should only wait for WaitDelay before + // closing the pipes and returning. Wait should return ErrWaitDelay + // to indicate that the piped output may be incomplete even though the + // command returned a “success” code. + t.Run("Exit-hang", func(t *testing.T) { + t.Parallel() + + cmd := startHang(t, context.Background(), 1*time.Millisecond, nil, 10*time.Millisecond, "-subsleep=10m", "-probe=1ms") + err := cmd.Wait() + t.Logf("stderr:\n%s", cmd.Stderr) + t.Logf("[%d] %v", cmd.Process.Pid, err) + + // This child process should exit immediately, + // leaving a grandchild process writing probes in a loop. + // Since the child has no ExitError to report but we did not + // read all of its output, Wait should return ErrWaitDelay. + if !errors.Is(err, exec.ErrWaitDelay) { + t.Errorf("Wait error = %v; want %T", err, exec.ErrWaitDelay) + } + }) + + // If the Cancel function sends a signal that the process can handle, and it + // handles that signal without actually exiting, then it should be terminated + // after the WaitDelay. + t.Run("SIGINT-ignored", func(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skipf("skipping: os.Interrupt is not implemented on Windows") + } + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + cmd := startHang(t, ctx, tooLong, os.Interrupt, 10*time.Millisecond, "-interrupt=false") + cancel() + err := cmd.Wait() + t.Logf("stderr:\n%s", cmd.Stderr) + t.Logf("[%d] %v", cmd.Process.Pid, err) + + // This command ignores SIGINT, sleeping until it is killed. + // Wait should return the usual error for a killed process. + if ee := new(*exec.ExitError); !errors.As(err, ee) { + t.Errorf("Wait error = %v; want %T", err, *ee) + } + }) + + // If the process handles the cancellation signal and exits with status 0, + // Wait should report a non-nil error (because the process had to be + // interrupted), and it should be a context error (because there is no error + // to report from the child process itself). + t.Run("SIGINT-handled", func(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skipf("skipping: os.Interrupt is not implemented on Windows") + } + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + cmd := startHang(t, ctx, tooLong, os.Interrupt, 0, "-interrupt=true") + cancel() + err := cmd.Wait() + t.Logf("stderr:\n%s", cmd.Stderr) + t.Logf("[%d] %v", cmd.Process.Pid, err) + + if !errors.Is(err, ctx.Err()) { + t.Errorf("Wait error = %v; want %v", err, ctx.Err()) + } + if ps := cmd.ProcessState; !ps.Exited() { + t.Errorf("cmd did not exit: %v", ps) + } else if code := ps.ExitCode(); code != 0 { + t.Errorf("cmd.ProcessState.ExitCode() = %v; want 0", code) + } + }) + + // If the Cancel function sends SIGQUIT, it should be handled in the usual + // way: a Go program should dump its goroutines and exit with non-success + // status. (We expect SIGQUIT to be a common pattern in real-world use.) + t.Run("SIGQUIT", func(t *testing.T) { + if quitSignal == nil { + t.Skipf("skipping: SIGQUIT is not supported on %v", runtime.GOOS) + } + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + cmd := startHang(t, ctx, tooLong, quitSignal, 0) + cancel() + err := cmd.Wait() + t.Logf("stderr:\n%s", cmd.Stderr) + t.Logf("[%d] %v", cmd.Process.Pid, err) + + if ee := new(*exec.ExitError); !errors.As(err, ee) { + t.Errorf("Wait error = %v; want %v", err, ctx.Err()) + } + + if ps := cmd.ProcessState; !ps.Exited() { + t.Errorf("cmd did not exit: %v", ps) + } else if code := ps.ExitCode(); code != 2 { + // The default os/signal handler exits with code 2. + t.Errorf("cmd.ProcessState.ExitCode() = %v; want 2", code) + } + + if !strings.Contains(fmt.Sprint(cmd.Stderr), "\n\ngoroutine ") { + t.Errorf("cmd.Stderr does not contain a goroutine dump") + } + }) +} + +func TestCancelErrors(t *testing.T) { + t.Parallel() + + // If Cancel returns a non-ErrProcessDone error and the process + // exits successfully, Wait should wrap the error from Cancel. + t.Run("success after error", func(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + cmd := helperCommandContext(t, ctx, "pipetest") + stdin, err := cmd.StdinPipe() + if err != nil { + t.Fatal(err) + } + + errArbitrary := errors.New("arbitrary error") + cmd.Cancel = func() error { + stdin.Close() + t.Logf("Cancel returning %v", errArbitrary) + return errArbitrary + } + if err := cmd.Start(); err != nil { + t.Fatal(err) + } + cancel() + + err = cmd.Wait() + t.Logf("[%d] %v", cmd.Process.Pid, err) + if !errors.Is(err, errArbitrary) || err == errArbitrary { + t.Errorf("Wait error = %v; want an error wrapping %v", err, errArbitrary) + } + }) + + // If Cancel returns an error equivalent to ErrProcessDone, + // Wait should ignore that error. (ErrProcessDone indicates that the + // process was already done before we tried to interrupt it — maybe we + // just didn't notice because Wait hadn't been called yet.) + t.Run("success after ErrProcessDone", func(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + cmd := helperCommandContext(t, ctx, "pipetest") + stdin, err := cmd.StdinPipe() + if err != nil { + t.Fatal(err) + } + + stdout, err := cmd.StdoutPipe() + if err != nil { + t.Fatal(err) + } + + // We intentionally race Cancel against the process exiting, + // but ensure that the process wins the race (and return ErrProcessDone + // from Cancel to report that). + interruptCalled := make(chan struct{}) + done := make(chan struct{}) + cmd.Cancel = func() error { + close(interruptCalled) + <-done + t.Logf("Cancel returning an error wrapping ErrProcessDone") + return fmt.Errorf("%w: stdout closed", os.ErrProcessDone) + } + + if err := cmd.Start(); err != nil { + t.Fatal(err) + } + + cancel() + <-interruptCalled + stdin.Close() + io.Copy(io.Discard, stdout) // reaches EOF when the process exits + close(done) + + err = cmd.Wait() + t.Logf("[%d] %v", cmd.Process.Pid, err) + if err != nil { + t.Errorf("Wait error = %v; want nil", err) + } + }) + + // If Cancel returns an error and the process is killed after + // WaitDelay, Wait should report the usual SIGKILL ExitError, not the + // error from Cancel. + t.Run("killed after error", func(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + cmd := helperCommandContext(t, ctx, "pipetest") + stdin, err := cmd.StdinPipe() + if err != nil { + t.Fatal(err) + } + defer stdin.Close() + + errArbitrary := errors.New("arbitrary error") + var interruptCalled atomic.Bool + cmd.Cancel = func() error { + t.Logf("Cancel called") + interruptCalled.Store(true) + return errArbitrary + } + cmd.WaitDelay = 1 * time.Millisecond + if err := cmd.Start(); err != nil { + t.Fatal(err) + } + cancel() + + err = cmd.Wait() + t.Logf("[%d] %v", cmd.Process.Pid, err) + + // Ensure that Cancel actually had the opportunity to + // return the error. + if !interruptCalled.Load() { + t.Errorf("Cancel was not called when the context was canceled") + } + + // This test should kill the child process after 1ms, + // To maximize compatibility with existing uses of exec.CommandContext, the + // resulting error should be an exec.ExitError without additional wrapping. + if ee, ok := err.(*exec.ExitError); !ok { + t.Errorf("Wait error = %v; want %T", err, *ee) + } + }) + + // If Cancel returns ErrProcessDone but the process is not actually done + // (and has to be killed), Wait should report the usual SIGKILL ExitError, + // not the error from Cancel. + t.Run("killed after spurious ErrProcessDone", func(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + cmd := helperCommandContext(t, ctx, "pipetest") + stdin, err := cmd.StdinPipe() + if err != nil { + t.Fatal(err) + } + defer stdin.Close() + + var interruptCalled atomic.Bool + cmd.Cancel = func() error { + t.Logf("Cancel returning an error wrapping ErrProcessDone") + interruptCalled.Store(true) + return fmt.Errorf("%w: stdout closed", os.ErrProcessDone) + } + cmd.WaitDelay = 1 * time.Millisecond + if err := cmd.Start(); err != nil { + t.Fatal(err) + } + cancel() + + err = cmd.Wait() + t.Logf("[%d] %v", cmd.Process.Pid, err) + + // Ensure that Cancel actually had the opportunity to + // return the error. + if !interruptCalled.Load() { + t.Errorf("Cancel was not called when the context was canceled") + } + + // This test should kill the child process after 1ms, + // To maximize compatibility with existing uses of exec.CommandContext, the + // resulting error should be an exec.ExitError without additional wrapping. + if ee, ok := err.(*exec.ExitError); !ok { + t.Errorf("Wait error of type %T; want %T", err, ee) + } + }) + + // If Cancel returns an error and the process exits with an + // unsuccessful exit code, the process error should take precedence over the + // Cancel error. + t.Run("nonzero exit after error", func(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + cmd := helperCommandContext(t, ctx, "stderrfail") + stderr, err := cmd.StderrPipe() + if err != nil { + t.Fatal(err) + } + + errArbitrary := errors.New("arbitrary error") + interrupted := make(chan struct{}) + cmd.Cancel = func() error { + close(interrupted) + return errArbitrary + } + if err := cmd.Start(); err != nil { + t.Fatal(err) + } + cancel() + <-interrupted + io.Copy(io.Discard, stderr) + + err = cmd.Wait() + t.Logf("[%d] %v", cmd.Process.Pid, err) + + if ee, ok := err.(*exec.ExitError); !ok || ee.ProcessState.ExitCode() != 1 { + t.Errorf("Wait error = %v; want exit status 1", err) + } + }) +} + +// TestConcurrentExec is a regression test for https://go.dev/issue/61080. +// +// Forking multiple child processes concurrently would sometimes hang on darwin. +// (This test hung on a gomote with -count=100 after only a few iterations.) +func TestConcurrentExec(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + + // This test will spawn nHangs subprocesses that hang reading from stdin, + // and nExits subprocesses that exit immediately. + // + // When issue #61080 was present, a long-lived "hang" subprocess would + // occasionally inherit the fork/exec status pipe from an "exit" subprocess, + // causing the parent process (which expects to see an EOF on that pipe almost + // immediately) to unexpectedly block on reading from the pipe. + var ( + nHangs = runtime.GOMAXPROCS(0) + nExits = runtime.GOMAXPROCS(0) + hangs, exits sync.WaitGroup + ) + hangs.Add(nHangs) + exits.Add(nExits) + + // ready is done when the goroutines have done as much work as possible to + // prepare to create subprocesses. It isn't strictly necessary for the test, + // but helps to increase the repro rate by making it more likely that calls to + // syscall.StartProcess for the "hang" and "exit" goroutines overlap. + var ready sync.WaitGroup + ready.Add(nHangs + nExits) + + for i := 0; i < nHangs; i++ { + go func() { + defer hangs.Done() + + cmd := helperCommandContext(t, ctx, "pipetest") + stdin, err := cmd.StdinPipe() + if err != nil { + ready.Done() + t.Error(err) + return + } + cmd.Cancel = stdin.Close + ready.Done() + + ready.Wait() + if err := cmd.Start(); err != nil { + if !errors.Is(err, context.Canceled) { + t.Error(err) + } + return + } + + cmd.Wait() + }() + } + + for i := 0; i < nExits; i++ { + go func() { + defer exits.Done() + + cmd := helperCommandContext(t, ctx, "exit", "0") + ready.Done() + + ready.Wait() + if err := cmd.Run(); err != nil { + t.Error(err) + } + }() + } + + exits.Wait() + cancel() + hangs.Wait() +} diff --git a/src/os/exec/exec_unix.go b/src/os/exec/exec_unix.go new file mode 100644 index 0000000..3ed672a --- /dev/null +++ b/src/os/exec/exec_unix.go @@ -0,0 +1,24 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build !plan9 && !windows + +package exec + +import ( + "io/fs" + "syscall" +) + +// skipStdinCopyError optionally specifies a function which reports +// whether the provided stdin copy error should be ignored. +func skipStdinCopyError(err error) bool { + // Ignore EPIPE errors copying to stdin if the program + // completed successfully otherwise. + // See Issue 9173. + pe, ok := err.(*fs.PathError) + return ok && + pe.Op == "write" && pe.Path == "|1" && + pe.Err == syscall.EPIPE +} diff --git a/src/os/exec/exec_unix_test.go b/src/os/exec/exec_unix_test.go new file mode 100644 index 0000000..d26c93a --- /dev/null +++ b/src/os/exec/exec_unix_test.go @@ -0,0 +1,17 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build unix + +package exec_test + +import ( + "os" + "syscall" +) + +var ( + quitSignal os.Signal = syscall.SIGQUIT + pipeSignal os.Signal = syscall.SIGPIPE +) diff --git a/src/os/exec/exec_windows.go b/src/os/exec/exec_windows.go new file mode 100644 index 0000000..e7a2ee6 --- /dev/null +++ b/src/os/exec/exec_windows.go @@ -0,0 +1,23 @@ +// Copyright 2017 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package exec + +import ( + "io/fs" + "syscall" +) + +// skipStdinCopyError optionally specifies a function which reports +// whether the provided stdin copy error should be ignored. +func skipStdinCopyError(err error) bool { + // Ignore ERROR_BROKEN_PIPE and ERROR_NO_DATA errors copying + // to stdin if the program completed successfully otherwise. + // See Issue 20445. + const _ERROR_NO_DATA = syscall.Errno(0xe8) + pe, ok := err.(*fs.PathError) + return ok && + pe.Op == "write" && pe.Path == "|1" && + (pe.Err == syscall.ERROR_BROKEN_PIPE || pe.Err == _ERROR_NO_DATA) +} diff --git a/src/os/exec/exec_windows_test.go b/src/os/exec/exec_windows_test.go new file mode 100644 index 0000000..efd3710 --- /dev/null +++ b/src/os/exec/exec_windows_test.go @@ -0,0 +1,109 @@ +// Copyright 2021 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build windows + +package exec_test + +import ( + "fmt" + "internal/testenv" + "io" + "os" + "os/exec" + "strconv" + "strings" + "syscall" + "testing" +) + +var ( + quitSignal os.Signal = nil + pipeSignal os.Signal = syscall.SIGPIPE +) + +func init() { + registerHelperCommand("pipehandle", cmdPipeHandle) +} + +func cmdPipeHandle(args ...string) { + handle, _ := strconv.ParseUint(args[0], 16, 64) + pipe := os.NewFile(uintptr(handle), "") + _, err := fmt.Fprint(pipe, args[1]) + if err != nil { + fmt.Fprintf(os.Stderr, "writing to pipe failed: %v\n", err) + os.Exit(1) + } + pipe.Close() +} + +func TestPipePassing(t *testing.T) { + t.Parallel() + + r, w, err := os.Pipe() + if err != nil { + t.Error(err) + } + const marker = "arrakis, dune, desert planet" + childProc := helperCommand(t, "pipehandle", strconv.FormatUint(uint64(w.Fd()), 16), marker) + childProc.SysProcAttr = &syscall.SysProcAttr{AdditionalInheritedHandles: []syscall.Handle{syscall.Handle(w.Fd())}} + err = childProc.Start() + if err != nil { + t.Error(err) + } + w.Close() + response, err := io.ReadAll(r) + if err != nil { + t.Error(err) + } + r.Close() + if string(response) != marker { + t.Errorf("got %q; want %q", string(response), marker) + } + err = childProc.Wait() + if err != nil { + t.Error(err) + } +} + +func TestNoInheritHandles(t *testing.T) { + t.Parallel() + + cmd := testenv.Command(t, "cmd", "/c exit 88") + cmd.SysProcAttr = &syscall.SysProcAttr{NoInheritHandles: true} + err := cmd.Run() + exitError, ok := err.(*exec.ExitError) + if !ok { + t.Fatalf("got error %v; want ExitError", err) + } + if exitError.ExitCode() != 88 { + t.Fatalf("got exit code %d; want 88", exitError.ExitCode()) + } +} + +// start a child process without the user code explicitly starting +// with a copy of the parent's SYSTEMROOT. +// (See issue 25210.) +func TestChildCriticalEnv(t *testing.T) { + t.Parallel() + cmd := helperCommand(t, "echoenv", "SYSTEMROOT") + + // Explicitly remove SYSTEMROOT from the command's environment. + var env []string + for _, kv := range cmd.Environ() { + k, _, ok := strings.Cut(kv, "=") + if !ok || !strings.EqualFold(k, "SYSTEMROOT") { + env = append(env, kv) + } + } + cmd.Env = env + + out, err := cmd.CombinedOutput() + if err != nil { + t.Fatal(err) + } + if strings.TrimSpace(string(out)) == "" { + t.Error("no SYSTEMROOT found") + } +} diff --git a/src/os/exec/internal/fdtest/exists_plan9.go b/src/os/exec/internal/fdtest/exists_plan9.go new file mode 100644 index 0000000..8886e06 --- /dev/null +++ b/src/os/exec/internal/fdtest/exists_plan9.go @@ -0,0 +1,20 @@ +// Copyright 2021 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build plan9 + +package fdtest + +import ( + "syscall" +) + +const errBadFd = syscall.ErrorString("fd out of range or not open") + +// Exists returns true if fd is a valid file descriptor. +func Exists(fd uintptr) bool { + var buf [1]byte + _, err := syscall.Fstat(int(fd), buf[:]) + return err != errBadFd +} diff --git a/src/os/exec/internal/fdtest/exists_test.go b/src/os/exec/internal/fdtest/exists_test.go new file mode 100644 index 0000000..a02dddf --- /dev/null +++ b/src/os/exec/internal/fdtest/exists_test.go @@ -0,0 +1,21 @@ +// Copyright 2021 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package fdtest + +import ( + "os" + "runtime" + "testing" +) + +func TestExists(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("Exists not implemented for windows") + } + + if !Exists(os.Stdout.Fd()) { + t.Errorf("Exists(%d) got false want true", os.Stdout.Fd()) + } +} diff --git a/src/os/exec/internal/fdtest/exists_unix.go b/src/os/exec/internal/fdtest/exists_unix.go new file mode 100644 index 0000000..472a802 --- /dev/null +++ b/src/os/exec/internal/fdtest/exists_unix.go @@ -0,0 +1,19 @@ +// Copyright 2021 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build unix || wasm + +// Package fdtest provides test helpers for working with file descriptors across exec. +package fdtest + +import ( + "syscall" +) + +// Exists returns true if fd is a valid file descriptor. +func Exists(fd uintptr) bool { + var s syscall.Stat_t + err := syscall.Fstat(int(fd), &s) + return err != syscall.EBADF +} diff --git a/src/os/exec/internal/fdtest/exists_windows.go b/src/os/exec/internal/fdtest/exists_windows.go new file mode 100644 index 0000000..72b8ccf --- /dev/null +++ b/src/os/exec/internal/fdtest/exists_windows.go @@ -0,0 +1,12 @@ +// Copyright 2021 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build windows + +package fdtest + +// Exists is not implemented on windows and panics. +func Exists(fd uintptr) bool { + panic("unimplemented") +} diff --git a/src/os/exec/internal_test.go b/src/os/exec/internal_test.go new file mode 100644 index 0000000..68d517f --- /dev/null +++ b/src/os/exec/internal_test.go @@ -0,0 +1,61 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package exec + +import ( + "io" + "testing" +) + +func TestPrefixSuffixSaver(t *testing.T) { + tests := []struct { + N int + writes []string + want string + }{ + { + N: 2, + writes: nil, + want: "", + }, + { + N: 2, + writes: []string{"a"}, + want: "a", + }, + { + N: 2, + writes: []string{"abc", "d"}, + want: "abcd", + }, + { + N: 2, + writes: []string{"abc", "d", "e"}, + want: "ab\n... omitting 1 bytes ...\nde", + }, + { + N: 2, + writes: []string{"ab______________________yz"}, + want: "ab\n... omitting 22 bytes ...\nyz", + }, + { + N: 2, + writes: []string{"ab_______________________y", "z"}, + want: "ab\n... omitting 23 bytes ...\nyz", + }, + } + for i, tt := range tests { + w := &prefixSuffixSaver{N: tt.N} + for _, s := range tt.writes { + n, err := io.WriteString(w, s) + if err != nil || n != len(s) { + t.Errorf("%d. WriteString(%q) = %v, %v; want %v, %v", i, s, n, err, len(s), nil) + } + } + if got := string(w.Bytes()); got != tt.want { + t.Errorf("%d. Bytes = %q; want %q", i, got, tt.want) + } + } +} diff --git a/src/os/exec/lp_linux_test.go b/src/os/exec/lp_linux_test.go new file mode 100644 index 0000000..60cb13e --- /dev/null +++ b/src/os/exec/lp_linux_test.go @@ -0,0 +1,88 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package exec_test + +import ( + "errors" + "internal/syscall/unix" + "internal/testenv" + "os" + "os/exec" + "path/filepath" + "syscall" + "testing" +) + +func TestFindExecutableVsNoexec(t *testing.T) { + t.Parallel() + + // This test case relies on faccessat2(2) syscall, which appeared in Linux v5.8. + if major, minor := unix.KernelVersion(); major < 5 || (major == 5 && minor < 8) { + t.Skip("requires Linux kernel v5.8 with faccessat2(2) syscall") + } + + tmp := t.TempDir() + + // Create a tmpfs mount. + err := syscall.Mount("tmpfs", tmp, "tmpfs", 0, "") + if testenv.SyscallIsNotSupported(err) { + // Usually this means lack of CAP_SYS_ADMIN, but there might be + // other reasons, especially in restricted test environments. + t.Skipf("requires ability to mount tmpfs (%v)", err) + } else if err != nil { + t.Fatalf("mount %s failed: %v", tmp, err) + } + t.Cleanup(func() { + if err := syscall.Unmount(tmp, 0); err != nil { + t.Error(err) + } + }) + + // Create an executable. + path := filepath.Join(tmp, "program") + err = os.WriteFile(path, []byte("#!/bin/sh\necho 123\n"), 0o755) + if err != nil { + t.Fatal(err) + } + + // Check that it works as expected. + _, err = exec.LookPath(path) + if err != nil { + t.Fatalf("findExecutable: got %v, want nil", err) + } + + for { + err = exec.Command(path).Run() + if err == nil { + break + } + if errors.Is(err, syscall.ETXTBSY) { + // A fork+exec in another process may be holding open the FD that we used + // to write the executable (see https://go.dev/issue/22315). + // Since the descriptor should have CLOEXEC set, the problem should resolve + // as soon as the forked child reaches its exec call. + // Keep retrying until that happens. + } else { + t.Fatalf("exec: got %v, want nil", err) + } + } + + // Remount with noexec flag. + err = syscall.Mount("", tmp, "", syscall.MS_REMOUNT|syscall.MS_NOEXEC, "") + if testenv.SyscallIsNotSupported(err) { + t.Skipf("requires ability to re-mount tmpfs (%v)", err) + } else if err != nil { + t.Fatalf("remount %s with noexec failed: %v", tmp, err) + } + + if err := exec.Command(path).Run(); err == nil { + t.Fatal("exec on noexec filesystem: got nil, want error") + } + + _, err = exec.LookPath(path) + if err == nil { + t.Fatalf("LookPath: got nil, want error") + } +} diff --git a/src/os/exec/lp_plan9.go b/src/os/exec/lp_plan9.go new file mode 100644 index 0000000..9344b14 --- /dev/null +++ b/src/os/exec/lp_plan9.go @@ -0,0 +1,66 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package exec + +import ( + "errors" + "io/fs" + "os" + "path/filepath" + "strings" +) + +// ErrNotFound is the error resulting if a path search failed to find an executable file. +var ErrNotFound = errors.New("executable file not found in $path") + +func findExecutable(file string) error { + d, err := os.Stat(file) + if err != nil { + return err + } + if m := d.Mode(); !m.IsDir() && m&0111 != 0 { + return nil + } + return fs.ErrPermission +} + +// LookPath searches for an executable named file in the +// directories named by the path environment variable. +// If file begins with "/", "#", "./", or "../", it is tried +// directly and the path is not consulted. +// On success, the result is an absolute path. +// +// In older versions of Go, LookPath could return a path relative to the current directory. +// As of Go 1.19, LookPath will instead return that path along with an error satisfying +// errors.Is(err, ErrDot). See the package documentation for more details. +func LookPath(file string) (string, error) { + // skip the path lookup for these prefixes + skip := []string{"/", "#", "./", "../"} + + for _, p := range skip { + if strings.HasPrefix(file, p) { + err := findExecutable(file) + if err == nil { + return file, nil + } + return "", &Error{file, err} + } + } + + path := os.Getenv("path") + for _, dir := range filepath.SplitList(path) { + path := filepath.Join(dir, file) + if err := findExecutable(path); err == nil { + if !filepath.IsAbs(path) { + if execerrdot.Value() != "0" { + return path, &Error{file, ErrDot} + } + execerrdot.IncNonDefault() + } + return path, nil + } + } + return "", &Error{file, ErrNotFound} +} diff --git a/src/os/exec/lp_test.go b/src/os/exec/lp_test.go new file mode 100644 index 0000000..77d8e84 --- /dev/null +++ b/src/os/exec/lp_test.go @@ -0,0 +1,33 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package exec + +import ( + "testing" +) + +var nonExistentPaths = []string{ + "some-non-existent-path", + "non-existent-path/slashed", +} + +func TestLookPathNotFound(t *testing.T) { + for _, name := range nonExistentPaths { + path, err := LookPath(name) + if err == nil { + t.Fatalf("LookPath found %q in $PATH", name) + } + if path != "" { + t.Fatalf("LookPath path == %q when err != nil", path) + } + perr, ok := err.(*Error) + if !ok { + t.Fatal("LookPath error is not an exec.Error") + } + if perr.Name != name { + t.Fatalf("want Error name %q, got %q", name, perr.Name) + } + } +} diff --git a/src/os/exec/lp_unix.go b/src/os/exec/lp_unix.go new file mode 100644 index 0000000..fd2c6ef --- /dev/null +++ b/src/os/exec/lp_unix.go @@ -0,0 +1,82 @@ +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build unix + +package exec + +import ( + "errors" + "internal/syscall/unix" + "io/fs" + "os" + "path/filepath" + "strings" + "syscall" +) + +// ErrNotFound is the error resulting if a path search failed to find an executable file. +var ErrNotFound = errors.New("executable file not found in $PATH") + +func findExecutable(file string) error { + d, err := os.Stat(file) + if err != nil { + return err + } + m := d.Mode() + if m.IsDir() { + return syscall.EISDIR + } + err = unix.Eaccess(file, unix.X_OK) + // ENOSYS means Eaccess is not available or not implemented. + // EPERM can be returned by Linux containers employing seccomp. + // In both cases, fall back to checking the permission bits. + if err == nil || (err != syscall.ENOSYS && err != syscall.EPERM) { + return err + } + if m&0111 != 0 { + return nil + } + return fs.ErrPermission +} + +// LookPath searches for an executable named file in the +// directories named by the PATH environment variable. +// If file contains a slash, it is tried directly and the PATH is not consulted. +// Otherwise, on success, the result is an absolute path. +// +// In older versions of Go, LookPath could return a path relative to the current directory. +// As of Go 1.19, LookPath will instead return that path along with an error satisfying +// errors.Is(err, ErrDot). See the package documentation for more details. +func LookPath(file string) (string, error) { + // NOTE(rsc): I wish we could use the Plan 9 behavior here + // (only bypass the path if file begins with / or ./ or ../) + // but that would not match all the Unix shells. + + if strings.Contains(file, "/") { + err := findExecutable(file) + if err == nil { + return file, nil + } + return "", &Error{file, err} + } + path := os.Getenv("PATH") + for _, dir := range filepath.SplitList(path) { + if dir == "" { + // Unix shell semantics: path element "" means "." + dir = "." + } + path := filepath.Join(dir, file) + if err := findExecutable(path); err == nil { + if !filepath.IsAbs(path) { + if execerrdot.Value() != "0" { + return path, &Error{file, ErrDot} + } + execerrdot.IncNonDefault() + } + return path, nil + } + } + return "", &Error{file, ErrNotFound} +} diff --git a/src/os/exec/lp_unix_test.go b/src/os/exec/lp_unix_test.go new file mode 100644 index 0000000..181b1f0 --- /dev/null +++ b/src/os/exec/lp_unix_test.go @@ -0,0 +1,50 @@ +// Copyright 2013 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build unix + +package exec + +import ( + "os" + "testing" +) + +func TestLookPathUnixEmptyPath(t *testing.T) { + // Not parallel: uses os.Chdir. + + tmp, err := os.MkdirTemp("", "TestLookPathUnixEmptyPath") + if err != nil { + t.Fatal("TempDir failed: ", err) + } + defer os.RemoveAll(tmp) + wd, err := os.Getwd() + if err != nil { + t.Fatal("Getwd failed: ", err) + } + err = os.Chdir(tmp) + if err != nil { + t.Fatal("Chdir failed: ", err) + } + defer os.Chdir(wd) + + f, err := os.OpenFile("exec_me", os.O_CREATE|os.O_EXCL, 0700) + if err != nil { + t.Fatal("OpenFile failed: ", err) + } + err = f.Close() + if err != nil { + t.Fatal("Close failed: ", err) + } + + t.Setenv("PATH", "") + + path, err := LookPath("exec_me") + if err == nil { + t.Fatal("LookPath found exec_me in empty $PATH") + } + if path != "" { + t.Fatalf("LookPath path == %q when err != nil", path) + } +} diff --git a/src/os/exec/lp_wasm.go b/src/os/exec/lp_wasm.go new file mode 100644 index 0000000..f2c8e9c --- /dev/null +++ b/src/os/exec/lp_wasm.go @@ -0,0 +1,23 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build wasm + +package exec + +import ( + "errors" +) + +// ErrNotFound is the error resulting if a path search failed to find an executable file. +var ErrNotFound = errors.New("executable file not found in $PATH") + +// LookPath searches for an executable named file in the +// directories named by the PATH environment variable. +// If file contains a slash, it is tried directly and the PATH is not consulted. +// The result may be an absolute path or a path relative to the current directory. +func LookPath(file string) (string, error) { + // Wasm can not execute processes, so act as if there are no executables at all. + return "", &Error{file, ErrNotFound} +} diff --git a/src/os/exec/lp_windows.go b/src/os/exec/lp_windows.go new file mode 100644 index 0000000..066d38d --- /dev/null +++ b/src/os/exec/lp_windows.go @@ -0,0 +1,145 @@ +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package exec + +import ( + "errors" + "io/fs" + "os" + "path/filepath" + "strings" + "syscall" +) + +// ErrNotFound is the error resulting if a path search failed to find an executable file. +var ErrNotFound = errors.New("executable file not found in %PATH%") + +func chkStat(file string) error { + d, err := os.Stat(file) + if err != nil { + return err + } + if d.IsDir() { + return fs.ErrPermission + } + return nil +} + +func hasExt(file string) bool { + i := strings.LastIndex(file, ".") + if i < 0 { + return false + } + return strings.LastIndexAny(file, `:\/`) < i +} + +func findExecutable(file string, exts []string) (string, error) { + if len(exts) == 0 { + return file, chkStat(file) + } + if hasExt(file) { + if chkStat(file) == nil { + return file, nil + } + } + for _, e := range exts { + if f := file + e; chkStat(f) == nil { + return f, nil + } + } + return "", fs.ErrNotExist +} + +// LookPath searches for an executable named file in the +// directories named by the PATH environment variable. +// LookPath also uses PATHEXT environment variable to match +// a suitable candidate. +// If file contains a slash, it is tried directly and the PATH is not consulted. +// Otherwise, on success, the result is an absolute path. +// +// In older versions of Go, LookPath could return a path relative to the current directory. +// As of Go 1.19, LookPath will instead return that path along with an error satisfying +// errors.Is(err, ErrDot). See the package documentation for more details. +func LookPath(file string) (string, error) { + var exts []string + x := os.Getenv(`PATHEXT`) + if x != "" { + for _, e := range strings.Split(strings.ToLower(x), `;`) { + if e == "" { + continue + } + if e[0] != '.' { + e = "." + e + } + exts = append(exts, e) + } + } else { + exts = []string{".com", ".exe", ".bat", ".cmd"} + } + + if strings.ContainsAny(file, `:\/`) { + f, err := findExecutable(file, exts) + if err == nil { + return f, nil + } + return "", &Error{file, err} + } + + // On Windows, creating the NoDefaultCurrentDirectoryInExePath + // environment variable (with any value or no value!) signals that + // path lookups should skip the current directory. + // In theory we are supposed to call NeedCurrentDirectoryForExePathW + // "as the registry location of this environment variable can change" + // but that seems exceedingly unlikely: it would break all users who + // have configured their environment this way! + // https://docs.microsoft.com/en-us/windows/win32/api/processenv/nf-processenv-needcurrentdirectoryforexepathw + // See also go.dev/issue/43947. + var ( + dotf string + dotErr error + ) + if _, found := syscall.Getenv("NoDefaultCurrentDirectoryInExePath"); !found { + if f, err := findExecutable(filepath.Join(".", file), exts); err == nil { + if execerrdot.Value() == "0" { + execerrdot.IncNonDefault() + return f, nil + } + dotf, dotErr = f, &Error{file, ErrDot} + } + } + + path := os.Getenv("path") + for _, dir := range filepath.SplitList(path) { + if f, err := findExecutable(filepath.Join(dir, file), exts); err == nil { + if dotErr != nil { + // https://go.dev/issue/53536: if we resolved a relative path implicitly, + // and it is the same executable that would be resolved from the explicit %PATH%, + // prefer the explicit name for the executable (and, likely, no error) instead + // of the equivalent implicit name with ErrDot. + // + // Otherwise, return the ErrDot for the implicit path as soon as we find + // out that the explicit one doesn't match. + dotfi, dotfiErr := os.Lstat(dotf) + fi, fiErr := os.Lstat(f) + if dotfiErr != nil || fiErr != nil || !os.SameFile(dotfi, fi) { + return dotf, dotErr + } + } + + if !filepath.IsAbs(f) { + if execerrdot.Value() != "0" { + return f, &Error{file, ErrDot} + } + execerrdot.IncNonDefault() + } + return f, nil + } + } + + if dotErr != nil { + return dotf, dotErr + } + return "", &Error{file, ErrNotFound} +} diff --git a/src/os/exec/lp_windows_test.go b/src/os/exec/lp_windows_test.go new file mode 100644 index 0000000..4d85a5f --- /dev/null +++ b/src/os/exec/lp_windows_test.go @@ -0,0 +1,612 @@ +// Copyright 2013 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Use an external test to avoid os/exec -> internal/testenv -> os/exec +// circular dependency. + +package exec_test + +import ( + "errors" + "fmt" + "internal/testenv" + "io" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + "testing" +) + +func init() { + registerHelperCommand("exec", cmdExec) + registerHelperCommand("lookpath", cmdLookPath) +} + +func cmdLookPath(args ...string) { + p, err := exec.LookPath(args[0]) + if err != nil { + fmt.Fprintf(os.Stderr, "LookPath failed: %v\n", err) + os.Exit(1) + } + fmt.Print(p) +} + +func cmdExec(args ...string) { + cmd := exec.Command(args[1]) + cmd.Dir = args[0] + if errors.Is(cmd.Err, exec.ErrDot) { + cmd.Err = nil + } + output, err := cmd.CombinedOutput() + if err != nil { + fmt.Fprintf(os.Stderr, "Child: %s %s", err, string(output)) + os.Exit(1) + } + fmt.Printf("%s", string(output)) +} + +func installExe(t *testing.T, dest, src string) { + fsrc, err := os.Open(src) + if err != nil { + t.Fatal("os.Open failed: ", err) + } + defer fsrc.Close() + fdest, err := os.Create(dest) + if err != nil { + t.Fatal("os.Create failed: ", err) + } + defer fdest.Close() + _, err = io.Copy(fdest, fsrc) + if err != nil { + t.Fatal("io.Copy failed: ", err) + } +} + +func installBat(t *testing.T, dest string) { + f, err := os.Create(dest) + if err != nil { + t.Fatalf("failed to create batch file: %v", err) + } + defer f.Close() + fmt.Fprintf(f, "@echo %s\n", dest) +} + +func installProg(t *testing.T, dest, srcExe string) { + err := os.MkdirAll(filepath.Dir(dest), 0700) + if err != nil { + t.Fatal("os.MkdirAll failed: ", err) + } + if strings.ToLower(filepath.Ext(dest)) == ".bat" { + installBat(t, dest) + return + } + installExe(t, dest, srcExe) +} + +type lookPathTest struct { + rootDir string + PATH string + PATHEXT string + files []string + searchFor string + fails bool // test is expected to fail +} + +func (test lookPathTest) runProg(t *testing.T, env []string, cmd *exec.Cmd) (string, error) { + cmd.Env = env + cmd.Dir = test.rootDir + args := append([]string(nil), cmd.Args...) + args[0] = filepath.Base(args[0]) + cmdText := fmt.Sprintf("%q command", strings.Join(args, " ")) + out, err := cmd.CombinedOutput() + if (err != nil) != test.fails { + if test.fails { + t.Fatalf("test=%+v: %s succeeded, but expected to fail", test, cmdText) + } + t.Fatalf("test=%+v: %s failed, but expected to succeed: %v - %v", test, cmdText, err, string(out)) + } + if err != nil { + return "", fmt.Errorf("test=%+v: %s failed: %v - %v", test, cmdText, err, string(out)) + } + // normalise program output + p := string(out) + // trim terminating \r and \n that batch file outputs + for len(p) > 0 && (p[len(p)-1] == '\n' || p[len(p)-1] == '\r') { + p = p[:len(p)-1] + } + if !filepath.IsAbs(p) { + return p, nil + } + if p[:len(test.rootDir)] != test.rootDir { + t.Fatalf("test=%+v: %s output is wrong: %q must have %q prefix", test, cmdText, p, test.rootDir) + } + return p[len(test.rootDir)+1:], nil +} + +func updateEnv(env []string, name, value string) []string { + for i, e := range env { + if strings.HasPrefix(strings.ToUpper(e), name+"=") { + env[i] = name + "=" + value + return env + } + } + return append(env, name+"="+value) +} + +func createEnv(dir, PATH, PATHEXT string) []string { + env := os.Environ() + env = updateEnv(env, "PATHEXT", PATHEXT) + // Add dir in front of every directory in the PATH. + dirs := filepath.SplitList(PATH) + for i := range dirs { + dirs[i] = filepath.Join(dir, dirs[i]) + } + path := strings.Join(dirs, ";") + env = updateEnv(env, "PATH", os.Getenv("SystemRoot")+"/System32;"+path) + return env +} + +// createFiles copies srcPath file into multiply files. +// It uses dir as prefix for all destination files. +func createFiles(t *testing.T, dir string, files []string, srcPath string) { + for _, f := range files { + installProg(t, filepath.Join(dir, f), srcPath) + } +} + +func (test lookPathTest) run(t *testing.T, tmpdir, printpathExe string) { + test.rootDir = tmpdir + createFiles(t, test.rootDir, test.files, printpathExe) + env := createEnv(test.rootDir, test.PATH, test.PATHEXT) + // Run "cmd.exe /c test.searchFor" with new environment and + // work directory set. All candidates are copies of printpath.exe. + // These will output their program paths when run. + should, errCmd := test.runProg(t, env, testenv.Command(t, "cmd", "/c", test.searchFor)) + // Run the lookpath program with new environment and work directory set. + have, errLP := test.runProg(t, env, helperCommand(t, "lookpath", test.searchFor)) + // Compare results. + if errCmd == nil && errLP == nil { + // both succeeded + if should != have { + t.Fatalf("test=%+v:\ncmd /c ran: %s\nlookpath found: %s", test, should, have) + } + return + } + if errCmd != nil && errLP != nil { + // both failed -> continue + return + } + if errCmd != nil { + t.Fatal(errCmd) + } + if errLP != nil { + t.Fatal(errLP) + } +} + +var lookPathTests = []lookPathTest{ + { + PATHEXT: `.COM;.EXE;.BAT`, + PATH: `p1;p2`, + files: []string{`p1\a.exe`, `p2\a.exe`, `p2\a`}, + searchFor: `a`, + }, + { + PATHEXT: `.COM;.EXE;.BAT`, + PATH: `p1.dir;p2.dir`, + files: []string{`p1.dir\a`, `p2.dir\a.exe`}, + searchFor: `a`, + }, + { + PATHEXT: `.COM;.EXE;.BAT`, + PATH: `p1;p2`, + files: []string{`p1\a.exe`, `p2\a.exe`}, + searchFor: `a.exe`, + }, + { + PATHEXT: `.COM;.EXE;.BAT`, + PATH: `p1;p2`, + files: []string{`p1\a.exe`, `p2\b.exe`}, + searchFor: `b`, + }, + { + PATHEXT: `.COM;.EXE;.BAT`, + PATH: `p1;p2`, + files: []string{`p1\b`, `p2\a`}, + searchFor: `a`, + fails: true, // TODO(brainman): do not know why this fails + }, + // If the command name specifies a path, the shell searches + // the specified path for an executable file matching + // the command name. If a match is found, the external + // command (the executable file) executes. + { + PATHEXT: `.COM;.EXE;.BAT`, + PATH: `p1;p2`, + files: []string{`p1\a.exe`, `p2\a.exe`}, + searchFor: `p2\a`, + }, + // If the command name specifies a path, the shell searches + // the specified path for an executable file matching the command + // name. ... If no match is found, the shell reports an error + // and command processing completes. + { + PATHEXT: `.COM;.EXE;.BAT`, + PATH: `p1;p2`, + files: []string{`p1\b.exe`, `p2\a.exe`}, + searchFor: `p2\b`, + fails: true, + }, + // If the command name does not specify a path, the shell + // searches the current directory for an executable file + // matching the command name. If a match is found, the external + // command (the executable file) executes. + { + PATHEXT: `.COM;.EXE;.BAT`, + PATH: `p1;p2`, + files: []string{`a`, `p1\a.exe`, `p2\a.exe`}, + searchFor: `a`, + }, + // The shell now searches each directory specified by the + // PATH environment variable, in the order listed, for an + // executable file matching the command name. If a match + // is found, the external command (the executable file) executes. + { + PATHEXT: `.COM;.EXE;.BAT`, + PATH: `p1;p2`, + files: []string{`p1\a.exe`, `p2\a.exe`}, + searchFor: `a`, + }, + // The shell now searches each directory specified by the + // PATH environment variable, in the order listed, for an + // executable file matching the command name. If no match + // is found, the shell reports an error and command processing + // completes. + { + PATHEXT: `.COM;.EXE;.BAT`, + PATH: `p1;p2`, + files: []string{`p1\a.exe`, `p2\a.exe`}, + searchFor: `b`, + fails: true, + }, + // If the command name includes a file extension, the shell + // searches each directory for the exact file name specified + // by the command name. + { + PATHEXT: `.COM;.EXE;.BAT`, + PATH: `p1;p2`, + files: []string{`p1\a.exe`, `p2\a.exe`}, + searchFor: `a.exe`, + }, + { + PATHEXT: `.COM;.EXE;.BAT`, + PATH: `p1;p2`, + files: []string{`p1\a.exe`, `p2\a.exe`}, + searchFor: `a.com`, + fails: true, // includes extension and not exact file name match + }, + { + PATHEXT: `.COM;.EXE;.BAT`, + PATH: `p1`, + files: []string{`p1\a.exe.exe`}, + searchFor: `a.exe`, + }, + { + PATHEXT: `.COM;.BAT`, + PATH: `p1;p2`, + files: []string{`p1\a.exe`, `p2\a.exe`}, + searchFor: `a.exe`, + }, + // If the command name does not include a file extension, the shell + // adds the extensions listed in the PATHEXT environment variable, + // one by one, and searches the directory for that file name. Note + // that the shell tries all possible file extensions in a specific + // directory before moving on to search the next directory + // (if there is one). + { + PATHEXT: `.COM;.EXE`, + PATH: `p1;p2`, + files: []string{`p1\a.bat`, `p2\a.exe`}, + searchFor: `a`, + }, + { + PATHEXT: `.COM;.EXE;.BAT`, + PATH: `p1;p2`, + files: []string{`p1\a.bat`, `p2\a.exe`}, + searchFor: `a`, + }, + { + PATHEXT: `.COM;.EXE;.BAT`, + PATH: `p1;p2`, + files: []string{`p1\a.bat`, `p1\a.exe`, `p2\a.bat`, `p2\a.exe`}, + searchFor: `a`, + }, + { + PATHEXT: `.COM`, + PATH: `p1;p2`, + files: []string{`p1\a.bat`, `p2\a.exe`}, + searchFor: `a`, + fails: true, // tried all extensions in PATHEXT, but none matches + }, +} + +func TestLookPathWindows(t *testing.T) { + if testing.Short() { + maySkipHelperCommand("lookpath") + t.Skipf("skipping test in short mode that would build a helper binary") + } + t.Parallel() + + tmp := t.TempDir() + printpathExe := buildPrintPathExe(t, tmp) + + // Run all tests. + for i, test := range lookPathTests { + i, test := i, test + t.Run(fmt.Sprint(i), func(t *testing.T) { + t.Parallel() + + dir := filepath.Join(tmp, "d"+strconv.Itoa(i)) + err := os.Mkdir(dir, 0700) + if err != nil { + t.Fatal("Mkdir failed: ", err) + } + test.run(t, dir, printpathExe) + }) + } +} + +type commandTest struct { + PATH string + files []string + dir string + arg0 string + want string + fails bool // test is expected to fail +} + +func (test commandTest) isSuccess(rootDir, output string, err error) error { + if err != nil { + return fmt.Errorf("test=%+v: exec: %v %v", test, err, output) + } + path := output + if path[:len(rootDir)] != rootDir { + return fmt.Errorf("test=%+v: %q must have %q prefix", test, path, rootDir) + } + path = path[len(rootDir)+1:] + if path != test.want { + return fmt.Errorf("test=%+v: want %q, got %q", test, test.want, path) + } + return nil +} + +func (test commandTest) runOne(t *testing.T, rootDir string, env []string, dir, arg0 string) { + cmd := helperCommand(t, "exec", dir, arg0) + cmd.Dir = rootDir + cmd.Env = env + output, err := cmd.CombinedOutput() + err = test.isSuccess(rootDir, string(output), err) + if (err != nil) != test.fails { + if test.fails { + t.Errorf("test=%+v: succeeded, but expected to fail", test) + } else { + t.Error(err) + } + } +} + +func (test commandTest) run(t *testing.T, rootDir, printpathExe string) { + createFiles(t, rootDir, test.files, printpathExe) + PATHEXT := `.COM;.EXE;.BAT` + env := createEnv(rootDir, test.PATH, PATHEXT) + test.runOne(t, rootDir, env, test.dir, test.arg0) +} + +var commandTests = []commandTest{ + // testing commands with no slash, like `a.exe` + { + // should find a.exe in current directory + files: []string{`a.exe`}, + arg0: `a.exe`, + want: `a.exe`, + }, + { + // like above, but add PATH in attempt to break the test + PATH: `p2;p`, + files: []string{`a.exe`, `p\a.exe`, `p2\a.exe`}, + arg0: `a.exe`, + want: `a.exe`, + }, + { + // like above, but use "a" instead of "a.exe" for command + PATH: `p2;p`, + files: []string{`a.exe`, `p\a.exe`, `p2\a.exe`}, + arg0: `a`, + want: `a.exe`, + }, + // testing commands with slash, like `.\a.exe` + { + // should find p\a.exe + files: []string{`p\a.exe`}, + arg0: `p\a.exe`, + want: `p\a.exe`, + }, + { + // like above, but adding `.` in front of executable should still be OK + files: []string{`p\a.exe`}, + arg0: `.\p\a.exe`, + want: `p\a.exe`, + }, + { + // like above, but with PATH added in attempt to break it + PATH: `p2`, + files: []string{`p\a.exe`, `p2\a.exe`}, + arg0: `p\a.exe`, + want: `p\a.exe`, + }, + { + // like above, but make sure .exe is tried even for commands with slash + PATH: `p2`, + files: []string{`p\a.exe`, `p2\a.exe`}, + arg0: `p\a`, + want: `p\a.exe`, + }, + // tests commands, like `a.exe`, with c.Dir set + { + // should not find a.exe in p, because LookPath(`a.exe`) will fail + files: []string{`p\a.exe`}, + dir: `p`, + arg0: `a.exe`, + want: `p\a.exe`, + fails: true, + }, + { + // LookPath(`a.exe`) will find `.\a.exe`, but prefixing that with + // dir `p\a.exe` will refer to a non-existent file + files: []string{`a.exe`, `p\not_important_file`}, + dir: `p`, + arg0: `a.exe`, + want: `a.exe`, + fails: true, + }, + { + // like above, but making test succeed by installing file + // in referred destination (so LookPath(`a.exe`) will still + // find `.\a.exe`, but we successfully execute `p\a.exe`) + files: []string{`a.exe`, `p\a.exe`}, + dir: `p`, + arg0: `a.exe`, + want: `p\a.exe`, + }, + { + // like above, but add PATH in attempt to break the test + PATH: `p2;p`, + files: []string{`a.exe`, `p\a.exe`, `p2\a.exe`}, + dir: `p`, + arg0: `a.exe`, + want: `p\a.exe`, + }, + { + // like above, but use "a" instead of "a.exe" for command + PATH: `p2;p`, + files: []string{`a.exe`, `p\a.exe`, `p2\a.exe`}, + dir: `p`, + arg0: `a`, + want: `p\a.exe`, + }, + { + // finds `a.exe` in the PATH regardless of dir set + // because LookPath returns full path in that case + PATH: `p2;p`, + files: []string{`p\a.exe`, `p2\a.exe`}, + dir: `p`, + arg0: `a.exe`, + want: `p2\a.exe`, + }, + // tests commands, like `.\a.exe`, with c.Dir set + { + // should use dir when command is path, like ".\a.exe" + files: []string{`p\a.exe`}, + dir: `p`, + arg0: `.\a.exe`, + want: `p\a.exe`, + }, + { + // like above, but with PATH added in attempt to break it + PATH: `p2`, + files: []string{`p\a.exe`, `p2\a.exe`}, + dir: `p`, + arg0: `.\a.exe`, + want: `p\a.exe`, + }, + { + // like above, but make sure .exe is tried even for commands with slash + PATH: `p2`, + files: []string{`p\a.exe`, `p2\a.exe`}, + dir: `p`, + arg0: `.\a`, + want: `p\a.exe`, + }, +} + +func TestCommand(t *testing.T) { + if testing.Short() { + maySkipHelperCommand("exec") + t.Skipf("skipping test in short mode that would build a helper binary") + } + t.Parallel() + + tmp := t.TempDir() + printpathExe := buildPrintPathExe(t, tmp) + + // Run all tests. + for i, test := range commandTests { + i, test := i, test + t.Run(fmt.Sprint(i), func(t *testing.T) { + t.Parallel() + + dir := filepath.Join(tmp, "d"+strconv.Itoa(i)) + err := os.Mkdir(dir, 0700) + if err != nil { + t.Fatal("Mkdir failed: ", err) + } + test.run(t, dir, printpathExe) + }) + } +} + +// buildPrintPathExe creates a Go program that prints its own path. +// dir is a temp directory where executable will be created. +// The function returns full path to the created program. +func buildPrintPathExe(t *testing.T, dir string) string { + const name = "printpath" + srcname := name + ".go" + err := os.WriteFile(filepath.Join(dir, srcname), []byte(printpathSrc), 0644) + if err != nil { + t.Fatalf("failed to create source: %v", err) + } + if err != nil { + t.Fatalf("failed to execute template: %v", err) + } + outname := name + ".exe" + cmd := testenv.Command(t, testenv.GoToolPath(t), "build", "-o", outname, srcname) + cmd.Dir = dir + out, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("failed to build executable: %v - %v", err, string(out)) + } + return filepath.Join(dir, outname) +} + +const printpathSrc = ` +package main + +import ( + "os" + "syscall" + "unsafe" +) + +func getMyName() (string, error) { + var sysproc = syscall.MustLoadDLL("kernel32.dll").MustFindProc("GetModuleFileNameW") + b := make([]uint16, syscall.MAX_PATH) + r, _, err := sysproc.Call(0, uintptr(unsafe.Pointer(&b[0])), uintptr(len(b))) + n := uint32(r) + if n == 0 { + return "", err + } + return syscall.UTF16ToString(b[0:n]), nil +} + +func main() { + path, err := getMyName() + if err != nil { + os.Stderr.Write([]byte("getMyName failed: " + err.Error() + "\n")) + os.Exit(1) + } + os.Stdout.Write([]byte(path)) +} +` diff --git a/src/os/exec/read3.go b/src/os/exec/read3.go new file mode 100644 index 0000000..8327d73 --- /dev/null +++ b/src/os/exec/read3.go @@ -0,0 +1,91 @@ +// Copyright 2020 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build ignore + +// This is a test program that verifies that it can read from +// descriptor 3 and that no other descriptors are open. +// This is not done via TestHelperProcess and GO_EXEC_TEST_PID +// because we want to ensure that this program does not use cgo, +// because C libraries can open file descriptors behind our backs +// and confuse the test. See issue 25628. +package main + +import ( + "fmt" + "internal/poll" + "io" + "os" + "os/exec" + "os/exec/internal/fdtest" + "runtime" + "strings" +) + +func main() { + fd3 := os.NewFile(3, "fd3") + defer fd3.Close() + + bs, err := io.ReadAll(fd3) + if err != nil { + fmt.Printf("ReadAll from fd 3: %v\n", err) + os.Exit(1) + } + + // Now verify that there are no other open fds. + // stdin == 0 + // stdout == 1 + // stderr == 2 + // descriptor from parent == 3 + // All descriptors 4 and up should be available, + // except for any used by the network poller. + for fd := uintptr(4); fd <= 100; fd++ { + if poll.IsPollDescriptor(fd) { + continue + } + + if !fdtest.Exists(fd) { + continue + } + + fmt.Printf("leaked parent file. fdtest.Exists(%d) got true want false\n", fd) + + fdfile := fmt.Sprintf("/proc/self/fd/%d", fd) + link, err := os.Readlink(fdfile) + fmt.Printf("readlink(%q) = %q, %v\n", fdfile, link, err) + + var args []string + switch runtime.GOOS { + case "plan9": + args = []string{fmt.Sprintf("/proc/%d/fd", os.Getpid())} + case "aix", "solaris", "illumos": + args = []string{fmt.Sprint(os.Getpid())} + default: + args = []string{"-p", fmt.Sprint(os.Getpid())} + } + + // Determine which command to use to display open files. + ofcmd := "lsof" + switch runtime.GOOS { + case "dragonfly", "freebsd", "netbsd", "openbsd": + ofcmd = "fstat" + case "plan9": + ofcmd = "/bin/cat" + case "aix": + ofcmd = "procfiles" + case "solaris", "illumos": + ofcmd = "pfiles" + } + + cmd := exec.Command(ofcmd, args...) + out, err := cmd.CombinedOutput() + if err != nil { + fmt.Fprintf(os.Stderr, "%s failed: %v\n", strings.Join(cmd.Args, " "), err) + } + fmt.Printf("%s", out) + os.Exit(1) + } + + os.Stdout.Write(bs) +} -- cgit v1.2.3