Skip to content

Commit 3ce203d

Browse files
committed
os/exec: return error when PATH lookup would use current directory
Following discussion on #43724, change os/exec to take the approach of golang.org/x/sys/execabs, refusing to respect path entries mentioning relative paths by default. Code that insists on being able to find executables in relative directories in the path will need to add a couple lines to override the error. See the updated package docs in exec.go for more details. Fixes #43724. Fixes #43947. Change-Id: I73c1214f322b60b4167a23e956e933d50470fe13 Reviewed-on: https://go-review.googlesource.com/c/go/+/381374 Reviewed-by: Bryan Mills <bcmills@google.com> TryBot-Result: Gopher Robot <gobot@golang.org> Run-TryBot: Bryan Mills <bcmills@google.com>
1 parent 8c5917c commit 3ce203d

File tree

13 files changed

+248
-172
lines changed

13 files changed

+248
-172
lines changed

api/next/43724.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
pkg os/exec, type Cmd struct, Err error #43724
2+
pkg os/exec, var ErrDot error #43724

src/cmd/dist/build.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
var (
2828
goarch string
2929
gorootBin string
30+
gorootBinGo string
3031
gohostarch string
3132
gohostos string
3233
goos string
@@ -114,6 +115,12 @@ func xinit() {
114115
goroot = filepath.Clean(b)
115116
gorootBin = pathf("%s/bin", goroot)
116117

118+
// Don't run just 'go' because the build infrastructure
119+
// runs cmd/dist inside go/bin often, and on Windows
120+
// it will be found in the current directory and refuse to exec.
121+
// All exec calls rewrite "go" into gorootBinGo.
122+
gorootBinGo = pathf("%s/bin/go", goroot)
123+
117124
b = os.Getenv("GOROOT_FINAL")
118125
if b == "" {
119126
b = goroot

src/cmd/dist/test.go

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ func cmdtest() {
2727
gogcflags = os.Getenv("GO_GCFLAGS")
2828

2929
var t tester
30+
3031
var noRebuild bool
3132
flag.BoolVar(&t.listMode, "list", false, "list available tests")
3233
flag.BoolVar(&t.rebuild, "rebuild", false, "rebuild everything first")
@@ -96,15 +97,9 @@ type distTest struct {
9697
func (t *tester) run() {
9798
timelog("start", "dist test")
9899

99-
var exeSuffix string
100-
if goos == "windows" {
101-
exeSuffix = ".exe"
102-
}
103-
if _, err := os.Stat(filepath.Join(gorootBin, "go"+exeSuffix)); err == nil {
104-
os.Setenv("PATH", fmt.Sprintf("%s%c%s", gorootBin, os.PathListSeparator, os.Getenv("PATH")))
105-
}
100+
os.Setenv("PATH", fmt.Sprintf("%s%c%s", gorootBin, os.PathListSeparator, os.Getenv("PATH")))
106101

107-
cmd := exec.Command("go", "env", "CGO_ENABLED")
102+
cmd := exec.Command(gorootBinGo, "env", "CGO_ENABLED")
108103
cmd.Stderr = new(bytes.Buffer)
109104
slurp, err := cmd.Output()
110105
if err != nil {
@@ -419,7 +414,7 @@ func (t *tester) registerStdTest(pkg string) {
419414
args = append(args, "-run=^$")
420415
}
421416
args = append(args, stdMatches...)
422-
cmd := exec.Command("go", args...)
417+
cmd := exec.Command(gorootBinGo, args...)
423418
cmd.Stdout = os.Stdout
424419
cmd.Stderr = os.Stderr
425420
return cmd.Run()
@@ -456,7 +451,7 @@ func (t *tester) registerRaceBenchTest(pkg string) {
456451
args = append(args, "-bench=.*")
457452
}
458453
args = append(args, benchMatches...)
459-
cmd := exec.Command("go", args...)
454+
cmd := exec.Command(gorootBinGo, args...)
460455
cmd.Stdout = os.Stdout
461456
cmd.Stderr = os.Stderr
462457
return cmd.Run()
@@ -484,7 +479,7 @@ func (t *tester) registerTests() {
484479
} else {
485480
// Use a format string to only list packages and commands that have tests.
486481
const format = "{{if (or .TestGoFiles .XTestGoFiles)}}{{.ImportPath}}{{end}}"
487-
cmd := exec.Command("go", "list", "-f", format)
482+
cmd := exec.Command(gorootBinGo, "list", "-f", format)
488483
if t.race {
489484
cmd.Args = append(cmd.Args, "-tags=race")
490485
}
@@ -619,7 +614,7 @@ func (t *tester) registerTests() {
619614
fmt.Println("skipping terminal test; stdout/stderr not terminals")
620615
return nil
621616
}
622-
cmd := exec.Command("go", "test")
617+
cmd := exec.Command(gorootBinGo, "test")
623618
setDir(cmd, filepath.Join(os.Getenv("GOROOT"), "src/cmd/go/testdata/testterminal18153"))
624619
cmd.Stdout = os.Stdout
625620
cmd.Stderr = os.Stderr
@@ -1003,7 +998,11 @@ func flattenCmdline(cmdline []interface{}) (bin string, args []string) {
1003998
}
1004999
list = out
10051000

1006-
return list[0], list[1:]
1001+
bin = list[0]
1002+
if bin == "go" {
1003+
bin = gorootBinGo
1004+
}
1005+
return bin, list[1:]
10071006
}
10081007

10091008
func (t *tester) addCmd(dt *distTest, dir string, cmdline ...interface{}) *exec.Cmd {
@@ -1157,7 +1156,7 @@ func (t *tester) registerHostTest(name, heading, dir, pkg string) {
11571156
}
11581157

11591158
func (t *tester) runHostTest(dir, pkg string) error {
1160-
out, err := exec.Command("go", "env", "GOEXE", "GOTMPDIR").Output()
1159+
out, err := exec.Command(gorootBinGo, "env", "GOEXE", "GOTMPDIR").Output()
11611160
if err != nil {
11621161
return err
11631162
}

src/cmd/dist/util.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,11 @@ func run(dir string, mode int, cmd ...string) string {
7171
errprintf("run: %s\n", strings.Join(cmd, " "))
7272
}
7373

74-
xcmd := exec.Command(cmd[0], cmd[1:]...)
74+
bin := cmd[0]
75+
if bin == "go" {
76+
bin = gorootBinGo
77+
}
78+
xcmd := exec.Command(bin, cmd[1:]...)
7579
setDir(xcmd, dir)
7680
var data []byte
7781
var err error

src/cmd/go/testdata/script/cgo_path.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ env GOCACHE=$WORK/gocache # Looking for compile flags, so need a clean cache.
1414
[windows] exists -exec p/gcc.bat p/clang.bat
1515
! exists p/bug.txt
1616
! go build -x
17-
stderr '^cgo: exec (clang|gcc): (clang|gcc) resolves to executable relative to current directory \(.[/\\](clang|gcc)(.bat)?\)$'
17+
stderr '^cgo: C compiler "(clang|gcc)" not found: exec: "(clang|gcc)": cannot run executable found relative to current directory'
1818
! exists p/bug.txt
1919

2020
-- go.mod --

src/internal/execabs/execabs.go

Lines changed: 3 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,7 @@ package execabs
1212

1313
import (
1414
"context"
15-
"fmt"
1615
"os/exec"
17-
"path/filepath"
18-
"reflect"
19-
"unsafe"
2016
)
2117

2218
var ErrNotFound = exec.ErrNotFound
@@ -27,44 +23,14 @@ type (
2723
ExitError = exec.ExitError
2824
)
2925

30-
func relError(file, path string) error {
31-
return fmt.Errorf("%s resolves to executable relative to current directory (.%c%s)", file, filepath.Separator, path)
32-
}
33-
3426
func LookPath(file string) (string, error) {
35-
path, err := exec.LookPath(file)
36-
if err != nil {
37-
return "", err
38-
}
39-
if filepath.Base(file) == file && !filepath.IsAbs(path) {
40-
return "", relError(file, path)
41-
}
42-
return path, nil
43-
}
44-
45-
func fixCmd(name string, cmd *exec.Cmd) {
46-
if filepath.Base(name) == name && !filepath.IsAbs(cmd.Path) {
47-
// exec.Command was called with a bare binary name and
48-
// exec.LookPath returned a path which is not absolute.
49-
// Set cmd.lookPathErr and clear cmd.Path so that it
50-
// cannot be run.
51-
lookPathErr := (*error)(unsafe.Pointer(reflect.ValueOf(cmd).Elem().FieldByName("lookPathErr").Addr().Pointer()))
52-
if *lookPathErr == nil {
53-
*lookPathErr = relError(name, cmd.Path)
54-
}
55-
cmd.Path = ""
56-
}
27+
return exec.LookPath(file)
5728
}
5829

5930
func CommandContext(ctx context.Context, name string, arg ...string) *exec.Cmd {
60-
cmd := exec.CommandContext(ctx, name, arg...)
61-
fixCmd(name, cmd)
62-
return cmd
63-
31+
return exec.CommandContext(ctx, name, arg...)
6432
}
6533

6634
func Command(name string, arg ...string) *exec.Cmd {
67-
cmd := exec.Command(name, arg...)
68-
fixCmd(name, cmd)
69-
return cmd
35+
return exec.Command(name, arg...)
7036
}

src/internal/execabs/execabs_test.go

Lines changed: 0 additions & 103 deletions
This file was deleted.

src/os/exec/dot_test.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// Copyright 2020 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package exec_test
6+
7+
import (
8+
"errors"
9+
"internal/testenv"
10+
"io/ioutil"
11+
"os"
12+
. "os/exec"
13+
"path/filepath"
14+
"runtime"
15+
"strings"
16+
"testing"
17+
)
18+
19+
func TestLookPath(t *testing.T) {
20+
testenv.MustHaveExec(t)
21+
22+
tmpDir := filepath.Join(t.TempDir(), "testdir")
23+
if err := os.Mkdir(tmpDir, 0777); err != nil {
24+
t.Fatal(err)
25+
}
26+
27+
executable := "execabs-test"
28+
if runtime.GOOS == "windows" {
29+
executable += ".exe"
30+
}
31+
if err := ioutil.WriteFile(filepath.Join(tmpDir, executable), []byte{1, 2, 3}, 0777); err != nil {
32+
t.Fatal(err)
33+
}
34+
cwd, err := os.Getwd()
35+
if err != nil {
36+
t.Fatal(err)
37+
}
38+
defer func() {
39+
if err := os.Chdir(cwd); err != nil {
40+
panic(err)
41+
}
42+
}()
43+
if err = os.Chdir(tmpDir); err != nil {
44+
t.Fatal(err)
45+
}
46+
origPath := os.Getenv("PATH")
47+
defer os.Setenv("PATH", origPath)
48+
49+
// Add "." to PATH so that exec.LookPath looks in the current directory on all systems.
50+
// And try to trick it with "../testdir" too.
51+
for _, dir := range []string{".", "../testdir"} {
52+
os.Setenv("PATH", dir+string(filepath.ListSeparator)+origPath)
53+
t.Run("PATH="+dir, func(t *testing.T) {
54+
good := dir + "/execabs-test"
55+
if found, err := LookPath(good); err != nil || !strings.HasPrefix(found, good) {
56+
t.Fatalf("LookPath(%q) = %q, %v, want \"%s...\", nil", good, found, err, good)
57+
}
58+
if runtime.GOOS == "windows" {
59+
good = dir + `\execabs-test`
60+
if found, err := LookPath(good); err != nil || !strings.HasPrefix(found, good) {
61+
t.Fatalf("LookPath(%q) = %q, %v, want \"%s...\", nil", good, found, err, good)
62+
}
63+
}
64+
65+
if _, err := LookPath("execabs-test"); err == nil {
66+
t.Fatalf("LookPath didn't fail when finding a non-relative path")
67+
} else if !errors.Is(err, ErrDot) {
68+
t.Fatalf("LookPath returned unexpected error: want Is ErrDot, got %q", err)
69+
}
70+
71+
cmd := Command("execabs-test")
72+
if cmd.Err == nil {
73+
t.Fatalf("Command didn't fail when finding a non-relative path")
74+
} else if !errors.Is(cmd.Err, ErrDot) {
75+
t.Fatalf("Command returned unexpected error: want Is ErrDot, got %q", cmd.Err)
76+
}
77+
cmd.Err = nil
78+
79+
// Clearing cmd.Err should let the execution proceed,
80+
// and it should fail because it's not a valid binary.
81+
if err := cmd.Run(); err == nil {
82+
t.Fatalf("Run did not fail: expected exec error")
83+
} else if errors.Is(err, ErrDot) {
84+
t.Fatalf("Run returned unexpected error ErrDot: want error like ENOEXEC: %q", err)
85+
}
86+
})
87+
}
88+
}

0 commit comments

Comments
 (0)