]> Cypherpunks repositories - gostls13.git/commitdiff
[release-branch.go1.25] cmd/go: disable support for multiple vcs in one module
authorRoland Shoemaker <bracewell@google.com>
Mon, 9 Jun 2025 18:23:46 +0000 (11:23 -0700)
committerCarlos Amedee <carlos@golang.org>
Tue, 8 Jul 2025 16:29:36 +0000 (09:29 -0700)
Removes the somewhat redundant vcs.FromDir, "allowNesting" argument,
which was always enabled, and disallow multiple VCS metadata folders
being present in a single directory. This makes VCS injection attacks
much more difficult.

Also adds a GODEBUG, allowmultiplevcs, which re-enables this behavior.

Thanks to RyotaK (https://ryotak.net) of GMO Flatt Security Inc for
reporting this issue.

Fixes #74380
Fixes CVE-2025-4674

Change-Id: I95b619588ecb6661770aa4e1d6023d6cb22e2263
Reviewed-on: https://go-review.googlesource.com/c/go/+/686338
Reviewed-by: David Chase <drchase@google.com>
Auto-Submit: Carlos Amedee <carlos@golang.org>
TryBot-Bypass: Carlos Amedee <carlos@golang.org>

doc/godebug.md
src/cmd/go/internal/load/pkg.go
src/cmd/go/internal/modfetch/repo.go
src/cmd/go/internal/vcs/vcs.go
src/cmd/go/internal/vcs/vcs_test.go
src/cmd/go/testdata/script/test_multivcs.txt [new file with mode: 0644]
src/cmd/go/testdata/script/version_buildvcs_nested.txt
src/internal/godebugs/table.go
src/runtime/metrics/doc.go

index d107b1baf15d7988bf073cc97958e7b8da399f8e..aaa0f9dd55e5707b20b49117437f6741938ed15a 100644 (file)
@@ -189,6 +189,11 @@ crypto/x509.CreateCertificate. The setting `x509sha256skid=0` reverts to SHA-1.
 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
index e913f9885234fda9330bb6b7ead384cd39228b18..1f791546f90088ed9268f2f90d351ca6fedddf23 100644 (file)
@@ -2496,7 +2496,6 @@ func (p *Package) setBuildInfo(ctx context.Context, autoVCS bool) {
        var repoDir string
        var vcsCmd *vcs.Cmd
        var err error
-       const allowNesting = true
 
        wantVCS := false
        switch cfg.BuildBuildvcs {
@@ -2516,7 +2515,7 @@ func (p *Package) setBuildInfo(ctx context.Context, autoVCS bool) {
                        // (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 @@ func (p *Package) setBuildInfo(ctx context.Context, autoVCS bool) {
        }
        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 @@ func (p *Package) setBuildInfo(ctx context.Context, autoVCS bool) {
                        }
                        goto omitVCS
                }
-               modRepoDir, _, err := vcs.FromDir(p.Module.Dir, "", allowNesting)
+               modRepoDir, _, err := vcs.FromDir(p.Module.Dir, "")
                if err != nil {
                        setVCSError(err)
                        return
index 0fdb2a87369ccf2820fdcf646a4d16f319011360..5d4d679e8327ee08fa1026ca70d2d74aeb495bab 100644 (file)
@@ -235,7 +235,7 @@ func LookupLocal(ctx context.Context, codeRoot string, path string, dir string)
 
        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
                        }
index ebcb2efb348650df985f4afffadd622435824250..7e081eb41a1c4edd67493c9231ca684af5f24dff 100644 (file)
@@ -8,6 +8,7 @@ import (
        "bytes"
        "errors"
        "fmt"
+       "internal/godebug"
        "internal/lazyregexp"
        "internal/singleflight"
        "io/fs"
@@ -869,11 +870,13 @@ type vcsPath struct {
        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 @@ func FromDir(dir, srcRoot string, allowNesting bool) (repoDir string, vcsCmd *Cm
        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
+                               }
+                               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
                                }
-                               // 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)
+                               return "", nil, fmt.Errorf("multiple VCS detected: %s in %q, and %s in %q",
+                                       vcsCmd.Cmd, repoDir, vcs.Cmd, dir)
                        }
                }
 
index c1431549481f47c967db02470c100b569e639729..361d85bcfb326f11331601614e107b79233e3404 100644 (file)
@@ -239,7 +239,7 @@ func TestFromDir(t *testing.T) {
                        }
 
                        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 (file)
index 0000000..538cbf7
--- /dev/null
@@ -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
index 6dab8474b59d44b862d98f46d8608bf2d56b1b21..22cd71c454b712161c47e0f2b2d8a63f80a77014 100644 (file)
@@ -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
index 38dc7b0fac8b421ecb4c6db3e2bd7fd3fd6499a8..2d008825459bb27f2499a22c1f7b50d4fa44c663 100644 (file)
@@ -26,6 +26,7 @@ type Info struct {
 // 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},
index 32fc436e1a17e5f3fb80e914c492cdab7190dc21..a1902bc6d78c7f909abc9eef9b3cad4d46502208 100644 (file)
@@ -230,6 +230,11 @@ Below is the full list of supported metrics, ordered lexicographically.
        /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.