doc/godebug.md | 5 +++++ src/cmd/go/internal/load/pkg.go | 14 +++++++------- src/cmd/go/internal/modfetch/repo.go | 2 +- src/cmd/go/internal/vcs/vcs.go | 28 +++++++++++++++++++--------- src/cmd/go/internal/vcs/vcs_test.go | 2 +- src/cmd/go/testdata/script/test_multivcs.txt | 54 +++++++++++++++++++++++++++++++++++++++++++++++++++++ src/cmd/go/testdata/script/version_buildvcs_nested.txt | 20 +++++++++++++++----- src/internal/godebugs/table.go | 1 + src/runtime/metrics/doc.go | 5 +++++ diff --git a/doc/godebug.md b/doc/godebug.md index d107b1baf15d7988bf073cc97958e7b8da399f8e..aaa0f9dd55e5707b20b49117437f6741938ed15a 100644 --- a/doc/godebug.md +++ b/doc/godebug.md @@ -189,6 +189,11 @@ Go 1.25 corrected the semantics of contention reports for runtime-internal locks, and so removed the [`runtimecontentionstacks` setting](/pkg/runtime#hdr-Environment_Variables). +Go 1.25 (starting with Go 1.25 RC 2) disabled build information stamping when +multiple VCS are detected due to concerns around VCS injection attacks. This +behavior and setting was backported to Go 1.24.5 and Go 1.23.11. This behavior +can be renabled with the setting `allowmultiplevcs=1`. + ### Go 1.24 Go 1.24 added a new `fips140` setting that controls whether the Go diff --git a/src/cmd/go/internal/load/pkg.go b/src/cmd/go/internal/load/pkg.go index e913f9885234fda9330bb6b7ead384cd39228b18..1f791546f90088ed9268f2f90d351ca6fedddf23 100644 --- a/src/cmd/go/internal/load/pkg.go +++ b/src/cmd/go/internal/load/pkg.go @@ -2496,7 +2496,6 @@ var repoDir string var vcsCmd *vcs.Cmd var err error - const allowNesting = true wantVCS := false switch cfg.BuildBuildvcs { @@ -2516,7 +2515,7 @@ // "bootstrap" (instead of "std"), with GOROOT set to GOROOT_BOOTSTRAP // (so the bootstrap toolchain packages don't even appear to be in GOROOT). goto omitVCS } - repoDir, vcsCmd, err = vcs.FromDir(base.Cwd(), "", allowNesting) + repoDir, vcsCmd, err = vcs.FromDir(base.Cwd(), "") if err != nil && !errors.Is(err, os.ErrNotExist) { setVCSError(err) return @@ -2539,10 +2538,11 @@ } } if repoDir != "" && vcsCmd.Status != nil { // Check that the current directory, package, and module are in the same - // repository. vcs.FromDir allows nested Git repositories, but nesting - // is not allowed for other VCS tools. The current directory may be outside - // p.Module.Dir when a workspace is used. - pkgRepoDir, _, err := vcs.FromDir(p.Dir, "", allowNesting) + // repository. vcs.FromDir disallows nested VCS and multiple VCS in the + // same repository, unless the GODEBUG allowmultiplevcs is set. The + // current directory may be outside p.Module.Dir when a workspace is + // used. + pkgRepoDir, _, err := vcs.FromDir(p.Dir, "") if err != nil { setVCSError(err) return @@ -2554,7 +2554,7 @@ return } goto omitVCS } - modRepoDir, _, err := vcs.FromDir(p.Module.Dir, "", allowNesting) + modRepoDir, _, err := vcs.FromDir(p.Module.Dir, "") if err != nil { setVCSError(err) return diff --git a/src/cmd/go/internal/modfetch/repo.go b/src/cmd/go/internal/modfetch/repo.go index 0fdb2a87369ccf2820fdcf646a4d16f319011360..5d4d679e8327ee08fa1026ca70d2d74aeb495bab 100644 --- a/src/cmd/go/internal/modfetch/repo.go +++ b/src/cmd/go/internal/modfetch/repo.go @@ -235,7 +235,7 @@ } return lookupLocalCache.Do(path, func() Repo { return newCachingRepo(ctx, path, func(ctx context.Context) (Repo, error) { - repoDir, vcsCmd, err := vcs.FromDir(dir, "", true) + repoDir, vcsCmd, err := vcs.FromDir(dir, "") if err != nil { return nil, err } diff --git a/src/cmd/go/internal/vcs/vcs.go b/src/cmd/go/internal/vcs/vcs.go index ebcb2efb348650df985f4afffadd622435824250..7e081eb41a1c4edd67493c9231ca684af5f24dff 100644 --- a/src/cmd/go/internal/vcs/vcs.go +++ b/src/cmd/go/internal/vcs/vcs.go @@ -8,6 +8,7 @@ import ( "bytes" "errors" "fmt" + "internal/godebug" "internal/lazyregexp" "internal/singleflight" "io/fs" @@ -869,11 +870,13 @@ check func(match map[string]string) error // additional checks schemelessRepo bool // if true, the repo pattern lacks a scheme } +var allowmultiplevcs = godebug.New("allowmultiplevcs") + // FromDir inspects dir and its parents to determine the // version control system and code repository to use. // If no repository is found, FromDir returns an error // equivalent to os.ErrNotExist. -func FromDir(dir, srcRoot string, allowNesting bool) (repoDir string, vcsCmd *Cmd, err error) { +func FromDir(dir, srcRoot string) (repoDir string, vcsCmd *Cmd, err error) { // Clean and double-check that dir is in (a subdirectory of) srcRoot. dir = filepath.Clean(dir) if srcRoot != "" { @@ -887,21 +890,28 @@ origDir := dir for len(dir) > len(srcRoot) { for _, vcs := range vcsList { if isVCSRoot(dir, vcs.RootNames) { - // Record first VCS we find. - // If allowNesting is false (as it is in GOPATH), keep looking for - // repositories in parent directories and report an error if one is - // found to mitigate VCS injection attacks. if vcsCmd == nil { + // Record first VCS we find. vcsCmd = vcs repoDir = dir - if allowNesting { + if allowmultiplevcs.Value() == "1" { + allowmultiplevcs.IncNonDefault() return repoDir, vcsCmd, nil } + // If allowmultiplevcs is not set, keep looking for + // repositories in current and parent directories and report + // an error if one is found to mitigate VCS injection + // attacks. continue } - // Otherwise, we have one VCS inside a different VCS. - return "", nil, fmt.Errorf("directory %q uses %s, but parent %q uses %s", - repoDir, vcsCmd.Cmd, dir, vcs.Cmd) + if vcsCmd == vcsGit && vcs == vcsGit { + // Nested Git is allowed, as this is how things like + // submodules work. Git explicitly protects against + // injection against itself. + continue + } + return "", nil, fmt.Errorf("multiple VCS detected: %s in %q, and %s in %q", + vcsCmd.Cmd, repoDir, vcs.Cmd, dir) } } diff --git a/src/cmd/go/internal/vcs/vcs_test.go b/src/cmd/go/internal/vcs/vcs_test.go index c1431549481f47c967db02470c100b569e639729..361d85bcfb326f11331601614e107b79233e3404 100644 --- a/src/cmd/go/internal/vcs/vcs_test.go +++ b/src/cmd/go/internal/vcs/vcs_test.go @@ -239,7 +239,7 @@ f.Close() } wantRepoDir := filepath.Dir(dir) - gotRepoDir, gotVCS, err := FromDir(dir, tempDir, false) + gotRepoDir, gotVCS, err := FromDir(dir, tempDir) if err != nil { t.Errorf("FromDir(%q, %q): %v", dir, tempDir, err) continue diff --git a/src/cmd/go/testdata/script/test_multivcs.txt b/src/cmd/go/testdata/script/test_multivcs.txt new file mode 100644 index 0000000000000000000000000000000000000000..538cbf700b458531239dd3658ce48326b08bd885 --- /dev/null +++ b/src/cmd/go/testdata/script/test_multivcs.txt @@ -0,0 +1,54 @@ +# To avoid VCS injection attacks, we should not accept multiple different VCS metadata +# folders within a single module (either in the same directory, or nested in different +# directories.) +# +# This behavior should be disabled by setting the allowmultiplevcs GODEBUG. + +[short] skip +[!git] skip + +cd samedir + +exec git init . + +# Without explicitly requesting buildvcs, the go command should silently continue +# without determining the correct VCS. +go test -c -o $devnull . + +# If buildvcs is explicitly requested, we expect the go command to fail +! go test -buildvcs -c -o $devnull . +stderr '^error obtaining VCS status: multiple VCS detected:' + +env GODEBUG=allowmultiplevcs=1 +go test -buildvcs -c -o $devnull . + +env GODEBUG= +cd ../nested +exec git init . +# cd a +go test -c -o $devnull ./a +! go test -buildvcs -c -o $devnull ./a +stderr '^error obtaining VCS status: multiple VCS detected:' +# allowmultiplevcs doesn't disable the check that the current directory, package, and +# module are in the same repository. +env GODEBUG=allowmultiplevcs=1 +! go test -buildvcs -c -o $devnull ./a +stderr '^error obtaining VCS status: main package is in repository' + +-- samedir/go.mod -- +module example + +go 1.18 +-- samedir/example.go -- +package main +-- samedir/.bzr/test -- +hello + +-- nested/go.mod -- +module example + +go 1.18 +-- nested/a/example.go -- +package main +-- nested/a/.bzr/test -- +hello diff --git a/src/cmd/go/testdata/script/version_buildvcs_nested.txt b/src/cmd/go/testdata/script/version_buildvcs_nested.txt index 6dab8474b59d44b862d98f46d8608bf2d56b1b21..22cd71c454b712161c47e0f2b2d8a63f80a77014 100644 --- a/src/cmd/go/testdata/script/version_buildvcs_nested.txt +++ b/src/cmd/go/testdata/script/version_buildvcs_nested.txt @@ -9,25 +9,35 @@ cd root go mod init example.com/root exec git init -# Nesting repositories in parent directories are ignored, as the current -# directory main package, and containing main module are in the same repository. -# This is an error in GOPATH mode (to prevent VCS injection), but for modules, -# we assume users have control over repositories they've checked out. + +# Nesting repositories in parent directories are an error, to prevent VCS injection. +# This can be disabled with the allowmultiplevcs GODEBUG. mkdir hgsub cd hgsub exec hg init cp ../../main.go main.go ! go build +stderr '^error obtaining VCS status: multiple VCS detected: hg in ".*hgsub", and git in ".*root"$' +stderr '^\tUse -buildvcs=false to disable VCS stamping.$' +env GODEBUG=allowmultiplevcs=1 +! go build stderr '^error obtaining VCS status: main module is in repository ".*root" but current directory is in repository ".*hgsub"$' stderr '^\tUse -buildvcs=false to disable VCS stamping.$' go build -buildvcs=false +env GODEBUG= go mod init example.com/root/hgsub +! go build +stderr '^error obtaining VCS status: multiple VCS detected: hg in ".*hgsub", and git in ".*root"$' +stderr '^\tUse -buildvcs=false to disable VCS stamping.$' +env GODEBUG=allowmultiplevcs=1 go build +env GODEBUG= cd .. # It's an error to build a package from a nested Git repository if the package # is in a separate repository from the current directory or from the module -# root directory. +# root directory. Otherwise nested Git repositories are allowed, as this is +# how Git implements submodules (and protects against Git based VCS injection.) mkdir gitsub cd gitsub exec git init diff --git a/src/internal/godebugs/table.go b/src/internal/godebugs/table.go index 38dc7b0fac8b421ecb4c6db3e2bd7fd3fd6499a8..2d008825459bb27f2499a22c1f7b50d4fa44c663 100644 --- a/src/internal/godebugs/table.go +++ b/src/internal/godebugs/table.go @@ -26,6 +26,7 @@ // // Note: After adding entries to this table, update the list in doc/godebug.md as well. // (Otherwise the test in this package will fail.) var All = []Info{ + {Name: "allowmultiplevcs", Package: "cmd/go"}, {Name: "asynctimerchan", Package: "time", Changed: 23, Old: "1"}, {Name: "containermaxprocs", Package: "runtime", Changed: 25, Old: "0"}, {Name: "dataindependenttiming", Package: "crypto/subtle", Opaque: true}, diff --git a/src/runtime/metrics/doc.go b/src/runtime/metrics/doc.go index 32fc436e1a17e5f3fb80e914c492cdab7190dc21..a1902bc6d78c7f909abc9eef9b3cad4d46502208 100644 --- a/src/runtime/metrics/doc.go +++ b/src/runtime/metrics/doc.go @@ -230,6 +230,11 @@ /gc/stack/starting-size:bytes The stack size of new goroutines. + /godebug/non-default-behavior/allowmultiplevcs:events + The number of non-default behaviors executed by the cmd/go + package due to a non-default GODEBUG=allowmultiplevcs=... + setting. + /godebug/non-default-behavior/asynctimerchan:events The number of non-default behaviors executed by the time package due to a non-default GODEBUG=asynctimerchan=... setting.