From 97a43a6db48222c372a4d067ab13d3b42a5869bc Mon Sep 17 00:00:00 2001 From: Luna Nova Date: Sun, 14 Sep 2025 10:51:15 -0700 Subject: [PATCH 1/4] go: teach internal linker to use correct ELF interpreter Teaches go's internal linker to use the GO_LDSO env var as the path for the dynamic linker, instead of defaulting to an FHS /lib path. GO_LDSO is automatically set to the appropriate value by checking $NIX_CC/nix-support/dynamic-linker External linking is set as the default for cross compile situations where CGO is supported. Teaching go to correctly handle cross in the internal linker is hard, and our system linker already knows how to find the right ELF interpreter. For cross situations where CGO is not supported we assume either non-ELF binaries or static binaries will be produced. --- pkgs/build-support/go/module.nix | 3 ++ pkgs/development/compilers/go/1.24.nix | 10 +++++ pkgs/development/compilers/go/1.25.nix | 10 +++++ .../compilers/go/go-env-go_ldso.patch | 44 +++++++++++++++++++ 4 files changed, 67 insertions(+) create mode 100644 pkgs/development/compilers/go/go-env-go_ldso.patch diff --git a/pkgs/build-support/go/module.nix b/pkgs/build-support/go/module.nix index c1c644e4294e..d2d12be74229 100644 --- a/pkgs/build-support/go/module.nix +++ b/pkgs/build-support/go/module.nix @@ -250,6 +250,9 @@ lib.extendMkDerivation { export GOPATH="$TMPDIR/go" export GOPROXY=off export GOSUMDB=off + if [ -f "$NIX_CC_FOR_TARGET/nix-support/dynamic-linker" ]; then + export GO_LDSO=$(cat $NIX_CC_FOR_TARGET/nix-support/dynamic-linker) + fi cd "$modRoot" '' + lib.optionalString (finalAttrs.vendorHash != null) '' diff --git a/pkgs/development/compilers/go/1.24.nix b/pkgs/development/compilers/go/1.24.nix index 947123c5e170..3390a34eb7aa 100644 --- a/pkgs/development/compilers/go/1.24.nix +++ b/pkgs/development/compilers/go/1.24.nix @@ -64,6 +64,7 @@ stdenv.mkDerivation (finalAttrs: { }) ./remove-tools-1.11.patch ./go_no_vendor_checks-1.23.patch + ./go-env-go_ldso.patch ]; inherit (stdenv.targetPlatform.go) GOOS GOARCH GOARM; @@ -99,6 +100,9 @@ stdenv.mkDerivation (finalAttrs: { buildPhase = '' runHook preBuild export GOCACHE=$TMPDIR/go-cache + if [ -f "$NIX_CC/nix-support/dynamic-linker" ]; then + export GO_LDSO=$(cat $NIX_CC/nix-support/dynamic-linker) + fi export PATH=$(pwd)/bin:$PATH @@ -106,6 +110,12 @@ stdenv.mkDerivation (finalAttrs: { # Independent from host/target, CC should produce code for the building system. # We only set it when cross-compiling. export CC=${buildPackages.stdenv.cc}/bin/cc + # Prefer external linker for cross when CGO is supported, since + # we haven't taught go's internal linker to pick the correct ELF + # interpreter for cross + # When CGO is not supported we rely on static binaries being built + # since they don't need an ELF interpreter + export GO_EXTLINK_ENABLED=${toString finalAttrs.CGO_ENABLED} ''} ulimit -a diff --git a/pkgs/development/compilers/go/1.25.nix b/pkgs/development/compilers/go/1.25.nix index b25f60b3d5ae..ab81706c7d27 100644 --- a/pkgs/development/compilers/go/1.25.nix +++ b/pkgs/development/compilers/go/1.25.nix @@ -69,6 +69,7 @@ stdenv.mkDerivation (finalAttrs: { }) ./remove-tools-1.11.patch ./go_no_vendor_checks-1.23.patch + ./go-env-go_ldso.patch ]; inherit (stdenv.targetPlatform.go) GOOS GOARCH GOARM; @@ -104,6 +105,9 @@ stdenv.mkDerivation (finalAttrs: { buildPhase = '' runHook preBuild export GOCACHE=$TMPDIR/go-cache + if [ -f "$NIX_CC/nix-support/dynamic-linker" ]; then + export GO_LDSO=$(cat $NIX_CC/nix-support/dynamic-linker) + fi export PATH=$(pwd)/bin:$PATH @@ -111,6 +115,12 @@ stdenv.mkDerivation (finalAttrs: { # Independent from host/target, CC should produce code for the building system. # We only set it when cross-compiling. export CC=${buildPackages.stdenv.cc}/bin/cc + # Prefer external linker for cross when CGO is supported, since + # we haven't taught go's internal linker to pick the correct ELF + # interpreter for cross + # When CGO is not supported we rely on static binaries being built + # since they don't need an ELF interpreter + export GO_EXTLINK_ENABLED=${toString finalAttrs.CGO_ENABLED} ''} ulimit -a diff --git a/pkgs/development/compilers/go/go-env-go_ldso.patch b/pkgs/development/compilers/go/go-env-go_ldso.patch new file mode 100644 index 000000000000..395b1dab3bf4 --- /dev/null +++ b/pkgs/development/compilers/go/go-env-go_ldso.patch @@ -0,0 +1,44 @@ +diff --git a/src/cmd/dist/buildruntime.go b/src/cmd/dist/buildruntime.go +index 87e8867176..dbb635011f 100644 +--- a/src/cmd/dist/buildruntime.go ++++ b/src/cmd/dist/buildruntime.go +@@ -62,7 +62,7 @@ func mkbuildcfg(file string) { + fmt.Fprintf(&buf, "const DefaultGORISCV64 = `%s`\n", goriscv64) + fmt.Fprintf(&buf, "const defaultGOEXPERIMENT = `%s`\n", goexperiment) + fmt.Fprintf(&buf, "const defaultGO_EXTLINK_ENABLED = `%s`\n", goextlinkenabled) +- fmt.Fprintf(&buf, "const defaultGO_LDSO = `%s`\n", defaultldso) ++ fmt.Fprintf(&buf, "const DefaultGO_LDSO = `%s`\n", defaultldso) + fmt.Fprintf(&buf, "const version = `%s`\n", findgoversion()) + fmt.Fprintf(&buf, "const defaultGOOS = runtime.GOOS\n") + fmt.Fprintf(&buf, "const defaultGOARCH = runtime.GOARCH\n") +diff --git a/src/cmd/link/internal/ld/elf.go b/src/cmd/link/internal/ld/elf.go +index 6ff1d94383..147a7d91c9 100644 +--- a/src/cmd/link/internal/ld/elf.go ++++ b/src/cmd/link/internal/ld/elf.go +@@ -1927,8 +1927,12 @@ func asmbElf(ctxt *Link) { + sh.Flags = uint64(elf.SHF_ALLOC) + sh.Addralign = 1 + +- if interpreter == "" && buildcfg.GOOS == runtime.GOOS && buildcfg.GOARCH == runtime.GOARCH && buildcfg.GO_LDSO != "" { +- interpreter = buildcfg.GO_LDSO ++ if interpreter == "" { ++ if goLdso := os.Getenv("GO_LDSO"); goLdso != "" { ++ interpreter = goLdso ++ } else if buildcfg.GOOS == runtime.GOOS && buildcfg.GOARCH == runtime.GOARCH { ++ interpreter = buildcfg.DefaultGO_LDSO ++ } + } + + if interpreter == "" { +diff --git a/src/internal/buildcfg/cfg.go b/src/internal/buildcfg/cfg.go +index 7e4ee365df..8aaa70fb36 100644 +--- a/src/internal/buildcfg/cfg.go ++++ b/src/internal/buildcfg/cfg.go +@@ -33,7 +33,6 @@ var ( + GORISCV64 = goriscv64() + GOWASM = gowasm() + ToolTags = toolTags() +- GO_LDSO = defaultGO_LDSO + GOFIPS140 = gofips140() + Version = version + ) From 08aadbf8d4dbdab061b455e3181b16046445b48b Mon Sep 17 00:00:00 2001 From: Luna Nova Date: Sun, 14 Sep 2025 10:52:42 -0700 Subject: [PATCH 2/4] go: position independent executables by default Followup to setting --enable-default-pie for our gcc, we teach go to build PIE by default when supported. --- pkgs/build-support/go/module.nix | 7 ------ pkgs/development/compilers/go/1.24.nix | 1 + pkgs/development/compilers/go/1.25.nix | 23 +++++++++++++++++++ .../compilers/go/go-default-pie.patch | 13 +++++++++++ 4 files changed, 37 insertions(+), 7 deletions(-) create mode 100644 pkgs/development/compilers/go/go-default-pie.patch diff --git a/pkgs/build-support/go/module.nix b/pkgs/build-support/go/module.nix index d2d12be74229..685f0071bc82 100644 --- a/pkgs/build-support/go/module.nix +++ b/pkgs/build-support/go/module.nix @@ -269,13 +269,6 @@ lib.extendMkDerivation { } '' + '' - - # currently pie is only enabled by default in pkgsMusl - # this will respect the `hardening{Disable,Enable}` flags if set - if [[ $NIX_HARDENING_ENABLE =~ "pie" ]]; then - export GOFLAGS="-buildmode=pie $GOFLAGS" - fi - runHook postConfigure '' ); diff --git a/pkgs/development/compilers/go/1.24.nix b/pkgs/development/compilers/go/1.24.nix index 3390a34eb7aa..653b14f6ca1e 100644 --- a/pkgs/development/compilers/go/1.24.nix +++ b/pkgs/development/compilers/go/1.24.nix @@ -65,6 +65,7 @@ stdenv.mkDerivation (finalAttrs: { ./remove-tools-1.11.patch ./go_no_vendor_checks-1.23.patch ./go-env-go_ldso.patch + ./go-default-pie.patch ]; inherit (stdenv.targetPlatform.go) GOOS GOARCH GOARM; diff --git a/pkgs/development/compilers/go/1.25.nix b/pkgs/development/compilers/go/1.25.nix index ab81706c7d27..e7676644128c 100644 --- a/pkgs/development/compilers/go/1.25.nix +++ b/pkgs/development/compilers/go/1.25.nix @@ -10,8 +10,12 @@ buildPackages, pkgsBuildTarget, targetPackages, + # for testing testers, + runCommand, + bintools, skopeo, + clickhouse-backup, buildGo125Module, }: @@ -19,6 +23,7 @@ let goBootstrap = buildPackages.callPackage ./bootstrap122.nix { }; skopeoTest = skopeo.override { buildGoModule = buildGo125Module; }; + clickhouse-backupTest = clickhouse-backup.override { buildGoModule = buildGo125Module; }; # We need a target compiler which is still runnable at build time, # to handle the cross-building case where build != host == target @@ -70,6 +75,7 @@ stdenv.mkDerivation (finalAttrs: { ./remove-tools-1.11.patch ./go_no_vendor_checks-1.23.patch ./go-env-go_ldso.patch + ./go-default-pie.patch ]; inherit (stdenv.targetPlatform.go) GOOS GOARCH GOARM; @@ -179,6 +185,23 @@ stdenv.mkDerivation (finalAttrs: { command = "go version"; version = "go${finalAttrs.version}"; }; + # Picked clickhouse-backup as a package that sets CGO_ENABLED=0 + # Running and outputting the right version proves a working ELF interpreter was picked + clickhouse-backup = testers.testVersion { package = clickhouse-backupTest; }; + clickhouse-backup-is-pie = runCommand "has-pie" { meta.broken = stdenv.hostPlatform.isStatic; } '' + ${lib.optionalString (!isCross) '' + if ${lib.getExe' bintools "readelf"} -p .comment ${lib.getExe clickhouse-backup} | grep -Fq "GCC: (GNU)"; then + echo "${lib.getExe clickhouse-backup} has a GCC .comment, but it should have used the internal go linker" + exit 1 + fi + ''} + if ${lib.getExe' bintools "readelf"} -h ${lib.getExe clickhouse-backup} | grep -q "Type:.*DYN"; then + touch $out + else + echo "ERROR: clickhouse-backup is NOT PIE" + exit 1 + fi + ''; }; }; diff --git a/pkgs/development/compilers/go/go-default-pie.patch b/pkgs/development/compilers/go/go-default-pie.patch new file mode 100644 index 000000000000..d26ac8f6cd0c --- /dev/null +++ b/pkgs/development/compilers/go/go-default-pie.patch @@ -0,0 +1,13 @@ +diff --git a/src/internal/platform/supported.go b/src/internal/platform/supported.go +index f9706a6988..abac42d550 100644 +--- a/src/internal/platform/supported.go ++++ b/src/internal/platform/supported.go +@@ -249,7 +253,7 @@ func DefaultPIE(goos, goarch string, isRace bool) bool { + case "darwin": + return true + } +- return false ++ return BuildModeSupported("gc", "pie", goos, goarch) + } + + // ExecutableHasDWARF reports whether the linked executable includes DWARF From ca6a37f817e815f31445af2a373b7747e62f372e Mon Sep 17 00:00:00 2001 From: Luna Nova Date: Sun, 14 Sep 2025 10:53:46 -0700 Subject: [PATCH 3/4] linkerd: remove unnecessary buildmode=pie This is no longer needed now that our go produces PIE by default --- pkgs/applications/networking/cluster/linkerd/generic.nix | 5 ----- 1 file changed, 5 deletions(-) diff --git a/pkgs/applications/networking/cluster/linkerd/generic.nix b/pkgs/applications/networking/cluster/linkerd/generic.nix index ef5835bcd6db..20f07da27e2b 100644 --- a/pkgs/applications/networking/cluster/linkerd/generic.nix +++ b/pkgs/applications/networking/cluster/linkerd/generic.nix @@ -31,11 +31,6 @@ buildGoModule rec { env GOFLAGS="" go generate ./jaeger/static env GOFLAGS="" go generate ./multicluster/static env GOFLAGS="" go generate ./viz/static - - # Necessary for building Musl - if [[ $NIX_HARDENING_ENABLE =~ "pie" ]]; then - export GOFLAGS="-buildmode=pie $GOFLAGS" - fi ''; tags = [ From dbb57d8d86f6fcc92d28c3d6835a83d85bb5e4b5 Mon Sep 17 00:00:00 2001 From: Luna Nova Date: Tue, 7 Oct 2025 17:25:46 -0700 Subject: [PATCH 4/4] go: only enable PIE by default when building for GOARCH matching target We rely on teaching go to use the correct ELF interpreter for PIE in a way that can't work for packages that go off and do their own GOARCH=something cross builds without touching nixpkgs cross machinery, so update the default pie patch to bake targetPlatform's GOARCH and only activate for it. tailscale works as a testcase for this as it cross compiles many arches in tstest/archtest/qemu_test.go --- pkgs/development/compilers/go/1.24.nix | 4 +++- pkgs/development/compilers/go/1.25.nix | 4 +++- pkgs/development/compilers/go/go-default-pie.patch | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/pkgs/development/compilers/go/1.24.nix b/pkgs/development/compilers/go/1.24.nix index 653b14f6ca1e..8ee9c0f16725 100644 --- a/pkgs/development/compilers/go/1.24.nix +++ b/pkgs/development/compilers/go/1.24.nix @@ -65,7 +65,9 @@ stdenv.mkDerivation (finalAttrs: { ./remove-tools-1.11.patch ./go_no_vendor_checks-1.23.patch ./go-env-go_ldso.patch - ./go-default-pie.patch + (replaceVars ./go-default-pie.patch { + inherit (stdenv.targetPlatform.go) GOARCH; + }) ]; inherit (stdenv.targetPlatform.go) GOOS GOARCH GOARM; diff --git a/pkgs/development/compilers/go/1.25.nix b/pkgs/development/compilers/go/1.25.nix index e7676644128c..60a8c24ae425 100644 --- a/pkgs/development/compilers/go/1.25.nix +++ b/pkgs/development/compilers/go/1.25.nix @@ -75,7 +75,9 @@ stdenv.mkDerivation (finalAttrs: { ./remove-tools-1.11.patch ./go_no_vendor_checks-1.23.patch ./go-env-go_ldso.patch - ./go-default-pie.patch + (replaceVars ./go-default-pie.patch { + inherit (stdenv.targetPlatform.go) GOARCH; + }) ]; inherit (stdenv.targetPlatform.go) GOOS GOARCH GOARM; diff --git a/pkgs/development/compilers/go/go-default-pie.patch b/pkgs/development/compilers/go/go-default-pie.patch index d26ac8f6cd0c..bce8d8dafd3d 100644 --- a/pkgs/development/compilers/go/go-default-pie.patch +++ b/pkgs/development/compilers/go/go-default-pie.patch @@ -7,7 +7,7 @@ index f9706a6988..abac42d550 100644 return true } - return false -+ return BuildModeSupported("gc", "pie", goos, goarch) ++ return goarch == "@GOARCH@" && BuildModeSupported("gc", "pie", goos, goarch) } // ExecutableHasDWARF reports whether the linked executable includes DWARF