Skip to content

Commit 36facaa

Browse files
Bryan C. Millsgopherbot
Bryan C. Mills
authored andcommitted
os/exec: avoid writing to Cmd.Path in Cmd.Start on Windows
Fixes #62596. Change-Id: I9003318ac1c4e3036f32383e62e9ba08c383d5c2 Cq-Include-Trybots: luci.golang.try:gotip-linux-amd64-race,gotip-windows-amd64-race,gotip-windows-amd64-longtest Reviewed-on: https://go-review.googlesource.com/c/go/+/527820 Reviewed-by: Ian Lance Taylor <[email protected]> Auto-Submit: Bryan Mills <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]>
1 parent 6ecd5f7 commit 36facaa

File tree

3 files changed

+79
-10
lines changed

3 files changed

+79
-10
lines changed

src/os/exec/exec.go

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,26 @@ func Command(name string, arg ...string) *Cmd {
426426
if err != nil {
427427
cmd.Err = err
428428
}
429+
} else if runtime.GOOS == "windows" && filepath.IsAbs(name) {
430+
// We may need to add a filename extension from PATHEXT
431+
// or verify an extension that is already present.
432+
// (We need to do this even for names that already have an extension
433+
// in case of weird names like "foo.bat.exe".)
434+
//
435+
// Since the path is absolute, its extension should be unambiguous
436+
// and independent of cmd.Dir, and we can go ahead and update cmd.Path to
437+
// reflect it.
438+
//
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 := LookPath(name)
443+
if lp != "" {
444+
cmd.Path = lp
445+
}
446+
if err != nil {
447+
cmd.Err = err
448+
}
429449
}
430450
return cmd
431451
}
@@ -649,12 +669,28 @@ func (c *Cmd) Start() error {
649669
}
650670
return c.Err
651671
}
652-
if runtime.GOOS == "windows" {
653-
lp, err := lookExtensions(c.Path, c.Dir)
672+
lp := c.Path
673+
if runtime.GOOS == "windows" && !filepath.IsAbs(c.Path) {
674+
// If c.Path is relative, we had to wait until now
675+
// to resolve it in case c.Dir was changed.
676+
// (If it is absolute, we already resolved its extension in Command
677+
// and shouldn't need to do so again.)
678+
//
679+
// Unfortunately, we cannot write the result back to c.Path because programs
680+
// may assume that they can call Start concurrently with reading the path.
681+
// (It is safe and non-racy to do so on Unix platforms, and users might not
682+
// test with the race detector on all platforms;
683+
// see https://go.dev/issue/62596.)
684+
//
685+
// So we will pass the fully resolved path to os.StartProcess, but leave
686+
// c.Path as is: missing a bit of logging information seems less harmful
687+
// than triggering a surprising data race, and if the user really cares
688+
// about that bit of logging they can always use LookPath to resolve it.
689+
var err error
690+
lp, err = lookExtensions(c.Path, c.Dir)
654691
if err != nil {
655692
return err
656693
}
657-
c.Path = lp
658694
}
659695
if c.Cancel != nil && c.ctx == nil {
660696
return errors.New("exec: command with a non-nil Cancel was not created with CommandContext")
@@ -690,7 +726,7 @@ func (c *Cmd) Start() error {
690726
return err
691727
}
692728

693-
c.Process, err = os.StartProcess(c.Path, c.argv(), &os.ProcAttr{
729+
c.Process, err = os.StartProcess(lp, c.argv(), &os.ProcAttr{
694730
Dir: c.Dir,
695731
Files: childFiles,
696732
Env: env,

src/os/exec/exec_test.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1819,3 +1819,19 @@ func TestConcurrentExec(t *testing.T) {
18191819
cancel()
18201820
hangs.Wait()
18211821
}
1822+
1823+
// TestPathRace tests that [Cmd.String] can be called concurrently
1824+
// with [Cmd.Start].
1825+
func TestPathRace(t *testing.T) {
1826+
cmd := helperCommand(t, "exit", "0")
1827+
1828+
done := make(chan struct{})
1829+
go func() {
1830+
out, err := cmd.CombinedOutput()
1831+
t.Logf("%v: %v\n%s", cmd, err, out)
1832+
close(done)
1833+
}()
1834+
1835+
t.Logf("running in background: %v", cmd)
1836+
<-done
1837+
}

src/os/exec/lp_windows_test.go

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -554,14 +554,8 @@ func TestCommand(t *testing.T) {
554554
if wantPath == "" {
555555
if strings.Contains(tt.arg0, `\`) {
556556
wantPath = tt.arg0
557-
if filepath.Ext(wantPath) == "" {
558-
wantPath += filepath.Ext(tt.want)
559-
}
560557
} else if tt.wantErrDot {
561558
wantPath = strings.TrimPrefix(tt.want, tt.dir+`\`)
562-
if filepath.Base(wantPath) == wantPath {
563-
wantPath = `.\` + wantPath
564-
}
565559
} else {
566560
wantPath = filepath.Join(root, tt.want)
567561
}
@@ -572,3 +566,26 @@ func TestCommand(t *testing.T) {
572566
})
573567
}
574568
}
569+
570+
func TestAbsCommandWithDoubledExtension(t *testing.T) {
571+
t.Parallel()
572+
573+
comPath := filepath.Join(t.TempDir(), "example.com")
574+
batPath := comPath + ".bat"
575+
installBat(t, batPath)
576+
577+
cmd := exec.Command(comPath)
578+
out, err := cmd.CombinedOutput()
579+
t.Logf("%v:\n%s", cmd, out)
580+
if err == nil {
581+
got := strings.TrimSpace(string(out))
582+
if got != batPath {
583+
t.Errorf("wanted output %#q", batPath)
584+
}
585+
} else {
586+
t.Errorf("%v: %v", cmd, err)
587+
}
588+
if cmd.Path != batPath {
589+
t.Errorf("exec.Command(%#q).Path =\n %#q\nwant %#q", comPath, cmd.Path, batPath)
590+
}
591+
}

0 commit comments

Comments
 (0)