cmd/go/internal/work: disallow testgo binary from installing to GOROOT

Installing to GOROOT makes tests non-parallelizable, since each test
depends on the installed contents of GOROOT already being up-to-date
and may reasonably assume that those contents do not change over the
course of the test.

Fixes #37573
Updates #30316

Change-Id: I2afe95ad11347bee3bb7c2d77a657db6d691cf05
Reviewed-on: https://go-review.googlesource.com/c/go/+/225897
Reviewed-by: Michael Matloob <matloob@golang.org>
diff --git a/src/cmd/go/go_test.go b/src/cmd/go/go_test.go
index 641cab8..39e387b 100644
--- a/src/cmd/go/go_test.go
+++ b/src/cmd/go/go_test.go
@@ -199,6 +199,7 @@
 			return strings.TrimSpace(string(out))
 		}
 		testGOROOT = goEnv("GOROOT")
+		os.Setenv("TESTGO_GOROOT", testGOROOT)
 
 		// The whole GOROOT/pkg tree was installed using the GOHOSTOS/GOHOSTARCH
 		// toolchain (installed in GOROOT/pkg/tool/GOHOSTOS_GOHOSTARCH).
diff --git a/src/cmd/go/internal/work/exec.go b/src/cmd/go/internal/work/exec.go
index d781ad2..dbe31a6 100644
--- a/src/cmd/go/internal/work/exec.go
+++ b/src/cmd/go/internal/work/exec.go
@@ -8,11 +8,6 @@
 
 import (
 	"bytes"
-	"cmd/go/internal/base"
-	"cmd/go/internal/cache"
-	"cmd/go/internal/cfg"
-	"cmd/go/internal/load"
-	"cmd/go/internal/str"
 	"encoding/json"
 	"errors"
 	"fmt"
@@ -30,6 +25,12 @@
 	"strings"
 	"sync"
 	"time"
+
+	"cmd/go/internal/base"
+	"cmd/go/internal/cache"
+	"cmd/go/internal/cfg"
+	"cmd/go/internal/load"
+	"cmd/go/internal/str"
 )
 
 // actionList returns the list of actions in the dag rooted at root
@@ -490,6 +491,10 @@
 		return nil
 	}
 
+	if err := allowInstall(a); err != nil {
+		return err
+	}
+
 	// make target directory
 	dir, _ := filepath.Split(a.Target)
 	if dir != "" {
@@ -1192,6 +1197,10 @@
 		return err
 	}
 
+	if err := allowInstall(a); err != nil {
+		return err
+	}
+
 	// make target directory
 	dir, _ := filepath.Split(a.Target)
 	if dir != "" {
@@ -1366,6 +1375,10 @@
 }
 
 func (b *Builder) installShlibname(a *Action) error {
+	if err := allowInstall(a); err != nil {
+		return err
+	}
+
 	// TODO: BuildN
 	a1 := a.Deps[0]
 	err := ioutil.WriteFile(a.Target, []byte(filepath.Base(a1.Target)+"\n"), 0666)
@@ -1416,6 +1429,10 @@
 	}
 	defer b.flushOutput(a)
 
+	if err := allowInstall(a); err != nil {
+		return err
+	}
+
 	if err := b.Mkdir(a.Objdir); err != nil {
 		return err
 	}
@@ -1481,8 +1498,12 @@
 		// advertise it by touching the mtimes (usually the libraries are up
 		// to date).
 		if !a.buggyInstall && !b.IsCmdList {
-			now := time.Now()
-			os.Chtimes(a.Target, now, now)
+			if cfg.BuildN {
+				b.Showcmd("", "touch %s", a.Target)
+			} else if err := allowInstall(a); err == nil {
+				now := time.Now()
+				os.Chtimes(a.Target, now, now)
+			}
 		}
 		return nil
 	}
@@ -1493,6 +1514,9 @@
 		a.built = a1.built
 		return nil
 	}
+	if err := allowInstall(a); err != nil {
+		return err
+	}
 
 	if err := b.Mkdir(a.Objdir); err != nil {
 		return err
@@ -1522,6 +1546,13 @@
 	return b.moveOrCopyFile(a.Target, a1.built, perm, false)
 }
 
