Skip to content

Commit 54c9d77

Browse files
rolandshoemakercagedmantis
authored andcommitted
cmd/go: disable support for multiple vcs in one module
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: I5787d90cdca8deb3aca6f154efb627df1e7d2789 Reviewed-on: https://go-review.googlesource.com/c/go/+/686515 LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: David Chase <[email protected]> Commit-Queue: Carlos Amedee <[email protected]> Reviewed-by: Carlos Amedee <[email protected]>
1 parent fca43a8 commit 54c9d77

File tree

9 files changed

+108
-23
lines changed

9 files changed

+108
-23
lines changed

doc/godebug.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,11 @@ crypto/x509.CreateCertificate. The setting `x509sha256skid=0` reverts to SHA-1.
189189
Go 1.25 corrected the semantics of contention reports for runtime-internal locks,
190190
and so removed the [`runtimecontentionstacks` setting](/pkg/runtime#hdr-Environment_Variables).
191191

192+
Go 1.25 (starting with Go 1.25 RC 2) disabled build information stamping when
193+
multiple VCS are detected due to concerns around VCS injection attacks. This
194+
behavior and setting was backported to Go 1.24.5 and Go 1.23.11. This behavior
195+
can be renabled with the setting `allowmultiplevcs=1`.
196+
192197
### Go 1.24
193198

194199
Go 1.24 added a new `fips140` setting that controls whether the Go

src/cmd/go/internal/load/pkg.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2496,7 +2496,6 @@ func (p *Package) setBuildInfo(ctx context.Context, autoVCS bool) {
24962496
var repoDir string
24972497
var vcsCmd *vcs.Cmd
24982498
var err error
2499-
const allowNesting = true
25002499

25012500
wantVCS := false
25022501
switch cfg.BuildBuildvcs {
@@ -2516,7 +2515,7 @@ func (p *Package) setBuildInfo(ctx context.Context, autoVCS bool) {
25162515
// (so the bootstrap toolchain packages don't even appear to be in GOROOT).
25172516
goto omitVCS
25182517
}
2519-
repoDir, vcsCmd, err = vcs.FromDir(base.Cwd(), "", allowNesting)
2518+
repoDir, vcsCmd, err = vcs.FromDir(base.Cwd(), "")
25202519
if err != nil && !errors.Is(err, os.ErrNotExist) {
25212520
setVCSError(err)
25222521
return
@@ -2539,10 +2538,11 @@ func (p *Package) setBuildInfo(ctx context.Context, autoVCS bool) {
25392538
}
25402539
if repoDir != "" && vcsCmd.Status != nil {
25412540
// Check that the current directory, package, and module are in the same
2542-
// repository. vcs.FromDir allows nested Git repositories, but nesting
2543-
// is not allowed for other VCS tools. The current directory may be outside
2544-
// p.Module.Dir when a workspace is used.
2545-
pkgRepoDir, _, err := vcs.FromDir(p.Dir, "", allowNesting)
2541+
// repository. vcs.FromDir disallows nested VCS and multiple VCS in the
2542+
// same repository, unless the GODEBUG allowmultiplevcs is set. The
2543+
// current directory may be outside p.Module.Dir when a workspace is
2544+
// used.
2545+
pkgRepoDir, _, err := vcs.FromDir(p.Dir, "")
25462546
if err != nil {
25472547
setVCSError(err)
25482548
return
@@ -2554,7 +2554,7 @@ func (p *Package) setBuildInfo(ctx context.Context, autoVCS bool) {
25542554
}
25552555
goto omitVCS
25562556
}
2557-
modRepoDir, _, err := vcs.FromDir(p.Module.Dir, "", allowNesting)
2557+
modRepoDir, _, err := vcs.FromDir(p.Module.Dir, "")
25582558
if err != nil {
25592559
setVCSError(err)
25602560
return

src/cmd/go/internal/modfetch/repo.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ func LookupLocal(ctx context.Context, codeRoot string, path string, dir string)
235235

236236
return lookupLocalCache.Do(path, func() Repo {
237237
return newCachingRepo(ctx, path, func(ctx context.Context) (Repo, error) {
238-
repoDir, vcsCmd, err := vcs.FromDir(dir, "", true)
238+
repoDir, vcsCmd, err := vcs.FromDir(dir, "")
239239
if err != nil {
240240
return nil, err
241241
}

src/cmd/go/internal/vcs/vcs.go

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"bytes"
99
"errors"
1010
"fmt"
11+
"internal/godebug"
1112
"internal/lazyregexp"
1213
"internal/singleflight"
1314
"io/fs"
@@ -869,11 +870,13 @@ type vcsPath struct {
869870
schemelessRepo bool // if true, the repo pattern lacks a scheme
870871
}
871872

873+
var allowmultiplevcs = godebug.New("allowmultiplevcs")
874+
872875
// FromDir inspects dir and its parents to determine the
873876
// version control system and code repository to use.
874877
// If no repository is found, FromDir returns an error
875878
// equivalent to os.ErrNotExist.
876-
func FromDir(dir, srcRoot string, allowNesting bool) (repoDir string, vcsCmd *Cmd, err error) {
879+
func FromDir(dir, srcRoot string) (repoDir string, vcsCmd *Cmd, err error) {
877880
// Clean and double-check that dir is in (a subdirectory of) srcRoot.
878881
dir = filepath.Clean(dir)
879882
if srcRoot != "" {
@@ -887,21 +890,28 @@ func FromDir(dir, srcRoot string, allowNesting bool) (repoDir string, vcsCmd *Cm
887890
for len(dir) > len(srcRoot) {
888891
for _, vcs := range vcsList {
889892
if isVCSRoot(dir, vcs.RootNames) {
890-
// Record first VCS we find.
891-
// If allowNesting is false (as it is in GOPATH), keep looking for
892-
// repositories in parent directories and report an error if one is
893-
// found to mitigate VCS injection attacks.
894893
if vcsCmd == nil {
894+
// Record first VCS we find.
895895
vcsCmd = vcs
896896
repoDir = dir
897-
if allowNesting {
897+
if allowmultiplevcs.Value() == "1" {
898+
allowmultiplevcs.IncNonDefault()
898899
return repoDir, vcsCmd, nil
899900
}
901+
// If allowmultiplevcs is not set, keep looking for
902+
// repositories in current and parent directories and report
903+
// an error if one is found to mitigate VCS injection
904+
// attacks.
905+
continue
906+
}
907+
if vcsCmd == vcsGit && vcs == vcsGit {
908+
// Nested Git is allowed, as this is how things like
909+
// submodules work. Git explicitly protects against
910+
// injection against itself.
900911
continue
901912
}
902-
// Otherwise, we have one VCS inside a different VCS.
903-
return "", nil, fmt.Errorf("directory %q uses %s, but parent %q uses %s",
904-
repoDir, vcsCmd.Cmd, dir, vcs.Cmd)
913+
return "", nil, fmt.Errorf("multiple VCS detected: %s in %q, and %s in %q",
914+
vcsCmd.Cmd, repoDir, vcs.Cmd, dir)
905915
}
906916
}
907917

src/cmd/go/internal/vcs/vcs_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@ func TestFromDir(t *testing.T) {
239239
}
240240

241241
wantRepoDir := filepath.Dir(dir)
242-
gotRepoDir, gotVCS, err := FromDir(dir, tempDir, false)
242+
gotRepoDir, gotVCS, err := FromDir(dir, tempDir)
243243
if err != nil {
244244
t.Errorf("FromDir(%q, %q): %v", dir, tempDir, err)
245245
continue
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# To avoid VCS injection attacks, we should not accept multiple different VCS metadata
2+
# folders within a single module (either in the same directory, or nested in different
3+
# directories.)
4+
#
5+
# This behavior should be disabled by setting the allowmultiplevcs GODEBUG.
6+
7+
[short] skip
8+
[!git] skip
9+
10+
cd samedir
11+
12+
exec git init .
13+
14+
# Without explicitly requesting buildvcs, the go command should silently continue
15+
# without determining the correct VCS.
16+
go test -c -o $devnull .
17+
18+
# If buildvcs is explicitly requested, we expect the go command to fail
19+
! go test -buildvcs -c -o $devnull .
20+
stderr '^error obtaining VCS status: multiple VCS detected:'
21+
22+
env GODEBUG=allowmultiplevcs=1
23+
go test -buildvcs -c -o $devnull .
24+
25+
env GODEBUG=
26+
cd ../nested
27+
exec git init .
28+
# cd a
29+
go test -c -o $devnull ./a
30+
! go test -buildvcs -c -o $devnull ./a
31+
stderr '^error obtaining VCS status: multiple VCS detected:'
32+
# allowmultiplevcs doesn't disable the check that the current directory, package, and
33+
# module are in the same repository.
34+
env GODEBUG=allowmultiplevcs=1
35+
! go test -buildvcs -c -o $devnull ./a
36+
stderr '^error obtaining VCS status: main package is in repository'
37+
38+
-- samedir/go.mod --
39+
module example
40+
41+
go 1.18
42+
-- samedir/example.go --
43+
package main
44+
-- samedir/.bzr/test --
45+
hello
46+
47+
-- nested/go.mod --
48+
module example
49+
50+
go 1.18
51+
-- nested/a/example.go --
52+
package main
53+
-- nested/a/.bzr/test --
54+
hello

src/cmd/go/testdata/script/version_buildvcs_nested.txt

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,25 +9,35 @@ cd root
99
go mod init example.com/root
1010
exec git init
1111

12-
# Nesting repositories in parent directories are ignored, as the current
13-
# directory main package, and containing main module are in the same repository.
14-
# This is an error in GOPATH mode (to prevent VCS injection), but for modules,
15-
# we assume users have control over repositories they've checked out.
12+
13+
# Nesting repositories in parent directories are an error, to prevent VCS injection.
14+
# This can be disabled with the allowmultiplevcs GODEBUG.
1615
mkdir hgsub
1716
cd hgsub
1817
exec hg init
1918
cp ../../main.go main.go
2019
! go build
20+
stderr '^error obtaining VCS status: multiple VCS detected: hg in ".*hgsub", and git in ".*root"$'
21+
stderr '^\tUse -buildvcs=false to disable VCS stamping.$'
22+
env GODEBUG=allowmultiplevcs=1
23+
! go build
2124
stderr '^error obtaining VCS status: main module is in repository ".*root" but current directory is in repository ".*hgsub"$'
2225
stderr '^\tUse -buildvcs=false to disable VCS stamping.$'
2326
go build -buildvcs=false
27+
env GODEBUG=
2428
go mod init example.com/root/hgsub
29+
! go build
30+
stderr '^error obtaining VCS status: multiple VCS detected: hg in ".*hgsub", and git in ".*root"$'
31+
stderr '^\tUse -buildvcs=false to disable VCS stamping.$'
32+
env GODEBUG=allowmultiplevcs=1
2533
go build
34+
env GODEBUG=
2635
cd ..
2736

2837
# It's an error to build a package from a nested Git repository if the package
2938
# is in a separate repository from the current directory or from the module
30-
# root directory.
39+
# root directory. Otherwise nested Git repositories are allowed, as this is
40+
# how Git implements submodules (and protects against Git based VCS injection.)
3141
mkdir gitsub
3242
cd gitsub
3343
exec git init

src/internal/godebugs/table.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ type Info struct {
2626
// Note: After adding entries to this table, update the list in doc/godebug.md as well.
2727
// (Otherwise the test in this package will fail.)
2828
var All = []Info{
29+
{Name: "allowmultiplevcs", Package: "cmd/go"},
2930
{Name: "asynctimerchan", Package: "time", Changed: 23, Old: "1"},
3031
{Name: "containermaxprocs", Package: "runtime", Changed: 25, Old: "0"},
3132
{Name: "dataindependenttiming", Package: "crypto/subtle", Opaque: true},

src/runtime/metrics/doc.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,11 @@ Below is the full list of supported metrics, ordered lexicographically.
230230
/gc/stack/starting-size:bytes
231231
The stack size of new goroutines.
232232
233+
/godebug/non-default-behavior/allowmultiplevcs:events
234+
The number of non-default behaviors executed by the cmd/go
235+
package due to a non-default GODEBUG=allowmultiplevcs=...
236+
setting.
237+
233238
/godebug/non-default-behavior/asynctimerchan:events
234239
The number of non-default behaviors executed by the time package
235240
due to a non-default GODEBUG=asynctimerchan=... setting.

0 commit comments

Comments
 (0)