ZEON256 ZEON256
← BACK

Go on Linux does not build PIE by default

ZEON256
ZEON256 2026-10-29
go linux security

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:

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:

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:

Bash
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” because cmd/link still bakes in a PT_INTERP segment for any ELF dynamic object (unless you pass the linker -d) — see the interpreter setup in elf.go. ldd telling 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.