summaryrefslogtreecommitdiffstats
path: root/src/os/exec
diff options
context:
space:
mode:
Diffstat (limited to 'src/os/exec')
-rw-r--r--src/os/exec/bench_test.go23
-rw-r--r--src/os/exec/dot_test.go192
-rw-r--r--src/os/exec/env_test.go67
-rw-r--r--src/os/exec/example_test.go169
-rw-r--r--src/os/exec/exec.go1303
-rw-r--r--src/os/exec/exec_linux_test.go45
-rw-r--r--src/os/exec/exec_other_test.go14
-rw-r--r--src/os/exec/exec_plan9.go19
-rw-r--r--src/os/exec/exec_posix_test.go276
-rw-r--r--src/os/exec/exec_test.go1784
-rw-r--r--src/os/exec/exec_unix.go24
-rw-r--r--src/os/exec/exec_unix_test.go17
-rw-r--r--src/os/exec/exec_windows.go23
-rw-r--r--src/os/exec/exec_windows_test.go109
-rw-r--r--src/os/exec/internal/fdtest/exists_plan9.go20
-rw-r--r--src/os/exec/internal/fdtest/exists_test.go21
-rw-r--r--src/os/exec/internal/fdtest/exists_unix.go19
-rw-r--r--src/os/exec/internal/fdtest/exists_windows.go12
-rw-r--r--src/os/exec/internal_test.go61
-rw-r--r--src/os/exec/lp_linux_test.go88
-rw-r--r--src/os/exec/lp_plan9.go66
-rw-r--r--src/os/exec/lp_test.go33
-rw-r--r--src/os/exec/lp_unix.go82
-rw-r--r--src/os/exec/lp_unix_test.go50
-rw-r--r--src/os/exec/lp_wasm.go23
-rw-r--r--src/os/exec/lp_windows.go145
-rw-r--r--src/os/exec/lp_windows_test.go612
-rw-r--r--src/os/exec/read3.go91
28 files changed, 5388 insertions, 0 deletions
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 <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)
+ }
+ })
+
+ // 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)
+}