cmd/go: use the resolved buildmode in build IDs and build info When -buildmode=default is used, the link action ID hashed the literal string "default", so building with -buildmode=default and building with the buildmode it resolves to produced binaries that were identical except for their build IDs. Hash the resolved buildmode (ldBuildmode) instead, so that equivalent builds share a build ID and cached link outputs. Additionally, the build info stamped into binaries normalized -buildmode=default to "exe" without accounting for platforms where the default is PIE (android, ios, darwin, and windows without -race), misreporting the buildmode actually given to the linker. Record "pie" on those platforms instead. Together these make it possible to reproduce a binary built with -buildmode=default from its stamped build info alone. Fixes #63559 Change-Id: If3ac6a6a84fbf20097ff3009e228ce07705d6835 GitHub-Last-Rev: 1ddf5bfef00c64cc589d90e4e8bf4257f8d05b88 GitHub-Pull-Request: golang/go#80312 Reviewed-on: https://go-review.googlesource.com/c/go/+/798680 Reviewed-by: Mark Freeman <markfreeman@google.com> LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: Sean Liao <sean@liao.dev> Reviewed-by: Michael Pratt <mpratt@google.com>
diff --git a/src/cmd/go/internal/load/pkg.go b/src/cmd/go/internal/load/pkg.go index 6c1e51e..7061630 100644 --- a/src/cmd/go/internal/load/pkg.go +++ b/src/cmd/go/internal/load/pkg.go
@@ -2443,6 +2443,9 @@ if buildmode == "default" { if p.Name == "main" { buildmode = "exe" + if platform.DefaultPIE(cfg.Goos, cfg.Goarch, cfg.BuildRace) { + buildmode = "pie" + } } else { buildmode = "archive" }
diff --git a/src/cmd/go/internal/work/exec.go b/src/cmd/go/internal/work/exec.go index db3b7bf..41d1673 100644 --- a/src/cmd/go/internal/work/exec.go +++ b/src/cmd/go/internal/work/exec.go
@@ -1669,7 +1669,10 @@ // Toolchain-independent configuration. fmt.Fprintf(h, "link\n") - fmt.Fprintf(h, "buildmode %s goos %s goarch %s\n", cfg.BuildBuildmode, cfg.Goos, cfg.Goarch) + // Hash the resolved buildmode (ldBuildmode), not cfg.BuildBuildmode, + // so that -buildmode=default produces the same build ID as the + // buildmode it resolves to. See go.dev/issue/63559. + fmt.Fprintf(h, "buildmode %s goos %s goarch %s\n", ldBuildmode, cfg.Goos, cfg.Goarch) fmt.Fprintf(h, "import %q\n", p.ImportPath) fmt.Fprintf(h, "omitdebug %v standard %v local %v prefix %q\n", p.Internal.OmitDebug, p.Standard, p.Internal.Local, p.Internal.LocalPrefix) fmt.Fprintf(h, "defaultgodebug %q\n", p.DefaultGODEBUG)
diff --git a/src/cmd/go/scriptconds_test.go b/src/cmd/go/scriptconds_test.go index 25053dd..277352d 100644 --- a/src/cmd/go/scriptconds_test.go +++ b/src/cmd/go/scriptconds_test.go
@@ -11,6 +11,7 @@ "errors" "fmt" "internal/buildcfg" + "internal/platform" "os" "os/exec" "path/filepath" @@ -45,6 +46,7 @@ add("git-sha256", script.OnceCondition("the local 'git' version is recent enough to support sha256 object/commit hashes", gitSupportsSHA256)) add("trimpath", script.OnceCondition("test binary was built with -trimpath", isTrimpath)) add("default-cgo", lazyBool("when CGO_ENABLED=1|0 was set in make.bash", defaultCgo)) + add("default-pie", script.Condition("-buildmode=default resolves to -buildmode=pie", defaultPIE)) return conds } @@ -149,3 +151,14 @@ func defaultCgo() bool { return buildcfg.DefaultCGO_ENABLED == "1" || buildcfg.DefaultCGO_ENABLED == "0" } + +// defaultPIE reports whether -buildmode=default resolves to -buildmode=pie for +// the script's GOOS/GOARCH. It assumes -race is not in effect, which is the only +// case where the resolved default buildmode depends on the race flag (PIE is not +// the default with -race on windows). Scripts that build with -race must not rely +// on this condition. +func defaultPIE(s *script.State) (bool, error) { + GOOS, _ := s.LookupEnv("GOOS") + GOARCH, _ := s.LookupEnv("GOARCH") + return platform.DefaultPIE(GOOS, GOARCH, false), nil +}
diff --git a/src/cmd/go/testdata/script/README b/src/cmd/go/testdata/script/README index 7105215..cd04842 100644 --- a/src/cmd/go/testdata/script/README +++ b/src/cmd/go/testdata/script/README
@@ -391,6 +391,8 @@ cmd/go GOOS/GOARCH != GOHOSTOS/GOHOSTARCH [default-cgo] when CGO_ENABLED=1|0 was set in make.bash +[default-pie] + -buildmode=default resolves to -buildmode=pie [exec:*] <suffix> names an executable in the test binary's PATH [fuzz]
diff --git a/src/cmd/go/testdata/script/build_buildmode_default_reproducible.txt b/src/cmd/go/testdata/script/build_buildmode_default_reproducible.txt new file mode 100644 index 0000000..a841e5c --- /dev/null +++ b/src/cmd/go/testdata/script/build_buildmode_default_reproducible.txt
@@ -0,0 +1,37 @@ +# Issue 63559: building with -buildmode=default should produce +# a binary identical to one built with the buildmode that default +# resolves to, and the build info stamped into the binary should +# record the resolved buildmode. + +[short] skip 'links binaries' +# gccgo does not set -fPIE for the default->pie case (unlike explicit +# -buildmode=pie), so the two binaries would not be identical there. +[compiler:gccgo] skip + +# Build with -buildmode=default and, separately, with the buildmode that +# default resolves to. Use distinct build caches so that the second build +# cannot be served from the first build's link cache: the binaries must be +# byte-identical because they are truly reproducible, not because one was +# copied from the other. +env GOCACHE=$WORK/cache-default +go build -buildmode=default -o default$GOEXE . + +env GOCACHE=$WORK/cache-explicit +[!default-pie] go build -buildmode=exe -o explicit$GOEXE . +[default-pie] go build -buildmode=pie -o explicit$GOEXE . + +cmp -q default$GOEXE explicit$GOEXE + +# The build info must record the resolved buildmode, not the literal "default". +go version -m default$GOEXE +[!default-pie] stdout -buildmode=exe +[default-pie] stdout -buildmode=pie + +-- go.mod -- +module m + +go 1.26 +-- main.go -- +package main + +func main() {}
diff --git a/src/cmd/go/testdata/script/version.txt b/src/cmd/go/testdata/script/version.txt index 722859f..5958dca 100644 --- a/src/cmd/go/testdata/script/version.txt +++ b/src/cmd/go/testdata/script/version.txt
@@ -47,14 +47,16 @@ go version fortune.exe stdout '^fortune.exe: .+' go version -m fortune.exe -stdout -buildmode=exe +[!default-pie] stdout -buildmode=exe +[default-pie] stdout -buildmode=pie stdout '^\tpath\trsc.io/fortune' stdout '^\tmod\trsc.io/fortune\tv1.0.0' # Check the build info of a binary built from $GOROOT/src/cmd go build -o test2json.exe cmd/test2json go version -m test2json.exe -stdout -buildmode=exe +[!default-pie] stdout -buildmode=exe +[default-pie] stdout -buildmode=pie stdout '^test2json.exe: .+' stdout '^\tpath\tcmd/test2json$' ! stdout 'mod[^e]'