Skip to content

cmd/link: cgo cross linking using the clang --target flag creates crashing programs #73406

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

Open
apsaltis-ddog opened this issue Apr 16, 2025 · 3 comments
Labels
BugReport Issues describing a possible bug in the Go implementation. compiler/runtime Issues related to the Go compiler and/or runtime. NeedsFix The path to resolution is known, but the work has not been done.
Milestone

Comments

@apsaltis-ddog
Copy link

apsaltis-ddog commented Apr 16, 2025

Go version

go version go1.24.2 linux/amd64

Output of go env in your module/workspace:

(Some output here and below manually modified to hide internal configuration)

AR='ar'
CC='gcc'
CGO_CFLAGS='-O2 -g'
CGO_CPPFLAGS=''
CGO_CXXFLAGS='-O2 -g'
CGO_ENABLED='1'
CGO_FFLAGS='-O2 -g'
CGO_LDFLAGS='-O2 -g'
CXX='g++'
GCCGO='gccgo'
GO111MODULE=''
GOAMD64='v1'
GOARCH='amd64'
GOAUTH='netrc'
GOBIN=''
GOCACHE='$HOME/.cache/go-build'
GOCACHEPROG=''
GODEBUG=''
GOENV='$HOME/.config/go/env'
GOEXE=''
GOEXPERIMENT=''
GOFIPS140='off'
GOFLAGS=''
GOGCCFLAGS='-fPIC -m64 -pthread -Wl,--no-gc-sections -fmessage-length=0 -ffile-prefix-map=/tmp/go-build353822596=/tmp/go-build -gno-record-gcc-switches'
GOHOSTARCH='amd64'
GOHOSTOS='linux'
GOINSECURE=''
GOMOD='$HOME/tmp/go-cross/go.mod'
GOMODCACHE='$HOME/go/pkg/mod'
GONOPROXY=''
GONOSUMDB='$INTERNAL'
GOOS='linux'
GOPATH='$HOME/go'
GOPRIVATE=''
GOPROXY='$INTERNAL,direct'
GOROOT='$HOME/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.24.2.linux-amd64'
GOSUMDB='sum.golang.org'
GOTELEMETRY='local'
GOTELEMETRYDIR='$HOME/.config/go/telemetry'
GOTMPDIR=''
GOTOOLCHAIN='auto'
GOTOOLDIR='$HOME/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.24.2.linux-amd64/pkg/tool/linux_amd64'
GOVCS=''
GOVERSION='go1.24.2'
GOWORK=''
PKG_CONFIG='pkg-config'

What did you do?

Attempted to cross-compile a cgo binary from amd64 to arm64 using a private LLVM/Clang-based toolchain and sysroot. I was able to reproduce this from arm64 crossing to amd64 as well.

