Go on Linux does not build PIE by default
go build on Linux gives you a plain non-PIE executable, no ASLR for the binary itself. PIE-by-default
is only a thing on android, ios, windows, and darwin. Straight from
src/internal/platform/supported.go:
// DefaultPIE reports whether goos/goarch produces a PIE binary when using the
// "default" buildmode. On Windows this is affected by -race,
// so force the caller to pass that in to centralize that choice.
func DefaultPIE (goos , goarch string , isRace bool ) bool {
switch goos {
case "android" , "ios" :
return true
case "windows" :
if isRace {
// PIE is not supported with -race on windows;
// see https://go.dev/cl/416174.
return false
}
return true
case "darwin" :
return true
}
return false
}Which cmd/go consumes in
src/cmd/go/internal/work/init.go:
case "default" :
ldBuildmode = "exe"
if platform .DefaultPIE (cfg .Goos , cfg .Goarch , cfg .BuildRace ) {
ldBuildmode = "pie"
if cfg .Goos != "windows" && ! gccgo {
codegenArg = "-shared"
}
}linux falls through to return false, so ldBuildmode stays "exe". This is a deliberate upstream
position, not an oversight — when Gentoo/Arch asked for PIE-by-default in
#36352, ianlancetaylor answered:
Go does not build PIE by default on master.
(The distros patch it themselves.)
Getting a static PIE
The trick is that PIE does not imply dynamic linking. With cgo disabled, the internal linker handles
-buildmode=pie (see InternalLinkPIESupported
— linux/amd64, linux/arm64, linux/loong64, linux/ppc64, linux/ppc64le, linux/s390x) and
emits no DT_NEEDED entries at all:
CGO_ENABLED =0 go build -buildmode=pie -o app .Verified with go1.26.3 on linux/amd64:
$ file nopie pie
nopie: ELF 64-bit LSB executable, x86-64, ..., statically linked
pie: ELF 64-bit LSB pie executable, x86-64, ..., dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2
$ ldd pie
statically linked
$ readelf -d pie # no DT_NEEDED entries, just FLAGS_1: PIE
Two gotchas:
file(1)says “dynamically linked” becausecmd/linkstill bakes in aPT_INTERPsegment for any ELF dynamic object (unless you pass the linker-d) — see the interpreter setup inelf.go.lddtelling you “statically linked” is the honest answer.- If cgo is in the picture, internal linking is off the table and you have to lean on the external
linker instead:
-ldflags='-linkmode external -extldflags "-static-pie"'(#41944).
Good deeper writeup: Building static binaries with Go on Linux — Eli Bendersky.