Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Module Info (runtime.modinfo) prevents binary comparison #49573

Closed
markdascher opened this issue Nov 13, 2021 · 2 comments
Closed

Module Info (runtime.modinfo) prevents binary comparison #49573

markdascher opened this issue Nov 13, 2021 · 2 comments

Comments

@markdascher
Copy link

What version of Go are you using (go version)?

$ go version
go version go1.17.2 darwin/amd64

Does this issue reproduce with the latest release?

Yes, it also happens when I build go version go1.17.3 darwin/amd64 from source.

What operating system and processor architecture are you using (go env)?

go env Output
$ go env
GO111MODULE="auto"
GOARCH="amd64"
GOBIN=""
GOCACHE="/Users/…/Library/Caches/go-build"
GOENV="/Users/…/Library/Application Support/go/env"
GOEXE=""
GOEXPERIMENT=""
GOFLAGS=""
GOHOSTARCH="amd64"
GOHOSTOS="darwin"
GOINSECURE=""
GOMODCACHE="/Users/…/go/pkg/mod"
GONOPROXY=""
GONOSUMDB=""
GOOS="darwin"
GOPATH="/Users/…/go"
GOPRIVATE=""
GOPROXY="https://proxy.golang.org,direct"
GOROOT="/usr/local/Cellar/go/1.17.2/libexec"
GOSUMDB="sum.golang.org"
GOTMPDIR=""
GOTOOLDIR="/usr/local/Cellar/go/1.17.2/libexec/pkg/tool/darwin_amd64"
GOVCS=""
GOVERSION="go1.17.2"
GCCGO="gccgo"
AR="ar"
CC="clang"
CXX="clang++"
CGO_ENABLED="1"
GOMOD=""
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
PKG_CONFIG="pkg-config"
GOGCCFLAGS="-fPIC -arch x86_64 -m64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -fdebug-prefix-map=/var/folders/qg/q9y62zhd5fsdgs48lbmy7mrr0000gq/T/go-build2909188506=/tmp/go-build -gno-record-gcc-switches -fno-common"

What did you do?

$ mkdir test
$ cd test
$ go mod init test
$ cat > tools.go <<EOF
//go:build tools

package tools

import _ "github.com/mitchellh/gox"
EOF
$ go mod tidy

$ go clean -cache -testcache -modcache
$ go install -ldflags="-s -w" -trimpath github.com/mitchellh/gox
$ cksum ~/go/bin/gox
4117901733 2801072 ~/go/bin/gox

$ go mod vendor
$ go clean -cache -testcache -modcache
$ go install -ldflags="-s -w" -trimpath github.com/mitchellh/gox
$ cksum ~/go/bin/gox                                            
2432579230 2801072 ~/go/bin/gox

What did you expect to see?

Expected to see the vendored build produce the same binary, with checksum 4117901733. Or at the very least, expected there to be some combination of build flags where go mod vendor doesn't affect the build output.

What did you see instead?

The checksum changed to 2432579230. The most obvious difference comes from runtime.modinfo, where the versioned result doesn't include module hashes:

$ go version -m ~/go/bin/gox 
~/go/bin/gox: go1.17.2
	path	github.com/mitchellh/gox
	mod	github.com/mitchellh/gox	v1.0.1	h1:x0jD3dcHk9a9xPSDN6YEL4xL6Qz0dvNYm8yZqui5chI=
	dep	github.com/hashicorp/go-version	v1.0.0	h1:21MVWPKDphxa7ineQQTrCU5brh7OuVVAzGOCnnCPtE8=
	dep	github.com/mitchellh/iochan	v1.0.0	h1:C+X3KsSTLFVBr/tK1eYN/vs4rJcvsiLU338UhYPJWeY=

$ go version -m ~/go/bin/gox 
~/go/bin/gox: go1.17.2
	path	github.com/mitchellh/gox
	mod	github.com/mitchellh/gox	v1.0.1	
	dep	github.com/hashicorp/go-version	v1.0.0	
	dep	github.com/mitchellh/iochan	v1.0.0	

