Skip to content

Commit d0146bd

Browse files
dmitshurgopherbot
authored andcommitted
os/exec: only use cachedLookExtensions if Cmd.Path is unmodified
Caching the invocation of lookExtensions on an absolute path in Command and reusing the cached result in Start is only viable if Cmd.Path isn't set to a different value after Command returns. For #66586. Fixes #68314. Change-Id: I57007850aca2011b11344180c00faded737617b5 Reviewed-on: https://go-review.googlesource.com/c/go/+/596875 Reviewed-by: qiu laidongfeng2 <[email protected]> Reviewed-by: Ian Lance Taylor <[email protected]> Auto-Submit: Dmitri Shuralyov <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Dmitri Shuralyov <[email protected]>
1 parent ad77cef commit d0146bd

File tree

2 files changed

+56
-31
lines changed

2 files changed

+56
-31
lines changed

src/os/exec/exec.go

+35-30
Original file line numberDiff line numberDiff line change
@@ -334,8 +334,10 @@ type Cmd struct {
334334
lookPathErr error
335335

336336
// cachedLookExtensions caches the result of calling lookExtensions.
337+
// It is set when Command is called with an absolute path, letting it do
338+
// the work of resolving the extension, so Start doesn't need to do it again.
337339
// This is only used on Windows.
338-
cachedLookExtensions string
340+
cachedLookExtensions struct{ in, out string }
339341
}
340342

341343
// A ctxResult reports the result of watching the Context associated with a
@@ -436,12 +438,12 @@ func Command(name string, arg ...string) *Cmd {
436438
// Since the path is absolute, its extension should be unambiguous
437439
// and independent of cmd.Dir, and we can go ahead and cache the lookup now.
438440
//
439-
// Note that we cannot add an extension here for relative paths, because
440-
// cmd.Dir may be set after we return from this function and that may cause
441-
// the command to resolve to a different extension.
442-
lp, err := lookExtensions(name, "")
443-
cmd.cachedLookExtensions = lp
444-
if err != nil {
441+
// Note that we don't cache anything here for relative paths, because
442+
// cmd.Dir may be set after we return from this function and that may
443+
// cause the command to resolve to a different extension.
444+
if lp, err := lookExtensions(name, ""); err == nil {
445+
cmd.cachedLookExtensions.in, cmd.cachedLookExtensions.out = name, lp
446+
} else {
445447
cmd.Err = err
446448
}
447449
}
@@ -642,29 +644,32 @@ func (c *Cmd) Start() error {
642644
return c.Err
643645
}
644646
lp := c.Path
645-
if c.cachedLookExtensions != "" {
646-
lp = c.cachedLookExtensions
647-
}
648-
if runtime.GOOS == "windows" && c.cachedLookExtensions == "" {
649-
// If c.Path is relative, we had to wait until now
650-
// to resolve it in case c.Dir was changed.
651-
// (If it is absolute, we already resolved its extension in Command
652-
// and shouldn't need to do so again.)
653-
//
654-
// Unfortunately, we cannot write the result back to c.Path because programs
655-
// may assume that they can call Start concurrently with reading the path.
656-
// (It is safe and non-racy to do so on Unix platforms, and users might not
657-
// test with the race detector on all platforms;
658-
// see https://go.dev/issue/62596.)
659-
//
660-
// So we will pass the fully resolved path to os.StartProcess, but leave
661-
// c.Path as is: missing a bit of logging information seems less harmful
662-
// than triggering a surprising data race, and if the user really cares
663-
// about that bit of logging they can always use LookPath to resolve it.
664-
var err error
665-
lp, err = lookExtensions(c.Path, c.Dir)
666-
if err != nil {
667-
return err
647+
if runtime.GOOS == "windows" {
648+
if c.Path == c.cachedLookExtensions.in {
649+
// If Command was called with an absolute path, we already resolved
650+
// its extension and shouldn't need to do so again (provided c.Path
651+
// wasn't set to another value between the calls to Command and Start).
652+
lp = c.cachedLookExtensions.out
653+
} else {
654+
// If *Cmd was made without using Command at all, or if Command was
655+
// called with a relative path, we had to wait until now to resolve
656+
// it in case c.Dir was changed.
657+
//
658+
// Unfortunately, we cannot write the result back to c.Path because programs
659+
// may assume that they can call Start concurrently with reading the path.
660+
// (It is safe and non-racy to do so on Unix platforms, and users might not
661+
// test with the race detector on all platforms;
662+
// see https://go.dev/issue/62596.)
663+
//
664+
// So we will pass the fully resolved path to os.StartProcess, but leave
665+
// c.Path as is: missing a bit of logging information seems less harmful
666+
// than triggering a surprising data race, and if the user really cares
667+
// about that bit of logging they can always use LookPath to resolve it.
668+
var err error
669+
lp, err = lookExtensions(c.Path, c.Dir)
670+
if err != nil {
671+
return err
672+
}
668673
}
669674
}
670675
if c.Cancel != nil && c.ctx == nil {

src/os/exec/exec_test.go

+21-1
Original file line numberDiff line numberDiff line change
@@ -1838,7 +1838,7 @@ func TestPathRace(t *testing.T) {
18381838

18391839
func TestAbsPathExec(t *testing.T) {
18401840
testenv.MustHaveExec(t)
1841-
testenv.MustHaveGoBuild(t) // must have GOROOT/bin/gofmt, but close enough
1841+
testenv.MustHaveGoBuild(t) // must have GOROOT/bin/{go,gofmt}
18421842

18431843
// A simple exec of a full path should work.
18441844
// Go 1.22 broke this on Windows, requiring ".exe"; see #66586.
@@ -1863,4 +1863,24 @@ func TestAbsPathExec(t *testing.T) {
18631863
if err == nil {
18641864
t.Errorf("using exec.Cmd{Path: %#q}: unexpected success", cmd.Path)
18651865
}
1866+
1867+
// A simple exec after modifying Cmd.Path should work.
1868+
// This broke on Windows. See go.dev/issue/68314.
1869+
t.Run("modified", func(t *testing.T) {
1870+
if exec.Command(filepath.Join(testenv.GOROOT(t), "bin/go")).Run() == nil {
1871+
// The implementation of the test case below relies on the go binary
1872+
// exiting with a non-zero exit code when run without any arguments.
1873+
// In the unlikely case that changes, we need to use another binary.
1874+
t.Fatal("test case needs updating to verify fix for go.dev/issue/68314")
1875+
}
1876+
exe1 := filepath.Join(testenv.GOROOT(t), "bin/go")
1877+
exe2 := filepath.Join(testenv.GOROOT(t), "bin/gofmt")
1878+
cmd := exec.Command(exe1)
1879+
cmd.Path = exe2
1880+
cmd.Args = []string{cmd.Path}
1881+
err := cmd.Run()
1882+
if err != nil {
1883+
t.Error("ran wrong binary")
1884+
}
1885+
})
18661886
}

0 commit comments

Comments
 (0)