Minimal example (which doesn't run in the playground due to needing cgo): https://go.dev/play/p/a0qGj4Vakf-

Built with:

CGO_ENABLED=1 GOOS=linux GOARCH=arm64 CC=/path/to/private/clang CGO_CFLAGS="--sysroot=/path/to/private/sysroot-arm --target=aarch64-unknown-linux-gnu" \
    go build \
        -ldflags="-v -extld C/path/to/private/clang -extldflags '-fuse-ld=lld --sysroot=/path/to/private/sysroot-arm --target=aarch64-unknown-linux-gnu --verbose'" \
        -gcflags=-v

The output binary can be run on an ARM host, or via qemu with something like:

qemu-aarch64 -L /path/to/private/sysroot-arm ./go-cross

What did you see happen?

The go tool link output from the above go build command contained a linker invocation like this:

(The "host link" outputs below are particularly long, and is more readable in an editor)

host link: "/path/to/private/clang" "-o" "/tmp/go-build481106842/b001/exe/a.out" "-rdynamic" "/tmp/go-link-168810886/go.o" "/tmp/go-link-168810886/000000.o" "/tmp/go-link-168810886/000001.o" "/tmp/go-link-168810886/000002.o" "/tmp/go-link-168810886/000003.o" "/tmp/go-link-168810886/000004.o" "/tmp/go-link-168810886/000005.o" "/tmp/go-link-168810886/000006.o" "/tmp/go-link-168810886/000007.o" "/tmp/go-link-168810886/000008.o" "/tmp/go-link-168810886/000009.o" "/tmp/go-link-168810886/000010.o" "/tmp/go-link-168810886/000011.o" "/tmp/go-link-168810886/000012.o" "/tmp/go-link-168810886/000013.o" "/tmp/go-link-168810886/000014.o" "/tmp/go-link-168810886/000015.o" "-O2" "-g" "-O2" "-g" "-lpthread" "-fuse-ld=lld" "--sysroot=/path/to/private/sysroot-arm" "--target=aarch64-unknown-linux-gnu" "--verbose"

If I use the same compiler targeting the host architecture, using a sysroot compiled for it (in this case amd64/x86_64), the output is different:

CGO_ENABLED=1 GOOS=linux GOARCH=amd64 CC=/path/to/private/clang CGO_CFLAGS="--sysroot=/path/to/private/sysroot-amd64 --target=x86_64-unknown-linux-gnu" \
    go build \
        -ldflags="-v -extld C/path/to/private/clang -extldflags '-fuse-ld=lld --sysroot=/path/to/private/sysroot-amd64 --target=x86_64-unknown-linux-gnu --verbose'" \
        -gcflags=-v

...

host link: "$HOME/tmp/llvm-17.0.6-dist-x86_64/bin/clang" "-m64" "-Wl,--build-id=0xe3c881e6feaf37cd5c2a7e4e643c549ff859ee17" "-o" "/tmp/go-build3873136084/b001/exe/a.out" "-Wl,--export-dynamic-symbol=_cgo_panic" "-Wl,--export-dynamic-symbol=_cgo_topofstack" "-Wl,--export-dynamic-symbol=crosscall2" "-Qunused-arguments" "-Wl,--compress-debug-sections=zlib" "/tmp/go-link-1498717480/go.o" "/tmp/go-link-1498717480/000000.o" "/tmp/go-link-1498717480/000001.o" "/tmp/go-link-1498717480/000002.o" "/tmp/go-link-1498717480/000003.o" "/tmp/go-link-1498717480/000004.o" "/tmp/go-link-1498717480/000005.o" "/tmp/go-link-1498717480/000006.o" "/tmp/go-link-1498717480/000007.o" "/tmp/go-link-1498717480/000008.o" "/tmp/go-link-1498717480/000009.o" "/tmp/go-link-1498717480/000010.o" "/tmp/go-link-1498717480/000011.o" "/tmp/go-link-1498717480/000012.o" "/tmp/go-link-1498717480/000013.o" "/tmp/go-link-1498717480/000014.o" "/tmp/go-link-1498717480/000015.o" "-O2" "-g" "-O2" "-g" "-lpthread" "-no-pie" "-fuse-ld=lld" "--sysroot=/path/to/private/sysroot-amd64" "--target=x86_64-unknown-linux-gnu" "--verbose"

Both commands succeed, and produce a binary.

These flag differences are numerous and consequential: running the cross-compiled binary (in this particular case, using qemu) crashes at startup like this:

> qemu-aarch64 -L /path/to/private/sysroot-arm ./go-cross
runtime: pcHeader: magic= 0xfffffff1 pad1= 0 pad2= 0 minLC= 4 ptrSize= 8 pcHeader.textStart= 0xd9d20 text= 0x7f051fca9d20 pluginpath=
fatal error: invalid function symbol table
runtime: panic before malloc heap initialized

runtime stack:
runtime.throw({0x7f051fbff423?, 0x0?})
	$HOME/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.24.2.linux-amd64/src/runtime/panic.go:1101 +0x38 fp=0x4000008000b0 sp=0x400000800080 pc=0x7f051fd11208
runtime.moduledataverify1(0x7f051fd8d5e0?)
	$HOME/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.24.2.linux-amd64/src/runtime/symtab.go:623 +0x644 fp=0x4000008001d0 sp=0x4000008000b0 pc=0x7f051fd13704
runtime.moduledataverify(...)
	$HOME/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.24.2.linux-amd64/src/runtime/symtab.go:599
runtime.schedinit()
	$HOME/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.24.2.linux-amd64/src/runtime/proc.go:834 +0x88 fp=0x400000800260 sp=0x4000008001d0 pc=0x7f051fce2fd8
runtime.rt0_go()
	$HOME/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.24.2.linux-amd64/src/runtime/asm_arm64.s:86 +0xa4 fp=0x400000800290 sp=0x400000800260 pc=0x7f051fd15154

The binary fails similarly on an actual ARM host as well. When I investigated this, it seems that the critical difference is the -no-pie flag in the second (amd64, same as the build host) output, as adding -buildmode pie to the cross-compiling go build prevents the binary from crashing like this.

What did you expect to see?

The flags in the two "host link" invocations above should be more similar (aside from arch-specific flags), and the cross-compiled program should not crash as mentioned above when run in an emulator or a system with that architecture.

@gopherbot gopherbot added the compiler/runtime Issues related to the Go compiler and/or runtime. label Apr 16, 2025
@apsaltis-ddog
Copy link
Author

apsaltis-ddog commented Apr 16, 2025

I think the underlying problem is in how go tool link is doing tests to determine what flags it can pass to the underlying linker. While exploring, I found trimLinkerArgv in the linker code. Notably, it contains, -target in a few places, but doesn't mention --target anywhere.

Clang supports both ways of specifying targets, but they are used differently: -target only expects the target in a second arg, and --target only expects it in the same arg (--target=$ARG).

From the looks of it, in this list:

prefixesToKeep := []string{
"-f",
"-m",
"-p",
"-Wl,",
"-arch",
"-isysroot",
"--sysroot",
"-target",
}

-target should be --target, or --target should be added to the list. I'd be happy to create a PR that would do this -- this created non-crashing binaries for me.

@gabyhelp gabyhelp added the BugReport Issues describing a possible bug in the Go implementation. label Apr 16, 2025
@mknyszek
Copy link
Contributor

@apsaltis-ddog Your fix seems reasonable. Thanks for investigating. Feel free to send a patch!

@mknyszek mknyszek added the NeedsFix The path to resolution is known, but the work has not been done. label Apr 16, 2025
@mknyszek mknyszek added this to the Backlog milestone Apr 16, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
BugReport Issues describing a possible bug in the Go implementation. compiler/runtime Issues related to the Go compiler and/or runtime. NeedsFix The path to resolution is known, but the work has not been done.
Projects
Development

No branches or pull requests

4 participants