I can eliminate the discrepancy by modifying src/cmd/go/internal/load/pkg.go to skip the modload.PackageBuildInfo() call. And then confirmed -ldflags="-s -w" wasn't needed, since -trimpath is enough to make the two builds identical using the modified release.

I realize the compiler isn't obligated to produce identical outputs in this situation, and that the metadata is normally useful. However, these differences make it impossible to double-check for correctness when migrating to and from vendored mode. Unfortunately, compiler bugs are probably more common than we'd like to admit, and even if the compiler is perfect, my understanding of them definitely isn't. 🙂 So I've gotten in the habit of double-checking things.

Basically, it would be nice to have an off switch. See #41895 for a similar issue. In my case, passing in -linkshared was not permitted. (And even if it worked, it certainly wasn't designed for this.) There are new options -buildinfo and -buildvcs in master, but they only control new fields being added to runtime.modinfo in the next release. This could actually be a good opportunity to come up with a more flexible option for that, like -buildinfo=flags,vcs and -buildinfo=none, since those options haven't been released yet.

I ran into similar difficulty when migrating from GOPATH mode to module-aware mode, and suspect that runtime.modinfo was a culprit there too.

@seankhliao
Copy link
Member

Duplicate of #46400

@seankhliao seankhliao marked this as a duplicate of #46400 Nov 13, 2021
@markdascher
Copy link
Author

Duplicate of #46400

Oh nice, that would solve the problem. Thanks.

Also wanted to follow-up on the related GOPATH aspect, since I looked into it further:

I ran into similar difficulty when migrating from GOPATH mode to module-aware mode, and suspect that runtime.modinfo was a culprit there too.

Removing runtime.modinfo helps, but there's actually a much bigger obstacle to making a module-aware build produce the same binary as a GOPATH build, at least when combined with a vendor folder. GOPATH mode tracks vendored import paths differently at runtime, meaning that the value of reflect's PkgPath will actually change. Those differences show up in the binary as extra github.com/…/vendor/ string prefixes.

My workaround for that is to move the contents of $GOPATH/src/github.com/…/vendor into $GOPATH/src/github.com/…/_vendor/src, and then set GOPATH=$GOPATH:$GOPATH/src/github.com/…/_vendor. Basically using a second GOPATH to simulate vendoring. Just have to make sure $GOPATH/src is clean, and it only works for a single project-wide vendor folder.

The second challenge is that -trimpath works differently in module-aware mode, including suffixes like @v0.0.0. The -toolexec option comes in handy here, using a custom program to rewrite the individual -trimpath rules passed into asm/compile. Having gone this far, it was simple to exclude _gomod_.go to remove runtime.modinfo.

With those pieces in place, we can run go build with the -toolexec, -trimpath and -ldflags='-buildid … -s -w' options to get identical binaries.

TLDR: #46400 will solve the original issue, and the proposed -buildinfo=none wouldn't actually help GOPATH mode at all. In the meantime, the following-toolexec program works fine for either case.

var version = regexp.MustCompile("(=>[^;@]*)@v[0-9][^/;]*")

func main() {
	var args []string
	trimpath := false

	for _, arg := range os.Args[1:] {
		if trimpath {
			// Remove version suffixes from trimmed paths.
			arg = version.ReplaceAllString(arg, "$1")
			trimpath = false
		} else if arg == "-trimpath" {
			trimpath = true
		} else if strings.HasSuffix(arg, "/_gomod_.go") {
			// Omit runtime.modinfo when compiling.
			continue
		}
		args = append(args, arg)
	}

	cmd := exec.Command(args[0], args[1:]...)
	cmd.Stdin = os.Stdin
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr

	err := cmd.Run()
	if err != nil {
		if ee, ok := err.(*exec.ExitError); ok {
			os.Exit(ee.ProcessState.ExitCode())
		} else {
			fmt.Fprintln(os.Stderr, err.Error())
			os.Exit(1)
		}
	}
}

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

3 participants