+// allowInstall returns a non-nil error if this invocation of the go command is
+// allowed to install a.Target.
+//
+// (The build of cmd/go running under its own test is forbidden from installing
+// to its original GOROOT.)
+var allowInstall = func(*Action) error { return nil }
+
 // cleanup removes a's object dir to keep the amount of
 // on-disk garbage down in a large build. On an operating system
 // with aggressive buffering, cleaning incrementally like
@@ -1685,6 +1716,10 @@
 		return nil
 	}
 
+	if err := allowInstall(a); err != nil {
+		return err
+	}
+
 	dir, _ := filepath.Split(a.Target)
 	if dir != "" {
 		if err := b.Mkdir(dir); err != nil {
diff --git a/src/cmd/go/internal/work/testgo.go b/src/cmd/go/internal/work/testgo.go
index 3e623c6..931f49a 100644
--- a/src/cmd/go/internal/work/testgo.go
+++ b/src/cmd/go/internal/work/testgo.go
@@ -8,10 +8,41 @@
 
 package work
 
-import "os"
+import (
+	"cmd/go/internal/cfg"
+	"cmd/go/internal/search"
+	"fmt"
+	"os"
+	"path/filepath"
+	"runtime"
+)
 
 func init() {
 	if v := os.Getenv("TESTGO_VERSION"); v != "" {
 		runtimeVersion = v
 	}
+
+	if testGOROOT := os.Getenv("TESTGO_GOROOT"); testGOROOT != "" {
+		// Disallow installs to the GOROOT from which testgo was built.
+		// Installs to other GOROOTs — such as one set explicitly within a test — are ok.
+		allowInstall = func(a *Action) error {
+			if cfg.BuildN {
+				return nil
+			}
+
+			rel := search.InDir(a.Target, testGOROOT)
+			if rel == "" {
+				return nil
+			}
+
+			callerPos := ""
+			if _, file, line, ok := runtime.Caller(1); ok {
+				if shortFile := search.InDir(file, filepath.Join(testGOROOT, "src")); shortFile != "" {
+					file = shortFile
+				}
+				callerPos = fmt.Sprintf("%s:%d: ", file, line)
+			}
+			return fmt.Errorf("%stestgo must not write to GOROOT (installing to %s)", callerPos, filepath.Join("GOROOT", rel))
+		}
+	}
 }
diff --git a/src/cmd/go/script_test.go b/src/cmd/go/script_test.go
index 87afb6a..ebadce8 100644
--- a/src/cmd/go/script_test.go
+++ b/src/cmd/go/script_test.go
@@ -126,6 +126,7 @@
 		"GOPROXY=" + proxyURL,
 		"GOPRIVATE=",
 		"GOROOT=" + testGOROOT,
+		"TESTGO_GOROOT=" + testGOROOT,
 		"GOSUMDB=" + testSumDBVerifierKey,
 		"GONOPROXY=",
 		"GONOSUMDB=",
diff --git a/src/cmd/go/testdata/script/README b/src/cmd/go/testdata/script/README
index f4c92e6..e22ddca 100644
--- a/src/cmd/go/testdata/script/README
+++ b/src/cmd/go/testdata/script/README
@@ -34,6 +34,7 @@
 	GOPATH=$WORK/gopath
 	GOPROXY=<local module proxy serving from cmd/go/testdata/mod>
 	GOROOT=<actual GOROOT>
+	TESTGO_GOROOT=<GOROOT used to build cmd/go, for use in tests that may change GOROOT>
 	HOME=/no-home
 	PATH=<actual PATH>
 	TMPDIR=$WORK/tmp
diff --git a/src/cmd/go/testdata/script/get_update_all.txt b/src/cmd/go/testdata/script/get_update_all.txt
index 1f2f5bf..d0b9860 100644
--- a/src/cmd/go/testdata/script/get_update_all.txt
+++ b/src/cmd/go/testdata/script/get_update_all.txt
@@ -3,5 +3,5 @@
 
 [!net] skip
 
-go get -u .../
-! stderr 'duplicate loads of' # make sure old packages are removed from cache
\ No newline at end of file
+go get -u -n .../
+! stderr 'duplicate loads of' # make sure old packages are removed from cache