Skip to content

Commit 3d77a0b

Browse files
committed
os/exec: second call to Cmd.Start is always an error
Previously it would return an error only if the first call resulted in process creation, contra the intent of the comment at exec.Cmd: // A Cmd cannot be reused after calling its [Cmd.Start], [Cmd.Run], // [Cmd.Output], or [Cmd.CombinedOutput] methods. Also, clear the Cmd.goroutines slice in case of failure to start a process, so that the closures can be GC'd and their pipe fds finalized and closed. Fixes golang#76746 Change-Id: Ic63a4dced0aa52c2d4be7d44f6dcfc84ee22282c Reviewed-on: https://go-review.googlesource.com/c/go/+/728642 LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: Damien Neil <dneil@google.com>
1 parent 7ecb1f3 commit 3d77a0b

2 files changed

Lines changed: 33 additions & 1 deletion

File tree

src/os/exec/exec.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ import (
102102
"runtime"
103103
"strconv"
104104
"strings"
105+
"sync/atomic"
105106
"syscall"
106107
"time"
107108
)
@@ -354,6 +355,9 @@ type Cmd struct {
354355
// the work of resolving the extension, so Start doesn't need to do it again.
355356
// This is only used on Windows.
356357
cachedLookExtensions struct{ in, out string }
358+
359+
// startCalled records that Start was attempted, regardless of outcome.
360+
startCalled atomic.Bool
357361
}
358362

359363
// A ctxResult reports the result of watching the Context associated with a
@@ -635,7 +639,8 @@ func (c *Cmd) Run() error {
635639
func (c *Cmd) Start() error {
636640
// Check for doubled Start calls before we defer failure cleanup. If the prior
637641
// call to Start succeeded, we don't want to spuriously close its pipes.
638-
if c.Process != nil {
642+
// It is an error to call Start twice even if the first call did not create a process.
643+
if c.startCalled.Swap(true) {
639644
return errors.New("exec: already started")
640645
}
641646

@@ -647,6 +652,7 @@ func (c *Cmd) Start() error {
647652
if !started {
648653
closeDescriptors(c.parentIOPipes)
649654
c.parentIOPipes = nil
655+
c.goroutine = nil // aid GC, finalization of pipe fds
650656
}
651657
}()
652658

src/os/exec/exec_test.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1839,3 +1839,29 @@ func TestAbsPathExec(t *testing.T) {
18391839
}
18401840
})
18411841
}
1842+
1843+
// Calling Start twice is an error, regardless of outcome.
1844+
func TestStart_twice(t *testing.T) {
1845+
testenv.MustHaveExec(t)
1846+
1847+
cmd := exec.Command("/bin/nonesuch")
1848+
for i, want := range []string{
1849+
cond(runtime.GOOS == "windows",
1850+
`exec: "/bin/nonesuch": executable file not found in %PATH%`,
1851+
"fork/exec /bin/nonesuch: no such file or directory"),
1852+
"exec: already started",
1853+
} {
1854+
err := cmd.Start()
1855+
if got := fmt.Sprint(err); got != want {
1856+
t.Errorf("Start call #%d return err %q, want %q", i+1, got, want)
1857+
}
1858+
}
1859+
}
1860+
1861+
func cond[T any](cond bool, t, f T) T {
1862+
if cond {
1863+
return t
1864+
} else {
1865+
return f
1866+
}
1867+
}

0 commit comments

Comments
 (0)